├── .env.example ├── list.go ├── example ├── client_with_token.go ├── main.go ├── collections.go ├── client_with_version.go ├── deafult_client.go ├── bulk.go └── products.go ├── .gitignore ├── rand └── string.go ├── utils └── file.go ├── README.md ├── go.mod ├── graphql ├── graphql_test.go └── graphql.go ├── location.go ├── LICENSE ├── fulfillment.go ├── mock ├── location_service.go ├── fulfillment_service.go ├── collection_service.go ├── order_service.go ├── inventory_service.go ├── metafield_service.go ├── bulk_service.go └── product_service.go ├── bulk_test.go ├── client.go ├── metafield.go ├── collection.go ├── inventory.go ├── go.sum ├── order.go ├── product.go └── bulk.go /.env.example: -------------------------------------------------------------------------------- 1 | STORE_ACCESS_TOKEN= 2 | STORE_API_KEY= 3 | STORE_PASSWORD= 4 | STORE_NAME= 5 | STORE_API_VERSION= 6 | -------------------------------------------------------------------------------- /list.go: -------------------------------------------------------------------------------- 1 | package shopify 2 | 3 | type ListOptions struct { 4 | Query string 5 | First int 6 | Last int 7 | After string 8 | Before string 9 | Reverse bool 10 | } 11 | -------------------------------------------------------------------------------- /example/client_with_token.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | shopify "github.com/r0busta/go-shopify-graphql/v9" 7 | ) 8 | 9 | func clientWithToken() *shopify.Client { 10 | return shopify.NewClientWithToken(os.Getenv("STORE_ACCESS_TOKEN"), os.Getenv("STORE_NAME")) 11 | } 12 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | client := defaultClient() 5 | // client := clientWithToken() 6 | // client := clientWithVersion() 7 | 8 | // Collections 9 | collections(client) 10 | 11 | // Products 12 | listProducts(client) 13 | // createProduct(client) 14 | 15 | // Bulk operations 16 | // bulk(client) 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | .DS_Store 18 | test 19 | 20 | *.local 21 | .vscode 22 | .idea 23 | .env 24 | -------------------------------------------------------------------------------- /example/collections.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/r0busta/go-shopify-graphql/v9" 8 | ) 9 | 10 | func collections(client *shopify.Client) { 11 | // Get all collections 12 | collections, err := client.Collection.ListAll(context.Background()) 13 | if err != nil { 14 | panic(err) 15 | } 16 | 17 | // Print out the result 18 | for _, c := range collections { 19 | fmt.Println(c.Handle) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example/client_with_version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | shopify "github.com/r0busta/go-shopify-graphql/v9" 7 | graphqlclient "github.com/r0busta/go-shopify-graphql/v9/graphql" 8 | ) 9 | 10 | func clientWithVersion() *shopify.Client { 11 | gqlClient := graphqlclient.NewClient(os.Getenv("STORE_NAME"), graphqlclient.WithToken(os.Getenv("STORE_ACCESS_TOKEN")), graphqlclient.WithVersion("2022-10")) 12 | 13 | return shopify.NewClient(shopify.WithGraphQLClient(gqlClient)) 14 | } 15 | -------------------------------------------------------------------------------- /rand/string.go: -------------------------------------------------------------------------------- 1 | package rand 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | const charset = "abcdefghijklmnopqrstuvwxyz" + 9 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 10 | 11 | var seededRand *rand.Rand = rand.New( 12 | rand.NewSource(time.Now().UnixNano())) 13 | 14 | func StringWithCharset(length int, charset string) string { 15 | b := make([]byte, length) 16 | for i := range b { 17 | b[i] = charset[seededRand.Intn(len(charset))] 18 | } 19 | return string(b) 20 | } 21 | 22 | func String(length int) string { 23 | return StringWithCharset(length, charset) 24 | } 25 | -------------------------------------------------------------------------------- /utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "os" 7 | ) 8 | 9 | func CloseFile(f *os.File) { 10 | if err := f.Close(); err != nil { 11 | panic(err) 12 | } 13 | } 14 | 15 | func ReadFile(file string) (string, error) { 16 | var bytes []byte 17 | bytes, err := os.ReadFile(file) 18 | data := string(bytes) 19 | 20 | return data, err 21 | } 22 | 23 | func DownloadFile(filepath string, url string) error { 24 | resp, err := http.Get(url) 25 | if err != nil { 26 | return err 27 | } 28 | defer resp.Body.Close() 29 | 30 | out, err := os.Create(filepath) 31 | if err != nil { 32 | return err 33 | } 34 | defer CloseFile(out) 35 | 36 | _, err = io.Copy(out, resp.Body) 37 | 38 | return err 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-shopify-graphql 2 | 3 | A simple client using the Shopify GraphQL Admin API. 4 | 5 | ## Getting started 6 | 7 | Hello World example 8 | 9 | ### 1. Setup 10 | 11 | ```bash 12 | export STORE_API_KEY= 13 | export STORE_PASSWORD= 14 | export STORE_NAME= 15 | ``` 16 | 17 | ### 2. Program 18 | 19 | ```go 20 | package main 21 | 22 | import ( 23 | "context" 24 | "fmt" 25 | 26 | shopify "github.com/r0busta/go-shopify-graphql/v9" 27 | ) 28 | 29 | func main() { 30 | // Create client 31 | client := shopify.NewDefaultClient() 32 | 33 | // Get all collections 34 | collections, err := client.Collection.ListAll(context.Background()) 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | // Print out the result 40 | for _, c := range collections { 41 | fmt.Println(c.Handle) 42 | } 43 | } 44 | ``` 45 | 46 | ### 3. Run 47 | 48 | ```bash 49 | go run . 50 | ``` 51 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/r0busta/go-shopify-graphql/v9 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/golang/mock v1.6.0 7 | github.com/json-iterator/go v1.1.12 8 | github.com/r0busta/go-shopify-graphql-model/v4 v4.1.0 9 | github.com/r0busta/graphql v1.2.0 10 | github.com/sirupsen/logrus v1.9.3 11 | github.com/stretchr/testify v1.10.0 12 | gopkg.in/guregu/null.v4 v4.0.0 13 | ) 14 | 15 | require ( 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/emirpasic/gods v1.18.1 // indirect 18 | github.com/kr/text v0.2.0 // indirect 19 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 20 | github.com/modern-go/reflect2 v1.0.2 // indirect 21 | github.com/pmezard/go-difflib v1.0.0 // indirect 22 | github.com/rogpeppe/go-internal v1.13.1 // indirect 23 | github.com/thoas/go-funk v0.9.3 // indirect 24 | golang.org/x/net v0.33.0 // indirect 25 | golang.org/x/sys v0.28.0 // indirect 26 | gopkg.in/yaml.v3 v3.0.1 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /graphql/graphql_test.go: -------------------------------------------------------------------------------- 1 | package graphqlclient 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAPIURLWithVersion(t *testing.T) { 8 | transport := &transport{} 9 | WithVersion("2019-10")(transport) 10 | 11 | expected := "admin/api/2019-10" 12 | actual := transport.apiBasePath 13 | if actual != expected { 14 | t.Errorf("WithVersion apiBasePath = %s, expected %s", actual, expected) 15 | } 16 | } 17 | 18 | func TestAPIURLWithEmptyVersion(t *testing.T) { 19 | transport := &transport{} 20 | WithVersion("")(transport) 21 | 22 | expected := "" 23 | actual := transport.apiBasePath 24 | if actual != expected { 25 | t.Errorf("WithVersion apiBasePath = %s, expected %s", actual, expected) 26 | } 27 | } 28 | 29 | func TestBuildAPIEndpoint(t *testing.T) { 30 | expected := "https://store.myshopify.com/admin/api/graphql.json" 31 | actual := buildAPIEndpoint("store", "admin/api") 32 | if actual != expected { 33 | t.Errorf("buildAPIEndpoint = %s, expected %s", actual, expected) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /location.go: -------------------------------------------------------------------------------- 1 | package shopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/r0busta/go-shopify-graphql-model/v4/graph/model" 8 | ) 9 | 10 | //go:generate mockgen -destination=./mock/location_service.go -package=mock . LocationService 11 | type LocationService interface { 12 | Get(ctx context.Context, id string) (*model.Location, error) 13 | } 14 | 15 | type LocationServiceOp struct { 16 | client *Client 17 | } 18 | 19 | var _ LocationService = &LocationServiceOp{} 20 | 21 | func (s *LocationServiceOp) Get(ctx context.Context, id string) (*model.Location, error) { 22 | q := `query location($id: ID!) { 23 | location(id: $id){ 24 | id 25 | name 26 | } 27 | }` 28 | 29 | vars := map[string]interface{}{ 30 | "id": id, 31 | } 32 | 33 | var out struct { 34 | *model.Location `json:"location"` 35 | } 36 | err := s.client.gql.QueryString(ctx, q, vars, &out) 37 | if err != nil { 38 | return nil, fmt.Errorf("query: %w", err) 39 | } 40 | 41 | return out.Location, nil 42 | } 43 | -------------------------------------------------------------------------------- /example/deafult_client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | shopify "github.com/r0busta/go-shopify-graphql/v9" 7 | graphqlclient "github.com/r0busta/go-shopify-graphql/v9/graphql" 8 | ) 9 | 10 | func defaultClient() *shopify.Client { 11 | if os.Getenv("STORE_API_KEY") == "" || os.Getenv("STORE_PASSWORD") == "" || os.Getenv("STORE_NAME") == "" { 12 | panic("Shopify Admin API Key and/or Password (aka access token) and/or store name not set") 13 | } 14 | 15 | if os.Getenv("STORE_API_VERSION") != "" { 16 | apiKey := os.Getenv("STORE_API_KEY") 17 | accessToken := os.Getenv("STORE_PASSWORD") 18 | storeName := os.Getenv("STORE_NAME") 19 | opts := []graphqlclient.Option{ 20 | graphqlclient.WithVersion(os.Getenv("STORE_API_VERSION")), 21 | graphqlclient.WithPrivateAppAuth(apiKey, accessToken), 22 | } 23 | 24 | gql := graphqlclient.NewClient(storeName, opts...) 25 | 26 | return shopify.NewClient(shopify.WithGraphQLClient(gql)) 27 | } 28 | 29 | return shopify.NewDefaultClient() 30 | } 31 | -------------------------------------------------------------------------------- /example/bulk.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/r0busta/go-shopify-graphql-model/v4/graph/model" 8 | "github.com/r0busta/go-shopify-graphql/v9" 9 | ) 10 | 11 | func bulk(client *shopify.Client) { 12 | q := ` 13 | { 14 | products{ 15 | edges { 16 | node { 17 | id 18 | variants { 19 | edges { 20 | node { 21 | id 22 | media{ 23 | edges { 24 | node { 25 | ... on MediaImage { 26 | id 27 | image { 28 | url 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | }` 41 | 42 | products := []*model.Product{} 43 | err := client.BulkOperation.BulkQuery(context.Background(), q, &products) 44 | if err != nil { 45 | panic(err) 46 | } 47 | 48 | // Print out the result 49 | for _, p := range products { 50 | for _, v := range p.Variants.Edges { 51 | for _, m := range v.Node.Media.Edges { 52 | fmt.Println(m.Node.(*model.MediaImage).Image.URL) 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kirill Zhuchkov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /fulfillment.go: -------------------------------------------------------------------------------- 1 | package shopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/r0busta/go-shopify-graphql-model/v4/graph/model" 8 | ) 9 | 10 | //go:generate mockgen -destination=./mock/fulfillment_service.go -package=mock . FulfillmentService 11 | type FulfillmentService interface { 12 | Create(ctx context.Context, input model.FulfillmentV2Input) error 13 | } 14 | 15 | type FulfillmentServiceOp struct { 16 | client *Client 17 | } 18 | 19 | var _ FulfillmentService = &FulfillmentServiceOp{} 20 | 21 | type mutationFulfillmentCreateV2 struct { 22 | FulfillmentCreateV2Result struct { 23 | UserErrors []model.UserError `json:"userErrors,omitempty"` 24 | } `graphql:"fulfillmentCreateV2(fulfillment: $fulfillment)" json:"fulfillmentCreateV2"` 25 | } 26 | 27 | func (s *FulfillmentServiceOp) Create(ctx context.Context, fulfillment model.FulfillmentV2Input) error { 28 | m := mutationFulfillmentCreateV2{} 29 | 30 | vars := map[string]interface{}{ 31 | "fulfillment": fulfillment, 32 | } 33 | err := s.client.gql.Mutate(ctx, &m, vars) 34 | if err != nil { 35 | return fmt.Errorf("mutation: %w", err) 36 | } 37 | 38 | if len(m.FulfillmentCreateV2Result.UserErrors) > 0 { 39 | return fmt.Errorf("UserErrors: %+v", m.FulfillmentCreateV2Result.UserErrors) 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /mock/location_service.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/r0busta/go-shopify-graphql/v9 (interfaces: LocationService) 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | model "github.com/r0busta/go-shopify-graphql-model/v4/graph/model" 13 | ) 14 | 15 | // MockLocationService is a mock of LocationService interface. 16 | type MockLocationService struct { 17 | ctrl *gomock.Controller 18 | recorder *MockLocationServiceMockRecorder 19 | } 20 | 21 | // MockLocationServiceMockRecorder is the mock recorder for MockLocationService. 22 | type MockLocationServiceMockRecorder struct { 23 | mock *MockLocationService 24 | } 25 | 26 | // NewMockLocationService creates a new mock instance. 27 | func NewMockLocationService(ctrl *gomock.Controller) *MockLocationService { 28 | mock := &MockLocationService{ctrl: ctrl} 29 | mock.recorder = &MockLocationServiceMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockLocationService) EXPECT() *MockLocationServiceMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // Get mocks base method. 39 | func (m *MockLocationService) Get(arg0 context.Context, arg1 string) (*model.Location, error) { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "Get", arg0, arg1) 42 | ret0, _ := ret[0].(*model.Location) 43 | ret1, _ := ret[1].(error) 44 | return ret0, ret1 45 | } 46 | 47 | // Get indicates an expected call of Get. 48 | func (mr *MockLocationServiceMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockLocationService)(nil).Get), arg0, arg1) 51 | } 52 | -------------------------------------------------------------------------------- /mock/fulfillment_service.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/r0busta/go-shopify-graphql/v9 (interfaces: FulfillmentService) 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | model "github.com/r0busta/go-shopify-graphql-model/v4/graph/model" 13 | ) 14 | 15 | // MockFulfillmentService is a mock of FulfillmentService interface. 16 | type MockFulfillmentService struct { 17 | ctrl *gomock.Controller 18 | recorder *MockFulfillmentServiceMockRecorder 19 | } 20 | 21 | // MockFulfillmentServiceMockRecorder is the mock recorder for MockFulfillmentService. 22 | type MockFulfillmentServiceMockRecorder struct { 23 | mock *MockFulfillmentService 24 | } 25 | 26 | // NewMockFulfillmentService creates a new mock instance. 27 | func NewMockFulfillmentService(ctrl *gomock.Controller) *MockFulfillmentService { 28 | mock := &MockFulfillmentService{ctrl: ctrl} 29 | mock.recorder = &MockFulfillmentServiceMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockFulfillmentService) EXPECT() *MockFulfillmentServiceMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // Create mocks base method. 39 | func (m *MockFulfillmentService) Create(arg0 context.Context, arg1 model.FulfillmentV2Input) error { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "Create", arg0, arg1) 42 | ret0, _ := ret[0].(error) 43 | return ret0 44 | } 45 | 46 | // Create indicates an expected call of Create. 47 | func (mr *MockFulfillmentServiceMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockFulfillmentService)(nil).Create), arg0, arg1) 50 | } 51 | -------------------------------------------------------------------------------- /bulk_test.go: -------------------------------------------------------------------------------- 1 | package shopify_test 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | 8 | "github.com/r0busta/go-shopify-graphql-model/v4/graph/model" 9 | "github.com/r0busta/go-shopify-graphql/v9" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestBulkOperationEndToEnd(t *testing.T) { 15 | require.NotZero(t, os.Getenv("STORE_API_KEY")) 16 | require.NotZero(t, os.Getenv("STORE_PASSWORD")) 17 | require.NotZero(t, os.Getenv("STORE_NAME")) 18 | require.NotZero(t, os.Getenv("STORE_ACCESS_TOKEN")) 19 | 20 | tests := []struct { 21 | name string 22 | client *shopify.Client 23 | }{{ 24 | name: "default client", 25 | client: shopify.NewDefaultClient(), 26 | }, { 27 | name: "client with a token", 28 | client: shopify.NewClientWithToken(os.Getenv("STORE_ACCESS_TOKEN"), os.Getenv("STORE_NAME")), 29 | }} 30 | for _, tt := range tests { 31 | tt := tt 32 | t.Run(tt.name, func(t *testing.T) { 33 | q := ` 34 | { 35 | products{ 36 | edges { 37 | node { 38 | id 39 | variants { 40 | edges { 41 | node { 42 | id 43 | media{ 44 | edges { 45 | node { 46 | ... on MediaImage { 47 | id 48 | image { 49 | url 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | }` 62 | 63 | res := []*model.Product{} 64 | err := tt.client.BulkOperation.BulkQuery(context.Background(), q, &res) 65 | require.NoError(t, err) 66 | 67 | assert.Greater(t, len(res), 1) 68 | assert.NotZero(t, res[0].ID) 69 | 70 | assert.Greater(t, len(res[0].Variants.Edges), 1) 71 | assert.NotZero(t, res[0].Variants.Edges[0].Node.ID) 72 | 73 | assert.Equal(t, len(res[0].Variants.Edges[0].Node.Media.Edges), 1) 74 | assert.NotZero(t, res[0].Variants.Edges[0].Node.Media.Edges[0].Node.(*model.MediaImage).ID) 75 | assert.NotEmpty(t, res[0].Variants.Edges[0].Node.Media.Edges[0].Node.(*model.MediaImage).Image.URL) 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /graphql/graphql.go: -------------------------------------------------------------------------------- 1 | package graphqlclient 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/r0busta/graphql" 8 | ) 9 | 10 | const ( 11 | shopifyBaseDomain = "myshopify.com" 12 | shopifyAccessTokenHeader = "X-Shopify-Access-Token" 13 | 14 | defaultAPIProtocol = "https" 15 | defaultAPIEndpoint = "graphql.json" 16 | defaultAPIBasePath = "admin/api" 17 | ) 18 | 19 | // Option is used to configure options. 20 | type Option func(t *transport) 21 | 22 | // WithVersion optionally sets the API version if the passed string is valid. 23 | func WithVersion(apiVersion string) Option { 24 | return func(t *transport) { 25 | if apiVersion != "" { 26 | t.apiBasePath = fmt.Sprintf("%s/%s", defaultAPIBasePath, apiVersion) 27 | } 28 | } 29 | } 30 | 31 | // WithToken optionally sets access token. 32 | func WithToken(token string) Option { 33 | return func(t *transport) { 34 | t.accessToken = token 35 | } 36 | } 37 | 38 | // WithPrivateAppAuth optionally sets private app credentials (API key and access token). 39 | func WithPrivateAppAuth(apiKey string, accessToken string) Option { 40 | return func(t *transport) { 41 | t.apiKey = apiKey 42 | t.accessToken = accessToken 43 | } 44 | } 45 | 46 | type transport struct { 47 | accessToken string 48 | apiKey string 49 | apiBasePath string 50 | } 51 | 52 | func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { 53 | isAccessTokenSet := t.accessToken != "" 54 | areBasicAuthCredentialsSet := t.apiKey != "" && isAccessTokenSet 55 | 56 | if areBasicAuthCredentialsSet { 57 | req.SetBasicAuth(t.apiKey, t.accessToken) 58 | } else if isAccessTokenSet { 59 | req.Header.Set(shopifyAccessTokenHeader, t.accessToken) 60 | } 61 | 62 | return http.DefaultTransport.RoundTrip(req) 63 | } 64 | 65 | // NewClient creates a new client (in fact, just a simple wrapper for a graphql.Client). 66 | func NewClient(shopName string, opts ...Option) *graphql.Client { 67 | transport := &transport{ 68 | apiBasePath: defaultAPIBasePath, 69 | } 70 | 71 | for _, opt := range opts { 72 | opt(transport) 73 | } 74 | 75 | httpClient := &http.Client{ 76 | Transport: transport, 77 | } 78 | 79 | url := buildAPIEndpoint(shopName, transport.apiBasePath) 80 | 81 | return graphql.NewClient(url, httpClient) 82 | } 83 | 84 | func buildAPIEndpoint(shopName string, apiPathPrefix string) string { 85 | return fmt.Sprintf("%s://%s.%s/%s/%s", defaultAPIProtocol, shopName, shopifyBaseDomain, apiPathPrefix, defaultAPIEndpoint) 86 | } 87 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package shopify 2 | 3 | import ( 4 | "os" 5 | 6 | graphqlclient "github.com/r0busta/go-shopify-graphql/v9/graphql" 7 | "github.com/r0busta/graphql" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | const ( 12 | defaultShopifyAPIVersion = "2025-01" 13 | ) 14 | 15 | type Client struct { 16 | gql graphql.GraphQL 17 | 18 | Product ProductService 19 | Inventory InventoryService 20 | Collection CollectionService 21 | Order OrderService 22 | Fulfillment FulfillmentService 23 | Location LocationService 24 | Metafield MetafieldService 25 | BulkOperation BulkOperationService 26 | } 27 | 28 | type Option func(shopClient *Client) 29 | 30 | func WithGraphQLClient(gql graphql.GraphQL) Option { 31 | return func(c *Client) { 32 | c.gql = gql 33 | } 34 | } 35 | 36 | func NewClient(opts ...Option) *Client { 37 | c := &Client{} 38 | 39 | for _, opt := range opts { 40 | opt(c) 41 | } 42 | 43 | if c.gql == nil { 44 | log.Fatalln("GraphQL client not set") 45 | } 46 | 47 | c.Product = &ProductServiceOp{client: c} 48 | c.Inventory = &InventoryServiceOp{client: c} 49 | c.Collection = &CollectionServiceOp{client: c} 50 | c.Order = &OrderServiceOp{client: c} 51 | c.Fulfillment = &FulfillmentServiceOp{client: c} 52 | c.Location = &LocationServiceOp{client: c} 53 | c.Metafield = &MetafieldServiceOp{client: c} 54 | c.BulkOperation = &BulkOperationServiceOp{client: c} 55 | 56 | return c 57 | } 58 | 59 | func NewDefaultClient() *Client { 60 | apiKey := os.Getenv("STORE_API_KEY") 61 | accessToken := os.Getenv("STORE_PASSWORD") 62 | storeName := os.Getenv("STORE_NAME") 63 | if apiKey == "" || accessToken == "" || storeName == "" { 64 | log.Fatalln("Shopify API key and/or password (aka access token) and/or store name not set") 65 | } 66 | 67 | gql := newShopifyGraphQLClientWithBasicAuth(apiKey, accessToken, storeName) 68 | 69 | return NewClient(WithGraphQLClient(gql)) 70 | } 71 | 72 | func NewPrivateClient() *Client { 73 | return NewClientWithToken(os.Getenv("STORE_PASSWORD"), os.Getenv("STORE_NAME")) 74 | } 75 | 76 | func NewClientWithToken(accessToken string, storeName string, opts ...Option) *Client { 77 | if accessToken == "" || storeName == "" { 78 | log.Fatalln("Shopify API access token and/or store name not set") 79 | } 80 | 81 | gql := newShopifyGraphQLClientWithToken(accessToken, storeName) 82 | 83 | return NewClient(WithGraphQLClient(gql)) 84 | } 85 | 86 | func newShopifyGraphQLClientWithBasicAuth(apiKey string, accessToken string, storeName string) *graphql.Client { 87 | opts := []graphqlclient.Option{ 88 | graphqlclient.WithVersion(defaultShopifyAPIVersion), 89 | graphqlclient.WithPrivateAppAuth(apiKey, accessToken), 90 | } 91 | 92 | return graphqlclient.NewClient(storeName, opts...) 93 | } 94 | 95 | func newShopifyGraphQLClientWithToken(accessToken string, storeName string) *graphql.Client { 96 | opts := []graphqlclient.Option{ 97 | graphqlclient.WithVersion(defaultShopifyAPIVersion), 98 | graphqlclient.WithToken(accessToken), 99 | } 100 | 101 | return graphqlclient.NewClient(storeName, opts...) 102 | } 103 | 104 | func (c *Client) GraphQLClient() graphql.GraphQL { 105 | return c.gql 106 | } 107 | -------------------------------------------------------------------------------- /example/products.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/r0busta/go-shopify-graphql-model/v4/graph/model" 8 | "github.com/r0busta/go-shopify-graphql/v9" 9 | "gopkg.in/guregu/null.v4" 10 | ) 11 | 12 | func listProducts(client *shopify.Client) { 13 | // Get products 14 | products, err := client.Product.List(context.Background(), "") 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | // Print out the result 20 | for _, p := range products { 21 | fmt.Println(p.Title) 22 | } 23 | } 24 | 25 | func createProduct(client *shopify.Client) { 26 | status := model.ProductStatusDraft 27 | input := model.ProductCreateInput{ 28 | Title: model.NewString("go-shopify-graphql T-Shirt"), 29 | Handle: model.NewString("go-shopify-graphql-t-shirt"), 30 | Status: &status, 31 | ProductOptions: []model.OptionCreateInput{ 32 | { 33 | Name: model.NewString("Color"), 34 | Values: []model.OptionValueCreateInput{ 35 | { 36 | Name: model.NewString("Red"), 37 | }, 38 | { 39 | Name: model.NewString("Blue"), 40 | }, 41 | }, 42 | }, 43 | { 44 | Name: model.NewString("Size"), 45 | Values: []model.OptionValueCreateInput{ 46 | { 47 | Name: model.NewString("Small"), 48 | }, 49 | { 50 | Name: model.NewString("Medium"), 51 | }, 52 | { 53 | Name: model.NewString("Large"), 54 | }, 55 | }, 56 | }, 57 | }, 58 | } 59 | 60 | media := []model.CreateMediaInput{ 61 | { 62 | OriginalSource: "https://picsum.photos/seed/c854bed7-d604-4b8f-a5d3-0c44bb01a534/600/300", 63 | MediaContentType: model.MediaContentTypeImage, 64 | }, 65 | { 66 | OriginalSource: "https://picsum.photos/seed/3474e3f2-7ffe-4349-89f9-0deca2a87986/600/300", 67 | MediaContentType: model.MediaContentTypeImage, 68 | }, 69 | } 70 | 71 | id, err := client.Product.Create(context.Background(), input, media) 72 | if err != nil { 73 | panic(err) 74 | } 75 | 76 | product, err := client.Product.Get(context.Background(), *id) 77 | if err != nil { 78 | panic(err) 79 | } 80 | 81 | fmt.Println("Created product", product.Title, product.ID) 82 | 83 | variants := []model.ProductVariantsBulkInput{{ 84 | InventoryItem: &model.InventoryItemInput{ 85 | Sku: model.NewString("t-shirt-1-red-small"), 86 | }, 87 | Price: model.NewNullString(null.StringFrom("10.00")), 88 | OptionValues: []model.VariantOptionValueInput{ 89 | { 90 | OptionName: model.NewString("Color"), 91 | Name: model.NewString("Red"), 92 | }, 93 | { 94 | OptionName: model.NewString("Size"), 95 | Name: model.NewString("Small"), 96 | }, 97 | }, 98 | }, { 99 | InventoryItem: &model.InventoryItemInput{ 100 | Sku: model.NewString("t-shirt-1-blue-large"), 101 | }, 102 | Price: model.NewNullString(null.StringFrom("10.00")), 103 | OptionValues: []model.VariantOptionValueInput{ 104 | { 105 | OptionName: model.NewString("Color"), 106 | Name: model.NewString("Blue"), 107 | }, 108 | { 109 | OptionName: model.NewString("Size"), 110 | Name: model.NewString("Large"), 111 | }, 112 | }, 113 | }} 114 | 115 | err = client.Product.VariantsBulkCreate(context.Background(), *id, variants, model.ProductVariantsBulkCreateStrategyRemoveStandaloneVariant) 116 | if err != nil { 117 | panic(err) 118 | } 119 | 120 | product, err = client.Product.Get(context.Background(), *id) 121 | if err != nil { 122 | panic(err) 123 | } 124 | 125 | fmt.Println("Added", len(product.Variants.Edges), "variants") 126 | 127 | err = client.Product.Delete(context.Background(), model.ProductDeleteInput{ 128 | ID: *id, 129 | }) 130 | if err != nil { 131 | panic(err) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /metafield.go: -------------------------------------------------------------------------------- 1 | package shopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/r0busta/go-shopify-graphql-model/v4/graph/model" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | //go:generate mockgen -destination=./mock/metafield_service.go -package=mock . MetafieldService 13 | type MetafieldService interface { 14 | ListAllShopMetafields(ctx context.Context) ([]model.Metafield, error) 15 | ListShopMetafieldsByNamespace(ctx context.Context, namespace string) ([]model.Metafield, error) 16 | 17 | GetShopMetafieldByKey(ctx context.Context, namespace, key string) (*model.Metafield, error) 18 | 19 | Delete(ctx context.Context, metafield model.MetafieldIdentifierInput) error 20 | DeleteBulk(ctx context.Context, metafield []model.MetafieldIdentifierInput) error 21 | } 22 | 23 | type MetafieldServiceOp struct { 24 | client *Client 25 | } 26 | 27 | var _ MetafieldService = &MetafieldServiceOp{} 28 | 29 | type mutationMetafieldDelete struct { 30 | MetafieldDeleteResult struct { 31 | UserErrors []model.UserError `json:"userErrors,omitempty"` 32 | } `graphql:"metafieldDelete(input: $input)" json:"metafieldDelete"` 33 | } 34 | 35 | func (s *MetafieldServiceOp) ListAllShopMetafields(ctx context.Context) ([]model.Metafield, error) { 36 | q := ` 37 | { 38 | shop{ 39 | metafields{ 40 | edges{ 41 | node{ 42 | createdAt 43 | description 44 | id 45 | key 46 | legacyResourceId 47 | namespace 48 | ownerType 49 | updatedAt 50 | value 51 | type 52 | } 53 | } 54 | } 55 | } 56 | } 57 | ` 58 | 59 | res := []model.Metafield{} 60 | err := s.client.BulkOperation.BulkQuery(ctx, q, &res) 61 | if err != nil { 62 | return nil, fmt.Errorf("bulk query: %w", err) 63 | } 64 | 65 | return res, nil 66 | } 67 | 68 | func (s *MetafieldServiceOp) ListShopMetafieldsByNamespace(ctx context.Context, namespace string) ([]model.Metafield, error) { 69 | q := ` 70 | { 71 | shop{ 72 | metafields(namespace: "$namespace"){ 73 | edges{ 74 | node{ 75 | createdAt 76 | description 77 | id 78 | key 79 | legacyResourceId 80 | namespace 81 | ownerType 82 | updatedAt 83 | value 84 | type 85 | } 86 | } 87 | } 88 | } 89 | } 90 | ` 91 | q = strings.ReplaceAll(q, "$namespace", namespace) 92 | 93 | res := []model.Metafield{} 94 | err := s.client.BulkOperation.BulkQuery(ctx, q, &res) 95 | if err != nil { 96 | return nil, fmt.Errorf("bulk query: %w", err) 97 | } 98 | 99 | return res, nil 100 | } 101 | 102 | func (s *MetafieldServiceOp) GetShopMetafieldByKey(ctx context.Context, namespace, key string) (*model.Metafield, error) { 103 | var q struct { 104 | Shop struct { 105 | Metafield model.Metafield `graphql:"metafield(namespace: $namespace, key: $key)"` 106 | } `graphql:"shop"` 107 | } 108 | vars := map[string]interface{}{ 109 | "namespace": namespace, 110 | "key": key, 111 | } 112 | 113 | err := s.client.gql.Query(ctx, &q, vars) 114 | if err != nil { 115 | return nil, fmt.Errorf("query: %w", err) 116 | } 117 | 118 | return &q.Shop.Metafield, nil 119 | } 120 | 121 | func (s *MetafieldServiceOp) DeleteBulk(ctx context.Context, metafields []model.MetafieldIdentifierInput) error { 122 | for _, m := range metafields { 123 | err := s.Delete(ctx, m) 124 | if err != nil { 125 | log.Warnf("Couldn't delete metafield (%v): %s", m, err) 126 | } 127 | } 128 | 129 | return nil 130 | } 131 | 132 | func (s *MetafieldServiceOp) Delete(ctx context.Context, metafield model.MetafieldIdentifierInput) error { 133 | m := mutationMetafieldDelete{} 134 | 135 | vars := map[string]interface{}{ 136 | "input": metafield, 137 | } 138 | err := s.client.gql.Mutate(ctx, &m, vars) 139 | if err != nil { 140 | return fmt.Errorf("mutation: %w", err) 141 | } 142 | 143 | if len(m.MetafieldDeleteResult.UserErrors) > 0 { 144 | return fmt.Errorf("%+v", m.MetafieldDeleteResult.UserErrors) 145 | } 146 | 147 | return nil 148 | } 149 | -------------------------------------------------------------------------------- /mock/collection_service.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/r0busta/go-shopify-graphql/v9 (interfaces: CollectionService) 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | model "github.com/r0busta/go-shopify-graphql-model/v4/graph/model" 13 | ) 14 | 15 | // MockCollectionService is a mock of CollectionService interface. 16 | type MockCollectionService struct { 17 | ctrl *gomock.Controller 18 | recorder *MockCollectionServiceMockRecorder 19 | } 20 | 21 | // MockCollectionServiceMockRecorder is the mock recorder for MockCollectionService. 22 | type MockCollectionServiceMockRecorder struct { 23 | mock *MockCollectionService 24 | } 25 | 26 | // NewMockCollectionService creates a new mock instance. 27 | func NewMockCollectionService(ctrl *gomock.Controller) *MockCollectionService { 28 | mock := &MockCollectionService{ctrl: ctrl} 29 | mock.recorder = &MockCollectionServiceMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockCollectionService) EXPECT() *MockCollectionServiceMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // Create mocks base method. 39 | func (m *MockCollectionService) Create(arg0 context.Context, arg1 model.CollectionInput) (*string, error) { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "Create", arg0, arg1) 42 | ret0, _ := ret[0].(*string) 43 | ret1, _ := ret[1].(error) 44 | return ret0, ret1 45 | } 46 | 47 | // Create indicates an expected call of Create. 48 | func (mr *MockCollectionServiceMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockCollectionService)(nil).Create), arg0, arg1) 51 | } 52 | 53 | // CreateBulk mocks base method. 54 | func (m *MockCollectionService) CreateBulk(arg0 context.Context, arg1 []model.CollectionInput) error { 55 | m.ctrl.T.Helper() 56 | ret := m.ctrl.Call(m, "CreateBulk", arg0, arg1) 57 | ret0, _ := ret[0].(error) 58 | return ret0 59 | } 60 | 61 | // CreateBulk indicates an expected call of CreateBulk. 62 | func (mr *MockCollectionServiceMockRecorder) CreateBulk(arg0, arg1 interface{}) *gomock.Call { 63 | mr.mock.ctrl.T.Helper() 64 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBulk", reflect.TypeOf((*MockCollectionService)(nil).CreateBulk), arg0, arg1) 65 | } 66 | 67 | // Get mocks base method. 68 | func (m *MockCollectionService) Get(arg0 context.Context, arg1 string) (*model.Collection, error) { 69 | m.ctrl.T.Helper() 70 | ret := m.ctrl.Call(m, "Get", arg0, arg1) 71 | ret0, _ := ret[0].(*model.Collection) 72 | ret1, _ := ret[1].(error) 73 | return ret0, ret1 74 | } 75 | 76 | // Get indicates an expected call of Get. 77 | func (mr *MockCollectionServiceMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { 78 | mr.mock.ctrl.T.Helper() 79 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCollectionService)(nil).Get), arg0, arg1) 80 | } 81 | 82 | // ListAll mocks base method. 83 | func (m *MockCollectionService) ListAll(arg0 context.Context) ([]model.Collection, error) { 84 | m.ctrl.T.Helper() 85 | ret := m.ctrl.Call(m, "ListAll", arg0) 86 | ret0, _ := ret[0].([]model.Collection) 87 | ret1, _ := ret[1].(error) 88 | return ret0, ret1 89 | } 90 | 91 | // ListAll indicates an expected call of ListAll. 92 | func (mr *MockCollectionServiceMockRecorder) ListAll(arg0 interface{}) *gomock.Call { 93 | mr.mock.ctrl.T.Helper() 94 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAll", reflect.TypeOf((*MockCollectionService)(nil).ListAll), arg0) 95 | } 96 | 97 | // Update mocks base method. 98 | func (m *MockCollectionService) Update(arg0 context.Context, arg1 model.CollectionInput) error { 99 | m.ctrl.T.Helper() 100 | ret := m.ctrl.Call(m, "Update", arg0, arg1) 101 | ret0, _ := ret[0].(error) 102 | return ret0 103 | } 104 | 105 | // Update indicates an expected call of Update. 106 | func (mr *MockCollectionServiceMockRecorder) Update(arg0, arg1 interface{}) *gomock.Call { 107 | mr.mock.ctrl.T.Helper() 108 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockCollectionService)(nil).Update), arg0, arg1) 109 | } 110 | -------------------------------------------------------------------------------- /mock/order_service.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/r0busta/go-shopify-graphql/v9 (interfaces: OrderService) 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | model "github.com/r0busta/go-shopify-graphql-model/v4/graph/model" 13 | shopify "github.com/r0busta/go-shopify-graphql/v9" 14 | graphql "github.com/r0busta/graphql" 15 | ) 16 | 17 | // MockOrderService is a mock of OrderService interface. 18 | type MockOrderService struct { 19 | ctrl *gomock.Controller 20 | recorder *MockOrderServiceMockRecorder 21 | } 22 | 23 | // MockOrderServiceMockRecorder is the mock recorder for MockOrderService. 24 | type MockOrderServiceMockRecorder struct { 25 | mock *MockOrderService 26 | } 27 | 28 | // NewMockOrderService creates a new mock instance. 29 | func NewMockOrderService(ctrl *gomock.Controller) *MockOrderService { 30 | mock := &MockOrderService{ctrl: ctrl} 31 | mock.recorder = &MockOrderServiceMockRecorder{mock} 32 | return mock 33 | } 34 | 35 | // EXPECT returns an object that allows the caller to indicate expected use. 36 | func (m *MockOrderService) EXPECT() *MockOrderServiceMockRecorder { 37 | return m.recorder 38 | } 39 | 40 | // Get mocks base method. 41 | func (m *MockOrderService) Get(arg0 context.Context, arg1 graphql.ID) (*model.Order, error) { 42 | m.ctrl.T.Helper() 43 | ret := m.ctrl.Call(m, "Get", arg0, arg1) 44 | ret0, _ := ret[0].(*model.Order) 45 | ret1, _ := ret[1].(error) 46 | return ret0, ret1 47 | } 48 | 49 | // Get indicates an expected call of Get. 50 | func (mr *MockOrderServiceMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { 51 | mr.mock.ctrl.T.Helper() 52 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockOrderService)(nil).Get), arg0, arg1) 53 | } 54 | 55 | // List mocks base method. 56 | func (m *MockOrderService) List(arg0 context.Context, arg1 shopify.ListOptions) ([]model.Order, error) { 57 | m.ctrl.T.Helper() 58 | ret := m.ctrl.Call(m, "List", arg0, arg1) 59 | ret0, _ := ret[0].([]model.Order) 60 | ret1, _ := ret[1].(error) 61 | return ret0, ret1 62 | } 63 | 64 | // List indicates an expected call of List. 65 | func (mr *MockOrderServiceMockRecorder) List(arg0, arg1 interface{}) *gomock.Call { 66 | mr.mock.ctrl.T.Helper() 67 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockOrderService)(nil).List), arg0, arg1) 68 | } 69 | 70 | // ListAfterCursor mocks base method. 71 | func (m *MockOrderService) ListAfterCursor(arg0 context.Context, arg1 shopify.ListOptions) ([]model.Order, *string, *string, error) { 72 | m.ctrl.T.Helper() 73 | ret := m.ctrl.Call(m, "ListAfterCursor", arg0, arg1) 74 | ret0, _ := ret[0].([]model.Order) 75 | ret1, _ := ret[1].(*string) 76 | ret2, _ := ret[2].(*string) 77 | ret3, _ := ret[3].(error) 78 | return ret0, ret1, ret2, ret3 79 | } 80 | 81 | // ListAfterCursor indicates an expected call of ListAfterCursor. 82 | func (mr *MockOrderServiceMockRecorder) ListAfterCursor(arg0, arg1 interface{}) *gomock.Call { 83 | mr.mock.ctrl.T.Helper() 84 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAfterCursor", reflect.TypeOf((*MockOrderService)(nil).ListAfterCursor), arg0, arg1) 85 | } 86 | 87 | // ListAll mocks base method. 88 | func (m *MockOrderService) ListAll(arg0 context.Context) ([]model.Order, error) { 89 | m.ctrl.T.Helper() 90 | ret := m.ctrl.Call(m, "ListAll", arg0) 91 | ret0, _ := ret[0].([]model.Order) 92 | ret1, _ := ret[1].(error) 93 | return ret0, ret1 94 | } 95 | 96 | // ListAll indicates an expected call of ListAll. 97 | func (mr *MockOrderServiceMockRecorder) ListAll(arg0 interface{}) *gomock.Call { 98 | mr.mock.ctrl.T.Helper() 99 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAll", reflect.TypeOf((*MockOrderService)(nil).ListAll), arg0) 100 | } 101 | 102 | // Update mocks base method. 103 | func (m *MockOrderService) Update(arg0 context.Context, arg1 model.OrderInput) error { 104 | m.ctrl.T.Helper() 105 | ret := m.ctrl.Call(m, "Update", arg0, arg1) 106 | ret0, _ := ret[0].(error) 107 | return ret0 108 | } 109 | 110 | // Update indicates an expected call of Update. 111 | func (mr *MockOrderServiceMockRecorder) Update(arg0, arg1 interface{}) *gomock.Call { 112 | mr.mock.ctrl.T.Helper() 113 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockOrderService)(nil).Update), arg0, arg1) 114 | } 115 | -------------------------------------------------------------------------------- /mock/inventory_service.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/r0busta/go-shopify-graphql/v9 (interfaces: InventoryService) 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | model "github.com/r0busta/go-shopify-graphql-model/v4/graph/model" 13 | ) 14 | 15 | // MockInventoryService is a mock of InventoryService interface. 16 | type MockInventoryService struct { 17 | ctrl *gomock.Controller 18 | recorder *MockInventoryServiceMockRecorder 19 | } 20 | 21 | // MockInventoryServiceMockRecorder is the mock recorder for MockInventoryService. 22 | type MockInventoryServiceMockRecorder struct { 23 | mock *MockInventoryService 24 | } 25 | 26 | // NewMockInventoryService creates a new mock instance. 27 | func NewMockInventoryService(ctrl *gomock.Controller) *MockInventoryService { 28 | mock := &MockInventoryService{ctrl: ctrl} 29 | mock.recorder = &MockInventoryServiceMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockInventoryService) EXPECT() *MockInventoryServiceMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // ActivateInventory mocks base method. 39 | func (m *MockInventoryService) ActivateInventory(arg0 context.Context, arg1, arg2 string) error { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "ActivateInventory", arg0, arg1, arg2) 42 | ret0, _ := ret[0].(error) 43 | return ret0 44 | } 45 | 46 | // ActivateInventory indicates an expected call of ActivateInventory. 47 | func (mr *MockInventoryServiceMockRecorder) ActivateInventory(arg0, arg1, arg2 interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ActivateInventory", reflect.TypeOf((*MockInventoryService)(nil).ActivateInventory), arg0, arg1, arg2) 50 | } 51 | 52 | // Adjust mocks base method. 53 | func (m *MockInventoryService) Adjust(arg0 context.Context, arg1 string, arg2 []model.InventoryAdjustQuantitiesInput) error { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "Adjust", arg0, arg1, arg2) 56 | ret0, _ := ret[0].(error) 57 | return ret0 58 | } 59 | 60 | // Adjust indicates an expected call of Adjust. 61 | func (mr *MockInventoryServiceMockRecorder) Adjust(arg0, arg1, arg2 interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Adjust", reflect.TypeOf((*MockInventoryService)(nil).Adjust), arg0, arg1, arg2) 64 | } 65 | 66 | // AdjustQuantities mocks base method. 67 | func (m *MockInventoryService) AdjustQuantities(arg0 context.Context, arg1, arg2 string, arg3 *string, arg4 []model.InventoryChangeInput) error { 68 | m.ctrl.T.Helper() 69 | ret := m.ctrl.Call(m, "AdjustQuantities", arg0, arg1, arg2, arg3, arg4) 70 | ret0, _ := ret[0].(error) 71 | return ret0 72 | } 73 | 74 | // AdjustQuantities indicates an expected call of AdjustQuantities. 75 | func (mr *MockInventoryServiceMockRecorder) AdjustQuantities(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { 76 | mr.mock.ctrl.T.Helper() 77 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AdjustQuantities", reflect.TypeOf((*MockInventoryService)(nil).AdjustQuantities), arg0, arg1, arg2, arg3, arg4) 78 | } 79 | 80 | // SetOnHandQuantities mocks base method. 81 | func (m *MockInventoryService) SetOnHandQuantities(arg0 context.Context, arg1 string, arg2 *string, arg3 []model.InventorySetQuantityInput) error { 82 | m.ctrl.T.Helper() 83 | ret := m.ctrl.Call(m, "SetOnHandQuantities", arg0, arg1, arg2, arg3) 84 | ret0, _ := ret[0].(error) 85 | return ret0 86 | } 87 | 88 | // SetOnHandQuantities indicates an expected call of SetOnHandQuantities. 89 | func (mr *MockInventoryServiceMockRecorder) SetOnHandQuantities(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { 90 | mr.mock.ctrl.T.Helper() 91 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetOnHandQuantities", reflect.TypeOf((*MockInventoryService)(nil).SetOnHandQuantities), arg0, arg1, arg2, arg3) 92 | } 93 | 94 | // Update mocks base method. 95 | func (m *MockInventoryService) Update(arg0 context.Context, arg1 string, arg2 model.InventoryItemInput) error { 96 | m.ctrl.T.Helper() 97 | ret := m.ctrl.Call(m, "Update", arg0, arg1, arg2) 98 | ret0, _ := ret[0].(error) 99 | return ret0 100 | } 101 | 102 | // Update indicates an expected call of Update. 103 | func (mr *MockInventoryServiceMockRecorder) Update(arg0, arg1, arg2 interface{}) *gomock.Call { 104 | mr.mock.ctrl.T.Helper() 105 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockInventoryService)(nil).Update), arg0, arg1, arg2) 106 | } 107 | -------------------------------------------------------------------------------- /mock/metafield_service.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/r0busta/go-shopify-graphql/v9 (interfaces: MetafieldService) 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | model "github.com/r0busta/go-shopify-graphql-model/v4/graph/model" 13 | ) 14 | 15 | // MockMetafieldService is a mock of MetafieldService interface. 16 | type MockMetafieldService struct { 17 | ctrl *gomock.Controller 18 | recorder *MockMetafieldServiceMockRecorder 19 | } 20 | 21 | // MockMetafieldServiceMockRecorder is the mock recorder for MockMetafieldService. 22 | type MockMetafieldServiceMockRecorder struct { 23 | mock *MockMetafieldService 24 | } 25 | 26 | // NewMockMetafieldService creates a new mock instance. 27 | func NewMockMetafieldService(ctrl *gomock.Controller) *MockMetafieldService { 28 | mock := &MockMetafieldService{ctrl: ctrl} 29 | mock.recorder = &MockMetafieldServiceMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockMetafieldService) EXPECT() *MockMetafieldServiceMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // Delete mocks base method. 39 | func (m *MockMetafieldService) Delete(arg0 context.Context, arg1 model.MetafieldIdentifierInput) error { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "Delete", arg0, arg1) 42 | ret0, _ := ret[0].(error) 43 | return ret0 44 | } 45 | 46 | // Delete indicates an expected call of Delete. 47 | func (mr *MockMetafieldServiceMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockMetafieldService)(nil).Delete), arg0, arg1) 50 | } 51 | 52 | // DeleteBulk mocks base method. 53 | func (m *MockMetafieldService) DeleteBulk(arg0 context.Context, arg1 []model.MetafieldIdentifierInput) error { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "DeleteBulk", arg0, arg1) 56 | ret0, _ := ret[0].(error) 57 | return ret0 58 | } 59 | 60 | // DeleteBulk indicates an expected call of DeleteBulk. 61 | func (mr *MockMetafieldServiceMockRecorder) DeleteBulk(arg0, arg1 interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBulk", reflect.TypeOf((*MockMetafieldService)(nil).DeleteBulk), arg0, arg1) 64 | } 65 | 66 | // GetShopMetafieldByKey mocks base method. 67 | func (m *MockMetafieldService) GetShopMetafieldByKey(arg0 context.Context, arg1, arg2 string) (*model.Metafield, error) { 68 | m.ctrl.T.Helper() 69 | ret := m.ctrl.Call(m, "GetShopMetafieldByKey", arg0, arg1, arg2) 70 | ret0, _ := ret[0].(*model.Metafield) 71 | ret1, _ := ret[1].(error) 72 | return ret0, ret1 73 | } 74 | 75 | // GetShopMetafieldByKey indicates an expected call of GetShopMetafieldByKey. 76 | func (mr *MockMetafieldServiceMockRecorder) GetShopMetafieldByKey(arg0, arg1, arg2 interface{}) *gomock.Call { 77 | mr.mock.ctrl.T.Helper() 78 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetShopMetafieldByKey", reflect.TypeOf((*MockMetafieldService)(nil).GetShopMetafieldByKey), arg0, arg1, arg2) 79 | } 80 | 81 | // ListAllShopMetafields mocks base method. 82 | func (m *MockMetafieldService) ListAllShopMetafields(arg0 context.Context) ([]model.Metafield, error) { 83 | m.ctrl.T.Helper() 84 | ret := m.ctrl.Call(m, "ListAllShopMetafields", arg0) 85 | ret0, _ := ret[0].([]model.Metafield) 86 | ret1, _ := ret[1].(error) 87 | return ret0, ret1 88 | } 89 | 90 | // ListAllShopMetafields indicates an expected call of ListAllShopMetafields. 91 | func (mr *MockMetafieldServiceMockRecorder) ListAllShopMetafields(arg0 interface{}) *gomock.Call { 92 | mr.mock.ctrl.T.Helper() 93 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAllShopMetafields", reflect.TypeOf((*MockMetafieldService)(nil).ListAllShopMetafields), arg0) 94 | } 95 | 96 | // ListShopMetafieldsByNamespace mocks base method. 97 | func (m *MockMetafieldService) ListShopMetafieldsByNamespace(arg0 context.Context, arg1 string) ([]model.Metafield, error) { 98 | m.ctrl.T.Helper() 99 | ret := m.ctrl.Call(m, "ListShopMetafieldsByNamespace", arg0, arg1) 100 | ret0, _ := ret[0].([]model.Metafield) 101 | ret1, _ := ret[1].(error) 102 | return ret0, ret1 103 | } 104 | 105 | // ListShopMetafieldsByNamespace indicates an expected call of ListShopMetafieldsByNamespace. 106 | func (mr *MockMetafieldServiceMockRecorder) ListShopMetafieldsByNamespace(arg0, arg1 interface{}) *gomock.Call { 107 | mr.mock.ctrl.T.Helper() 108 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListShopMetafieldsByNamespace", reflect.TypeOf((*MockMetafieldService)(nil).ListShopMetafieldsByNamespace), arg0, arg1) 109 | } 110 | -------------------------------------------------------------------------------- /collection.go: -------------------------------------------------------------------------------- 1 | package shopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/r0busta/go-shopify-graphql-model/v4/graph/model" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | //go:generate mockgen -destination=./mock/collection_service.go -package=mock . CollectionService 12 | type CollectionService interface { 13 | ListAll(ctx context.Context) ([]model.Collection, error) 14 | 15 | Get(ctx context.Context, id string) (*model.Collection, error) 16 | 17 | Create(ctx context.Context, collection model.CollectionInput) (*string, error) 18 | CreateBulk(ctx context.Context, collections []model.CollectionInput) error 19 | 20 | Update(ctx context.Context, collection model.CollectionInput) error 21 | } 22 | 23 | type CollectionServiceOp struct { 24 | client *Client 25 | } 26 | 27 | var _ CollectionService = &CollectionServiceOp{} 28 | 29 | type mutationCollectionCreate struct { 30 | CollectionCreateResult struct { 31 | Collection *struct { 32 | ID string `json:"id,omitempty"` 33 | } `json:"collection,omitempty"` 34 | 35 | UserErrors []model.UserError `json:"userErrors,omitempty"` 36 | } `graphql:"collectionCreate(input: $input)" json:"collectionCreate"` 37 | } 38 | 39 | type mutationCollectionUpdate struct { 40 | CollectionCreateResult struct { 41 | UserErrors []model.UserError `json:"userErrors,omitempty"` 42 | } `graphql:"collectionUpdate(input: $input)" json:"collectionUpdate"` 43 | } 44 | 45 | var collectionQuery = ` 46 | id 47 | handle 48 | title 49 | 50 | products(first:250, after: $cursor){ 51 | edges{ 52 | node{ 53 | id 54 | } 55 | cursor 56 | } 57 | pageInfo{ 58 | hasNextPage 59 | } 60 | } 61 | ` 62 | 63 | var collectionBulkQuery = ` 64 | id 65 | handle 66 | title 67 | ` 68 | 69 | func (s *CollectionServiceOp) ListAll(ctx context.Context) ([]model.Collection, error) { 70 | q := fmt.Sprintf(` 71 | { 72 | collections{ 73 | edges{ 74 | node{ 75 | %s 76 | } 77 | } 78 | } 79 | } 80 | `, collectionBulkQuery) 81 | 82 | res := []model.Collection{} 83 | err := s.client.BulkOperation.BulkQuery(ctx, q, &res) 84 | if err != nil { 85 | return nil, fmt.Errorf("bulk query: %w", err) 86 | } 87 | 88 | return res, nil 89 | } 90 | 91 | func (s *CollectionServiceOp) Get(ctx context.Context, id string) (*model.Collection, error) { 92 | out, err := s.getPage(ctx, id, "") 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | nextPageData := out 98 | hasNextPage := out.Products.PageInfo.HasNextPage 99 | for hasNextPage && len(nextPageData.Products.Edges) > 0 { 100 | cursor := nextPageData.Products.Edges[len(nextPageData.Products.Edges)-1].Cursor 101 | nextPageData, err := s.getPage(ctx, id, cursor) 102 | if err != nil { 103 | return nil, err 104 | } 105 | out.Products.Edges = append(out.Products.Edges, nextPageData.Products.Edges...) 106 | hasNextPage = nextPageData.Products.PageInfo.HasNextPage 107 | } 108 | 109 | return out, nil 110 | } 111 | 112 | func (s *CollectionServiceOp) getPage(ctx context.Context, id string, cursor string) (*model.Collection, error) { 113 | q := fmt.Sprintf(` 114 | query collection($id: ID!, $cursor: String) { 115 | collection(id: $id){ 116 | %s 117 | } 118 | } 119 | `, collectionQuery) 120 | 121 | vars := map[string]interface{}{ 122 | "id": id, 123 | } 124 | if cursor != "" { 125 | vars["cursor"] = cursor 126 | } 127 | 128 | out := struct { 129 | Collection *model.Collection `json:"collection"` 130 | }{} 131 | err := s.client.gql.QueryString(ctx, q, vars, &out) 132 | if err != nil { 133 | return nil, fmt.Errorf("query: %w", err) 134 | } 135 | 136 | return out.Collection, nil 137 | } 138 | 139 | func (s *CollectionServiceOp) CreateBulk(ctx context.Context, collections []model.CollectionInput) error { 140 | for _, c := range collections { 141 | _, err := s.client.Collection.Create(ctx, c) 142 | if err != nil { 143 | log.Warnf("Couldn't create collection (%v): %s", c, err) 144 | } 145 | } 146 | 147 | return nil 148 | } 149 | 150 | func (s *CollectionServiceOp) Create(ctx context.Context, collection model.CollectionInput) (*string, error) { 151 | m := mutationCollectionCreate{} 152 | 153 | vars := map[string]interface{}{ 154 | "input": collection, 155 | } 156 | err := s.client.gql.Mutate(ctx, &m, vars) 157 | if err != nil { 158 | return nil, fmt.Errorf("mutation: %w", err) 159 | } 160 | 161 | if len(m.CollectionCreateResult.UserErrors) > 0 { 162 | return nil, fmt.Errorf("%+v", m.CollectionCreateResult.UserErrors) 163 | } 164 | 165 | return &m.CollectionCreateResult.Collection.ID, nil 166 | } 167 | 168 | func (s *CollectionServiceOp) Update(ctx context.Context, collection model.CollectionInput) error { 169 | m := mutationCollectionUpdate{} 170 | 171 | vars := map[string]interface{}{ 172 | "input": collection, 173 | } 174 | err := s.client.gql.Mutate(ctx, &m, vars) 175 | if err != nil { 176 | return fmt.Errorf("mutation: %w", err) 177 | } 178 | 179 | if len(m.CollectionCreateResult.UserErrors) > 0 { 180 | return fmt.Errorf("%+v", m.CollectionCreateResult.UserErrors) 181 | } 182 | 183 | return nil 184 | } 185 | -------------------------------------------------------------------------------- /inventory.go: -------------------------------------------------------------------------------- 1 | package shopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/r0busta/go-shopify-graphql-model/v4/graph/model" 8 | ) 9 | 10 | //go:generate mockgen -destination=./mock/inventory_service.go -package=mock . InventoryService 11 | type InventoryService interface { 12 | Update(ctx context.Context, id string, input model.InventoryItemInput) error 13 | Adjust(ctx context.Context, locationID string, input []model.InventoryAdjustQuantitiesInput) error 14 | AdjustQuantities(ctx context.Context, reason, name string, referenceDocumentUri *string, changes []model.InventoryChangeInput) error 15 | SetOnHandQuantities(ctx context.Context, reason string, referenceDocumentUri *string, setQuantities []model.InventorySetQuantityInput) error 16 | ActivateInventory(ctx context.Context, locationID string, id string) error 17 | } 18 | 19 | type InventoryServiceOp struct { 20 | client *Client 21 | } 22 | 23 | var _ InventoryService = &InventoryServiceOp{} 24 | 25 | type mutationInventoryItemUpdate struct { 26 | InventoryItemUpdateResult struct { 27 | UserErrors []model.UserError `json:"userErrors,omitempty"` 28 | } `graphql:"inventoryItemUpdate(id: $id, input: $input)" json:"inventoryItemUpdate"` 29 | } 30 | 31 | type mutationInventoryBulkAdjustQuantityAtLocation struct { 32 | InventoryBulkAdjustQuantityAtLocationResult struct { 33 | UserErrors []model.UserError `json:"userErrors,omitempty"` 34 | } `graphql:"inventoryBulkAdjustQuantityAtLocation(locationId: $locationId, inventoryItemAdjustments: $inventoryItemAdjustments)" json:"inventoryBulkAdjustQuantityAtLocation"` 35 | } 36 | 37 | type mutationInventoryActivate struct { 38 | InventoryActivateResult struct { 39 | UserErrors []model.UserError `json:"userErrors,omitempty"` 40 | } `graphql:"inventoryActivate(inventoryItemId: $itemID, locationId: $locationId)" json:"inventoryActivate"` 41 | } 42 | 43 | type mutationInventoryAdjustQuantities struct { 44 | InventoryAdjustQuantitiesResult struct { 45 | UserErrors []model.UserError `json:"userErrors,omitempty"` 46 | } `graphql:"inventoryAdjustQuantities(input: $input)" json:"inventoryAdjustQuantities"` 47 | } 48 | 49 | type mutationInventorySetOnHandQuantities struct { 50 | InventorySetOnHandQuantitiesResult struct { 51 | UserErrors []model.UserError `json:"userErrors,omitempty"` 52 | } `graphql:"inventorySetOnHandQuantities(input: $input)" json:"inventorySetOnHandQuantities"` 53 | } 54 | 55 | func (s *InventoryServiceOp) Update(ctx context.Context, id string, input model.InventoryItemInput) error { 56 | m := mutationInventoryItemUpdate{} 57 | vars := map[string]interface{}{ 58 | "id": id, 59 | "input": input, 60 | } 61 | err := s.client.gql.Mutate(ctx, &m, vars) 62 | if err != nil { 63 | return fmt.Errorf("mutation: %w", err) 64 | } 65 | 66 | if len(m.InventoryItemUpdateResult.UserErrors) > 0 { 67 | return fmt.Errorf("%+v", m.InventoryItemUpdateResult.UserErrors) 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (s *InventoryServiceOp) Adjust(ctx context.Context, locationID string, input []model.InventoryAdjustQuantitiesInput) error { 74 | m := mutationInventoryBulkAdjustQuantityAtLocation{} 75 | vars := map[string]interface{}{ 76 | "locationId": locationID, 77 | "inventoryItemAdjustments": input, 78 | } 79 | err := s.client.gql.Mutate(ctx, &m, vars) 80 | if err != nil { 81 | return fmt.Errorf("mutation: %w", err) 82 | } 83 | 84 | if len(m.InventoryBulkAdjustQuantityAtLocationResult.UserErrors) > 0 { 85 | return fmt.Errorf("%+v", m.InventoryBulkAdjustQuantityAtLocationResult.UserErrors) 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func (s *InventoryServiceOp) AdjustQuantities(ctx context.Context, reason, name string, referenceDocumentUri *string, changes []model.InventoryChangeInput) error { 92 | m := mutationInventoryAdjustQuantities{} 93 | vars := map[string]interface{}{ 94 | "input": model.InventoryAdjustQuantitiesInput{ 95 | Name: name, 96 | Reason: reason, 97 | ReferenceDocumentURI: referenceDocumentUri, 98 | Changes: changes, 99 | }, 100 | } 101 | err := s.client.gql.Mutate(ctx, &m, vars) 102 | if err != nil { 103 | return fmt.Errorf("mutation: %w", err) 104 | } 105 | 106 | if len(m.InventoryAdjustQuantitiesResult.UserErrors) > 0 { 107 | return fmt.Errorf("%+v", m.InventoryAdjustQuantitiesResult.UserErrors) 108 | } 109 | 110 | return nil 111 | } 112 | 113 | func (s *InventoryServiceOp) SetOnHandQuantities(ctx context.Context, reason string, referenceDocumentUri *string, setQuantities []model.InventorySetQuantityInput) error { 114 | m := mutationInventorySetOnHandQuantities{} 115 | vars := map[string]interface{}{ 116 | "input": model.InventorySetOnHandQuantitiesInput{ 117 | Reason: reason, 118 | ReferenceDocumentURI: referenceDocumentUri, 119 | SetQuantities: setQuantities, 120 | }, 121 | } 122 | err := s.client.gql.Mutate(ctx, &m, vars) 123 | if err != nil { 124 | return fmt.Errorf("mutation: %w", err) 125 | } 126 | 127 | if len(m.InventorySetOnHandQuantitiesResult.UserErrors) > 0 { 128 | return fmt.Errorf("%+v", m.InventorySetOnHandQuantitiesResult.UserErrors) 129 | } 130 | 131 | return nil 132 | } 133 | 134 | func (s *InventoryServiceOp) ActivateInventory(ctx context.Context, locationID string, id string) error { 135 | m := mutationInventoryActivate{} 136 | vars := map[string]interface{}{ 137 | "itemID": id, 138 | "locationId": locationID, 139 | } 140 | err := s.client.gql.Mutate(ctx, &m, vars) 141 | if err != nil { 142 | return fmt.Errorf("mutation: %w", err) 143 | } 144 | 145 | if len(m.InventoryActivateResult.UserErrors) > 0 { 146 | return fmt.Errorf("%+v", m.InventoryActivateResult.UserErrors) 147 | } 148 | 149 | return nil 150 | } 151 | -------------------------------------------------------------------------------- /mock/bulk_service.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/r0busta/go-shopify-graphql/v9 (interfaces: BulkOperationService) 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | time "time" 11 | 12 | gomock "github.com/golang/mock/gomock" 13 | model "github.com/r0busta/go-shopify-graphql-model/v4/graph/model" 14 | ) 15 | 16 | // MockBulkOperationService is a mock of BulkOperationService interface. 17 | type MockBulkOperationService struct { 18 | ctrl *gomock.Controller 19 | recorder *MockBulkOperationServiceMockRecorder 20 | } 21 | 22 | // MockBulkOperationServiceMockRecorder is the mock recorder for MockBulkOperationService. 23 | type MockBulkOperationServiceMockRecorder struct { 24 | mock *MockBulkOperationService 25 | } 26 | 27 | // NewMockBulkOperationService creates a new mock instance. 28 | func NewMockBulkOperationService(ctrl *gomock.Controller) *MockBulkOperationService { 29 | mock := &MockBulkOperationService{ctrl: ctrl} 30 | mock.recorder = &MockBulkOperationServiceMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use. 35 | func (m *MockBulkOperationService) EXPECT() *MockBulkOperationServiceMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // BulkQuery mocks base method. 40 | func (m *MockBulkOperationService) BulkQuery(arg0 context.Context, arg1 string, arg2 interface{}) error { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "BulkQuery", arg0, arg1, arg2) 43 | ret0, _ := ret[0].(error) 44 | return ret0 45 | } 46 | 47 | // BulkQuery indicates an expected call of BulkQuery. 48 | func (mr *MockBulkOperationServiceMockRecorder) BulkQuery(arg0, arg1, arg2 interface{}) *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BulkQuery", reflect.TypeOf((*MockBulkOperationService)(nil).BulkQuery), arg0, arg1, arg2) 51 | } 52 | 53 | // CancelRunningBulkQuery mocks base method. 54 | func (m *MockBulkOperationService) CancelRunningBulkQuery(arg0 context.Context) error { 55 | m.ctrl.T.Helper() 56 | ret := m.ctrl.Call(m, "CancelRunningBulkQuery", arg0) 57 | ret0, _ := ret[0].(error) 58 | return ret0 59 | } 60 | 61 | // CancelRunningBulkQuery indicates an expected call of CancelRunningBulkQuery. 62 | func (mr *MockBulkOperationServiceMockRecorder) CancelRunningBulkQuery(arg0 interface{}) *gomock.Call { 63 | mr.mock.ctrl.T.Helper() 64 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelRunningBulkQuery", reflect.TypeOf((*MockBulkOperationService)(nil).CancelRunningBulkQuery), arg0) 65 | } 66 | 67 | // GetCurrentBulkQuery mocks base method. 68 | func (m *MockBulkOperationService) GetCurrentBulkQuery(arg0 context.Context) (*model.BulkOperation, error) { 69 | m.ctrl.T.Helper() 70 | ret := m.ctrl.Call(m, "GetCurrentBulkQuery", arg0) 71 | ret0, _ := ret[0].(*model.BulkOperation) 72 | ret1, _ := ret[1].(error) 73 | return ret0, ret1 74 | } 75 | 76 | // GetCurrentBulkQuery indicates an expected call of GetCurrentBulkQuery. 77 | func (mr *MockBulkOperationServiceMockRecorder) GetCurrentBulkQuery(arg0 interface{}) *gomock.Call { 78 | mr.mock.ctrl.T.Helper() 79 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentBulkQuery", reflect.TypeOf((*MockBulkOperationService)(nil).GetCurrentBulkQuery), arg0) 80 | } 81 | 82 | // GetCurrentBulkQueryResultURL mocks base method. 83 | func (m *MockBulkOperationService) GetCurrentBulkQueryResultURL(arg0 context.Context) (*string, error) { 84 | m.ctrl.T.Helper() 85 | ret := m.ctrl.Call(m, "GetCurrentBulkQueryResultURL", arg0) 86 | ret0, _ := ret[0].(*string) 87 | ret1, _ := ret[1].(error) 88 | return ret0, ret1 89 | } 90 | 91 | // GetCurrentBulkQueryResultURL indicates an expected call of GetCurrentBulkQueryResultURL. 92 | func (mr *MockBulkOperationServiceMockRecorder) GetCurrentBulkQueryResultURL(arg0 interface{}) *gomock.Call { 93 | mr.mock.ctrl.T.Helper() 94 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentBulkQueryResultURL", reflect.TypeOf((*MockBulkOperationService)(nil).GetCurrentBulkQueryResultURL), arg0) 95 | } 96 | 97 | // PostBulkQuery mocks base method. 98 | func (m *MockBulkOperationService) PostBulkQuery(arg0 context.Context, arg1 string) (*string, error) { 99 | m.ctrl.T.Helper() 100 | ret := m.ctrl.Call(m, "PostBulkQuery", arg0, arg1) 101 | ret0, _ := ret[0].(*string) 102 | ret1, _ := ret[1].(error) 103 | return ret0, ret1 104 | } 105 | 106 | // PostBulkQuery indicates an expected call of PostBulkQuery. 107 | func (mr *MockBulkOperationServiceMockRecorder) PostBulkQuery(arg0, arg1 interface{}) *gomock.Call { 108 | mr.mock.ctrl.T.Helper() 109 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostBulkQuery", reflect.TypeOf((*MockBulkOperationService)(nil).PostBulkQuery), arg0, arg1) 110 | } 111 | 112 | // ShouldGetBulkQueryResultURL mocks base method. 113 | func (m *MockBulkOperationService) ShouldGetBulkQueryResultURL(arg0 context.Context, arg1 *string) (*string, error) { 114 | m.ctrl.T.Helper() 115 | ret := m.ctrl.Call(m, "ShouldGetBulkQueryResultURL", arg0, arg1) 116 | ret0, _ := ret[0].(*string) 117 | ret1, _ := ret[1].(error) 118 | return ret0, ret1 119 | } 120 | 121 | // ShouldGetBulkQueryResultURL indicates an expected call of ShouldGetBulkQueryResultURL. 122 | func (mr *MockBulkOperationServiceMockRecorder) ShouldGetBulkQueryResultURL(arg0, arg1 interface{}) *gomock.Call { 123 | mr.mock.ctrl.T.Helper() 124 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShouldGetBulkQueryResultURL", reflect.TypeOf((*MockBulkOperationService)(nil).ShouldGetBulkQueryResultURL), arg0, arg1) 125 | } 126 | 127 | // WaitForCurrentBulkQuery mocks base method. 128 | func (m *MockBulkOperationService) WaitForCurrentBulkQuery(arg0 context.Context, arg1 time.Duration) (*model.BulkOperation, error) { 129 | m.ctrl.T.Helper() 130 | ret := m.ctrl.Call(m, "WaitForCurrentBulkQuery", arg0, arg1) 131 | ret0, _ := ret[0].(*model.BulkOperation) 132 | ret1, _ := ret[1].(error) 133 | return ret0, ret1 134 | } 135 | 136 | // WaitForCurrentBulkQuery indicates an expected call of WaitForCurrentBulkQuery. 137 | func (mr *MockBulkOperationServiceMockRecorder) WaitForCurrentBulkQuery(arg0, arg1 interface{}) *gomock.Call { 138 | mr.mock.ctrl.T.Helper() 139 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForCurrentBulkQuery", reflect.TypeOf((*MockBulkOperationService)(nil).WaitForCurrentBulkQuery), arg0, arg1) 140 | } 141 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 6 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 7 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 8 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 9 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 10 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 11 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 12 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 13 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 14 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 15 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 16 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 17 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 18 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 19 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 20 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/r0busta/go-shopify-graphql-model/v4 v4.1.0 h1:ScJWP3rIqKrMH9ql6CxlGZisx01jOtb+AB71Wy3GwGQ= 24 | github.com/r0busta/go-shopify-graphql-model/v4 v4.1.0/go.mod h1:jwaN7JM4+D1ssSHOqK0xa08ZiEOVRuiTL5x+Jakc1AU= 25 | github.com/r0busta/graphql v1.2.0 h1:vbq+Fyuegajydfu9mQsjNBXAJTfMYrczOLJdhLVhsrQ= 26 | github.com/r0busta/graphql v1.2.0/go.mod h1:tnBqVGxQVmck/AhJ6VSd2zr5JjXqDm7Rn9ThGRrRg0g= 27 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 28 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 29 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 30 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 31 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 32 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 33 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 34 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 35 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 36 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 37 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 38 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 39 | github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw= 40 | github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= 41 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 42 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 43 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 44 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 45 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 46 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 47 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 48 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 49 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 50 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 51 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 52 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 53 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 54 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 55 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 57 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 59 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 60 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 61 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 62 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 63 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 64 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 65 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 66 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 67 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 68 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 69 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 70 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 71 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 72 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 73 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 74 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 75 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 76 | gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg= 77 | gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI= 78 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 79 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 80 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 81 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 82 | -------------------------------------------------------------------------------- /mock/product_service.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/r0busta/go-shopify-graphql/v9 (interfaces: ProductService) 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | model "github.com/r0busta/go-shopify-graphql-model/v4/graph/model" 13 | ) 14 | 15 | // MockProductService is a mock of ProductService interface. 16 | type MockProductService struct { 17 | ctrl *gomock.Controller 18 | recorder *MockProductServiceMockRecorder 19 | } 20 | 21 | // MockProductServiceMockRecorder is the mock recorder for MockProductService. 22 | type MockProductServiceMockRecorder struct { 23 | mock *MockProductService 24 | } 25 | 26 | // NewMockProductService creates a new mock instance. 27 | func NewMockProductService(ctrl *gomock.Controller) *MockProductService { 28 | mock := &MockProductService{ctrl: ctrl} 29 | mock.recorder = &MockProductServiceMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockProductService) EXPECT() *MockProductServiceMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // Create mocks base method. 39 | func (m *MockProductService) Create(arg0 context.Context, arg1 model.ProductCreateInput, arg2 []model.CreateMediaInput) (*string, error) { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "Create", arg0, arg1, arg2) 42 | ret0, _ := ret[0].(*string) 43 | ret1, _ := ret[1].(error) 44 | return ret0, ret1 45 | } 46 | 47 | // Create indicates an expected call of Create. 48 | func (mr *MockProductServiceMockRecorder) Create(arg0, arg1, arg2 interface{}) *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockProductService)(nil).Create), arg0, arg1, arg2) 51 | } 52 | 53 | // Delete mocks base method. 54 | func (m *MockProductService) Delete(arg0 context.Context, arg1 model.ProductDeleteInput) error { 55 | m.ctrl.T.Helper() 56 | ret := m.ctrl.Call(m, "Delete", arg0, arg1) 57 | ret0, _ := ret[0].(error) 58 | return ret0 59 | } 60 | 61 | // Delete indicates an expected call of Delete. 62 | func (mr *MockProductServiceMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call { 63 | mr.mock.ctrl.T.Helper() 64 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockProductService)(nil).Delete), arg0, arg1) 65 | } 66 | 67 | // Get mocks base method. 68 | func (m *MockProductService) Get(arg0 context.Context, arg1 string) (*model.Product, error) { 69 | m.ctrl.T.Helper() 70 | ret := m.ctrl.Call(m, "Get", arg0, arg1) 71 | ret0, _ := ret[0].(*model.Product) 72 | ret1, _ := ret[1].(error) 73 | return ret0, ret1 74 | } 75 | 76 | // Get indicates an expected call of Get. 77 | func (mr *MockProductServiceMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { 78 | mr.mock.ctrl.T.Helper() 79 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockProductService)(nil).Get), arg0, arg1) 80 | } 81 | 82 | // List mocks base method. 83 | func (m *MockProductService) List(arg0 context.Context, arg1 string) ([]model.Product, error) { 84 | m.ctrl.T.Helper() 85 | ret := m.ctrl.Call(m, "List", arg0, arg1) 86 | ret0, _ := ret[0].([]model.Product) 87 | ret1, _ := ret[1].(error) 88 | return ret0, ret1 89 | } 90 | 91 | // List indicates an expected call of List. 92 | func (mr *MockProductServiceMockRecorder) List(arg0, arg1 interface{}) *gomock.Call { 93 | mr.mock.ctrl.T.Helper() 94 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockProductService)(nil).List), arg0, arg1) 95 | } 96 | 97 | // ListAll mocks base method. 98 | func (m *MockProductService) ListAll(arg0 context.Context) ([]model.Product, error) { 99 | m.ctrl.T.Helper() 100 | ret := m.ctrl.Call(m, "ListAll", arg0) 101 | ret0, _ := ret[0].([]model.Product) 102 | ret1, _ := ret[1].(error) 103 | return ret0, ret1 104 | } 105 | 106 | // ListAll indicates an expected call of ListAll. 107 | func (mr *MockProductServiceMockRecorder) ListAll(arg0 interface{}) *gomock.Call { 108 | mr.mock.ctrl.T.Helper() 109 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAll", reflect.TypeOf((*MockProductService)(nil).ListAll), arg0) 110 | } 111 | 112 | // MediaCreate mocks base method. 113 | func (m *MockProductService) MediaCreate(arg0 context.Context, arg1 string, arg2 []model.CreateMediaInput) error { 114 | m.ctrl.T.Helper() 115 | ret := m.ctrl.Call(m, "MediaCreate", arg0, arg1, arg2) 116 | ret0, _ := ret[0].(error) 117 | return ret0 118 | } 119 | 120 | // MediaCreate indicates an expected call of MediaCreate. 121 | func (mr *MockProductServiceMockRecorder) MediaCreate(arg0, arg1, arg2 interface{}) *gomock.Call { 122 | mr.mock.ctrl.T.Helper() 123 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MediaCreate", reflect.TypeOf((*MockProductService)(nil).MediaCreate), arg0, arg1, arg2) 124 | } 125 | 126 | // Update mocks base method. 127 | func (m *MockProductService) Update(arg0 context.Context, arg1 model.ProductUpdateInput, arg2 []model.CreateMediaInput) error { 128 | m.ctrl.T.Helper() 129 | ret := m.ctrl.Call(m, "Update", arg0, arg1, arg2) 130 | ret0, _ := ret[0].(error) 131 | return ret0 132 | } 133 | 134 | // Update indicates an expected call of Update. 135 | func (mr *MockProductServiceMockRecorder) Update(arg0, arg1, arg2 interface{}) *gomock.Call { 136 | mr.mock.ctrl.T.Helper() 137 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockProductService)(nil).Update), arg0, arg1, arg2) 138 | } 139 | 140 | // VariantsBulkCreate mocks base method. 141 | func (m *MockProductService) VariantsBulkCreate(arg0 context.Context, arg1 string, arg2 []model.ProductVariantsBulkInput, arg3 model.ProductVariantsBulkCreateStrategy) error { 142 | m.ctrl.T.Helper() 143 | ret := m.ctrl.Call(m, "VariantsBulkCreate", arg0, arg1, arg2, arg3) 144 | ret0, _ := ret[0].(error) 145 | return ret0 146 | } 147 | 148 | // VariantsBulkCreate indicates an expected call of VariantsBulkCreate. 149 | func (mr *MockProductServiceMockRecorder) VariantsBulkCreate(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { 150 | mr.mock.ctrl.T.Helper() 151 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VariantsBulkCreate", reflect.TypeOf((*MockProductService)(nil).VariantsBulkCreate), arg0, arg1, arg2, arg3) 152 | } 153 | 154 | // VariantsBulkReorder mocks base method. 155 | func (m *MockProductService) VariantsBulkReorder(arg0 context.Context, arg1 string, arg2 []model.ProductVariantPositionInput) error { 156 | m.ctrl.T.Helper() 157 | ret := m.ctrl.Call(m, "VariantsBulkReorder", arg0, arg1, arg2) 158 | ret0, _ := ret[0].(error) 159 | return ret0 160 | } 161 | 162 | // VariantsBulkReorder indicates an expected call of VariantsBulkReorder. 163 | func (mr *MockProductServiceMockRecorder) VariantsBulkReorder(arg0, arg1, arg2 interface{}) *gomock.Call { 164 | mr.mock.ctrl.T.Helper() 165 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VariantsBulkReorder", reflect.TypeOf((*MockProductService)(nil).VariantsBulkReorder), arg0, arg1, arg2) 166 | } 167 | 168 | // VariantsBulkUpdate mocks base method. 169 | func (m *MockProductService) VariantsBulkUpdate(arg0 context.Context, arg1 string, arg2 []model.ProductVariantsBulkInput) error { 170 | m.ctrl.T.Helper() 171 | ret := m.ctrl.Call(m, "VariantsBulkUpdate", arg0, arg1, arg2) 172 | ret0, _ := ret[0].(error) 173 | return ret0 174 | } 175 | 176 | // VariantsBulkUpdate indicates an expected call of VariantsBulkUpdate. 177 | func (mr *MockProductServiceMockRecorder) VariantsBulkUpdate(arg0, arg1, arg2 interface{}) *gomock.Call { 178 | mr.mock.ctrl.T.Helper() 179 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VariantsBulkUpdate", reflect.TypeOf((*MockProductService)(nil).VariantsBulkUpdate), arg0, arg1, arg2) 180 | } 181 | -------------------------------------------------------------------------------- /order.go: -------------------------------------------------------------------------------- 1 | package shopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/r0busta/go-shopify-graphql-model/v4/graph/model" 9 | "github.com/r0busta/graphql" 10 | ) 11 | 12 | //go:generate mockgen -destination=./mock/order_service.go -package=mock . OrderService 13 | type OrderService interface { 14 | Get(ctx context.Context, id graphql.ID) (*model.Order, error) 15 | 16 | List(ctx context.Context, opts ListOptions) ([]model.Order, error) 17 | ListAll(ctx context.Context) ([]model.Order, error) 18 | 19 | ListAfterCursor(ctx context.Context, opts ListOptions) ([]model.Order, *string, *string, error) 20 | 21 | Update(ctx context.Context, input model.OrderInput) error 22 | } 23 | 24 | type OrderServiceOp struct { 25 | client *Client 26 | } 27 | 28 | var _ OrderService = &OrderServiceOp{} 29 | 30 | type mutationOrderUpdate struct { 31 | OrderUpdateResult struct { 32 | UserErrors []model.UserError `json:"userErrors,omitempty"` 33 | } `graphql:"orderUpdate(input: $input)" json:"orderUpdate"` 34 | } 35 | 36 | const orderBaseQuery = ` 37 | id 38 | legacyResourceId 39 | name 40 | createdAt 41 | customer{ 42 | id 43 | legacyResourceId 44 | firstName 45 | displayName 46 | email 47 | } 48 | clientIp 49 | shippingAddress{ 50 | address1 51 | address2 52 | city 53 | province 54 | country 55 | zip 56 | } 57 | shippingLine{ 58 | originalPriceSet{ 59 | presentmentMoney{ 60 | amount 61 | currencyCode 62 | } 63 | shopMoney{ 64 | amount 65 | currencyCode 66 | } 67 | } 68 | title 69 | } 70 | taxLines{ 71 | priceSet{ 72 | presentmentMoney{ 73 | amount 74 | currencyCode 75 | } 76 | shopMoney{ 77 | amount 78 | currencyCode 79 | } 80 | } 81 | rate 82 | ratePercentage 83 | title 84 | } 85 | totalReceivedSet{ 86 | presentmentMoney{ 87 | amount 88 | currencyCode 89 | } 90 | shopMoney{ 91 | amount 92 | currencyCode 93 | } 94 | } 95 | note 96 | tags 97 | transactions { 98 | processedAt 99 | status 100 | kind 101 | test 102 | amountSet { 103 | shopMoney { 104 | amount 105 | currencyCode 106 | } 107 | } 108 | } 109 | ` 110 | 111 | const orderLightQuery = ` 112 | id 113 | legacyResourceId 114 | name 115 | createdAt 116 | customer{ 117 | id 118 | legacyResourceId 119 | firstName 120 | displayName 121 | email 122 | } 123 | shippingAddress{ 124 | address1 125 | address2 126 | city 127 | province 128 | country 129 | zip 130 | } 131 | shippingLine{ 132 | title 133 | } 134 | totalReceivedSet{ 135 | shopMoney{ 136 | amount 137 | } 138 | } 139 | note 140 | tags 141 | ` 142 | 143 | const lineItemFragment = ` 144 | fragment lineItem on LineItem { 145 | id 146 | sku 147 | quantity 148 | fulfillableQuantity 149 | fulfillmentStatus 150 | product{ 151 | id 152 | legacyResourceId 153 | } 154 | vendor 155 | title 156 | variantTitle 157 | variant{ 158 | id 159 | legacyResourceId 160 | selectedOptions{ 161 | name 162 | value 163 | } 164 | } 165 | originalTotalSet{ 166 | presentmentMoney{ 167 | amount 168 | currencyCode 169 | } 170 | shopMoney{ 171 | amount 172 | currencyCode 173 | } 174 | } 175 | originalUnitPriceSet{ 176 | presentmentMoney{ 177 | amount 178 | currencyCode 179 | } 180 | shopMoney{ 181 | amount 182 | currencyCode 183 | } 184 | } 185 | discountedUnitPriceSet{ 186 | presentmentMoney{ 187 | amount 188 | currencyCode 189 | } 190 | shopMoney{ 191 | amount 192 | currencyCode 193 | } 194 | } 195 | discountedTotalSet{ 196 | presentmentMoney{ 197 | amount 198 | currencyCode 199 | } 200 | shopMoney{ 201 | amount 202 | currencyCode 203 | } 204 | } 205 | } 206 | ` 207 | 208 | const lineItemFragmentLight = ` 209 | fragment lineItem on LineItem { 210 | id 211 | sku 212 | quantity 213 | fulfillableQuantity 214 | fulfillmentStatus 215 | vendor 216 | title 217 | variantTitle 218 | } 219 | ` 220 | 221 | func (s *OrderServiceOp) Get(ctx context.Context, id graphql.ID) (*model.Order, error) { 222 | q := fmt.Sprintf(` 223 | query order($id: ID!) { 224 | node(id: $id){ 225 | ... on Order { 226 | %s 227 | lineItems(first:50){ 228 | edges{ 229 | node{ 230 | ...lineItem 231 | } 232 | } 233 | } 234 | fulfillmentOrders(first:5){ 235 | edges { 236 | node { 237 | id 238 | status 239 | lineItems(first:50){ 240 | edges { 241 | node { 242 | id 243 | remainingQuantity 244 | totalQuantity 245 | lineItem{ 246 | sku 247 | } 248 | } 249 | } 250 | } 251 | } 252 | } 253 | } 254 | } 255 | } 256 | } 257 | 258 | %s 259 | `, orderBaseQuery, lineItemFragment) 260 | 261 | vars := map[string]interface{}{ 262 | "id": id, 263 | } 264 | 265 | out := struct { 266 | Order *model.Order `json:"node"` 267 | }{} 268 | err := s.client.gql.QueryString(ctx, q, vars, &out) 269 | if err != nil { 270 | return nil, fmt.Errorf("query: %w", err) 271 | } 272 | 273 | return out.Order, nil 274 | } 275 | 276 | func (s *OrderServiceOp) List(ctx context.Context, opts ListOptions) ([]model.Order, error) { 277 | q := fmt.Sprintf(` 278 | { 279 | orders(query: "$query"){ 280 | edges{ 281 | node{ 282 | %s 283 | lineItems{ 284 | edges{ 285 | node{ 286 | ...lineItem 287 | } 288 | } 289 | } 290 | } 291 | } 292 | } 293 | } 294 | 295 | %s 296 | `, orderBaseQuery, lineItemFragment) 297 | 298 | q = strings.ReplaceAll(q, "$query", opts.Query) 299 | 300 | res := []model.Order{} 301 | err := s.client.BulkOperation.BulkQuery(ctx, q, &res) 302 | if err != nil { 303 | return nil, fmt.Errorf("bulk query: %w", err) 304 | } 305 | 306 | return res, nil 307 | } 308 | 309 | func (s *OrderServiceOp) ListAll(ctx context.Context) ([]model.Order, error) { 310 | q := fmt.Sprintf(` 311 | { 312 | orders(query: "$query"){ 313 | edges{ 314 | node{ 315 | %s 316 | lineItems{ 317 | edges{ 318 | node{ 319 | ...lineItem 320 | } 321 | } 322 | } 323 | } 324 | } 325 | } 326 | } 327 | 328 | %s 329 | `, orderBaseQuery, lineItemFragment) 330 | 331 | res := []model.Order{} 332 | err := s.client.BulkOperation.BulkQuery(ctx, q, &res) 333 | if err != nil { 334 | return nil, fmt.Errorf("bulk query: %w", err) 335 | } 336 | 337 | return res, nil 338 | } 339 | 340 | func (s *OrderServiceOp) ListAfterCursor(ctx context.Context, opts ListOptions) ([]model.Order, *string, *string, error) { 341 | q := fmt.Sprintf(` 342 | query orders($query: String, $first: Int, $last: Int, $before: String, $after: String, $reverse: Boolean) { 343 | orders(query: $query, first: $first, last: $last, before: $before, after: $after, reverse: $reverse){ 344 | edges{ 345 | node{ 346 | %s 347 | 348 | lineItems(first:25){ 349 | edges{ 350 | node{ 351 | ...lineItem 352 | } 353 | } 354 | } 355 | } 356 | cursor 357 | } 358 | pageInfo{ 359 | hasNextPage 360 | } 361 | } 362 | } 363 | 364 | %s 365 | `, orderLightQuery, lineItemFragmentLight) 366 | 367 | vars := map[string]interface{}{ 368 | "query": opts.Query, 369 | "reverse": opts.Reverse, 370 | } 371 | 372 | if opts.After != "" { 373 | vars["after"] = opts.After 374 | } else if opts.Before != "" { 375 | vars["before"] = opts.Before 376 | } 377 | 378 | if opts.First > 0 { 379 | vars["first"] = opts.First 380 | } else if opts.Last > 0 { 381 | vars["last"] = opts.Last 382 | } 383 | 384 | out := struct { 385 | Orders struct { 386 | Edges []struct { 387 | OrderQueryResult *model.Order `json:"node,omitempty"` 388 | Cursor string `json:"cursor,omitempty"` 389 | } `json:"edges,omitempty"` 390 | PageInfo struct { 391 | HasNextPage bool `json:"hasNextPage,omitempty"` 392 | } `json:"pageInfo,omitempty"` 393 | } `json:"orders,omitempty"` 394 | }{} 395 | err := s.client.gql.QueryString(ctx, q, vars, &out) 396 | if err != nil { 397 | return nil, nil, nil, fmt.Errorf("query: %w", err) 398 | } 399 | 400 | res := []model.Order{} 401 | var firstCursor *string 402 | var lastCursor *string 403 | if len(out.Orders.Edges) > 0 { 404 | firstCursor = &out.Orders.Edges[0].Cursor 405 | lastCursor = &out.Orders.Edges[len(out.Orders.Edges)-1].Cursor 406 | for _, o := range out.Orders.Edges { 407 | res = append(res, *o.OrderQueryResult) 408 | } 409 | } 410 | 411 | return res, firstCursor, lastCursor, nil 412 | } 413 | 414 | func (s *OrderServiceOp) Update(ctx context.Context, input model.OrderInput) error { 415 | m := mutationOrderUpdate{} 416 | 417 | vars := map[string]interface{}{ 418 | "input": input, 419 | } 420 | err := s.client.gql.Mutate(ctx, &m, vars) 421 | if err != nil { 422 | return fmt.Errorf("mutation: %w", err) 423 | } 424 | 425 | if len(m.OrderUpdateResult.UserErrors) > 0 { 426 | return fmt.Errorf("%+v", m.OrderUpdateResult.UserErrors) 427 | } 428 | 429 | return nil 430 | } 431 | -------------------------------------------------------------------------------- /product.go: -------------------------------------------------------------------------------- 1 | package shopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/r0busta/go-shopify-graphql-model/v4/graph/model" 9 | ) 10 | 11 | //go:generate mockgen -destination=./mock/product_service.go -package=mock . ProductService 12 | type ProductService interface { 13 | List(ctx context.Context, query string) ([]model.Product, error) 14 | ListAll(ctx context.Context) ([]model.Product, error) 15 | 16 | Get(ctx context.Context, id string) (*model.Product, error) 17 | 18 | Create(ctx context.Context, product model.ProductCreateInput, media []model.CreateMediaInput) (*string, error) 19 | 20 | Update(ctx context.Context, product model.ProductUpdateInput, media []model.CreateMediaInput) error 21 | 22 | Delete(ctx context.Context, product model.ProductDeleteInput) error 23 | 24 | VariantsBulkCreate(ctx context.Context, id string, input []model.ProductVariantsBulkInput, strategy model.ProductVariantsBulkCreateStrategy) error 25 | VariantsBulkUpdate(ctx context.Context, id string, input []model.ProductVariantsBulkInput) error 26 | VariantsBulkReorder(ctx context.Context, id string, input []model.ProductVariantPositionInput) error 27 | 28 | MediaCreate(ctx context.Context, id string, input []model.CreateMediaInput) error 29 | } 30 | 31 | type ProductServiceOp struct { 32 | client *Client 33 | } 34 | 35 | var _ ProductService = &ProductServiceOp{} 36 | 37 | type mutationProductCreate struct { 38 | ProductCreateResult struct { 39 | Product *struct { 40 | ID string `json:"id,omitempty"` 41 | } `json:"product,omitempty"` 42 | 43 | UserErrors []model.UserError `json:"userErrors,omitempty"` 44 | } `graphql:"productCreate(product: $product, media: $media)" json:"productCreate"` 45 | } 46 | 47 | type mutationProductUpdate struct { 48 | ProductUpdateResult struct { 49 | UserErrors []model.UserError `json:"userErrors,omitempty"` 50 | } `graphql:"productUpdate(product: $product, media: $media)" json:"productUpdate"` 51 | } 52 | 53 | type mutationProductDelete struct { 54 | ProductDeleteResult struct { 55 | UserErrors []model.UserError `json:"userErrors,omitempty"` 56 | } `graphql:"productDelete(input: $input)" json:"productDelete"` 57 | } 58 | 59 | type mutationProductVariantsBulkCreate struct { 60 | ProductVariantsBulkCreateResult struct { 61 | UserErrors []model.UserError `json:"userErrors,omitempty"` 62 | } `graphql:"productVariantsBulkCreate(productId: $productId, variants: $variants, strategy: $strategy)" json:"productVariantsBulkCreate"` 63 | } 64 | 65 | type mutationProductVariantsBulkUpdate struct { 66 | ProductVariantsBulkUpdateResult struct { 67 | UserErrors []model.UserError `json:"userErrors,omitempty"` 68 | } `graphql:"productVariantsBulkUpdate(productId: $productId, variants: $variants)" json:"productVariantsBulkUpdate"` 69 | } 70 | 71 | type mutationProductVariantsBulkReorder struct { 72 | ProductVariantsBulkReorderResult struct { 73 | UserErrors []model.UserError `json:"userErrors,omitempty"` 74 | } `graphql:"productVariantsBulkReorder(positions: $positions, productId: $productId)" json:"productVariantsBulkReorder"` 75 | } 76 | 77 | type mutationProductCreateMedia struct { 78 | ProductCreateMediaResult struct { 79 | MediaUserErrors []model.UserError `json:"mediaUserErrors,omitempty"` 80 | } `graphql:"productCreateMedia(productId: $productId, media: $media)" json:"productCreateMedia"` 81 | } 82 | 83 | const productBaseQuery = ` 84 | id 85 | legacyResourceId 86 | handle 87 | options{ 88 | id 89 | name 90 | values 91 | position 92 | } 93 | tags 94 | title 95 | description 96 | descriptionPlainSummary 97 | priceRangeV2{ 98 | minVariantPrice{ 99 | amount 100 | currencyCode 101 | } 102 | maxVariantPrice{ 103 | amount 104 | currencyCode 105 | } 106 | } 107 | productType 108 | vendor 109 | totalInventory 110 | onlineStoreUrl 111 | descriptionHtml 112 | seo{ 113 | description 114 | title 115 | } 116 | templateSuffix 117 | customProductType 118 | featuredImage{ 119 | id 120 | altText 121 | height 122 | width 123 | url 124 | } 125 | ` 126 | 127 | var productQuery = fmt.Sprintf(` 128 | %s 129 | variants(first:100, after: $cursor){ 130 | edges{ 131 | node{ 132 | id 133 | legacyResourceId 134 | title 135 | displayName 136 | sku 137 | selectedOptions{ 138 | name 139 | value 140 | optionValue{ 141 | id 142 | name 143 | } 144 | } 145 | position 146 | image { 147 | id 148 | altText 149 | height 150 | width 151 | url 152 | } 153 | compareAtPrice 154 | price 155 | inventoryQuantity 156 | inventoryItem{ 157 | id 158 | legacyResourceId 159 | sku 160 | } 161 | availableForSale 162 | } 163 | } 164 | pageInfo{ 165 | hasNextPage 166 | } 167 | } 168 | `, productBaseQuery) 169 | 170 | var productBulkQuery = fmt.Sprintf(` 171 | %s 172 | metafields{ 173 | edges{ 174 | node{ 175 | id 176 | legacyResourceId 177 | namespace 178 | key 179 | value 180 | type 181 | } 182 | } 183 | } 184 | variants{ 185 | edges{ 186 | node{ 187 | id 188 | legacyResourceId 189 | title 190 | displayName 191 | sku 192 | selectedOptions{ 193 | name 194 | value 195 | optionValue{ 196 | id 197 | name 198 | } 199 | } 200 | position 201 | image { 202 | id 203 | altText 204 | height 205 | width 206 | url 207 | } 208 | compareAtPrice 209 | price 210 | inventoryQuantity 211 | inventoryItem{ 212 | id 213 | legacyResourceId 214 | sku 215 | } 216 | availableForSale 217 | } 218 | } 219 | } 220 | `, productBaseQuery) 221 | 222 | func (s *ProductServiceOp) ListAll(ctx context.Context) ([]model.Product, error) { 223 | q := fmt.Sprintf(` 224 | { 225 | products{ 226 | edges{ 227 | node{ 228 | %s 229 | } 230 | } 231 | } 232 | } 233 | `, productBulkQuery) 234 | 235 | res := []model.Product{} 236 | err := s.client.BulkOperation.BulkQuery(ctx, q, &res) 237 | if err != nil { 238 | return []model.Product{}, err 239 | } 240 | 241 | return res, nil 242 | } 243 | 244 | func (s *ProductServiceOp) List(ctx context.Context, query string) ([]model.Product, error) { 245 | q := fmt.Sprintf(` 246 | { 247 | products(query: "$query"){ 248 | edges{ 249 | node{ 250 | %s 251 | } 252 | } 253 | } 254 | } 255 | `, productBulkQuery) 256 | 257 | q = strings.ReplaceAll(q, "$query", query) 258 | 259 | res := []model.Product{} 260 | err := s.client.BulkOperation.BulkQuery(ctx, q, &res) 261 | if err != nil { 262 | return nil, fmt.Errorf("bulk query: %w", err) 263 | } 264 | 265 | return res, nil 266 | } 267 | 268 | func (s *ProductServiceOp) Get(ctx context.Context, id string) (*model.Product, error) { 269 | out, err := s.getPage(ctx, id, "") 270 | if err != nil { 271 | return nil, err 272 | } 273 | 274 | nextPageData := out 275 | hasNextPage := out.Variants.PageInfo.HasNextPage 276 | for hasNextPage && len(nextPageData.Variants.Edges) > 0 { 277 | cursor := nextPageData.Variants.Edges[len(nextPageData.Variants.Edges)-1].Cursor 278 | nextPageData, err := s.getPage(ctx, id, cursor) 279 | if err != nil { 280 | return nil, fmt.Errorf("get page: %w", err) 281 | } 282 | out.Variants.Edges = append(out.Variants.Edges, nextPageData.Variants.Edges...) 283 | hasNextPage = nextPageData.Variants.PageInfo.HasNextPage 284 | } 285 | 286 | return out, nil 287 | } 288 | 289 | func (s *ProductServiceOp) getPage(ctx context.Context, id string, cursor string) (*model.Product, error) { 290 | q := fmt.Sprintf(` 291 | query product($id: ID!, $cursor: String) { 292 | product(id: $id){ 293 | %s 294 | } 295 | } 296 | `, productQuery) 297 | 298 | vars := map[string]interface{}{ 299 | "id": id, 300 | } 301 | if cursor != "" { 302 | vars["cursor"] = cursor 303 | } 304 | 305 | out := struct { 306 | Product *model.Product `json:"product"` 307 | }{} 308 | err := s.client.gql.QueryString(ctx, q, vars, &out) 309 | if err != nil { 310 | return nil, fmt.Errorf("query: %w", err) 311 | } 312 | 313 | return out.Product, nil 314 | } 315 | 316 | func (s *ProductServiceOp) Create(ctx context.Context, product model.ProductCreateInput, media []model.CreateMediaInput) (*string, error) { 317 | m := mutationProductCreate{} 318 | 319 | vars := map[string]interface{}{ 320 | "product": product, 321 | "media": media, 322 | } 323 | 324 | err := s.client.gql.Mutate(ctx, &m, vars) 325 | if err != nil { 326 | return nil, fmt.Errorf("mutation: %w", err) 327 | } 328 | 329 | if len(m.ProductCreateResult.UserErrors) > 0 { 330 | return nil, fmt.Errorf("%+v", m.ProductCreateResult.UserErrors) 331 | } 332 | 333 | return &m.ProductCreateResult.Product.ID, nil 334 | } 335 | 336 | func (s *ProductServiceOp) Update(ctx context.Context, product model.ProductUpdateInput, media []model.CreateMediaInput) error { 337 | m := mutationProductUpdate{} 338 | 339 | vars := map[string]interface{}{ 340 | "product": product, 341 | "media": media, 342 | } 343 | err := s.client.gql.Mutate(ctx, &m, vars) 344 | if err != nil { 345 | return fmt.Errorf("mutation: %w", err) 346 | } 347 | 348 | if len(m.ProductUpdateResult.UserErrors) > 0 { 349 | return fmt.Errorf("%+v", m.ProductUpdateResult.UserErrors) 350 | } 351 | 352 | return nil 353 | } 354 | 355 | func (s *ProductServiceOp) Delete(ctx context.Context, product model.ProductDeleteInput) error { 356 | m := mutationProductDelete{} 357 | 358 | vars := map[string]interface{}{ 359 | "input": product, 360 | } 361 | err := s.client.gql.Mutate(ctx, &m, vars) 362 | if err != nil { 363 | return fmt.Errorf("mutation: %w", err) 364 | } 365 | 366 | if len(m.ProductDeleteResult.UserErrors) > 0 { 367 | return fmt.Errorf("%+v", m.ProductDeleteResult.UserErrors) 368 | } 369 | 370 | return nil 371 | } 372 | 373 | func (s *ProductServiceOp) VariantsBulkCreate(ctx context.Context, id string, input []model.ProductVariantsBulkInput, strategy model.ProductVariantsBulkCreateStrategy) error { 374 | m := mutationProductVariantsBulkCreate{} 375 | 376 | vars := map[string]interface{}{ 377 | "productId": id, 378 | "variants": input, 379 | "strategy": strategy, 380 | } 381 | err := s.client.gql.Mutate(ctx, &m, vars) 382 | if err != nil { 383 | return fmt.Errorf("mutation: %w", err) 384 | } 385 | 386 | if len(m.ProductVariantsBulkCreateResult.UserErrors) > 0 { 387 | return fmt.Errorf("%+v", m.ProductVariantsBulkCreateResult.UserErrors) 388 | } 389 | 390 | return nil 391 | } 392 | 393 | func (s *ProductServiceOp) VariantsBulkUpdate(ctx context.Context, id string, input []model.ProductVariantsBulkInput) error { 394 | m := mutationProductVariantsBulkUpdate{} 395 | 396 | vars := map[string]interface{}{ 397 | "productId": id, 398 | "variants": input, 399 | } 400 | err := s.client.gql.Mutate(ctx, &m, vars) 401 | if err != nil { 402 | return fmt.Errorf("mutation: %w", err) 403 | } 404 | 405 | if len(m.ProductVariantsBulkUpdateResult.UserErrors) > 0 { 406 | return fmt.Errorf("%+v", m.ProductVariantsBulkUpdateResult.UserErrors) 407 | } 408 | 409 | return nil 410 | } 411 | 412 | func (s *ProductServiceOp) VariantsBulkReorder(ctx context.Context, id string, input []model.ProductVariantPositionInput) error { 413 | m := mutationProductVariantsBulkReorder{} 414 | 415 | vars := map[string]interface{}{ 416 | "productId": id, 417 | "positions": input, 418 | } 419 | err := s.client.gql.Mutate(ctx, &m, vars) 420 | if err != nil { 421 | return fmt.Errorf("mutation: %w", err) 422 | } 423 | 424 | if len(m.ProductVariantsBulkReorderResult.UserErrors) > 0 { 425 | return fmt.Errorf("%+v", m.ProductVariantsBulkReorderResult.UserErrors) 426 | } 427 | 428 | return nil 429 | } 430 | 431 | func (s *ProductServiceOp) MediaCreate(ctx context.Context, id string, input []model.CreateMediaInput) error { 432 | m := mutationProductCreateMedia{} 433 | 434 | vars := map[string]interface{}{ 435 | "productId": id, 436 | "media": input, 437 | } 438 | 439 | err := s.client.gql.Mutate(ctx, &m, vars) 440 | if err != nil { 441 | return fmt.Errorf("mutation: %w", err) 442 | } 443 | 444 | if len(m.ProductCreateMediaResult.MediaUserErrors) > 0 { 445 | return fmt.Errorf("%+v", m.ProductCreateMediaResult.MediaUserErrors) 446 | } 447 | 448 | return nil 449 | } 450 | -------------------------------------------------------------------------------- /bulk.go: -------------------------------------------------------------------------------- 1 | package shopify 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "os" 11 | "path/filepath" 12 | "reflect" 13 | "regexp" 14 | "time" 15 | 16 | jsoniter "github.com/json-iterator/go" 17 | "github.com/r0busta/go-shopify-graphql-model/v4/graph/model" 18 | "github.com/r0busta/go-shopify-graphql/v9/rand" 19 | "github.com/r0busta/go-shopify-graphql/v9/utils" 20 | log "github.com/sirupsen/logrus" 21 | "gopkg.in/guregu/null.v4" 22 | ) 23 | 24 | const ( 25 | edgesFieldName = "Edges" 26 | nodeFieldName = "Node" 27 | ) 28 | 29 | //go:generate mockgen -destination=./mock/bulk_service.go -package=mock . BulkOperationService 30 | type BulkOperationService interface { 31 | BulkQuery(ctx context.Context, query string, v interface{}) error 32 | 33 | PostBulkQuery(ctx context.Context, query string) (*string, error) 34 | GetCurrentBulkQuery(ctx context.Context) (*model.BulkOperation, error) 35 | GetCurrentBulkQueryResultURL(ctx context.Context) (*string, error) 36 | WaitForCurrentBulkQuery(ctx context.Context, interval time.Duration) (*model.BulkOperation, error) 37 | ShouldGetBulkQueryResultURL(ctx context.Context, id *string) (*string, error) 38 | CancelRunningBulkQuery(ctx context.Context) error 39 | } 40 | 41 | type BulkOperationServiceOp struct { 42 | client *Client 43 | } 44 | 45 | var _ BulkOperationService = &BulkOperationServiceOp{} 46 | 47 | type mutationBulkOperationRunQuery struct { 48 | BulkOperationRunQueryResult model.BulkOperationRunQueryPayload `graphql:"bulkOperationRunQuery(query: $query)" json:"bulkOperationRunQuery"` 49 | } 50 | 51 | type mutationBulkOperationRunQueryCancel struct { 52 | BulkOperationCancelResult model.BulkOperationCancelPayload `graphql:"bulkOperationCancel(id: $id)" json:"bulkOperationCancel"` 53 | } 54 | 55 | var gidRegex *regexp.Regexp 56 | 57 | func init() { 58 | gidRegex = regexp.MustCompile(`^gid://shopify/(\w+)/\d+`) 59 | } 60 | 61 | func (s *BulkOperationServiceOp) PostBulkQuery(ctx context.Context, query string) (*string, error) { 62 | m := mutationBulkOperationRunQuery{} 63 | vars := map[string]interface{}{ 64 | "query": null.StringFrom(query), 65 | } 66 | 67 | err := s.client.gql.Mutate(ctx, &m, vars) 68 | if err != nil { 69 | return nil, fmt.Errorf("error posting bulk query: %w", err) 70 | } 71 | if len(m.BulkOperationRunQueryResult.UserErrors) > 0 { 72 | errors, _ := json.MarshalIndent(m.BulkOperationRunQueryResult.UserErrors, "", " ") 73 | return nil, fmt.Errorf("error posting bulk query: %s", errors) 74 | } 75 | 76 | return &m.BulkOperationRunQueryResult.BulkOperation.ID, nil 77 | } 78 | 79 | func (s *BulkOperationServiceOp) GetCurrentBulkQuery(ctx context.Context) (*model.BulkOperation, error) { 80 | var q struct { 81 | CurrentBulkOperation struct { 82 | model.BulkOperation 83 | } 84 | } 85 | err := s.client.gql.Query(ctx, &q, nil) 86 | if err != nil { 87 | return nil, fmt.Errorf("query: %w", err) 88 | } 89 | return &q.CurrentBulkOperation.BulkOperation, nil 90 | } 91 | 92 | func (s *BulkOperationServiceOp) GetCurrentBulkQueryResultURL(ctx context.Context) (*string, error) { 93 | return s.ShouldGetBulkQueryResultURL(ctx, nil) 94 | } 95 | 96 | func (s *BulkOperationServiceOp) ShouldGetBulkQueryResultURL(ctx context.Context, id *string) (*string, error) { 97 | q, err := s.GetCurrentBulkQuery(ctx) 98 | if err != nil { 99 | return nil, fmt.Errorf("error getting current bulk operation: %w", err) 100 | } 101 | 102 | if id != nil && q.ID != *id { 103 | return nil, fmt.Errorf("Bulk operation ID doesn't match, got=%v, want=%v", q.ID, id) 104 | } 105 | 106 | q, _ = s.WaitForCurrentBulkQuery(ctx, 1*time.Second) 107 | if q.Status != model.BulkOperationStatusCompleted { 108 | return nil, fmt.Errorf("Bulk operation didn't complete, status=%s, error_code=%s", q.Status, q.ErrorCode) 109 | } 110 | 111 | if q.ErrorCode != nil && q.ErrorCode.String() != "" { 112 | return nil, fmt.Errorf("Bulk operation error: %s", q.ErrorCode) 113 | } 114 | 115 | if q.ObjectCount == "0" { 116 | return nil, nil 117 | } 118 | 119 | if q.URL == nil { 120 | return nil, fmt.Errorf("empty URL result") 121 | } 122 | 123 | return q.URL, nil 124 | } 125 | 126 | func (s *BulkOperationServiceOp) WaitForCurrentBulkQuery(ctx context.Context, interval time.Duration) (*model.BulkOperation, error) { 127 | q, err := s.GetCurrentBulkQuery(ctx) 128 | if err != nil { 129 | return q, fmt.Errorf("CurrentBulkOperation query error: %w", err) 130 | } 131 | 132 | for q.Status == model.BulkOperationStatusCreated || q.Status == model.BulkOperationStatusRunning || q.Status == model.BulkOperationStatusCanceling { 133 | log.Debugf("Bulk operation is still %s...", q.Status) 134 | time.Sleep(interval) 135 | 136 | q, err = s.GetCurrentBulkQuery(ctx) 137 | if err != nil { 138 | return q, fmt.Errorf("CurrentBulkOperation query error: %w", err) 139 | } 140 | } 141 | log.Debugf("Bulk operation ready, latest status=%s", q.Status) 142 | 143 | return q, nil 144 | } 145 | 146 | func (s *BulkOperationServiceOp) CancelRunningBulkQuery(ctx context.Context) error { 147 | q, err := s.GetCurrentBulkQuery(ctx) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | if q.Status == model.BulkOperationStatusCreated || q.Status == model.BulkOperationStatusRunning { 153 | log.Debugln("Canceling running operation") 154 | operationID := q.ID 155 | 156 | m := mutationBulkOperationRunQueryCancel{} 157 | vars := map[string]interface{}{ 158 | "id": operationID, 159 | } 160 | 161 | err = s.client.gql.Mutate(ctx, &m, vars) 162 | if err != nil { 163 | return fmt.Errorf("mutation: %w", err) 164 | } 165 | if len(m.BulkOperationCancelResult.UserErrors) > 0 { 166 | return fmt.Errorf("%+v", m.BulkOperationCancelResult.UserErrors) 167 | } 168 | 169 | q, err = s.GetCurrentBulkQuery(ctx) 170 | if err != nil { 171 | return err 172 | } 173 | for q.Status == model.BulkOperationStatusCreated || q.Status == model.BulkOperationStatusRunning || q.Status == model.BulkOperationStatusCanceling { 174 | log.Tracef("Bulk operation still %s...", q.Status) 175 | q, err = s.GetCurrentBulkQuery(ctx) 176 | if err != nil { 177 | return fmt.Errorf("get current bulk query: %w", err) 178 | } 179 | } 180 | log.Debugln("Bulk operation cancelled") 181 | } 182 | 183 | return nil 184 | } 185 | 186 | func (s *BulkOperationServiceOp) BulkQuery(ctx context.Context, query string, out interface{}) error { 187 | _, err := s.WaitForCurrentBulkQuery(ctx, 1*time.Second) 188 | if err != nil { 189 | return err 190 | } 191 | 192 | id, err := s.PostBulkQuery(ctx, query) 193 | if err != nil { 194 | return fmt.Errorf("post bulk query: %w", err) 195 | } 196 | 197 | if id == nil { 198 | return fmt.Errorf("Posted operation ID is nil") 199 | } 200 | 201 | url, err := s.ShouldGetBulkQueryResultURL(ctx, id) 202 | if err != nil { 203 | return fmt.Errorf("get bulk query result URL: %w", err) 204 | } 205 | 206 | if url == nil || *url == "" { 207 | return fmt.Errorf("Operation result URL is empty") 208 | } 209 | 210 | filename := fmt.Sprintf("%s%s", rand.String(10), ".jsonl") 211 | resultFile := filepath.Join(os.TempDir(), filename) 212 | defer os.Remove(resultFile) // Avoid storage overflow in high traffic environments 213 | err = utils.DownloadFile(resultFile, *url) 214 | if err != nil { 215 | return fmt.Errorf("download file: %w", err) 216 | } 217 | 218 | err = parseBulkQueryResult(resultFile, out) 219 | if err != nil { 220 | return fmt.Errorf("parse bulk query result: %w", err) 221 | } 222 | 223 | return nil 224 | } 225 | 226 | func parseBulkQueryResult(resultFilePath string, out interface{}) error { 227 | if reflect.TypeOf(out).Kind() != reflect.Ptr { 228 | return fmt.Errorf("the out arg is not a pointer") 229 | } 230 | 231 | outValue := reflect.ValueOf(out) 232 | outSlice := outValue.Elem() 233 | if outSlice.Kind() != reflect.Slice { 234 | return fmt.Errorf("the out arg is not a pointer to a slice interface") 235 | } 236 | 237 | sliceItemType := outSlice.Type().Elem() // slice item type 238 | sliceItemKind := sliceItemType.Kind() 239 | itemType := sliceItemType // slice item underlying type 240 | if sliceItemKind == reflect.Ptr { 241 | itemType = itemType.Elem() 242 | } 243 | 244 | resultPath, err := os.Open(resultFilePath) 245 | if err != nil { 246 | return fmt.Errorf("open file: %w", err) 247 | } 248 | defer utils.CloseFile(resultPath) 249 | 250 | reader := bufio.NewReader(resultPath) 251 | json := jsoniter.ConfigFastest 252 | 253 | connectionSink := make(map[string]interface{}) 254 | 255 | for { 256 | var line []byte 257 | line, err = reader.ReadBytes('\n') 258 | if err != nil { 259 | break 260 | } 261 | 262 | parentIDNode := json.Get(line, "__parentId") 263 | if parentIDNode.LastError() == nil { 264 | parentID := parentIDNode.ToString() 265 | 266 | gid := json.Get(line, "id") 267 | if gid.LastError() != nil { 268 | return fmt.Errorf("The connection type must query the `id` field") 269 | } 270 | edgeType, nodeType, connectionFieldName, err := concludeObjectType(gid.ToString()) 271 | if err != nil { 272 | return err 273 | } 274 | node := reflect.New(nodeType).Interface() 275 | err = json.Unmarshal(line, &node) 276 | if err != nil { 277 | return fmt.Errorf("unmarshalling: %w", err) 278 | } 279 | nodeVal := reflect.ValueOf(node).Elem() 280 | 281 | var edge interface{} 282 | var edgeVal reflect.Value 283 | var nodeField reflect.Value 284 | if edgeType.Kind() == reflect.Ptr { 285 | edge = reflect.New(edgeType.Elem()).Interface() 286 | nodeField = reflect.ValueOf(edge).Elem().FieldByName(nodeFieldName) 287 | edgeVal = reflect.ValueOf(edge) 288 | } else { 289 | edge = reflect.New(edgeType).Interface() 290 | 291 | if reflect.ValueOf(edge).Kind() == reflect.Ptr { 292 | nodeField = reflect.ValueOf(edge).Elem().FieldByName(nodeFieldName) 293 | } else { 294 | nodeField = reflect.ValueOf(edge).FieldByName(nodeFieldName) 295 | } 296 | 297 | edgeVal = reflect.ValueOf(edge).Elem() 298 | } 299 | 300 | if !nodeField.IsValid() { 301 | return fmt.Errorf("Edge in the '%s' doesn't have the Node field", connectionFieldName) 302 | } 303 | nodeField.Set(nodeVal) 304 | 305 | var edgesSlice reflect.Value 306 | var edges map[string]interface{} 307 | if val, ok := connectionSink[parentID]; ok { 308 | var ok2 bool 309 | if edges, ok2 = val.(map[string]interface{}); !ok2 { 310 | return fmt.Errorf("The connection sink for parent ID '%s' is not a map", parentID) 311 | } 312 | } else { 313 | edges = make(map[string]interface{}) 314 | } 315 | 316 | if val, ok := edges[connectionFieldName]; ok { 317 | edgesSlice = reflect.ValueOf(val) 318 | } else { 319 | edgesSliceCap := 50 320 | edgesSlice = reflect.MakeSlice(reflect.SliceOf(edgeType), 0, edgesSliceCap) 321 | } 322 | 323 | edgesSlice = reflect.Append(edgesSlice, edgeVal) 324 | 325 | edges[connectionFieldName] = edgesSlice.Interface() 326 | connectionSink[parentID] = edges 327 | 328 | continue 329 | } 330 | 331 | item := reflect.New(itemType).Interface() 332 | err = json.Unmarshal(line, &item) 333 | if err != nil { 334 | return fmt.Errorf("unmarshalling: %w", err) 335 | } 336 | itemVal := reflect.ValueOf(item) 337 | 338 | if sliceItemKind == reflect.Ptr { 339 | outSlice.Set(reflect.Append(outSlice, itemVal)) 340 | } else { 341 | outSlice.Set(reflect.Append(outSlice, itemVal.Elem())) 342 | } 343 | } 344 | 345 | if len(connectionSink) > 0 { 346 | err := attachNestedConnections(connectionSink, outSlice) 347 | if err != nil { 348 | return fmt.Errorf("error processing nested connections: %w", err) 349 | } 350 | } 351 | 352 | // check if ReadBytes returned an error different from EOF 353 | if err != nil && !errors.Is(err, io.EOF) { 354 | return fmt.Errorf("reading the result file: %w", err) 355 | } 356 | 357 | return nil 358 | } 359 | 360 | func attachNestedConnections(connectionSink map[string]interface{}, outSlice reflect.Value) error { 361 | for i := 0; i < outSlice.Len(); i++ { 362 | parent := outSlice.Index(i) 363 | if parent.Kind() == reflect.Ptr { 364 | parent = parent.Elem() 365 | } 366 | 367 | nodeField := parent.FieldByName("Node") 368 | if nodeField != (reflect.Value{}) { 369 | if nodeField.Kind() == reflect.Ptr { 370 | parent = nodeField.Elem() 371 | } else if nodeField.Kind() == reflect.Interface { 372 | parent = nodeField.Elem().Elem() 373 | } else { 374 | parent = nodeField 375 | } 376 | } 377 | 378 | parentIDField := parent.FieldByName("ID") 379 | if parentIDField == (reflect.Value{}) { 380 | return fmt.Errorf("No ID field on the first level") 381 | } 382 | if reflect.TypeOf(parentIDField).Kind() == reflect.Ptr { 383 | parentIDField = parentIDField.Elem() 384 | } 385 | 386 | var parentID string 387 | var ok bool 388 | if parentID, ok = parentIDField.Interface().(string); !ok { 389 | return fmt.Errorf("ID field on the first level is not a string") 390 | } 391 | 392 | var connection interface{} 393 | if connection, ok = connectionSink[parentID]; !ok { 394 | continue 395 | } 396 | 397 | edgeMap := reflect.ValueOf(connection) 398 | iter := edgeMap.MapRange() 399 | for iter.Next() { 400 | connectionName := iter.Key() 401 | connectionField := parent.FieldByName(connectionName.String()) 402 | if !connectionField.IsValid() { 403 | return fmt.Errorf("Connection '%s' is not defined on the parent type %s", connectionName.String(), parent.Type().String()) 404 | } 405 | 406 | var connectionValue reflect.Value 407 | var edgesField reflect.Value 408 | if connectionField.Kind() == reflect.Ptr { 409 | connectionValue = reflect.ValueOf(reflect.New(connectionField.Type().Elem()).Interface()) 410 | edgesField = connectionValue.Elem().FieldByName(edgesFieldName) 411 | } else { 412 | connectionValue = reflect.ValueOf(reflect.New(connectionField.Type()).Interface()) 413 | edgesField = connectionValue.Elem().FieldByName(edgesFieldName) 414 | } 415 | 416 | if !edgesField.IsValid() { 417 | return fmt.Errorf("Connection %s in the '%s' doesn't have the Edges field", connectionName.String(), parent.Type().String()) 418 | } 419 | 420 | edges := reflect.ValueOf(iter.Value().Interface()) 421 | edgesField.Set(edges) 422 | 423 | connectionField.Set(connectionValue) 424 | 425 | err := attachNestedConnections(connectionSink, iter.Value().Elem()) 426 | if err != nil { 427 | return fmt.Errorf("error attacing a nested connection: %w", err) 428 | } 429 | } 430 | } 431 | 432 | return nil 433 | } 434 | 435 | func concludeObjectType(gid string) (reflect.Type, reflect.Type, string, error) { 436 | submatches := gidRegex.FindStringSubmatch(gid) 437 | if len(submatches) != 2 { 438 | return reflect.TypeOf(nil), reflect.TypeOf(nil), "", fmt.Errorf("malformed gid=`%s`", gid) 439 | } 440 | resource := submatches[1] 441 | switch resource { 442 | case "LineItem": 443 | return reflect.TypeOf(model.LineItemEdge{}), reflect.TypeOf(&model.LineItem{}), fmt.Sprintf("%ss", resource), nil 444 | case "FulfillmentOrderLineItem": 445 | return reflect.TypeOf(model.FulfillmentOrderLineItemEdge{}), reflect.TypeOf(&model.FulfillmentOrderLineItem{}), "LineItems", nil 446 | case "FulfillmentOrder": 447 | return reflect.TypeOf(model.FulfillmentOrderEdge{}), reflect.TypeOf(&model.FulfillmentOrder{}), fmt.Sprintf("%ss", resource), nil 448 | case "MediaImage": 449 | return reflect.TypeOf(model.MediaEdge{}), reflect.TypeOf(&model.MediaImage{}), "Media", nil 450 | case "Video": 451 | return reflect.TypeOf(model.MediaEdge{}), reflect.TypeOf(&model.Video{}), "Media", nil 452 | case "Model3d": 453 | return reflect.TypeOf(model.MediaEdge{}), reflect.TypeOf(&model.Model3d{}), "Media", nil 454 | case "ExternalVideo": 455 | return reflect.TypeOf(model.MediaEdge{}), reflect.TypeOf(&model.ExternalVideo{}), "Media", nil 456 | case "Metafield": 457 | return reflect.TypeOf(model.MetafieldEdge{}), reflect.TypeOf(&model.Metafield{}), fmt.Sprintf("%ss", resource), nil 458 | case "Order": 459 | return reflect.TypeOf(model.OrderEdge{}), reflect.TypeOf(&model.Order{}), fmt.Sprintf("%ss", resource), nil 460 | case "Product": 461 | return reflect.TypeOf(model.ProductEdge{}), reflect.TypeOf(&model.Product{}), fmt.Sprintf("%ss", resource), nil 462 | case "ProductVariant": 463 | return reflect.TypeOf(model.ProductVariantEdge{}), reflect.TypeOf(&model.ProductVariant{}), "Variants", nil 464 | case "ProductImage": 465 | return reflect.TypeOf(model.ImageEdge{}), reflect.TypeOf(&model.Image{}), "Images", nil 466 | case "Collection": 467 | return reflect.TypeOf(model.CollectionEdge{}), reflect.TypeOf(&model.Collection{}), "Collections", nil 468 | case "InventoryLevel": 469 | return reflect.TypeOf(model.InventoryLevelEdge{}), reflect.TypeOf(&model.InventoryLevel{}), fmt.Sprintf("%ss", resource), nil 470 | default: 471 | return reflect.TypeOf(nil), reflect.TypeOf(nil), "", fmt.Errorf("`%s` not implemented type", resource) 472 | } 473 | } 474 | --------------------------------------------------------------------------------