shopify_toolkit 0.3.5 → 0.4.0
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 +4 -4
- data/README.md +31 -18
- data/exe/shopify-toolkit +0 -67
- data/lib/shopify_toolkit/admin_client.rb +2 -0
- data/lib/shopify_toolkit/command_line.rb +89 -0
- data/lib/shopify_toolkit/metafield_statements.rb +0 -2
- data/lib/shopify_toolkit/metaobject_statements.rb +117 -0
- data/lib/shopify_toolkit/migration.rb +63 -0
- data/lib/shopify_toolkit/migrator.rb +152 -0
- data/lib/shopify_toolkit/schema.rb +1 -0
- data/lib/shopify_toolkit/version.rb +1 -1
- data/lib/shopify_toolkit.rb +0 -1
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a1d6624a536a67ce6e7861ae95c0eed57e2713974b4901b9eca1720ecbd063ad
|
4
|
+
data.tar.gz: ee1bffe4a027352bdf58a39d9aa4412c5c0151da4784b56006c62b3b8b33dd30
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a3b3b9d85c5cd37f51531f433bd039f6830bdab42c2bc4a4b52a8b6b4cc7f6ec22cd2a52bc2faec59c5777077b6f615651987afffc8c468aa6b34563cd46c9be
|
7
|
+
data.tar.gz: 58b3102b6915f4344e532d2d329b3cabce6028412ccf5676eafc174a0479222320f1bd63af972a8613442c9f9c4c72a0db05e3b407040b4a624e369926da1e1c
|
data/README.md
CHANGED
@@ -11,9 +11,9 @@ A toolkit for working with Custom Shopify Apps built on Rails.
|
|
11
11
|
## Features/Roadmap
|
12
12
|
|
13
13
|
- [x] Shopify/Matrixify CSV tools
|
14
|
-
- [
|
14
|
+
- [x] Metafield/Metaobject migrations (just like ActiveRecord migrations, but for Shopify!)
|
15
15
|
- [x] Metafield Definitions management API
|
16
|
-
- [
|
16
|
+
- [x] Metaobject Definitions management API
|
17
17
|
- [ ] GraphQL Admin API code generation (syntax checking, etc)
|
18
18
|
- [ ] GraphQL Admin API client with built-in rate limiting
|
19
19
|
- [ ] GraphQL Admin API client with built-in caching
|
@@ -30,38 +30,51 @@ bundle add shopify_toolkit
|
|
30
30
|
|
31
31
|
## Usage
|
32
32
|
|
33
|
-
### Migrating Metafields
|
33
|
+
### Migrating Metafields and Metaobjects
|
34
34
|
|
35
35
|
Within a Rails application created with ShopifyApp, generate a new migration file:
|
36
36
|
|
37
37
|
```bash
|
38
|
-
|
38
|
+
touch config/shopify/migrate/20250528130134_add_product_press_releases.rb
|
39
39
|
```
|
40
40
|
|
41
|
-
|
42
|
-
in order to use the metafield statements:
|
43
|
-
|
41
|
+
Then, add the following code to the migration file:
|
44
42
|
```ruby
|
45
|
-
|
46
|
-
|
47
|
-
|
43
|
+
# config/shopify/migrate/20250528130134_add_product_press_releases.rb
|
44
|
+
class AddProductPressReleases < ShopifyToolkit::Migration
|
48
45
|
def up
|
49
|
-
|
50
|
-
|
51
|
-
|
46
|
+
create_metaobject_definition :press_release,
|
47
|
+
name: "Press Release",
|
48
|
+
displayNameKey: "name",
|
49
|
+
access: { storefront: "PUBLIC_READ" },
|
50
|
+
capabilities: {
|
51
|
+
onlineStore: { enabled: false },
|
52
|
+
publishable: { enabled: true },
|
53
|
+
translatable: { enabled: true },
|
54
|
+
renderable: { enabled: false },
|
55
|
+
},
|
56
|
+
fieldDefinitions: [
|
57
|
+
{ key: "name", name: "Title", required: true, type: "single_line_text_field" },
|
58
|
+
{ key: "body", name: "Body", required: true, type: "multi_line_text_field" },
|
59
|
+
]
|
60
|
+
|
61
|
+
metaobject_definition_id = get_metaobject_definition_gid :press_release
|
62
|
+
|
63
|
+
create_metafield :products, :press_release, :metaobject_reference, name: "Press Release", validations: [
|
64
|
+
{ name: "metaobject_definition_id", value: metaobject_definition_id }
|
65
|
+
]
|
52
66
|
end
|
53
67
|
|
54
68
|
def down
|
55
|
-
|
56
|
-
remove_metafield :products, :my_metafield
|
57
|
-
end
|
69
|
+
# Noop. We don't want to remove the metaobject definition, since it might be populated with data.
|
58
70
|
end
|
59
71
|
end
|
60
72
|
```
|
61
|
-
|
73
|
+
|
74
|
+
Then run the migrations:
|
62
75
|
|
63
76
|
```bash
|
64
|
-
|
77
|
+
bundle exec shopify-toolkit migrate
|
65
78
|
```
|
66
79
|
|
67
80
|
### Creating a Metafield Schema Definition
|
data/exe/shopify-toolkit
CHANGED
@@ -1,72 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require 'csv'
|
4
|
-
require 'active_record'
|
5
|
-
require 'thor'
|
6
|
-
require 'tmpdir'
|
7
3
|
require 'shopify_toolkit'
|
8
4
|
|
9
|
-
class ShopifyToolkit::CommandLine < Thor
|
10
|
-
RESERVED_COLUMN_NAMES = %w[select type id]
|
11
|
-
|
12
|
-
class Result < ActiveRecord::Base
|
13
|
-
def self.comments
|
14
|
-
distinct.pluck(:import_comment)
|
15
|
-
end
|
16
|
-
|
17
|
-
def self.with_comment(text)
|
18
|
-
where("import_comment LIKE ?", "%#{text}%")
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
desc "analyze CSV_PATH", "Analyze results file at path CSV_PATH"
|
23
|
-
method_option :force_import, type: :boolean, default: false
|
24
|
-
method_option :tmp_dir, type: :string, default: Dir.tmpdir
|
25
|
-
def analyze(csv_path)
|
26
|
-
csv_path = File.expand_path(csv_path)
|
27
|
-
underscore = ->(string) { string.downcase.gsub(/[^a-z0-9]+/, "_").gsub(/(^_+|_+$)/, "") }
|
28
|
-
csv = CSV.open(csv_path, liberal_parsing:true )
|
29
|
-
header_to_column = -> { RESERVED_COLUMN_NAMES.include?(_1.to_s) ? "#{_1}_1" : _1 }
|
30
|
-
headers = csv.shift.map(&underscore).map(&header_to_column)
|
31
|
-
basename = File.basename csv_path
|
32
|
-
database = "#{options[:tmp_dir]}/shopify-toolkit-analyze-#{underscore[basename]}.sqlite3"
|
33
|
-
should_import = options[:force_import] || !File.exist?(database)
|
34
|
-
to_record = ->(row) { headers.zip(row.each{ |c| c.delete!("\u0000") if String === c }).to_h.transform_keys(&header_to_column) }
|
35
|
-
|
36
|
-
File.delete(database) if should_import && File.exist?(database)
|
37
|
-
|
38
|
-
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database:)
|
39
|
-
|
40
|
-
if should_import
|
41
|
-
puts "==> Importing #{csv_path} into #{database}"
|
42
|
-
ActiveRecord::Schema.define do
|
43
|
-
create_table :results, force: true do |t|
|
44
|
-
t.json :data
|
45
|
-
headers.each { |header| t.string header }
|
46
|
-
end
|
47
|
-
add_index :results, :import_result if headers.include?('import_result')
|
48
|
-
end
|
49
|
-
csv.each_slice(5000) { |rows| print "."; Result.insert_all(rows.map(&to_record)) }
|
50
|
-
puts
|
51
|
-
end
|
52
|
-
|
53
|
-
puts "==> Starting console for #{basename}"
|
54
|
-
require "irb"
|
55
|
-
IRB.conf[:IRB_NAME] = basename
|
56
|
-
Result.class_eval { binding.irb(show_code: false) }
|
57
|
-
end
|
58
|
-
|
59
|
-
desc "schema_load", 'Load schema from "config/shopify/schema.rb"'
|
60
|
-
def schema_load
|
61
|
-
require "./config/environment"
|
62
|
-
::Shop.sole.with_shopify_session { ShopifyToolkit::Schema.load! }
|
63
|
-
end
|
64
|
-
|
65
|
-
desc "schema_dump", 'Dump schema to "config/shopify/schema.rb"'
|
66
|
-
def schema_dump
|
67
|
-
require "./config/environment"
|
68
|
-
::Shop.sole.with_shopify_session { ShopifyToolkit::Schema.dump! }
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
5
|
ShopifyToolkit::CommandLine.start(ARGV)
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require 'active_record'
|
3
|
+
require 'thor'
|
4
|
+
require 'tmpdir'
|
5
|
+
|
6
|
+
class ShopifyToolkit::CommandLine < Thor
|
7
|
+
RESERVED_COLUMN_NAMES = %w[select type id]
|
8
|
+
|
9
|
+
class Result < ActiveRecord::Base
|
10
|
+
def self.comments
|
11
|
+
distinct.pluck(:import_comment)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.with_comment(text)
|
15
|
+
where("import_comment LIKE ?", "%#{text}%")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "analyze CSV_PATH", "Analyze results file at path CSV_PATH"
|
20
|
+
method_option :force_import, type: :boolean, default: false
|
21
|
+
method_option :tmp_dir, type: :string, default: Dir.tmpdir
|
22
|
+
def analyze(csv_path)
|
23
|
+
csv_path = File.expand_path(csv_path)
|
24
|
+
underscore = ->(string) { string.downcase.gsub(/[^a-z0-9]+/, "_").gsub(/(^_+|_+$)/, "") }
|
25
|
+
csv = CSV.open(csv_path, liberal_parsing:true )
|
26
|
+
header_to_column = -> { RESERVED_COLUMN_NAMES.include?(_1.to_s) ? "#{_1}_1" : _1 }
|
27
|
+
headers = csv.shift.map(&underscore).map(&header_to_column)
|
28
|
+
basename = File.basename csv_path
|
29
|
+
database = "#{options[:tmp_dir]}/shopify-toolkit-analyze-#{underscore[basename]}.sqlite3"
|
30
|
+
should_import = options[:force_import] || !File.exist?(database)
|
31
|
+
to_record = ->(row) { headers.zip(row.each{ |c| c.delete!("\u0000") if String === c }).to_h.transform_keys(&header_to_column) }
|
32
|
+
|
33
|
+
File.delete(database) if should_import && File.exist?(database)
|
34
|
+
|
35
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database:)
|
36
|
+
|
37
|
+
if should_import
|
38
|
+
puts "==> Importing #{csv_path} into #{database}"
|
39
|
+
ActiveRecord::Schema.define do
|
40
|
+
create_table :results, force: true do |t|
|
41
|
+
t.json :data
|
42
|
+
headers.each { |header| t.string header }
|
43
|
+
end
|
44
|
+
add_index :results, :import_result if headers.include?('import_result')
|
45
|
+
end
|
46
|
+
csv.each_slice(5000) { |rows| print "."; Result.insert_all(rows.map(&to_record)) }
|
47
|
+
puts
|
48
|
+
end
|
49
|
+
|
50
|
+
puts "==> Starting console for #{basename}"
|
51
|
+
require "irb"
|
52
|
+
IRB.conf[:IRB_NAME] = basename
|
53
|
+
Result.class_eval { binding.irb(show_code: false) }
|
54
|
+
end
|
55
|
+
|
56
|
+
desc "migrate", "Run migrations"
|
57
|
+
def migrate
|
58
|
+
require "./config/environment"
|
59
|
+
::Shop.sole.with_shopify_session { ShopifyToolkit::Migrator.new.up }
|
60
|
+
end
|
61
|
+
|
62
|
+
desc "down", "Run migrations down"
|
63
|
+
def down
|
64
|
+
require "./config/environment"
|
65
|
+
::Shop.sole.with_shopify_session { ShopifyToolkit::Migrator.new.down }
|
66
|
+
end
|
67
|
+
|
68
|
+
desc "redo", "Run migrations down and up again"
|
69
|
+
def redo
|
70
|
+
require "./config/environment"
|
71
|
+
::Shop.sole.with_shopify_session { ShopifyToolkit::Migrator.new.redo }
|
72
|
+
end
|
73
|
+
|
74
|
+
desc "schema_load", 'Load schema from "config/shopify/schema.rb"'
|
75
|
+
def schema_load
|
76
|
+
require "./config/environment"
|
77
|
+
::Shop.sole.with_shopify_session { ShopifyToolkit::Schema.load! }
|
78
|
+
end
|
79
|
+
|
80
|
+
desc "schema_dump", 'Dump schema to "config/shopify/schema.rb"'
|
81
|
+
def schema_dump
|
82
|
+
require "./config/environment"
|
83
|
+
::Shop.sole.with_shopify_session { ShopifyToolkit::Schema.dump! }
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.exit_on_failure?
|
87
|
+
true
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
module ShopifyToolkit::MetaobjectStatements
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
include ShopifyToolkit::Migration::Logging
|
8
|
+
include ShopifyToolkit::AdminClient
|
9
|
+
|
10
|
+
def self.log_time(method_name)
|
11
|
+
current_method = instance_method(method_name)
|
12
|
+
define_method(method_name) do |*args, **kwargs, &block|
|
13
|
+
say_with_time("#{method_name}(#{args.map(&:inspect).join(', ')})") { current_method.bind(self).call(*args, **kwargs, &block) }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# create_metafield :products, :my_metafield, :single_line_text_field, name: "Prova"
|
18
|
+
# @param namespace: if nil the metafield will be app-specific (default: :custom)
|
19
|
+
log_time \
|
20
|
+
def create_metaobject_definition(type, **options)
|
21
|
+
# Skip creation if metafield already exists
|
22
|
+
existing_gid = get_metaobject_definition_gid(type)
|
23
|
+
if existing_gid
|
24
|
+
say "Metaobject #{type} already exists, skipping creation"
|
25
|
+
return existing_gid
|
26
|
+
end
|
27
|
+
|
28
|
+
# https://shopify.dev/docs/api/admin-graphql/2024-10/mutations/metafieldDefinitionCreate
|
29
|
+
query =
|
30
|
+
"# GraphQL
|
31
|
+
mutation CreateMetaobjectDefinition($definition: MetaobjectDefinitionCreateInput!) {
|
32
|
+
metaobjectDefinitionCreate(definition: $definition) {
|
33
|
+
metaobjectDefinition {
|
34
|
+
id
|
35
|
+
name
|
36
|
+
type
|
37
|
+
fieldDefinitions {
|
38
|
+
name
|
39
|
+
key
|
40
|
+
}
|
41
|
+
}
|
42
|
+
userErrors {
|
43
|
+
field
|
44
|
+
message
|
45
|
+
code
|
46
|
+
}
|
47
|
+
}
|
48
|
+
}
|
49
|
+
"
|
50
|
+
variables = { definition: { type:, **options } }
|
51
|
+
|
52
|
+
shopify_admin_client
|
53
|
+
.query(query:, variables:)
|
54
|
+
.tap { handle_shopify_admin_client_errors(_1, "data.metaobjectDefinitionCreate.userErrors") }
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_metaobject_definition_gid(type)
|
58
|
+
result =
|
59
|
+
shopify_admin_client
|
60
|
+
.query(
|
61
|
+
query:
|
62
|
+
"# GraphQL
|
63
|
+
query GetMetaobjectDefinitionID($type: String!) {
|
64
|
+
metaobjectDefinitionByType(type: $type) {
|
65
|
+
id
|
66
|
+
}
|
67
|
+
}",
|
68
|
+
variables: { type: type.to_s },
|
69
|
+
)
|
70
|
+
.tap { handle_shopify_admin_client_errors(_1) }
|
71
|
+
.body
|
72
|
+
|
73
|
+
result.dig("data", "metaobjectDefinitionByType", "id")
|
74
|
+
end
|
75
|
+
|
76
|
+
def update_metaobject_definition(type, **options)
|
77
|
+
existing_gid = get_metaobject_definition_gid(type)
|
78
|
+
|
79
|
+
raise "Metaobject #{type} does not exist" unless existing_gid
|
80
|
+
|
81
|
+
# https://shopify.dev/docs/api/admin-graphql/2024-10/mutations/metaobjectDefinitionUpdate
|
82
|
+
query =
|
83
|
+
"# GraphQL
|
84
|
+
mutation UpdateMetaobjectDefinition($id: ID!, $definition: MetaobjectDefinitionUpdateInput!) {
|
85
|
+
metaobjectDefinitionUpdate(id: $id, definition: $definition) {
|
86
|
+
metaobjectDefinition {
|
87
|
+
id
|
88
|
+
name
|
89
|
+
type
|
90
|
+
fieldDefinitions {
|
91
|
+
name
|
92
|
+
key
|
93
|
+
}
|
94
|
+
}
|
95
|
+
userErrors {
|
96
|
+
field
|
97
|
+
message
|
98
|
+
code
|
99
|
+
}
|
100
|
+
}
|
101
|
+
}
|
102
|
+
"
|
103
|
+
variables = { id: existing_gid, definition: { **options } }
|
104
|
+
|
105
|
+
shopify_admin_client
|
106
|
+
.query(query:, variables:)
|
107
|
+
.tap { handle_shopify_admin_client_errors(_1, "data.metaobjectDefinitionUpdate.userErrors") }
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.define(&block)
|
111
|
+
context = Object.new
|
112
|
+
context.extend(self)
|
113
|
+
|
114
|
+
context.instance_eval(&block) if block_given?(&block)
|
115
|
+
context
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/benchmark"
|
4
|
+
|
5
|
+
class ShopifyToolkit::Migration
|
6
|
+
include ShopifyToolkit::AdminClient
|
7
|
+
include ShopifyToolkit::MetafieldStatements
|
8
|
+
include ShopifyToolkit::MetaobjectStatements
|
9
|
+
include ShopifyToolkit::Migration::Logging
|
10
|
+
|
11
|
+
class IrreversibleMigration < StandardError
|
12
|
+
end
|
13
|
+
|
14
|
+
def logger
|
15
|
+
@logger ||= ActiveSupport::Logger.new(STDOUT).tap do |logger|
|
16
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
17
|
+
"#{severity}: #{msg}\n"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.[](api_version)
|
23
|
+
klass = Class.new(self)
|
24
|
+
klass.const_set(:API_VERSION, api_version)
|
25
|
+
klass
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :name, :version
|
29
|
+
|
30
|
+
def initialize(name, version)
|
31
|
+
@name = name
|
32
|
+
@version = version
|
33
|
+
end
|
34
|
+
|
35
|
+
def announce(message)
|
36
|
+
super "#{version} #{name}: #{message}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def migrate(direction)
|
40
|
+
case direction
|
41
|
+
when :up
|
42
|
+
announce("migrating")
|
43
|
+
time_elapsed = ActiveSupport::Benchmark.realtime { up }
|
44
|
+
announce("migrated (%.4fs)" % time_elapsed)
|
45
|
+
|
46
|
+
when :down
|
47
|
+
announce("reverting")
|
48
|
+
time_elapsed = ActiveSupport::Benchmark.realtime { down }
|
49
|
+
announce("reverted (%.4fs)" % time_elapsed)
|
50
|
+
|
51
|
+
else
|
52
|
+
raise ArgumentError, "Unknown migration direction: #{direction}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def up
|
57
|
+
# Implement in subclass
|
58
|
+
end
|
59
|
+
|
60
|
+
def down
|
61
|
+
raise IrreversibleMigration
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/benchmarkable"
|
4
|
+
require "active_support/core_ext/module/delegation"
|
5
|
+
|
6
|
+
class ShopifyToolkit::Migrator # :nodoc:
|
7
|
+
include ShopifyToolkit::AdminClient
|
8
|
+
include ShopifyToolkit::MetafieldStatements
|
9
|
+
|
10
|
+
singleton_class.attr_accessor :migrations_paths
|
11
|
+
self.migrations_paths = ["config/shopify/migrate"]
|
12
|
+
|
13
|
+
attr_reader :migrated_versions, :migrations, :migrations_paths
|
14
|
+
|
15
|
+
def initialize(migrations_paths: self.class.migrations_paths)
|
16
|
+
@migrations_paths = migrations_paths
|
17
|
+
@migrated_versions = read_or_create_metafield["migrated_versions"]
|
18
|
+
@migrations = load_migrations # [MigrationProxy<Migration1>, MigrationProxy<Migration2>, ...]
|
19
|
+
end
|
20
|
+
|
21
|
+
def current_version
|
22
|
+
migrated.max || 0
|
23
|
+
end
|
24
|
+
|
25
|
+
def up
|
26
|
+
pending_migrations = migrations.reject { migrated_versions.include?(_1.version) }
|
27
|
+
|
28
|
+
pending_migrations.each do |migration|
|
29
|
+
migration.migrate(:up)
|
30
|
+
migrated_versions << migration.version
|
31
|
+
end
|
32
|
+
ensure
|
33
|
+
update_metafield
|
34
|
+
end
|
35
|
+
|
36
|
+
def executed_migrations
|
37
|
+
migrations.select { migrated_versions.include?(_1.version) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def down
|
41
|
+
# For now we'll just rollback the last one
|
42
|
+
executed_migrations.last(1).each do |migration|
|
43
|
+
migration.migrate(:down)
|
44
|
+
migrated_versions.delete(migration.version)
|
45
|
+
end
|
46
|
+
ensure
|
47
|
+
update_metafield
|
48
|
+
end
|
49
|
+
|
50
|
+
def query(query, **variables)
|
51
|
+
shopify_admin_client
|
52
|
+
.query(query:, variables:)
|
53
|
+
.tap { handle_shopify_admin_client_errors(_1) }
|
54
|
+
.body
|
55
|
+
.dig("data")
|
56
|
+
end
|
57
|
+
|
58
|
+
def update_metafield
|
59
|
+
namespace = :shopify_toolkit
|
60
|
+
key = :migrations
|
61
|
+
|
62
|
+
value = { "migrated_versions" => migrated_versions.to_a.sort }.to_json
|
63
|
+
|
64
|
+
owner_id = query(%(query GetShopGId { shop { id } })).dig("shop", "id")
|
65
|
+
|
66
|
+
query = <<~GRAPHQL
|
67
|
+
mutation metafieldsSet($metafields: [MetafieldsSetInput!]!) {
|
68
|
+
metafieldsSet(metafields: $metafields) {
|
69
|
+
userErrors {
|
70
|
+
field
|
71
|
+
message
|
72
|
+
}
|
73
|
+
}
|
74
|
+
}
|
75
|
+
GRAPHQL
|
76
|
+
query(query, metafields: [{ namespace:, key:, ownerId: owner_id, value: }])
|
77
|
+
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
|
81
|
+
def read_or_create_metafield
|
82
|
+
namespace = :shopify_toolkit
|
83
|
+
key = :migrations
|
84
|
+
|
85
|
+
value = query(
|
86
|
+
"query ShopMetafield($namespace: String!, $key: String!) {shop {metafield(namespace: $namespace, key: $key) {value}}}",
|
87
|
+
namespace:, key:,
|
88
|
+
).dig("shop", "metafield", "value")
|
89
|
+
|
90
|
+
if value.nil?
|
91
|
+
create_metafield :shop, key, :json, namespace:, name: "Migrations metadata"
|
92
|
+
return { "migrated_versions" => [] }
|
93
|
+
end
|
94
|
+
|
95
|
+
JSON.parse(value)
|
96
|
+
end
|
97
|
+
|
98
|
+
def redo
|
99
|
+
down
|
100
|
+
up
|
101
|
+
end
|
102
|
+
|
103
|
+
def migration_files
|
104
|
+
paths = Array(migrations_paths)
|
105
|
+
Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }]
|
106
|
+
end
|
107
|
+
|
108
|
+
def parse_migration_filename(filename)
|
109
|
+
File.basename(filename).scan(/\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/).first
|
110
|
+
end
|
111
|
+
|
112
|
+
def load_migrations
|
113
|
+
migrations = migration_files.map do |file|
|
114
|
+
version, name, scope = parse_migration_filename(file)
|
115
|
+
raise "missing version #{file}" unless version
|
116
|
+
raise "missing name #{file}" unless name
|
117
|
+
version = version.to_i
|
118
|
+
name = name.camelize
|
119
|
+
|
120
|
+
MigrationProxy.new(name, version, file, scope)
|
121
|
+
end
|
122
|
+
|
123
|
+
migrations.sort_by(&:version)
|
124
|
+
end
|
125
|
+
|
126
|
+
# MigrationProxy is used to defer loading of the actual migration classes
|
127
|
+
# until they are needed
|
128
|
+
MigrationProxy = Struct.new(:name, :version, :filename, :scope) do
|
129
|
+
def initialize(name, version, filename, scope)
|
130
|
+
super
|
131
|
+
@migration = nil
|
132
|
+
end
|
133
|
+
|
134
|
+
def basename
|
135
|
+
File.basename(filename)
|
136
|
+
end
|
137
|
+
|
138
|
+
delegate :migrate, :up, :down, :announce, :say, :say_with_time, to: :migration
|
139
|
+
|
140
|
+
private
|
141
|
+
def migration
|
142
|
+
@migration ||= load_migration
|
143
|
+
end
|
144
|
+
|
145
|
+
def load_migration
|
146
|
+
Object.send(:remove_const, name) rescue nil
|
147
|
+
|
148
|
+
load(File.expand_path(filename))
|
149
|
+
name.constantize.new(name, version)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
data/lib/shopify_toolkit.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shopify_toolkit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Elia Schito
|
8
8
|
- Nebulab Team
|
9
|
-
autorequire:
|
10
9
|
bindir: exe
|
11
10
|
cert_chain: []
|
12
|
-
date: 2025-
|
11
|
+
date: 2025-05-27 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: railties
|
@@ -95,7 +94,6 @@ dependencies:
|
|
95
94
|
- - ">="
|
96
95
|
- !ruby/object:Gem::Version
|
97
96
|
version: '14.8'
|
98
|
-
description:
|
99
97
|
email:
|
100
98
|
- elia@schito.me
|
101
99
|
executables:
|
@@ -111,8 +109,12 @@ files:
|
|
111
109
|
- exe/shopify-toolkit
|
112
110
|
- lib/shopify_toolkit.rb
|
113
111
|
- lib/shopify_toolkit/admin_client.rb
|
112
|
+
- lib/shopify_toolkit/command_line.rb
|
114
113
|
- lib/shopify_toolkit/metafield_statements.rb
|
114
|
+
- lib/shopify_toolkit/metaobject_statements.rb
|
115
|
+
- lib/shopify_toolkit/migration.rb
|
115
116
|
- lib/shopify_toolkit/migration/logging.rb
|
117
|
+
- lib/shopify_toolkit/migrator.rb
|
116
118
|
- lib/shopify_toolkit/schema.rb
|
117
119
|
- lib/shopify_toolkit/version.rb
|
118
120
|
- sig/shopify_toolkit.rbs
|
@@ -125,7 +127,6 @@ metadata:
|
|
125
127
|
homepage_uri: https://github.com/nebulab/shopify_toolkit?tab=readme-ov-file#readme
|
126
128
|
source_code_uri: https://github.com/nebulab/shopify_toolkit
|
127
129
|
changelog_uri: https://github.com/nebulab/shopify_toolkit/releases
|
128
|
-
post_install_message:
|
129
130
|
rdoc_options: []
|
130
131
|
require_paths:
|
131
132
|
- lib
|
@@ -140,8 +141,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
140
141
|
- !ruby/object:Gem::Version
|
141
142
|
version: '0'
|
142
143
|
requirements: []
|
143
|
-
rubygems_version: 3.
|
144
|
-
signing_key:
|
144
|
+
rubygems_version: 3.6.2
|
145
145
|
specification_version: 4
|
146
146
|
summary: A collection of tools for dealing with Shopify apps.
|
147
147
|
test_files: []
|