shopify_toolkit 0.3.4 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e5e83e71a4d7f6e6d0d8746e307076e7908f4e6c03d28820e99fde10557c6ba
4
- data.tar.gz: c9d9bce64d5af477e1e46a18dc18b175088fde22288c2d97e935c495f06beb42
3
+ metadata.gz: a1d6624a536a67ce6e7861ae95c0eed57e2713974b4901b9eca1720ecbd063ad
4
+ data.tar.gz: ee1bffe4a027352bdf58a39d9aa4412c5c0151da4784b56006c62b3b8b33dd30
5
5
  SHA512:
6
- metadata.gz: 7c39acdbf5c2b15e1b0c5276aefd65063857bae0da5431b9a929b55d2d8fe51517ba40c71ffe508ddda2100965c378f9a99c494de398afc906cfdb5674e65b7c
7
- data.tar.gz: cfa96c9301a1502eb02ba5fb5c854ffa8a5e72a4960910ba17cd82d475c9edeeab39989fe5d5f6c879b94b7f55835be6e334100cec24304ba1a56621e0fb5faa
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
- - [ ] Metafield/Metaobject migrations (just like ActiveRecord migrations, but for Shopify!)
14
+ - [x] Metafield/Metaobject migrations (just like ActiveRecord migrations, but for Shopify!)
15
15
  - [x] Metafield Definitions management API
16
- - [ ] Metaobject Definitions management API
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 definitions using ActiveRecord Migrations
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
- rails generate migration AddMetafieldDefinitions
38
+ touch config/shopify/migrate/20250528130134_add_product_press_releases.rb
39
39
  ```
40
40
 
41
- Include the `ShopifyToolkit::MetafieldStatements` module in your migration file
42
- in order to use the metafield statements:
43
-
41
+ Then, add the following code to the migration file:
44
42
  ```ruby
45
- class AddMetafieldDefinitions < ActiveRecord::Migration[7.0]
46
- include ShopifyToolkit::MetafieldStatements
47
-
43
+ # config/shopify/migrate/20250528130134_add_product_press_releases.rb
44
+ class AddProductPressReleases < ShopifyToolkit::Migration
48
45
  def up
49
- Shop.first!.with_shopify_session do
50
- create_metafield :products, :my_metafield, :single_line_text_field, name: "My Metafield"
51
- end
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
- Shop.first!.with_shopify_session do
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
- Then run the migration:
73
+
74
+ Then run the migrations:
62
75
 
63
76
  ```bash
64
- rails db:migrate
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)
@@ -1,3 +1,5 @@
1
+ require "shopify_api"
2
+
1
3
  module ShopifyToolkit::AdminClient
2
4
  API_VERSION = "2024-10"
3
5
 
@@ -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
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "shopify_toolkit/migration/logging"
4
- require "shopify_toolkit/admin_client"
5
3
  require "active_support/concern"
6
4
 
7
5
  module ShopifyToolkit::MetafieldStatements
@@ -22,6 +20,12 @@ module ShopifyToolkit::MetafieldStatements
22
20
  def create_metafield(owner_type, key, type, namespace: :custom, name:, **options)
23
21
  ownerType = owner_type.to_s.singularize.upcase # Eg. "PRODUCT"
24
22
 
23
+ # Skip creation if metafield already exists
24
+ if get_metafield_gid(owner_type, key, namespace: namespace)
25
+ say "Metafield #{namespace}:#{key} already exists for #{owner_type}, skipping creation"
26
+ return
27
+ end
28
+
25
29
  # https://shopify.dev/docs/api/admin-graphql/2024-10/mutations/metafieldDefinitionCreate
26
30
  query =
27
31
  "# GraphQL
@@ -68,8 +72,7 @@ module ShopifyToolkit::MetafieldStatements
68
72
  .tap { handle_shopify_admin_client_errors(_1) }
69
73
  .body
70
74
 
71
- result.dig("data", "metafieldDefinitions", "nodes", 0, "id") or
72
- raise "Metafield not found for #{owner_type}##{namespace}:#{key}"
75
+ result.dig("data", "metafieldDefinitions", "nodes", 0, "id")
73
76
  end
74
77
 
75
78
  log_time \
@@ -79,6 +82,11 @@ module ShopifyToolkit::MetafieldStatements
79
82
  "For reserved namespaces, you must delete all associated metafields (delete_associated_metafields: true)"
80
83
  end
81
84
 
85
+ unless get_metafield_gid(owner_type, key, namespace: namespace)
86
+ say "Metafield #{namespace}:#{key} not found for #{owner_type}, skipping deletion"
87
+ return
88
+ end
89
+
82
90
  shopify_admin_client
83
91
  .query(
84
92
  # Documentation: https://shopify.dev/docs/api/admin-graphql/2024-10/mutations/metafieldDefinitionDelete
@@ -104,6 +112,11 @@ module ShopifyToolkit::MetafieldStatements
104
112
 
105
113
  log_time \
106
114
  def update_metafield(owner_type, key, namespace: :custom, **options)
115
+ unless get_metafield_gid(owner_type, key, namespace: namespace)
116
+ say "Metafield #{namespace}:#{key} not found for #{owner_type}, skipping update"
117
+ return
118
+ end
119
+
107
120
  shopify_admin_client
108
121
  .query(
109
122
  # Documentation: https://shopify.dev/docs/api/admin-graphql/2024-10/mutations/metafieldDefinitionUpdate
@@ -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
@@ -3,6 +3,7 @@
3
3
  require "stringio"
4
4
  require "shopify_api"
5
5
  require "rails"
6
+ require "active_support/core_ext/module/delegation"
6
7
 
7
8
  module ShopifyToolkit::Schema
8
9
  extend self
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ShopifyToolkit
4
- VERSION = "0.3.4"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -4,7 +4,6 @@ require_relative "shopify_toolkit/version"
4
4
  require "zeitwerk"
5
5
 
6
6
  module ShopifyToolkit
7
-
8
7
  def self.loader
9
8
  @loader ||= Zeitwerk::Loader.for_gem
10
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shopify_toolkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elia Schito
8
8
  - Nebulab Team
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-04-18 00:00:00.000000000 Z
11
+ date: 2025-05-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -109,8 +109,12 @@ files:
109
109
  - exe/shopify-toolkit
110
110
  - lib/shopify_toolkit.rb
111
111
  - lib/shopify_toolkit/admin_client.rb
112
+ - lib/shopify_toolkit/command_line.rb
112
113
  - lib/shopify_toolkit/metafield_statements.rb
114
+ - lib/shopify_toolkit/metaobject_statements.rb
115
+ - lib/shopify_toolkit/migration.rb
113
116
  - lib/shopify_toolkit/migration/logging.rb
117
+ - lib/shopify_toolkit/migrator.rb
114
118
  - lib/shopify_toolkit/schema.rb
115
119
  - lib/shopify_toolkit/version.rb
116
120
  - sig/shopify_toolkit.rbs
@@ -137,7 +141,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
137
141
  - !ruby/object:Gem::Version
138
142
  version: '0'
139
143
  requirements: []
140
- rubygems_version: 3.6.3
144
+ rubygems_version: 3.6.2
141
145
  specification_version: 4
142
146
  summary: A collection of tools for dealing with Shopify apps.
143
147
  test_files: []