hanami-cli 2.2.0.beta1 → 2.2.0.rc1

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +13 -2
  3. data/.rubocop.yml +2 -0
  4. data/CHANGELOG.md +38 -2
  5. data/Gemfile +3 -0
  6. data/README.md +2 -0
  7. data/docker-compose.yml +6 -1
  8. data/lib/hanami/cli/command.rb +1 -1
  9. data/lib/hanami/cli/commands/app/command.rb +9 -3
  10. data/lib/hanami/cli/commands/app/console.rb +1 -5
  11. data/lib/hanami/cli/commands/app/db/command.rb +139 -31
  12. data/lib/hanami/cli/commands/app/db/create.rb +6 -2
  13. data/lib/hanami/cli/commands/app/db/drop.rb +6 -2
  14. data/lib/hanami/cli/commands/app/db/migrate.rb +49 -8
  15. data/lib/hanami/cli/commands/app/db/prepare.rb +36 -8
  16. data/lib/hanami/cli/commands/app/db/seed.rb +17 -1
  17. data/lib/hanami/cli/commands/app/db/structure/dump.rb +11 -6
  18. data/lib/hanami/cli/commands/app/db/structure/load.rb +9 -5
  19. data/lib/hanami/cli/commands/app/db/utils/database.rb +44 -14
  20. data/lib/hanami/cli/commands/app/db/utils/mysql.rb +57 -4
  21. data/lib/hanami/cli/commands/app/db/utils/postgres.rb +13 -8
  22. data/lib/hanami/cli/commands/app/db/version.rb +6 -3
  23. data/lib/hanami/cli/commands/app/generate/action.rb +12 -2
  24. data/lib/hanami/cli/commands/app/generate/command.rb +13 -2
  25. data/lib/hanami/cli/commands/app/generate/migration.rb +20 -0
  26. data/lib/hanami/cli/commands/app/generate/relation.rb +0 -6
  27. data/lib/hanami/cli/commands/app/generate/repo.rb +3 -7
  28. data/lib/hanami/cli/commands/app/generate/slice.rb +16 -3
  29. data/lib/hanami/cli/commands/app.rb +21 -9
  30. data/lib/hanami/cli/files.rb +22 -0
  31. data/lib/hanami/cli/generators/app/action.rb +27 -15
  32. data/lib/hanami/cli/generators/app/migration.rb +8 -9
  33. data/lib/hanami/cli/generators/app/operation.rb +5 -4
  34. data/lib/hanami/cli/generators/app/relation.rb +5 -4
  35. data/lib/hanami/cli/generators/app/repo.rb +5 -4
  36. data/lib/hanami/cli/generators/app/ruby_file_writer.rb +39 -37
  37. data/lib/hanami/cli/generators/app/slice.rb +5 -3
  38. data/lib/hanami/cli/generators/app/slice_context.rb +6 -0
  39. data/lib/hanami/cli/generators/app/struct.rb +5 -4
  40. data/lib/hanami/cli/generators/context.rb +4 -4
  41. data/lib/hanami/cli/generators/gem/app/gitignore.erb +3 -0
  42. data/lib/hanami/cli/generators/gem/app/operation.erb +0 -4
  43. data/lib/hanami/cli/generators/gem/app/seeds.erb +15 -0
  44. data/lib/hanami/cli/generators/gem/app.rb +3 -2
  45. data/lib/hanami/cli/repl/irb.rb +2 -2
  46. data/lib/hanami/cli/version.rb +1 -1
  47. metadata +4 -4
  48. data/lib/hanami/cli/generators/app/slice/entities.erb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eda4f18125d25c640cbb75e50a7e636b93e29badd50f6540b37c6fc55bb7cb98
4
- data.tar.gz: 2d0f39b520d19a39866ccc354d40c1e9536f9b2c77d2ead33f340ddc64d899ef
3
+ metadata.gz: e6ace925a2c5186088911d5df3d309d72ee0359e4691f70199c3f53f99db0fc3
4
+ data.tar.gz: 7fd1959ffcaf64ebc0b95a839a68ed1f052370ed6a7043863068087a1893489d
5
5
  SHA512:
6
- metadata.gz: 07052611a88815d7ffc2e228d77b62a65cc708283688c13e3509d3004a186b264d4271e104e8e3dea4dfae52958ccf9e385bc1a95c5c9b732e985538c82bfc7f
7
- data.tar.gz: e6d3c5e3290fb2c19b6a301e11b20d70eb376a5c4da42ed69f7ceafa1590f5b14955b4cc9ce8bc0ad155e1db0ec3851aae06da43849d43441fe282cd94dd9860
6
+ metadata.gz: 6133308e48b0623616598a23d1b0fe079b08f31e991791e6ae76867441eec1d25427deef5755e445fd8c3f2f2e294c143eeb58272e278721897ab290c48024cb
7
+ data.tar.gz: '068ad2b891fa99e16ff6c0be6931e98d708ce3c7bab67e0dd3fd354ef00f4bdb110ad84007592ccc98e65ebbe643f7e4420b0406f8b0b76500968a5571718398'
@@ -41,6 +41,17 @@ jobs:
41
41
  - name: Run all tests
42
42
  run: bundle exec rake spec
43
43
  services:
44
+ mysql:
45
+ image: mysql:latest
46
+ env:
47
+ MYSQL_ROOT_PASSWORD: password
48
+ ports:
49
+ - 3307:3306
50
+ options: >-
51
+ --health-cmd "mysqladmin ping"
52
+ --health-interval 10s
53
+ --health-timeout 5s
54
+ --health-retries 3
44
55
  postgres:
45
56
  # Use postgres:14 for CLI compatibility with ubuntu-latest, currently ubuntu-22.04
46
57
  # See https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2204-Readme.md
@@ -48,10 +59,10 @@ jobs:
48
59
  env:
49
60
  POSTGRES_USER: postgres
50
61
  POSTGRES_PASSWORD: password
62
+ ports:
63
+ - 5432:5432
51
64
  options: >-
52
65
  --health-cmd pg_isready
53
66
  --health-interval 10s
54
67
  --health-timeout 5s
55
68
  --health-retries 5
56
- ports:
57
- - 5432:5432
data/.rubocop.yml CHANGED
@@ -42,3 +42,5 @@ Style/TrailingCommaInHashLiteral:
42
42
  Enabled: false
43
43
  Style/StringConcatenation:
44
44
  Enabled: false
45
+ Style/ZeroLengthPredicate:
46
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -2,9 +2,45 @@
2
2
 
3
3
  Hanami Command Line Interface
4
4
 
5
+ ## v2.2.0.rc1 - 2024-10-29
6
+
7
+ ### Added
8
+
9
+ - [Tim Riley, François Beausoleil] Generate a `config/db/seeds.rb` file in new apps (#255, #256)
10
+
11
+ ### Changed
12
+
13
+ - [Tim Riley] Add `--env` and `-e` options to all app commands, for setting the Hanami env (#246)
14
+ - [Tim Riley] Keep test database in sync by applying `hanami db` commands to both development and test databases when invoked in development environment (#247)
15
+ - [Kyle Plump] Add `--skip-route` flag to `generate action` and `generate slice` commands (#227)
16
+ - [Tim Riley] Include a `change do` block in generated migrations (#254)
17
+ - [Tim Riley] Generate MySQL database URL in `.env` that works with standard Homebrew MySQL installation (#249)
18
+ - [Tim Riley, Adam Lassek] Remove ROM extension boilerplate in operations generated by `hanami new` and `generate operation` (this is now applied automatically) (#240, #252)
19
+ - [François Beausoleil] Print a warning when running `db seed` but expected seeds files could not be found (#256)
20
+ - [Seb Wilgosz] Only register `generate` subcommands if the relevant gems are bundled (#242)
21
+ - [Anderson Saunders] When both IRB and pry are loaded, use IRB as the default engine for `hanami console` (#182)
22
+
23
+ ### Fixed
24
+
25
+ - [Tim Riley] Fix error dumping structure when there are no migrations (#244)
26
+ - [Tim Riley] Stop erroneous misconfigured DB warnings from `hanami db` commands when a database is configured once but shared across sliaces (#253)
27
+
28
+ ## v2.2.0.beta2 - 2024-09-25
29
+
30
+ ### Added
31
+
32
+ - [Tim Riley] MySQL support for `db` commands (#226)
33
+ - [Tim Riley] Support for multiple gateways in `db` commands (#232, #234, #237, #238)
34
+
35
+ ### Changed
36
+
37
+ - [Kyle Plump, Tim Riley] Delete `.keep` files when generating new files into previously empty directory (#224)
38
+ - [Sean Collins] Add `db/*.sqlite` to the `.gitignore` in new apps (#210)
39
+ - [Sean Collins] Print warnings for misconfigured databases when running `db` commands (#211)
40
+
5
41
  ## v2.2.0.beta1 - 2024-07-16
6
42
 
7
- ## Added
43
+ ### Added
8
44
 
9
45
  - [Sean Collins] Generate db files in `hanami new` and `generate slice`
10
46
  - [Tim Riley] Add `db` commands: `create`, `drop`, `migrate`, `structure dump` `structure load`, `seed` `prepare`, `version`
@@ -13,7 +49,7 @@ Hanami Command Line Interface
13
49
  - [Krzysztof] Add `generate component` command
14
50
  - [Sean Collins] Add `generate operation` command
15
51
 
16
- ## Changed
52
+ ### Changed
17
53
 
18
54
  - Drop support for Ruby 3.0
19
55
 
data/Gemfile CHANGED
@@ -16,8 +16,11 @@ gem "hanami-db", github: "hanami/db", branch: "main"
16
16
  gem "hanami-router", github: "hanami/router", branch: "main"
17
17
  gem "hanami-utils", github: "hanami/utils", branch: "main"
18
18
 
19
+ gem "dry-system", github: "dry-rb/dry-system", branch: "main"
20
+
19
21
  gem "rack"
20
22
 
23
+ gem "mysql2"
21
24
  gem "pg"
22
25
  gem "sqlite3"
23
26
 
data/README.md CHANGED
@@ -34,6 +34,8 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
34
34
 
35
35
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
36
36
 
37
+ In order to run all of the tests, you should run `docker compose up` separately, to run a `postgres` server.
38
+
37
39
  ## Contributing
38
40
 
39
41
  Bug reports and pull requests are welcome on GitHub at https://github.com/hanami/cli. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/hanami/cli/blob/main/CODE_OF_CONDUCT.md).
data/docker-compose.yml CHANGED
@@ -1,5 +1,10 @@
1
- version: "2"
2
1
  services:
2
+ mysql:
3
+ image: mysql:latest
4
+ ports:
5
+ - 3307:3306
6
+ environment:
7
+ MYSQL_ROOT_PASSWORD: password
3
8
  postgres:
4
9
  image: postgres:latest
5
10
  ports:
@@ -23,7 +23,7 @@ module Hanami
23
23
  def self.new(
24
24
  out: $stdout,
25
25
  err: $stderr,
26
- fs: Hanami::CLI::Files.new,
26
+ fs: Hanami::CLI::Files.new(out: out),
27
27
  inflector: Dry::Inflector.new,
28
28
  **opts
29
29
  )
@@ -18,8 +18,8 @@ module Hanami
18
18
  # @api private
19
19
  ACTION_SEPARATOR = "." # TODO: rename to container key separator
20
20
 
21
- # Overloads {Hanami::CLI::Commands::App::Command#call} to ensure an appropriate `HANAMI_ENV`
22
- # environment variable is set.
21
+ # Overloads {Hanami::CLI::Commands::App::Command#call} to ensure an appropriate
22
+ # `HANAMI_ENV` environment variable is set.
23
23
  #
24
24
  # Uses an `--env` option if provided, then falls back to an already-set `HANAMI_ENV`
25
25
  # environment variable, and defaults to "development" in the absence of both.
@@ -27,6 +27,12 @@ module Hanami
27
27
  # @since 2.0.0
28
28
  # @api private
29
29
  module Environment
30
+ # @since 2.2.0
31
+ # @api private
32
+ def self.prepended(klass)
33
+ klass.option :env, desc: "App environment (development, test, production)", aliases: ["e"]
34
+ end
35
+
30
36
  # @since 2.0.0
31
37
  # @api private
32
38
  def call(*args, **opts)
@@ -37,7 +43,7 @@ module Hanami
37
43
  ENV["HANAMI_ENV"] = hanami_env
38
44
  Hanami::Env.load
39
45
 
40
- super(*args, **opts)
46
+ super
41
47
  end
42
48
  end
43
49
 
@@ -51,11 +51,7 @@ module Hanami
51
51
  private
52
52
 
53
53
  def resolve_engine(engine, opts)
54
- if engine
55
- ENGINES.fetch(engine).(app, opts)
56
- else
57
- ENGINES.map { |(_, loader)| loader.(app, opts) }.compact.first
58
- end
54
+ ENGINES.fetch(engine, ENGINES[DEFAULT_ENGINE]).call(app, opts)
59
55
  end
60
56
  end
61
57
  end
@@ -17,97 +17,205 @@ module Hanami
17
17
  option :app, required: false, type: :flag, default: false, desc: "Use app database"
18
18
  option :slice, required: false, desc: "Use database for slice"
19
19
 
20
+ # @api private
20
21
  attr_reader :system_call
21
22
 
23
+ # @api private
24
+ attr_reader :test_env_executor
25
+
22
26
  def initialize(
23
27
  out:, err:,
24
28
  system_call: SystemCall.new,
29
+ test_env_executor: InteractiveSystemCall.new(out: out, err: err),
30
+ nested_command: false,
25
31
  **opts
26
32
  )
27
33
  super(out: out, err: err, **opts)
28
34
  @system_call = system_call
35
+ @test_env_executor = test_env_executor
36
+ @nested_command = nested_command
29
37
  end
30
38
 
31
39
  def run_command(klass, ...)
32
40
  klass.new(
33
41
  out: out,
34
- inflector: fs,
42
+ inflector: inflector,
35
43
  fs: fs,
36
44
  system_call: system_call,
45
+ test_env_executor: test_env_executor,
46
+ nested_command: true,
37
47
  ).call(...)
38
48
  end
39
49
 
50
+ def nested_command?
51
+ @nested_command
52
+ end
53
+
40
54
  private
41
55
 
42
- def databases(app: false, slice: nil)
43
- if app
44
- [database_for_app]
45
- elsif slice
46
- [database_for_slice(slice)]
47
- else
48
- all_databases
56
+ def databases(app: false, slice: nil, gateway: nil)
57
+ if gateway && !app && !slice
58
+ err.puts "When specifying --gateway, an --app or --slice must also be given"
59
+ exit 1
49
60
  end
50
- end
51
61
 
52
- def database_for_app
53
- build_database(app)
62
+ databases =
63
+ if slice
64
+ [database_for_slice(slice, gateway: gateway)]
65
+ elsif app
66
+ [database_for_slice(self.app, gateway: gateway)]
67
+ else
68
+ all_databases
69
+ end
70
+
71
+ databases.flatten
54
72
  end
55
73
 
56
- def database_for_slice(slice)
74
+ def database_for_slice(slice, gateway: nil)
57
75
  unless slice.is_a?(Class) && slice < Hanami::Slice
58
76
  slice_name = inflector.underscore(Shellwords.shellescape(slice)).to_sym
59
77
  slice = app.slices[slice_name]
60
78
  end
61
79
 
62
- build_database(slice)
80
+ ensure_database_slice slice
81
+
82
+ databases = build_databases(slice)
83
+
84
+ if gateway
85
+ databases.fetch(gateway.to_sym) do
86
+ err.puts %(No gateway "#{gateway}" in #{slice})
87
+ exit 1
88
+ end
89
+ else
90
+ databases.values
91
+ end
63
92
  end
64
93
 
65
- def all_databases
94
+ def all_databases # rubocop:disable Metrics/AbcSize
66
95
  slices = [app] + app.slices.with_nested
67
96
 
68
- slices_by_database_url = slices.each_with_object({}) { |slice, hsh|
69
- provider = slice.container.providers[:db]
70
- next unless provider
97
+ slice_gateways_by_database_url = slices.each_with_object({}) { |slice, hsh|
98
+ db_provider_source = slice.container.providers[:db]&.source
99
+ next unless db_provider_source
71
100
 
72
- database_url = provider.source.database_url
73
- hsh[database_url] ||= []
74
- hsh[database_url] << slice
101
+ db_provider_source.database_urls.each do |gateway, url|
102
+ hsh[url] ||= []
103
+ hsh[url] << {slice: slice, gateway: gateway}
104
+ end
75
105
  }
76
106
 
77
- databases = slices_by_database_url.each_with_object([]) { |(url, slices), arr|
78
- slices_with_config = slices.select { _1.root.join("config", "db").directory? }
107
+ slice_gateways_by_database_url.each_with_object([]) { |(url, slice_gateways), arr|
108
+ slice_gateways_with_config = slice_gateways.select {
109
+ migrate_dir = _1[:gateway] == :default ? "migrate" : "#{_1[:gateway]}_migrate"
79
110
 
80
- database = build_database(slices_with_config.first || slices.first)
111
+ _1[:slice].root.join("config", "db", migrate_dir).directory?
112
+ }
81
113
 
82
- warn_on_misconfigured_database database, slices_with_config
114
+ db_slice_gateway = slice_gateways_with_config.first || slice_gateways.first
115
+ database = Utils::Database.database_class(url).new(
116
+ slice: db_slice_gateway.fetch(:slice),
117
+ gateway_name: db_slice_gateway.fetch(:gateway),
118
+ system_call: system_call
119
+ )
120
+
121
+ warn_on_misconfigured_database database, slice_gateways_with_config.map { _1.fetch(:slice) }
83
122
 
84
123
  arr << database
85
124
  }
125
+ end
86
126
 
87
- databases
127
+ def build_databases(slice)
128
+ Utils::Database.from_slice(slice: slice, system_call: system_call)
88
129
  end
89
130
 
90
- def build_database(slice)
91
- Utils::Database[slice, system_call: system_call]
131
+ def ensure_database_slice(slice)
132
+ return if slice.container.providers[:db]
133
+
134
+ out.puts "#{slice} does not have a :db provider."
135
+ exit 1
92
136
  end
93
137
 
94
- def warn_on_misconfigured_database(database, slices)
138
+ def warn_on_misconfigured_database(database, slices) # rubocop:disable Metrics/AbcSize
95
139
  if slices.length > 1
96
140
  out.puts <<~STR
97
141
  WARNING: Database #{database.name} is configured for multiple config/db/ directories:
98
142
 
99
143
  #{slices.map { "- " + _1.root.relative_path_from(_1.app.root).join("config", "db").to_s }.join("\n")}
100
144
 
101
- Migrating database using #{database.slice.slice_name.to_s.inspect} slice only.
145
+ Using config in #{database.slice.slice_name.to_s.inspect} slice only.
102
146
 
103
147
  STR
104
- elsif slices.length < 1
148
+ elsif !database.db_config_dir?
149
+ relative_path = database.slice.root
150
+ .relative_path_from(database.slice.app.root)
151
+ .join("config", "db").to_s
152
+
105
153
  out.puts <<~STR
106
- WARNING: Database #{database.name} has no config/db/ directory.
154
+ WARNING: Database #{database.name} expects the folder #{relative_path}/ to exist but it does not.
107
155
 
108
156
  STR
109
157
  end
110
158
  end
159
+
160
+ # Invokes the currently executing `hanami` CLI command again, but with any `--env` args
161
+ # removed and the `HANAMI_ENV=test` env var set.
162
+ #
163
+ # This is called by certain `db` commands only, and runs only if the Hanami env is
164
+ # `:development`. This behavior important to streamline the local development
165
+ # experience, making sure that the test databases are kept in sync with operations run
166
+ # on the development databases.
167
+ #
168
+ # Spawning an entirely new process to change the env is a compromise approach until we
169
+ # can have an API for reinitializing the DB subsystem in-process with a different env.
170
+ def re_run_development_command_in_test
171
+ # Only invoke a new process if we've been called as `hanami`. This avoids awkward
172
+ # failures when testing commands via RSpec, for which the $0 is "/full/path/to/rspec".
173
+ return unless $0.end_with?("hanami")
174
+
175
+ # If this special env key is set, then a re-run has already been invoked. This would
176
+ # mean the current command is actually a nested command run by another db command. In
177
+ # this case, don't trigger a re-runs, because one is already in process.
178
+ return if nested_command?
179
+
180
+ # Re-runs in test are for development-env commands only.
181
+ return unless Hanami.env == :development
182
+
183
+ cmd = $0
184
+ cmd = "bundle exec #{cmd}" if ENV.key?("BUNDLE_BIN_PATH")
185
+
186
+ test_env_executor.call(
187
+ cmd, *argv_without_env_args,
188
+ env: {
189
+ "HANAMI_ENV" => "test",
190
+ "HANAMI_CLI_DB_COMMAND_RE_RUN_IN_TEST" => "true"
191
+ }
192
+ )
193
+ end
194
+
195
+ def re_running_in_test?
196
+ ENV.key?("HANAMI_CLI_DB_COMMAND_RE_RUN_IN_TEST")
197
+ end
198
+
199
+ # Returns the `ARGV` with every option argument included, but the `-e` or `--env` args
200
+ # removed.
201
+ def argv_without_env_args
202
+ new_argv = ARGV.dup
203
+
204
+ env_arg_index = new_argv.index {
205
+ _1 == "-e" || _1 == "--env" || _1.start_with?("-e=") || _1.start_with?("--env=")
206
+ }
207
+
208
+ if env_arg_index
209
+ # Remove the env argument
210
+ env_arg = new_argv.delete_at(env_arg_index)
211
+
212
+ # If the env argument is not in combined form ("--env foo" rather than "--env=foo"),
213
+ # then remove the following argument too
214
+ new_argv.delete_at(env_arg_index) if ["-e", "--env"].include?(env_arg)
215
+ end
216
+
217
+ new_argv
218
+ end
111
219
  end
112
220
  end
113
221
  end
@@ -9,10 +9,12 @@ module Hanami
9
9
  class Create < DB::Command
10
10
  desc "Create databases"
11
11
 
12
- def call(app: false, slice: nil, command_exit: method(:exit), **)
12
+ option :gateway, required: false, desc: "Use database for gateway"
13
+
14
+ def call(app: false, slice: nil, gateway: nil, command_exit: method(:exit), **)
13
15
  exit_codes = []
14
16
 
15
- databases(app: app, slice: slice).each do |database|
17
+ databases(app: app, slice: slice, gateway: gateway).each do |database|
16
18
  result = database.exec_create_command
17
19
  exit_codes << result.exit_code if result.respond_to?(:exit_code)
18
20
 
@@ -27,6 +29,8 @@ module Hanami
27
29
  exit_codes.each do |code|
28
30
  break command_exit.(code) if code > 0
29
31
  end
32
+
33
+ re_run_development_command_in_test
30
34
  end
31
35
  end
32
36
  end
@@ -9,10 +9,12 @@ module Hanami
9
9
  class Drop < DB::Command
10
10
  desc "Delete databases"
11
11
 
12
- def call(app: false, slice: nil, **)
12
+ option :gateway, required: false, desc: "Use database for gateway"
13
+
14
+ def call(app: false, slice: nil, gateway: nil, **)
13
15
  exit_codes = []
14
16
 
15
- databases(app: app, slice: slice).each do |database|
17
+ databases(app: app, slice: slice, gateway: gateway).each do |database|
16
18
  result = database.exec_drop_command
17
19
  exit_codes << result.exit_code if result.respond_to?(:exit_code)
18
20
 
@@ -27,6 +29,8 @@ module Hanami
27
29
  exit_codes.each do |code|
28
30
  break exit code if code > 0
29
31
  end
32
+
33
+ re_run_development_command_in_test
30
34
  end
31
35
  end
32
36
  end
@@ -9,22 +9,38 @@ module Hanami
9
9
  class Migrate < DB::Command
10
10
  desc "Migrates database"
11
11
 
12
+ option :gateway, required: false, desc: "Use database for gateway"
12
13
  option :target, desc: "Target migration number", aliases: ["-t"]
13
14
  option :dump, required: false, type: :boolean, default: true,
14
- desc: "Dump the database structure after migrating"
15
+ desc: "Dump the database structure after migrating"
15
16
 
16
- def call(target: nil, app: false, slice: nil, dump: true, command_exit: method(:exit), **)
17
- databases(app: app, slice: slice).each do |database|
18
- migrate_database(database, target: target)
17
+ def call(target: nil, app: false, slice: nil, gateway: nil, dump: true, command_exit: method(:exit), **)
18
+ databases(app: app, slice: slice, gateway: gateway).each do |database|
19
+ if migrations_dir_missing?(database)
20
+ warn_on_missing_migrations_dir(database)
21
+ elsif no_migrations?(database)
22
+ warn_on_empty_migrations_dir(database)
23
+ else
24
+ migrate_database(database, target: target)
25
+ end
26
+ end
27
+
28
+ # Only dump for the initial command, not a re-run of the command in test env
29
+ if dump && !re_running_in_test?
30
+ run_command(
31
+ Structure::Dump,
32
+ app: app, slice: slice, gateway: gateway,
33
+ command_exit: command_exit
34
+ )
19
35
  end
20
36
 
21
- run_command(Structure::Dump, app: app, slice: slice, command_exit: command_exit) if dump
37
+ re_run_development_command_in_test
22
38
  end
23
39
 
24
40
  private
25
41
 
26
42
  def migrate_database(database, target:)
27
- return true unless migrations?(database)
43
+ return true unless database.migrations_dir?
28
44
 
29
45
  measure "database #{database.name} migrated" do
30
46
  if target
@@ -37,8 +53,33 @@ module Hanami
37
53
  end
38
54
  end
39
55
 
40
- def migrations?(database)
41
- database.migrations_dir? && database.sequel_migrator.files.any?
56
+ def migrations_dir_missing?(database)
57
+ !database.migrations_dir?
58
+ end
59
+
60
+ def no_migrations?(database)
61
+ database.sequel_migrator.files.empty?
62
+ end
63
+
64
+ def warn_on_missing_migrations_dir(database)
65
+ out.puts <<~STR
66
+ WARNING: Database #{database.name} expects migrations to be located within #{relative_migrations_path(database)} but that folder does not exist.
67
+
68
+ No database migrations can be run for this database.
69
+ STR
70
+ end
71
+
72
+ def warn_on_empty_migrations_dir(database)
73
+ out.puts <<~STR
74
+ NOTE: Empty database migrations folder (#{relative_migrations_path(database)}) for #{database.name}
75
+ STR
76
+ end
77
+
78
+ def relative_migrations_path(database)
79
+ database
80
+ .migrations_path
81
+ .relative_path_from(database.slice.app.root)
82
+ .to_s + "/"
42
83
  end
43
84
  end
44
85
  end
@@ -10,11 +10,21 @@ module Hanami
10
10
  desc "Prepare databases"
11
11
 
12
12
  def call(app: false, slice: nil, **)
13
- exit_codes = []
13
+ command_exit = -> code { throw :command_exited, code }
14
+ command_exit_arg = {command_exit: command_exit}
14
15
 
16
+ # Since any slice may have multiple databases, we need to run the steps below in a
17
+ # particular order to satisfy our ROM/Sequel's migrator, which requires _all_ the
18
+ # databases in a slice to be created before we can use it.
19
+ #
20
+ # So before we do anything else, make sure to create/load every database first.
15
21
  databases(app: app, slice: slice).each do |database|
16
- command_exit = -> code { throw :command_exited, code }
17
- command_args = {slice: database.slice, command_exit: command_exit}
22
+ command_args = {
23
+ **command_exit_arg,
24
+ app: database.slice.app?,
25
+ slice: database.slice,
26
+ gateway: database.gateway_name.to_s
27
+ }
18
28
 
19
29
  exit_code = catch :command_exited do
20
30
  unless database.exists?
@@ -22,17 +32,35 @@ module Hanami
22
32
  run_command(DB::Structure::Load, **command_args)
23
33
  end
24
34
 
25
- run_command(DB::Migrate, **command_args)
26
- run_command(DB::Seed, **command_args)
27
35
  nil
28
36
  end
29
37
 
30
- exit_codes << exit_code if exit_code
38
+ return exit exit_code if exit_code.to_i > 1
31
39
  end
32
40
 
33
- exit_codes.each do |code|
34
- break exit code if code > 0
41
+ # Once all databases are created, the migrator will properly load for each slice, and
42
+ # we can migrate each database.
43
+ databases(app: app, slice: slice).each do |database|
44
+ command_args = {
45
+ **command_exit_arg,
46
+ app: database.slice.app?,
47
+ slice: database.slice,
48
+ gateway: database.gateway_name.to_s
49
+ }
50
+
51
+ exit_code = catch :command_exited do
52
+ run_command(DB::Migrate, **command_args)
53
+
54
+ nil
55
+ end
56
+
57
+ return exit exit_code if exit_code.to_i > 1
35
58
  end
59
+
60
+ # Finally, load the seeds for the slice overall, which is a once-per-slice operation.
61
+ run_command(DB::Seed, app: app, slice: slice) unless re_running_in_test?
62
+
63
+ re_run_development_command_in_test
36
64
  end
37
65
  end
38
66
  end
@@ -13,14 +13,30 @@ module Hanami
13
13
  desc "Load seed data"
14
14
 
15
15
  def call(app: false, slice: nil, **)
16
+ # We use `databases` below to discover the databases throughout the app and slices. It
17
+ # yields every database, so in a slice with multiple gateways, we'll see multiple
18
+ # databases for the slice.
19
+ #
20
+ # Since `db seed` is intended to run over whole slices only (not per-gateway), keep
21
+ # track of the seeded slices here, so we can avoid seeding a slice multiple times.
22
+ seeded_slices = []
23
+
16
24
  databases(app: app, slice: slice).each do |database|
25
+ next if seeded_slices.include?(database.slice)
26
+
17
27
  seeds_path = database.slice.root.join(SEEDS_PATH)
18
- next unless seeds_path.file?
28
+
29
+ unless seeds_path.file?
30
+ out.puts "no seeds found at #{seeds_path.relative_path_from(database.slice.app.root)}"
31
+ next
32
+ end
19
33
 
20
34
  relative_seeds_path = seeds_path.relative_path_from(database.slice.app.root)
21
35
  measure "seed data loaded from #{relative_seeds_path}" do
22
36
  load seeds_path.to_s
23
37
  end
38
+
39
+ seeded_slices << database.slice
24
40
  end
25
41
  end
26
42
  end