shopify_api-graphql-metafields 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.env.template +2 -0
- data/LICENSE.txt +21 -0
- data/README.md +239 -0
- data/Rakefile +8 -0
- data/lib/shopify_api/graphql/metafields/batch/set.rb +114 -0
- data/lib/shopify_api/graphql/metafields/batch.rb +30 -0
- data/lib/shopify_api/graphql/metafields/definition.rb +118 -0
- data/lib/shopify_api/graphql/metafields/delete.rb +38 -0
- data/lib/shopify_api/graphql/metafields/metafield.rb +29 -0
- data/lib/shopify_api/graphql/metafields/request.rb +23 -0
- data/lib/shopify_api/graphql/metafields/set.rb +39 -0
- data/lib/shopify_api/graphql/metafields/version.rb +9 -0
- data/lib/shopify_api/graphql/metafields.rb +35 -0
- metadata +174 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5e493b7cb3098ec7dd032a55e47d6ddf3a9537c7c98a4ad057f81ea236c25ed0
|
|
4
|
+
data.tar.gz: 4de07ec76fc07d3cfac3f75a32fdb938db3342d6ad122ffda1f0b0f7a53f852b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b601b92ce0407b819fcf72c9fe8921eeee55bc55a0ac45ab53f6764dfeac61e0001d715ab3e51f86e9f7beab6a2e9c7e7a9d9a6bd6ff7b00b5cea29eeb1eca6b
|
|
7
|
+
data.tar.gz: b24e57743b0d5db7b4c31231e86820b76dca0513ba96c1726bfb97640410c8ad4e9a5c4411006c3a2663b308044f9bc9a432241ba7546e6ca647ea04272241ca
|
data/.env.template
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ScreenStaring
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# ShopifyAPI::GraphQL::Metafields
|
|
2
|
+
|
|
3
|
+
User-friendly metafield wrapper around Shopify's GraphQL Admin API with built-in retry, batching, and error handling.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/ScreenStaring/shopify_api-graphql-metafields/actions)
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bundle add shopify_api-graphql-metafields
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
If bundler is not being used to manage dependencies:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
gem install shopify_api-graphql-metafields
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
`ShopifyAPI::GraphQL::Metafields` supports CRUD operations on Metafields and Metafield Definitions, and supports setting metafields in bulk.
|
|
22
|
+
|
|
23
|
+
### Metafields
|
|
24
|
+
|
|
25
|
+
#### Upsert Metafields
|
|
26
|
+
|
|
27
|
+
You can upsert a single metafield or multiple metafields:
|
|
28
|
+
|
|
29
|
+
```rb
|
|
30
|
+
require "shopify_api/graphql/metafields"
|
|
31
|
+
|
|
32
|
+
metafields = [
|
|
33
|
+
{
|
|
34
|
+
:owner_id => "gid://shopify/Product/123",
|
|
35
|
+
:namespace => "something",
|
|
36
|
+
:key => "foo",
|
|
37
|
+
# Not required if there's a metafield definition
|
|
38
|
+
:type => "number_integer",
|
|
39
|
+
:value => "123"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
:owner_id => "gid://shopify/Shop/123",
|
|
43
|
+
:namespace => "whatever",
|
|
44
|
+
:key => "bar",
|
|
45
|
+
:type => "json",
|
|
46
|
+
:value => JSON.dump(:a => 123, :b => "bee")
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
mf = ShopifyAPI::GraphQL::Metafields.new(shop, token)
|
|
51
|
+
|
|
52
|
+
begin
|
|
53
|
+
results = mf.set(metafields)
|
|
54
|
+
results.each do |result|
|
|
55
|
+
# result is a Hash with underscored Symbol keys
|
|
56
|
+
p result.slice(:id, :value)
|
|
57
|
+
end
|
|
58
|
+
rescue ShopifyAPI::GraphQL::Metafields::Error => e
|
|
59
|
+
p e
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
To override the fields returned see [Field Selection](#field-selection).
|
|
64
|
+
|
|
65
|
+
#### Delete Metafields
|
|
66
|
+
|
|
67
|
+
```rb
|
|
68
|
+
metafields = [
|
|
69
|
+
{
|
|
70
|
+
:owner_id => "gid://shopify/Product/123",
|
|
71
|
+
:namespace => "something",
|
|
72
|
+
:key => "foo"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
:owner_id => "gid://shopify/Shop/123",
|
|
76
|
+
:namespace => "whatever",
|
|
77
|
+
:key => "bar"
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
mf = ShopifyAPI::GraphQL::Metafields.new(shop, token)
|
|
82
|
+
|
|
83
|
+
begin
|
|
84
|
+
results = mf.delete(metafields)
|
|
85
|
+
results.each do |result|
|
|
86
|
+
# result is a Hash
|
|
87
|
+
p result.slice(:key, :namespace)
|
|
88
|
+
end
|
|
89
|
+
rescue ShopifyAPI::GraphQL::Metafields::Error => e
|
|
90
|
+
p e
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
To override the fields returned see [Field Selection](#field-selection).
|
|
95
|
+
|
|
96
|
+
### Field Selection
|
|
97
|
+
|
|
98
|
+
Use the `:fields` option to select what fields are returned. This accepts an `Array` of field names:
|
|
99
|
+
|
|
100
|
+
```rb
|
|
101
|
+
mf = ShopifyAPI::GraphQL::Metafields.new(shop, token)
|
|
102
|
+
mf.set(metafield, :fields => %w[compare_digest legacy_resource_id description])
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Or a String containing literal GraphQL fields:
|
|
106
|
+
|
|
107
|
+
```rb
|
|
108
|
+
mf.set(metafield, :fields => "legacyResourceId constraints { key }")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Setting Metafields in Bulk
|
|
112
|
+
|
|
113
|
+
```rb
|
|
114
|
+
require "shopify_api/graphql/metafields"
|
|
115
|
+
|
|
116
|
+
set = ShopifyAPI::GraphQL::Metafields::Batch::Set.new(shop, token)
|
|
117
|
+
set << metafield1
|
|
118
|
+
set << metafield2
|
|
119
|
+
# ...
|
|
120
|
+
set << metafield100
|
|
121
|
+
|
|
122
|
+
begin
|
|
123
|
+
result = set.flush!
|
|
124
|
+
rescue ShopifyAPI::GraphQL::Metafields::Batch::Error => e
|
|
125
|
+
p "Bulk set failed: #{e}"
|
|
126
|
+
if e.results.any?
|
|
127
|
+
p "We did have _some_ success:"
|
|
128
|
+
pp e.results
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
if e.errors.any?
|
|
132
|
+
p "We encountered some errors:"
|
|
133
|
+
pp e.errors
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
if e.user_errors.any?
|
|
137
|
+
p "We encountered some user errors:"
|
|
138
|
+
pp e.user_errors
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Note that this _does not_ use the Shopify Bulk API. If you want to use the Bulk API for metafields or other operations
|
|
144
|
+
see [`ShopifyAPI::GraphQL::Bulk`](https://github.com/ScreenStaring/shopify_api-graphql-bulk).
|
|
145
|
+
|
|
146
|
+
Once `#flush!` is called worker threads (if needed) will be spawned and metafields will be set 25 at a time, per Shopify's `Mutations.metafieldsSet` call limit.
|
|
147
|
+
You can control the number of threads via the `:worker_count` option:
|
|
148
|
+
|
|
149
|
+
```rb
|
|
150
|
+
set = ShopifyAPI::GraphQL::Metafields::Bulk.new(shop, token, :worker_count => 8)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Workers are spawned based on the batch size. If you have 50 metafields only 2 workers will be spawned. If you have 25 or less only no workers are spawned.
|
|
154
|
+
|
|
155
|
+
The default number of workers is 4.
|
|
156
|
+
|
|
157
|
+
The return value is an `Array` of results like one gets with `Metafield#set`. The indices will align with the input metafields' indices.
|
|
158
|
+
A `nil` value means that position errored.
|
|
159
|
+
|
|
160
|
+
### Deleting Metafields in Bulk
|
|
161
|
+
|
|
162
|
+
TODO!
|
|
163
|
+
|
|
164
|
+
If you need this you can use Shopify's Bulk API via [`ShopifyAPI::GraphQL::Bulk`](https://github.com/ScreenStaring/shopify_api-graphql-bulk).
|
|
165
|
+
|
|
166
|
+
### Metafield Definitions
|
|
167
|
+
|
|
168
|
+
```rb
|
|
169
|
+
require "shopify_api/graphql/metafields"
|
|
170
|
+
|
|
171
|
+
dfn = ShopifyAPI::GraphQL::Metafields::Definition.new(shop, token)
|
|
172
|
+
result = dfn.create(
|
|
173
|
+
:name => "My amaaaazing definition",
|
|
174
|
+
:namespace => "sshaw",
|
|
175
|
+
:key => "ingredients",
|
|
176
|
+
:description => "A product's ingredients",
|
|
177
|
+
:type => "multi_line_text_field",
|
|
178
|
+
:owner_type => "PRODUCT"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
id = result.dig(:created_definition, :id)
|
|
182
|
+
|
|
183
|
+
result = dfn.update(
|
|
184
|
+
:namespace => "sshaw",
|
|
185
|
+
:key => "ingredients",
|
|
186
|
+
:owner_type => "PRODUCT",
|
|
187
|
+
:description => "A products SUPER SECRET ingredients"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# More CRUD operations...
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### Field Selection
|
|
194
|
+
|
|
195
|
+
Similar to other metafield operation you can specify the returned fields:
|
|
196
|
+
|
|
197
|
+
```rb
|
|
198
|
+
id = { :namespace => "sshaw", :key => "ingredients", :owner_type => "PRODUCT" }
|
|
199
|
+
dfn.get(id, :fields => %w[id pinned_position])
|
|
200
|
+
|
|
201
|
+
# Or, specify a String:
|
|
202
|
+
dfn.get(id, :fields => "id pinnedPosition constraints { key value }")
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Why Use This Instead of Shopify's API Client?
|
|
206
|
+
|
|
207
|
+
- Easy-to-use
|
|
208
|
+
- Built-in retry
|
|
209
|
+
- Batching
|
|
210
|
+
- Lightweight
|
|
211
|
+
- You can use :snake_case hash keys
|
|
212
|
+
|
|
213
|
+
Overall, Shopify's API client is bloated trash that will give you development headaches and long-term maintenance nightmares.
|
|
214
|
+
|
|
215
|
+
We used to use it, staring way back in 2015, but eventually had to pivot away from their Ruby libraries due to developer frustration
|
|
216
|
+
and high maintenance cost (and don't get us started on the ShopifyApp gem!@#).
|
|
217
|
+
|
|
218
|
+
For more information see: Shopify/shopify-api-ruby#1181
|
|
219
|
+
|
|
220
|
+
## Development
|
|
221
|
+
|
|
222
|
+
[Shopify Development Tools](https://github.com/ScreenStaring/shopify-dev-tools/) is used to import the product fixtures and retrieve values. The `sdt` executable must be in your path.
|
|
223
|
+
|
|
224
|
+
A Shopify test store is required. `cp .env.template .env` and fill-in the values. Requests will be made to the given store. The token needs permission to create metafields and metafield definitions.
|
|
225
|
+
|
|
226
|
+
## See Also
|
|
227
|
+
|
|
228
|
+
- [Shopify Dev Tools](https://github.com/ScreenStaring/shopify-dev-tools/) - Command-line program to assist with the development and/or maintenance of Shopify apps and stores
|
|
229
|
+
- [`ShopifyAPI::GraphQL::Tiny`](https://github.com/ScreenStaring/shopify_api-graphql-tiny/) - Lightweight, no-nonsense, Shopify GraphQL Admin API client with built-in pagination and retry
|
|
230
|
+
- [`ShopifyAPI::GraphQL::Request`](https://github.com/ScreenStaring/shopify_api-graphql-request) - A higher-level wrapper around `Tiny` with improved exception handling and `:snake_case` hash key conversion
|
|
231
|
+
- [`TinyGID`](https://github.com/sshaw/tiny_gid/) - Build Global ID (`gid://`) URI strings from scalar values
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
Made by [ScreenStaring](https://screenstaring.com)
|
data/Rakefile
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shopify_api/graphql/request"
|
|
4
|
+
|
|
5
|
+
module ShopifyAPI
|
|
6
|
+
module GraphQL
|
|
7
|
+
module Metafields
|
|
8
|
+
module Batch
|
|
9
|
+
# TODO: Similar operation for delete
|
|
10
|
+
class Set < ShopifyAPI::GraphQL::Request
|
|
11
|
+
DEFAULT_UPLOAD_GROUPS = 4
|
|
12
|
+
|
|
13
|
+
def initialize(shop, token, options = nil)
|
|
14
|
+
super(shop, token, options)
|
|
15
|
+
|
|
16
|
+
@shop = shop
|
|
17
|
+
@token = token
|
|
18
|
+
|
|
19
|
+
@metafields = []
|
|
20
|
+
@options = options || {}
|
|
21
|
+
|
|
22
|
+
raise ArgumentError, "worker_count must be greater than 0" if @options[:worker_count] && @options[:worker_count].to_i < 1
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def <<(*metafields)
|
|
26
|
+
metafields.flatten!
|
|
27
|
+
|
|
28
|
+
# TODO: autoflush?
|
|
29
|
+
@metafields.concat(metafields)
|
|
30
|
+
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def flush!
|
|
35
|
+
return false unless @metafields.any?
|
|
36
|
+
|
|
37
|
+
batches = @metafields.each_slice(Metafields::Set::MAX)
|
|
38
|
+
# Round so we don't end up with a fractional (or 0) slice size
|
|
39
|
+
groups = (batches.size / worker_count.to_f).round
|
|
40
|
+
groups = 1 if groups < 1
|
|
41
|
+
|
|
42
|
+
workers = batches.each_slice(groups).with_index.map { |batch, i| spawn_worker(batch, i) }
|
|
43
|
+
workers.each(&:join)
|
|
44
|
+
|
|
45
|
+
collect_results(workers)
|
|
46
|
+
ensure
|
|
47
|
+
@metafields.clear
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def worker_count
|
|
53
|
+
@options[:worker_count] || DEFAULT_UPLOAD_GROUPS
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def collect_results(workers)
|
|
57
|
+
results = []
|
|
58
|
+
errors = []
|
|
59
|
+
user_errors = []
|
|
60
|
+
|
|
61
|
+
# Name contains its position within the batch, zero-padded so sorting by name
|
|
62
|
+
# is the same as sorting by position.
|
|
63
|
+
# The goal is for the success results' indices to align with input values'
|
|
64
|
+
workers.sort_by(&:name).each do |worker|
|
|
65
|
+
worker_results, worker_errors = worker.value
|
|
66
|
+
|
|
67
|
+
results.concat(worker_results)
|
|
68
|
+
errors.concat(worker_errors)
|
|
69
|
+
|
|
70
|
+
worker_errors.each do |error|
|
|
71
|
+
user_errors.concat(error.errors) if error.is_a?(ShopifyAPI::GraphQL::Request::UserError)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if errors.any?
|
|
76
|
+
message = errors.map(&:message).join(", ")
|
|
77
|
+
raise Error.new("Failed to set all metafields: #{errors.size} error(s): #{message}", results, errors, user_errors)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
results
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def spawn_worker(batches, id)
|
|
84
|
+
Thread.new do
|
|
85
|
+
Thread.current.name = sprintf("%s-batch-%06d", self.class, id)
|
|
86
|
+
|
|
87
|
+
errors = []
|
|
88
|
+
# We treat the result like the metafieldSet mutation where each position holds the input position's result.
|
|
89
|
+
# If there was no result the position is nil. This will be put back together across all threads and returned to
|
|
90
|
+
# the caller as if it was a single metafieldSet call
|
|
91
|
+
results = Array.new(batches.sum(&:size))
|
|
92
|
+
|
|
93
|
+
set = Metafields::Set.new(@shop, @token, @options)
|
|
94
|
+
|
|
95
|
+
offset = 0
|
|
96
|
+
|
|
97
|
+
batches.each do |batch|
|
|
98
|
+
begin
|
|
99
|
+
results[offset, batch.size] = set.execute(batch).dig(:data, :metafields_set, :metafields)
|
|
100
|
+
rescue => e
|
|
101
|
+
errors << e
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
offset += batch.size
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
[results, errors]
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "batch/set"
|
|
4
|
+
|
|
5
|
+
module ShopifyAPI
|
|
6
|
+
module GraphQL
|
|
7
|
+
module Metafields
|
|
8
|
+
module Batch
|
|
9
|
+
class Error < ShopifyAPI::GraphQL::Metafields::Error
|
|
10
|
+
# Array containing results of any successfully executed set calls
|
|
11
|
+
attr_reader :results
|
|
12
|
+
|
|
13
|
+
# Array containing errors, if any
|
|
14
|
+
attr_reader :errors
|
|
15
|
+
|
|
16
|
+
# Array containing user errors, if any
|
|
17
|
+
attr_reader :user_errors
|
|
18
|
+
|
|
19
|
+
def initialize(message, results = nil, errors = nil, user_errors = nil)
|
|
20
|
+
super message
|
|
21
|
+
|
|
22
|
+
@results = results || []
|
|
23
|
+
@errors = errors || []
|
|
24
|
+
@user_errors = user_errors || []
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShopifyAPI
|
|
4
|
+
module GraphQL
|
|
5
|
+
module Metafields
|
|
6
|
+
class Definition < Request
|
|
7
|
+
GET = <<-GQL
|
|
8
|
+
query MetafieldsCount($id: ID $identifier: MetafieldDefinitionIdentifierInput) {
|
|
9
|
+
metafieldDefinition(id: $id identifier: $identifier) {
|
|
10
|
+
%s
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
GQL
|
|
14
|
+
|
|
15
|
+
DEFAULT_GET_FIELDS = %w[id namespace key owner_type name description type { name }].freeze
|
|
16
|
+
|
|
17
|
+
CREATE =<<-GQL
|
|
18
|
+
mutation CreateMetafieldDefinition($definition: MetafieldDefinitionInput!) {
|
|
19
|
+
metafieldDefinitionCreate(definition: $definition) {
|
|
20
|
+
createdDefinition { %s }
|
|
21
|
+
#{Metafields::USER_ERRORS}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
GQL
|
|
25
|
+
|
|
26
|
+
DEFAULT_CREATE_FIELDS = DEFAULT_GET_FIELDS.dup
|
|
27
|
+
|
|
28
|
+
UPDATE =<<-GQL
|
|
29
|
+
mutation UpdateMetafieldDefinition($definition: MetafieldDefinitionUpdateInput!) {
|
|
30
|
+
metafieldDefinitionUpdate(definition: $definition) {
|
|
31
|
+
updatedDefinition { %s }
|
|
32
|
+
#{Metafields::USER_ERRORS}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
GQL
|
|
36
|
+
|
|
37
|
+
DEFAULT_UPDATE_FIELDS = DEFAULT_CREATE_FIELDS.dup.freeze
|
|
38
|
+
|
|
39
|
+
DELETE =<<-GQL
|
|
40
|
+
mutation DeleteMetafieldDefinition($id: ID $identifier: MetafieldDefinitionIdentifierInput $deleteMetafields: Boolean) {
|
|
41
|
+
metafieldDefinitionDelete(
|
|
42
|
+
id: $id
|
|
43
|
+
identifier: $identifier
|
|
44
|
+
deleteAllAssociatedMetafields: $deleteMetafields
|
|
45
|
+
) {
|
|
46
|
+
%s
|
|
47
|
+
#{Metafields::USER_ERRORS}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
GQL
|
|
51
|
+
|
|
52
|
+
DEFAULT_DELETE_FIELDS =<<-GQL
|
|
53
|
+
deletedDefinitionId
|
|
54
|
+
deletedDefinition {
|
|
55
|
+
key
|
|
56
|
+
namespace
|
|
57
|
+
ownerType
|
|
58
|
+
}
|
|
59
|
+
GQL
|
|
60
|
+
|
|
61
|
+
def create(definition, options = nil)
|
|
62
|
+
fields = options&.dig(:fields) || DEFAULT_CREATE_FIELDS
|
|
63
|
+
|
|
64
|
+
result = execute(
|
|
65
|
+
sprintf(CREATE, field_string(fields)),
|
|
66
|
+
:definition => definition
|
|
67
|
+
).dig(:data, :metafield_definition_create)
|
|
68
|
+
|
|
69
|
+
# We can use #except after 2.7
|
|
70
|
+
result.delete(:user_errors)
|
|
71
|
+
result
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def delete(id_or_identifier, options = nil)
|
|
75
|
+
fields = options&.dig(:fields) || DEFAULT_DELETE_FIELDS
|
|
76
|
+
|
|
77
|
+
result = execute(
|
|
78
|
+
sprintf(DELETE, field_string(fields)),
|
|
79
|
+
id_key(id_or_identifier) => id_or_identifier,
|
|
80
|
+
:delete_metafields => options && options[:delete_metafields] == true
|
|
81
|
+
).dig(:data, :metafield_definition_delete)
|
|
82
|
+
|
|
83
|
+
# We can use #except after 2.7
|
|
84
|
+
result.delete(:user_errors)
|
|
85
|
+
result
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def get(id_or_identifier, options = nil)
|
|
89
|
+
fields = options&.dig(:fields) || DEFAULT_GET_FIELDS
|
|
90
|
+
|
|
91
|
+
execute(
|
|
92
|
+
sprintf(GET, field_string(fields)),
|
|
93
|
+
id_key(id_or_identifier) => id_or_identifier,
|
|
94
|
+
).dig(:data, :metafield_definition)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def update(definition, options = nil)
|
|
98
|
+
fields = options&.dig(:fields) || DEFAULT_UPDATE_FIELDS
|
|
99
|
+
|
|
100
|
+
result = execute(
|
|
101
|
+
sprintf(UPDATE, field_string(fields)),
|
|
102
|
+
:definition => definition
|
|
103
|
+
).dig(:data, :metafield_definition_update)
|
|
104
|
+
|
|
105
|
+
# We can use #except after 2.7
|
|
106
|
+
result.delete(:user_errors)
|
|
107
|
+
result
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def id_key(id_or_identifier)
|
|
113
|
+
id_or_identifier.is_a?(Hash) ? :identifier : :id
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "request"
|
|
4
|
+
|
|
5
|
+
module ShopifyAPI
|
|
6
|
+
module GraphQL
|
|
7
|
+
module Metafields
|
|
8
|
+
class Delete < Request # :nodoc: all
|
|
9
|
+
MAX = 250
|
|
10
|
+
|
|
11
|
+
MUTATION =<<-GQL
|
|
12
|
+
mutation metafieldsDelete($metafields: [MetafieldIdentifierInput!]!) {
|
|
13
|
+
metafieldsDelete(metafields: $metafields) {
|
|
14
|
+
deletedMetafields { %s }
|
|
15
|
+
userErrors { field message }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
GQL
|
|
19
|
+
|
|
20
|
+
DEFAULT_FIELDS = %w[key namespace owner_id].freeze
|
|
21
|
+
|
|
22
|
+
def execute(metafields, options = nil)
|
|
23
|
+
metafields = wrap_array(metafields)
|
|
24
|
+
raise ArgumentError, "metafields required" if metafields.size == 0
|
|
25
|
+
raise ArgumentError, "#{metafields.size} metafields provided: Shopify allows a maximum of #{MAX}" if metafields.size > MAX
|
|
26
|
+
|
|
27
|
+
options = (options || {}).dup
|
|
28
|
+
fields = options.delete(:fields) || DEFAULT_FIELDS
|
|
29
|
+
super(
|
|
30
|
+
sprintf(MUTATION, field_string(fields)),
|
|
31
|
+
{ :metafields => metafields },
|
|
32
|
+
options
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "delete"
|
|
4
|
+
require_relative "set"
|
|
5
|
+
|
|
6
|
+
module ShopifyAPI
|
|
7
|
+
module GraphQL
|
|
8
|
+
module Metafields
|
|
9
|
+
class Metafield # :nodoc: all
|
|
10
|
+
def initialize(shop, token, options = nil)
|
|
11
|
+
@delete = Metafields::Delete.new(shop, token, options)
|
|
12
|
+
@set = Metafields::Set.new(shop, token, options)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def set(metafields, options = nil)
|
|
16
|
+
@set.execute(metafields, options).dig(:data, :metafields_set, :metafields)
|
|
17
|
+
rescue *WRAPPED_ERRORS => e
|
|
18
|
+
raise Error, "failed to set metafields: #{e}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def delete(metafields, options = nil)
|
|
22
|
+
@delete.execute(metafields, options).dig(:data, :metafields_delete, :deleted_metafields)
|
|
23
|
+
rescue *WRAPPED_ERRORS => e
|
|
24
|
+
raise Error, "failed to delete metafields: #{e}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shopify_api/graphql/request"
|
|
4
|
+
|
|
5
|
+
module ShopifyAPI
|
|
6
|
+
module GraphQL
|
|
7
|
+
module Metafields
|
|
8
|
+
class Request < ShopifyAPI::GraphQL::Request
|
|
9
|
+
protected
|
|
10
|
+
|
|
11
|
+
def wrap_array(object)
|
|
12
|
+
(object.is_a?(Array) ? object : [object]).compact
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def field_string(fields)
|
|
16
|
+
return fields unless fields.respond_to?(:map)
|
|
17
|
+
# TODO: check for breakage across the set of accepted fields
|
|
18
|
+
fields.map { |f| Strings::Case.camelcase(f.to_s) }.join("\n")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "request"
|
|
4
|
+
|
|
5
|
+
module ShopifyAPI
|
|
6
|
+
module GraphQL
|
|
7
|
+
module Metafields
|
|
8
|
+
class Set < Request # :nodoc: all
|
|
9
|
+
# Shopify limit per call
|
|
10
|
+
MAX = 25
|
|
11
|
+
|
|
12
|
+
MUTATION =<<-GQL
|
|
13
|
+
mutation metafieldsSet($metafields: [MetafieldsSetInput!]!) {
|
|
14
|
+
metafieldsSet(metafields: $metafields) {
|
|
15
|
+
metafields { %s }
|
|
16
|
+
#{Metafields::USER_ERRORS}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
GQL
|
|
20
|
+
|
|
21
|
+
DEFAULT_FIELDS = %w[id namespace key value].freeze
|
|
22
|
+
|
|
23
|
+
def execute(metafields, options = nil)
|
|
24
|
+
metafields = wrap_array(metafields)
|
|
25
|
+
raise ArgumentError, "metafields required" if metafields.size == 0
|
|
26
|
+
raise ArgumentError, "#{metafields.size} metafields provided: Shopify allows a maximum of #{MAX}. Use the Batch::Set class instead" if metafields.size > MAX
|
|
27
|
+
|
|
28
|
+
options = (options || {}).dup
|
|
29
|
+
fields = options.delete(:fields) || DEFAULT_FIELDS
|
|
30
|
+
super(
|
|
31
|
+
sprintf(MUTATION, field_string(fields)),
|
|
32
|
+
{ :metafields => metafields },
|
|
33
|
+
options
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shopify_api/graphql/tiny"
|
|
4
|
+
require "shopify_api/graphql/request"
|
|
5
|
+
|
|
6
|
+
require_relative "metafields/version"
|
|
7
|
+
|
|
8
|
+
module ShopifyAPI
|
|
9
|
+
module GraphQL
|
|
10
|
+
module Metafields
|
|
11
|
+
Error = Class.new(StandardError)
|
|
12
|
+
|
|
13
|
+
WRAPPED_ERRORS = [
|
|
14
|
+
ShopifyAPI::GraphQL::Tiny::Error,
|
|
15
|
+
ShopifyAPI::GraphQL::Request::Error
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
USER_ERRORS =<<-GQL
|
|
19
|
+
userErrors {
|
|
20
|
+
field
|
|
21
|
+
message
|
|
22
|
+
code
|
|
23
|
+
}
|
|
24
|
+
GQL
|
|
25
|
+
|
|
26
|
+
def self.new(shop, token, options = nil)
|
|
27
|
+
Metafield.new(shop, token, options)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
require_relative "metafields/metafield"
|
|
34
|
+
require_relative "metafields/batch"
|
|
35
|
+
require_relative "metafields/definition"
|
metadata
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: shopify_api-graphql-metafields
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Skye Shaw
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-16 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: shopify_api-graphql-request
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: shopify_api-graphql-tiny
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: strings-case
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: csv
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: dotenv
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rake
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rspec
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: tiny_gid
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - ">="
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0'
|
|
125
|
+
description:
|
|
126
|
+
email:
|
|
127
|
+
- skye.shaw@gmail.com
|
|
128
|
+
executables: []
|
|
129
|
+
extensions: []
|
|
130
|
+
extra_rdoc_files: []
|
|
131
|
+
files:
|
|
132
|
+
- ".env.template"
|
|
133
|
+
- LICENSE.txt
|
|
134
|
+
- README.md
|
|
135
|
+
- Rakefile
|
|
136
|
+
- lib/shopify_api/graphql/metafields.rb
|
|
137
|
+
- lib/shopify_api/graphql/metafields/batch.rb
|
|
138
|
+
- lib/shopify_api/graphql/metafields/batch/set.rb
|
|
139
|
+
- lib/shopify_api/graphql/metafields/definition.rb
|
|
140
|
+
- lib/shopify_api/graphql/metafields/delete.rb
|
|
141
|
+
- lib/shopify_api/graphql/metafields/metafield.rb
|
|
142
|
+
- lib/shopify_api/graphql/metafields/request.rb
|
|
143
|
+
- lib/shopify_api/graphql/metafields/set.rb
|
|
144
|
+
- lib/shopify_api/graphql/metafields/version.rb
|
|
145
|
+
homepage: https://github.com/ScreenStaring/shopify_api-graphql-metafields
|
|
146
|
+
licenses:
|
|
147
|
+
- MIT
|
|
148
|
+
metadata:
|
|
149
|
+
bug_tracker_uri: https://github.com/ScreenStaring/shopify_api-graphql-metafields/issues
|
|
150
|
+
changelog_uri: https://github.com/ScreenStaring/shopify_api-graphql-metafields/blob/master/Changes
|
|
151
|
+
documentation_uri: https://rubydoc.info/gems/shopify_api-graphql-metafields
|
|
152
|
+
source_code_uri: https://github.com/ScreenStaring/shopify_api-graphql-metafields
|
|
153
|
+
homepage_uri: https://github.com/ScreenStaring/shopify_api-graphql-metafields
|
|
154
|
+
post_install_message:
|
|
155
|
+
rdoc_options: []
|
|
156
|
+
require_paths:
|
|
157
|
+
- lib
|
|
158
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
159
|
+
requirements:
|
|
160
|
+
- - ">="
|
|
161
|
+
- !ruby/object:Gem::Version
|
|
162
|
+
version: 2.5.0
|
|
163
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
164
|
+
requirements:
|
|
165
|
+
- - ">="
|
|
166
|
+
- !ruby/object:Gem::Version
|
|
167
|
+
version: '0'
|
|
168
|
+
requirements: []
|
|
169
|
+
rubygems_version: 3.5.16
|
|
170
|
+
signing_key:
|
|
171
|
+
specification_version: 4
|
|
172
|
+
summary: User-friendly metafield wrapper around Shopify's GraphQL Admin API with built-in
|
|
173
|
+
retry, batching, and error handling
|
|
174
|
+
test_files: []
|