hanami-cli 2.2.0.beta1 → 2.2.0.beta2

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 (36) 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 +15 -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/db/command.rb +62 -30
  10. data/lib/hanami/cli/commands/app/db/create.rb +4 -2
  11. data/lib/hanami/cli/commands/app/db/drop.rb +4 -2
  12. data/lib/hanami/cli/commands/app/db/migrate.rb +40 -8
  13. data/lib/hanami/cli/commands/app/db/prepare.rb +34 -8
  14. data/lib/hanami/cli/commands/app/db/seed.rb +12 -0
  15. data/lib/hanami/cli/commands/app/db/structure/dump.rb +7 -5
  16. data/lib/hanami/cli/commands/app/db/structure/load.rb +7 -5
  17. data/lib/hanami/cli/commands/app/db/utils/database.rb +44 -14
  18. data/lib/hanami/cli/commands/app/db/utils/mysql.rb +57 -4
  19. data/lib/hanami/cli/commands/app/db/utils/postgres.rb +10 -8
  20. data/lib/hanami/cli/commands/app/db/version.rb +6 -3
  21. data/lib/hanami/cli/commands/app/generate/command.rb +13 -2
  22. data/lib/hanami/cli/commands/app/generate/migration.rb +20 -0
  23. data/lib/hanami/cli/commands/app/generate/relation.rb +0 -6
  24. data/lib/hanami/cli/commands/app/generate/repo.rb +3 -7
  25. data/lib/hanami/cli/files.rb +22 -0
  26. data/lib/hanami/cli/generators/app/migration.rb +6 -9
  27. data/lib/hanami/cli/generators/app/operation.rb +5 -4
  28. data/lib/hanami/cli/generators/app/relation.rb +5 -4
  29. data/lib/hanami/cli/generators/app/repo.rb +5 -4
  30. data/lib/hanami/cli/generators/app/ruby_file_writer.rb +39 -37
  31. data/lib/hanami/cli/generators/app/struct.rb +5 -4
  32. data/lib/hanami/cli/generators/context.rb +4 -4
  33. data/lib/hanami/cli/generators/gem/app/gitignore.erb +3 -0
  34. data/lib/hanami/cli/version.rb +1 -1
  35. metadata +3 -4
  36. 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: d62ed9aeff86f86693ffdf5acafb274e6e97a535a177bf46131c8c33141fc0e9
4
+ data.tar.gz: 86da7248a20712451a83b8e5167870e34fd508f2e5f41a107d9bf3a4dea1d376
5
5
  SHA512:
6
- metadata.gz: 07052611a88815d7ffc2e228d77b62a65cc708283688c13e3509d3004a186b264d4271e104e8e3dea4dfae52958ccf9e385bc1a95c5c9b732e985538c82bfc7f
7
- data.tar.gz: e6d3c5e3290fb2c19b6a301e11b20d70eb376a5c4da42ed69f7ceafa1590f5b14955b4cc9ce8bc0ad155e1db0ec3851aae06da43849d43441fe282cd94dd9860
6
+ metadata.gz: affe042dfe0daf2dc93e1252b06d2d51dd5ea605c1287ad2500371bed6ee98e902cf9d19667458f86edadabb671f923b986bcd2a76a0e7d8821f4e0b9af6ecf6
7
+ data.tar.gz: b1a097c66c10495ef31fd0783083d542ac6e7a5be7ab94f11b9e1a0d68dc865e04d6f970e08b88d33dd3939b47736cc8893989cb037d070d3c5d2ff86d35ccad
@@ -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,22 @@
2
2
 
3
3
  Hanami Command Line Interface
4
4
 
5
+ ## v2.2.0.beta2 - 2024-09-25
6
+
7
+ ### Added
8
+
9
+ - [Tim Riley] MySQL support for `db` commands (#226)
10
+ - [Tim Riley] Support for multiple gateways in `db` commands (#232, #234, #237, #238)
11
+
12
+ ### Changed
13
+
14
+ - [Kyle Plump, Tim Riley] Delete `.keep` files when generating new files into previously empty directory (#224)
15
+ - [Sean Collins] Add `db/*.sqlite` to the `.gitignore` in new apps (#210)
16
+ - [Sean Collins] Print warnings for misconfigured databases when running `db` commands (#211)
17
+
5
18
  ## v2.2.0.beta1 - 2024-07-16
6
19
 
7
- ## Added
20
+ ### Added
8
21
 
9
22
  - [Sean Collins] Generate db files in `hanami new` and `generate slice`
10
23
  - [Tim Riley] Add `db` commands: `create`, `drop`, `migrate`, `structure dump` `structure load`, `seed` `prepare`, `version`
@@ -13,7 +26,7 @@ Hanami Command Line Interface
13
26
  - [Krzysztof] Add `generate component` command
14
27
  - [Sean Collins] Add `generate operation` command
15
28
 
16
- ## Changed
29
+ ### Changed
17
30
 
18
31
  - Drop support for Ruby 3.0
19
32
 
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
  )
@@ -31,7 +31,7 @@ module Hanami
31
31
  def run_command(klass, ...)
32
32
  klass.new(
33
33
  out: out,
34
- inflector: fs,
34
+ inflector: inflector,
35
35
  fs: fs,
36
36
  system_call: system_call,
37
37
  ).call(...)
@@ -39,59 +39,87 @@ module Hanami
39
39
 
40
40
  private
41
41
 
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
42
+ def databases(app: false, slice: nil, gateway: nil)
43
+ if gateway && !app && !slice
44
+ err.puts "When specifying --gateway, an --app or --slice must also be given"
45
+ exit 1
49
46
  end
50
- end
51
47
 
52
- def database_for_app
53
- build_database(app)
48
+ databases =
49
+ if slice
50
+ [database_for_slice(slice, gateway: gateway)]
51
+ elsif app
52
+ [database_for_slice(self.app, gateway: gateway)]
53
+ else
54
+ all_databases
55
+ end
56
+
57
+ databases.flatten
54
58
  end
55
59
 
56
- def database_for_slice(slice)
60
+ def database_for_slice(slice, gateway: nil)
57
61
  unless slice.is_a?(Class) && slice < Hanami::Slice
58
62
  slice_name = inflector.underscore(Shellwords.shellescape(slice)).to_sym
59
63
  slice = app.slices[slice_name]
60
64
  end
61
65
 
62
- build_database(slice)
66
+ ensure_database_slice slice
67
+
68
+ databases = build_databases(slice)
69
+
70
+ if gateway
71
+ databases.fetch(gateway.to_sym) do
72
+ err.puts %(No gateway "#{gateway}" in #{slice})
73
+ exit 1
74
+ end
75
+ else
76
+ databases.values
77
+ end
63
78
  end
64
79
 
65
- def all_databases
80
+ def all_databases # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
66
81
  slices = [app] + app.slices.with_nested
67
82
 
68
- slices_by_database_url = slices.each_with_object({}) { |slice, hsh|
69
- provider = slice.container.providers[:db]
70
- next unless provider
83
+ slice_gateways_by_database_url = slices.each_with_object({}) { |slice, hsh|
84
+ db_provider_source = slice.container.providers[:db]&.source
85
+ next unless db_provider_source
71
86
 
72
- database_url = provider.source.database_url
73
- hsh[database_url] ||= []
74
- hsh[database_url] << slice
87
+ db_provider_source.database_urls.each do |gateway, url|
88
+ hsh[url] ||= []
89
+ hsh[url] << {slice: slice, gateway: gateway}
90
+ end
75
91
  }
76
92
 
77
- databases = slices_by_database_url.each_with_object([]) { |(url, slices), arr|
78
- slices_with_config = slices.select { _1.root.join("config", "db").directory? }
93
+ slice_gateways_by_database_url.each_with_object([]) { |(url, slice_gateways), arr|
94
+ slice_gateways_with_config = slice_gateways.select {
95
+ _1[:slice].root.join("config", "db").directory?
96
+ }
79
97
 
80
- database = build_database(slices_with_config.first || slices.first)
98
+ db_slice_gateway = slice_gateways_with_config.first || slice_gateways.first
99
+ database = Utils::Database.database_class(url).new(
100
+ slice: db_slice_gateway.fetch(:slice),
101
+ gateway_name: db_slice_gateway.fetch(:gateway),
102
+ system_call: system_call
103
+ )
81
104
 
82
- warn_on_misconfigured_database database, slices_with_config
105
+ warn_on_misconfigured_database database, slice_gateways.map { _1.fetch(:slice) }
83
106
 
84
107
  arr << database
85
108
  }
109
+ end
86
110
 
87
- databases
111
+ def build_databases(slice)
112
+ Utils::Database.from_slice(slice: slice, system_call: system_call)
88
113
  end
89
114
 
90
- def build_database(slice)
91
- Utils::Database[slice, system_call: system_call]
115
+ def ensure_database_slice(slice)
116
+ return if slice.container.providers[:db]
117
+
118
+ out.puts "#{slice} does not have a :db provider."
119
+ exit 1
92
120
  end
93
121
 
94
- def warn_on_misconfigured_database(database, slices)
122
+ def warn_on_misconfigured_database(database, slices) # rubocop:disable Metrics/AbcSize
95
123
  if slices.length > 1
96
124
  out.puts <<~STR
97
125
  WARNING: Database #{database.name} is configured for multiple config/db/ directories:
@@ -101,9 +129,13 @@ module Hanami
101
129
  Migrating database using #{database.slice.slice_name.to_s.inspect} slice only.
102
130
 
103
131
  STR
104
- elsif slices.length < 1
132
+ elsif !database.db_config_dir?
133
+ relative_path = database.slice.root
134
+ .relative_path_from(database.slice.app.root)
135
+ .join("config", "db").to_s
136
+
105
137
  out.puts <<~STR
106
- WARNING: Database #{database.name} has no config/db/ directory.
138
+ WARNING: Database #{database.name} expects the folder #{relative_path}/ to exist but it does not.
107
139
 
108
140
  STR
109
141
  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
 
@@ -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
 
@@ -9,22 +9,29 @@ 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
19
26
  end
20
27
 
21
- run_command(Structure::Dump, app: app, slice: slice, command_exit: command_exit) if dump
28
+ run_command(Structure::Dump, app: app, slice: slice, gateway: gateway, command_exit: command_exit) if dump
22
29
  end
23
30
 
24
31
  private
25
32
 
26
33
  def migrate_database(database, target:)
27
- return true unless migrations?(database)
34
+ return true unless database.migrations_dir?
28
35
 
29
36
  measure "database #{database.name} migrated" do
30
37
  if target
@@ -37,8 +44,33 @@ module Hanami
37
44
  end
38
45
  end
39
46
 
40
- def migrations?(database)
41
- database.migrations_dir? && database.sequel_migrator.files.any?
47
+ def migrations_dir_missing?(database)
48
+ !database.migrations_dir?
49
+ end
50
+
51
+ def no_migrations?(database)
52
+ database.sequel_migrator.files.empty?
53
+ end
54
+
55
+ def warn_on_missing_migrations_dir(database)
56
+ out.puts <<~STR
57
+ WARNING: Database #{database.name} expects migrations to be located within #{relative_migrations_path(database)} but that folder does not exist.
58
+
59
+ No database migrations can be run for this database.
60
+ STR
61
+ end
62
+
63
+ def warn_on_empty_migrations_dir(database)
64
+ out.puts <<~STR
65
+ NOTE: Empty database migrations folder (#{relative_migrations_path(database)}) for #{database.name}
66
+ STR
67
+ end
68
+
69
+ def relative_migrations_path(database)
70
+ database
71
+ .migrations_path
72
+ .relative_path_from(database.slice.app.root)
73
+ .to_s + "/"
42
74
  end
43
75
  end
44
76
  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,33 @@ 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)
36
62
  end
37
63
  end
38
64
  end
@@ -13,7 +13,17 @@ 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
28
  next unless seeds_path.file?
19
29
 
@@ -21,6 +31,8 @@ module Hanami
21
31
  measure "seed data loaded from #{relative_seeds_path}" do
22
32
  load seeds_path.to_s
23
33
  end
34
+
35
+ seeded_slices << database.slice
24
36
  end
25
37
  end
26
38
  end
@@ -11,13 +11,15 @@ module Hanami
11
11
  class Dump < DB::Command
12
12
  desc "Dumps database structure to config/db/structure.sql file"
13
13
 
14
+ option :gateway, required: false, desc: "Use database for gateway"
15
+
14
16
  # @api private
15
- def call(app: false, slice: nil, command_exit: method(:exit), **)
17
+ def call(app: false, slice: nil, gateway: nil, command_exit: method(:exit), **)
16
18
  exit_codes = []
17
19
 
18
- databases(app: app, slice: slice).each do |database|
19
- structure_path = database.slice.root.join("config", "db", "structure.sql")
20
- relative_structure_path = structure_path.relative_path_from(database.slice.app.root)
20
+ databases(app: app, slice: slice, gateway: gateway).each do |database|
21
+ relative_structure_path = database.structure_file
22
+ .relative_path_from(database.slice.app.root)
21
23
 
22
24
  measure("#{database.name} structure dumped to #{relative_structure_path}") do
23
25
  catch :dump_failed do
@@ -29,7 +31,7 @@ module Hanami
29
31
  throw :dump_failed, false
30
32
  end
31
33
 
32
- File.open(structure_path, "a") do |f|
34
+ File.open(database.structure_file, "a") do |f|
33
35
  f.puts "#{database.schema_migrations_sql_dump}\n"
34
36
  end
35
37
 
@@ -14,15 +14,17 @@ module Hanami
14
14
 
15
15
  desc "Loads database from config/db/structure.sql file"
16
16
 
17
+ option :gateway, required: false, desc: "Use database for gateway"
18
+
17
19
  # @api private
18
- def call(app: false, slice: nil, command_exit: method(:exit), **)
20
+ def call(app: false, slice: nil, gateway: nil, command_exit: method(:exit), **)
19
21
  exit_codes = []
20
22
 
21
- databases(app: app, slice: slice).each do |database|
22
- structure_path = database.slice.root.join(STRUCTURE_PATH)
23
- next unless structure_path.exist?
23
+ databases(app: app, slice: slice, gateway: gateway).each do |database|
24
+ next unless database.structure_file.exist?
24
25
 
25
- relative_structure_path = structure_path.relative_path_from(database.slice.app.root)
26
+ relative_structure_path = database.structure_file
27
+ .relative_path_from(database.slice.app.root)
26
28
 
27
29
  measure("#{database.name} structure loaded from #{relative_structure_path}") do
28
30
  catch :load_failed do
@@ -11,9 +11,6 @@ module Hanami
11
11
  # @api private
12
12
  # @since 2.2.0
13
13
  class Database
14
- MIGRATIONS_DIR = "config/db/migrate"
15
- private_constant :MIGRATIONS_DIR
16
-
17
14
  DATABASE_CLASS_RESOLVER = Hash.new { |_, key|
18
15
  raise "#{key} is not a supported db scheme"
19
16
  }.update(
@@ -29,27 +26,40 @@ module Hanami
29
26
  require_relative("postgres")
30
27
  Postgres
31
28
  },
32
- "mysql" => -> {
29
+ "mysql2" => -> {
33
30
  require_relative("mysql")
34
31
  Mysql
35
32
  }
36
33
  ).freeze
37
34
 
38
- def self.[](slice, system_call:)
35
+ def self.database_class(database_url)
36
+ database_scheme = URI(database_url).scheme
37
+ DATABASE_CLASS_RESOLVER[database_scheme].call
38
+ end
39
+
40
+ def self.from_slice(slice:, system_call:)
39
41
  provider = slice.container.providers[:db]
40
- raise "this is not a db slice" unless provider
42
+ raise "No :db provider for #{slice}" unless provider
41
43
 
42
- database_scheme = provider.source.database_url.then { URI(_1).scheme }
43
- database_class = DATABASE_CLASS_RESOLVER[database_scheme].call
44
- database_class.new(slice: slice, system_call: system_call)
44
+ provider.source.database_urls.map { |(gateway_name, database_url)|
45
+ database = database_class(database_url).new(
46
+ slice: slice,
47
+ gateway_name: gateway_name,
48
+ system_call: system_call
49
+ )
50
+
51
+ [gateway_name, database]
52
+ }.to_h
45
53
  end
46
54
 
47
55
  attr_reader :slice
56
+ attr_reader :gateway_name
48
57
 
49
58
  attr_reader :system_call
50
59
 
51
- def initialize(slice:, system_call:)
60
+ def initialize(slice:, gateway_name:, system_call:)
52
61
  @slice = slice
62
+ @gateway_name = gateway_name
53
63
  @system_call = system_call
54
64
  end
55
65
 
@@ -58,7 +68,7 @@ module Hanami
58
68
  end
59
69
 
60
70
  def database_url
61
- slice.container.providers[:db].source.database_url
71
+ slice.container.providers[:db].source.database_urls.fetch(gateway_name)
62
72
  end
63
73
 
64
74
  def database_uri
@@ -66,7 +76,7 @@ module Hanami
66
76
  end
67
77
 
68
78
  def gateway
69
- slice["db.config"].gateways[:default]
79
+ slice["db.config"].gateways[gateway_name]
70
80
  end
71
81
 
72
82
  def connection
@@ -127,8 +137,20 @@ module Hanami
127
137
  sequel_migrator.applied_migrations
128
138
  end
129
139
 
140
+ def db_config_path
141
+ slice.root.join("config", "db")
142
+ end
143
+
144
+ def db_config_dir?
145
+ db_config_path.directory?
146
+ end
147
+
130
148
  def migrations_path
131
- slice.root.join(MIGRATIONS_DIR)
149
+ if gateway_name == :default
150
+ db_config_path.join("migrate")
151
+ else
152
+ db_config_path.join("#{gateway_name}_migrate")
153
+ end
132
154
  end
133
155
 
134
156
  def migrations_dir?
@@ -136,10 +158,18 @@ module Hanami
136
158
  end
137
159
 
138
160
  def structure_file
139
- slice.root.join("config/db/structure.sql")
161
+ path = slice.root.join("config", "db")
162
+
163
+ if gateway_name == :default
164
+ path.join("structure.sql")
165
+ else
166
+ path.join("#{gateway_name}_structure.sql")
167
+ end
140
168
  end
141
169
 
142
170
  def schema_migrations_sql_dump
171
+ return unless migrations_dir?
172
+
143
173
  sql = +"INSERT INTO schema_migrations (filename) VALUES\n"
144
174
  sql << applied_migrations.map { |v| "('#{v}')" }.join(",\n")
145
175
  sql << ";"
@@ -11,18 +11,71 @@ module Hanami
11
11
  # @api private
12
12
  class Mysql < Database
13
13
  # @api private
14
- def create_command
15
- raise Hanami::CLI::NotImplementedError
14
+ def exec_create_command
15
+ return true if exists?
16
+
17
+ exec_cli("mysql", %(-e "CREATE DATABASE #{escaped_name}"))
18
+ end
19
+
20
+ # @api private
21
+ # @since 2.2.0
22
+ def exec_drop_command
23
+ return true unless exists?
24
+
25
+ exec_cli("mysql", %(-e "DROP DATABASE #{escaped_name}"))
26
+ end
27
+
28
+ # @api private
29
+ # @since 2.2.0
30
+ def exists?
31
+ result = exec_cli("mysql", %(-e "SHOW DATABASES LIKE '#{name}'" --batch))
32
+
33
+ result.successful? && result.out != ""
16
34
  end
17
35
 
18
36
  # @api private
37
+ # @since 2.2.0
19
38
  def exec_dump_command
20
- raise Hanami::CLI::NotImplementedError
39
+ exec_cli(
40
+ "mysqldump",
41
+ "--no-data --routines --skip-comments --result-file=#{structure_file} #{escaped_name}"
42
+ )
21
43
  end
22
44
 
23
45
  # @api private
46
+ # @since 2.2.0
24
47
  def exec_load_command
25
- raise Hanami::CLI::NotImplementedError
48
+ exec_cli(
49
+ "mysql",
50
+ %(--execute "SET FOREIGN_KEY_CHECKS = 0; SOURCE #{structure_file}; SET FOREIGN_KEY_CHECKS = 1" --database #{escaped_name})
51
+ )
52
+ end
53
+
54
+ private
55
+
56
+ def escaped_name
57
+ Shellwords.escape(name)
58
+ end
59
+
60
+ def exec_cli(cli_name, cli_args)
61
+ system_call.call(
62
+ "#{cli_name} #{cli_options} #{cli_args}",
63
+ env: cli_env_vars
64
+ )
65
+ end
66
+
67
+ def cli_options
68
+ [].tap { |opts|
69
+ opts << "--host=#{Shellwords.escape(database_uri.host)}" if database_uri.host
70
+ opts << "--port=#{Shellwords.escape(database_uri.port)}" if database_uri.port
71
+ opts << "--user=#{Shellwords.escape(database_uri.user)}" if database_uri.user
72
+ }.join(" ")
73
+ end
74
+
75
+ def cli_env_vars
76
+ @cli_env_vars ||= {}.tap do |vars|
77
+ vars["MYSQL_PWD"] = database_uri.password.to_s if database_uri.password
78
+ end
26
79
  end
27
80
  end
28
81
  end
@@ -54,6 +54,16 @@ module Hanami
54
54
  )
55
55
  end
56
56
 
57
+ def schema_migrations_sql_dump
58
+ search_path = slice["db.gateway"].connection
59
+ .fetch("SHOW search_path").to_a.first
60
+ .fetch(:search_path)
61
+
62
+ +"SET search_path TO #{search_path};\n\n" << super
63
+ end
64
+
65
+ private
66
+
57
67
  def escaped_name
58
68
  Shellwords.escape(name)
59
69
  end
@@ -66,14 +76,6 @@ module Hanami
66
76
  vars["PGPASSWORD"] = database_uri.password.to_s if database_uri.password
67
77
  end
68
78
  end
69
-
70
- def schema_migrations_sql_dump
71
- search_path = slice["db.gateway"].connection
72
- .fetch("SHOW search_path").to_a.first
73
- .fetch(:search_path)
74
-
75
- +"SET search_path TO #{search_path};\n\n" << super
76
- end
77
79
  end
78
80
  end
79
81
  end
@@ -9,11 +9,14 @@ module Hanami
9
9
  class Version < DB::Command
10
10
  desc "Print schema version"
11
11
 
12
+ option :gateway, required: false, desc: "Use database for gateway"
13
+
12
14
  # @api private
13
- def call(app: false, slice: nil, **)
14
- databases(app: app, slice: slice).each do |database|
15
+ def call(app: false, slice: nil, gateway: nil, **)
16
+ databases(app: app, slice: slice, gateway: gateway).each do |database|
15
17
  unless database.migrations_dir?
16
- out.puts "=> Cannot find version for slice #{database.slice.slice_name.to_s.inspect}: missing config/db/migrate/ dir"
18
+ relative_migrations_path = database.migrations_path.relative_path_from(database.slice.app.root)
19
+ out.puts "=> Cannot find version for database #{database.name}: no migrations directory at #{relative_migrations_path}/"
17
20
  return
18
21
  end
19
22
 
@@ -38,8 +38,19 @@ module Hanami
38
38
  # @since 2.2.0
39
39
  # @api private
40
40
  def call(name:, slice: nil, **)
41
- normalized_slice = inflector.underscore(slice) if slice
42
- generator.call(app.namespace, name, normalized_slice)
41
+ if slice
42
+ generator.call(
43
+ key: name,
44
+ namespace: slice,
45
+ base_path: fs.join("slices", inflector.underscore(slice))
46
+ )
47
+ else
48
+ generator.call(
49
+ key: name,
50
+ namespace: app.namespace,
51
+ base_path: "app"
52
+ )
53
+ end
43
54
  end
44
55
  end
45
56
  end
@@ -9,16 +9,36 @@ module Hanami
9
9
  # @api private
10
10
  class Migration < Command
11
11
  argument :name, required: true, desc: "Migration name"
12
+ option :gateway, desc: "Generate migration for gateway"
12
13
 
13
14
  example [
14
15
  %(create_posts),
15
16
  %(add_published_at_to_posts),
16
17
  %(create_users --slice=admin),
18
+ %(create_comments --slice=admin --gateway=extra),
17
19
  ]
18
20
 
19
21
  def generator_class
20
22
  Generators::App::Migration
21
23
  end
24
+
25
+ def call(name:, slice: nil, gateway: nil)
26
+ if slice
27
+ generator.call(
28
+ key: name,
29
+ namespace: slice,
30
+ base_path: fs.join("slices", inflector.underscore(slice)),
31
+ gateway: gateway
32
+ )
33
+ else
34
+ generator.call(
35
+ key: name,
36
+ namespace: app.namespace,
37
+ base_path: "app",
38
+ gateway: gateway
39
+ )
40
+ end
41
+ end
22
42
  end
23
43
  end
24
44
  end
@@ -21,12 +21,6 @@ module Hanami
21
21
  def generator_class
22
22
  Generators::App::Relation
23
23
  end
24
-
25
- # @since 2.2.0
26
- # @api private
27
- def call(name:, slice: nil, **opts)
28
- super(name: name, slice: slice, **opts)
29
- end
30
24
  end
31
25
  end
32
26
  end
@@ -30,13 +30,9 @@ module Hanami
30
30
 
31
31
  # @since 2.2.0
32
32
  # @api private
33
- def call(name:, slice: nil, **opts)
34
- normalized_name = if name.end_with?("_repo")
35
- name
36
- else
37
- "#{inflector.singularize(name)}_repo"
38
- end
39
- super(name: normalized_name, slice: slice, **opts)
33
+ def call(name:, **opts)
34
+ name = "#{inflector.singularize(name)}_repo" unless name.end_with?("_repo")
35
+ super
40
36
  end
41
37
  end
42
38
  end
@@ -18,7 +18,11 @@ module Hanami
18
18
  # @api private
19
19
  def write(path, *content)
20
20
  already_exists = exist?(path)
21
+
21
22
  super
23
+
24
+ delete_keepfiles(path) unless already_exists
25
+
22
26
  if already_exists
23
27
  updated(path)
24
28
  else
@@ -46,6 +50,24 @@ module Hanami
46
50
 
47
51
  attr_reader :out
48
52
 
53
+ # Removes .keep files in any directories leading up to the given path.
54
+ #
55
+ # Does not attempt to remove `.keep` files in the following scenarios:
56
+ # - When the given path is a `.keep` file itself.
57
+ # - When the given path is absolute, since ascending up this path may lead to removal of
58
+ # files outside the Hanami project directory.
59
+ def delete_keepfiles(path)
60
+ path = Pathname(path)
61
+
62
+ return if path.absolute?
63
+ return if path.relative_path_from(path.dirname).to_s == ".keep"
64
+
65
+ path.dirname.ascend do |part|
66
+ keepfile = (part + ".keep").to_path
67
+ delete(keepfile) if exist?(keepfile)
68
+ end
69
+ end
70
+
49
71
  def updated(path)
50
72
  out.puts "Updated #{path}"
51
73
  end
@@ -17,17 +17,14 @@ module Hanami
17
17
 
18
18
  # @since 2.2.0
19
19
  # @api private
20
- def call(_app_namespace, name, slice, **_opts)
21
- normalized_name = inflector.underscore(name)
22
- ensure_valid_name(normalized_name)
20
+ def call(key:, base_path:, gateway: nil, **_opts)
21
+ name = inflector.underscore(key)
22
+ ensure_valid_name(name)
23
23
 
24
- base = if slice
25
- fs.join("slices", slice, "config", "db", "migrate")
26
- else
27
- fs.join("config", "db", "migrate")
28
- end
24
+ base_path = nil if base_path == "app" # Migrations are in the root dir, not app/
25
+ migrate_dir = gateway ? "#{gateway}_migrate" : "migrate"
29
26
 
30
- path = fs.join(base, file_name(normalized_name))
27
+ path = fs.join(*[base_path, "config", "db", migrate_dir, file_name(name)].compact)
31
28
 
32
29
  fs.write(path, FILE_CONTENTS)
33
30
  end
@@ -19,16 +19,17 @@ module Hanami
19
19
 
20
20
  # @since 2.2.0
21
21
  # @api private
22
- def call(app_namespace, key, slice)
22
+ def call(key:, namespace:, base_path:)
23
23
  RubyFileWriter.new(
24
24
  fs: fs,
25
25
  inflector: inflector,
26
- app_namespace: app_namespace,
26
+ ).call(
27
+ namespace: namespace,
28
+ base_path: base_path,
27
29
  key: key,
28
- slice: slice,
29
30
  relative_parent_class: "Operation",
30
31
  body: ["def call", "end"],
31
- ).call
32
+ )
32
33
 
33
34
  unless key.match?(KEY_SEPARATOR)
34
35
  out.puts(
@@ -19,19 +19,20 @@ module Hanami
19
19
 
20
20
  # @since 2.2.0
21
21
  # @api private
22
- def call(app_namespace, key, slice)
22
+ def call(key:, namespace:, base_path:)
23
23
  schema_name = key.split(KEY_SEPARATOR).last
24
24
 
25
25
  RubyFileWriter.new(
26
26
  fs: fs,
27
27
  inflector: inflector,
28
- app_namespace: app_namespace,
28
+ ).call(
29
+ namespace: namespace,
29
30
  key: key,
30
- slice: slice,
31
+ base_path: base_path,
31
32
  extra_namespace: "Relations",
32
33
  relative_parent_class: "DB::Relation",
33
34
  body: ["schema :#{schema_name}, infer: true"],
34
- ).call
35
+ )
35
36
  end
36
37
 
37
38
  private
@@ -17,17 +17,18 @@ module Hanami
17
17
 
18
18
  # @since 2.2.0
19
19
  # @api private
20
- def call(app_namespace, key, slice)
20
+ def call(key:, namespace:, base_path:)
21
21
  RubyFileWriter.new(
22
22
  fs: fs,
23
23
  inflector: inflector,
24
- app_namespace: app_namespace,
24
+ ).call(
25
25
  key: key,
26
- slice: slice,
26
+ namespace: namespace,
27
+ base_path: base_path,
27
28
  extra_namespace: "Repos",
28
29
  relative_parent_class: "DB::Repo",
29
30
  body: [],
30
- ).call
31
+ )
31
32
  end
32
33
 
33
34
  private
@@ -14,31 +14,55 @@ module Hanami
14
14
  class RubyFileWriter
15
15
  # @since 2.2.0
16
16
  # @api private
17
+ def initialize(fs:, inflector:)
18
+ @fs = fs
19
+ @inflector = inflector
20
+ end
21
+
22
+ # @since 2.2.0
23
+ # @api private
24
+ def call(key:, namespace:, base_path:, relative_parent_class:, extra_namespace: nil, body: [])
25
+ ClassFile.new(
26
+ fs: fs,
27
+ inflector: inflector,
28
+ key: key,
29
+ namespace: namespace,
30
+ base_path: base_path,
31
+ relative_parent_class: relative_parent_class,
32
+ extra_namespace: extra_namespace,
33
+ body: body,
34
+ ).write
35
+ end
36
+
37
+ private
38
+
39
+ # @since 2.2.0
40
+ # @api private
41
+ attr_reader :fs, :inflector
42
+ end
43
+
44
+ class ClassFile
17
45
  def initialize(
18
46
  fs:,
19
47
  inflector:,
20
- app_namespace:,
21
48
  key:,
22
- slice:,
49
+ namespace:,
50
+ base_path:,
23
51
  relative_parent_class:,
24
52
  extra_namespace: nil,
25
53
  body: []
26
54
  )
27
55
  @fs = fs
28
56
  @inflector = inflector
29
- @app_namespace = app_namespace
30
57
  @key = key
31
- @slice = slice
58
+ @namespace = namespace
59
+ @base_path = base_path
32
60
  @extra_namespace = extra_namespace&.downcase
33
61
  @relative_parent_class = relative_parent_class
34
62
  @body = body
35
- raise_missing_slice_error_if_missing(slice) if slice
36
63
  end
37
64
 
38
- # @since 2.2.0
39
- # @api private
40
- def call
41
- fs.mkdir(directory)
65
+ def write
42
66
  fs.write(path, file_contents)
43
67
  end
44
68
 
@@ -49,9 +73,9 @@ module Hanami
49
73
  attr_reader(
50
74
  :fs,
51
75
  :inflector,
52
- :app_namespace,
53
76
  :key,
54
- :slice,
77
+ :namespace,
78
+ :base_path,
55
79
  :extra_namespace,
56
80
  :relative_parent_class,
57
81
  :body,
@@ -62,7 +86,6 @@ module Hanami
62
86
  def file_contents
63
87
  class_definition(
64
88
  class_name: class_name,
65
- container_namespace: container_namespace,
66
89
  local_namespaces: local_namespaces,
67
90
  )
68
91
  end
@@ -73,12 +96,6 @@ module Hanami
73
96
  key.split(KEY_SEPARATOR)[-1]
74
97
  end
75
98
 
76
- # @since 2.2.0
77
- # @api private
78
- def container_namespace
79
- slice || app_namespace
80
- end
81
-
82
99
  # @since 2.2.0
83
100
  # @api private
84
101
  def local_namespaces
@@ -88,16 +105,10 @@ module Hanami
88
105
  # @since 2.2.0
89
106
  # @api private
90
107
  def directory
91
- base = if slice
92
- fs.join("slices", slice)
93
- else
94
- fs.join("app")
95
- end
96
-
97
108
  @directory ||= if local_namespaces.any?
98
- fs.join(base, local_namespaces)
109
+ fs.join(base_path, local_namespaces)
99
110
  else
100
- fs.join(base)
111
+ base_path
101
112
  end
102
113
  end
103
114
 
@@ -109,8 +120,8 @@ module Hanami
109
120
 
110
121
  # @since 2.2.0
111
122
  # @api private
112
- def class_definition(class_name:, container_namespace:, local_namespaces:)
113
- container_module = normalize(container_namespace)
123
+ def class_definition(class_name:, local_namespaces:)
124
+ container_module = normalize(namespace)
114
125
 
115
126
  modules = local_namespaces
116
127
  .map { normalize(_1) }
@@ -133,15 +144,6 @@ module Hanami
133
144
  def normalize(name)
134
145
  inflector.camelize(name).gsub(/[^\p{Alnum}]/, "")
135
146
  end
136
-
137
- # @since 2.2.0
138
- # @api private
139
- def raise_missing_slice_error_if_missing(slice)
140
- if slice
141
- slice_directory = fs.join("slices", slice)
142
- raise MissingSliceError.new(slice) unless fs.directory?(slice_directory)
143
- end
144
- end
145
147
  end
146
148
  end
147
149
  end
@@ -17,16 +17,17 @@ module Hanami
17
17
 
18
18
  # @since 2.2.0
19
19
  # @api private
20
- def call(app_namespace, key, slice)
20
+ def call(key:, namespace:, base_path:)
21
21
  RubyFileWriter.new(
22
22
  fs: fs,
23
23
  inflector: inflector,
24
- app_namespace: app_namespace,
24
+ ).call(
25
25
  key: key,
26
- slice: slice,
26
+ namespace: namespace,
27
+ base_path: base_path,
27
28
  extra_namespace: "Structs",
28
29
  relative_parent_class: "DB::Struct",
29
- ).call
30
+ )
30
31
  end
31
32
 
32
33
  private
@@ -89,19 +89,19 @@ module Hanami
89
89
  # @since 2.2.0
90
90
  # @api private
91
91
  def generate_sqlite?
92
- database_option == Commands::Gem::New::DATABASE_SQLITE
92
+ generate_db? && database_option == Commands::Gem::New::DATABASE_SQLITE
93
93
  end
94
94
 
95
95
  # @since 2.2.0
96
96
  # @api private
97
97
  def generate_postgres?
98
- database_option == Commands::Gem::New::DATABASE_POSTGRES
98
+ generate_db? && database_option == Commands::Gem::New::DATABASE_POSTGRES
99
99
  end
100
100
 
101
101
  # @since 2.2.0
102
102
  # @api private
103
103
  def generate_mysql?
104
- database_option == Commands::Gem::New::DATABASE_MYSQL
104
+ generate_db? && database_option == Commands::Gem::New::DATABASE_MYSQL
105
105
  end
106
106
 
107
107
  # @since 2.2.0
@@ -112,7 +112,7 @@ module Hanami
112
112
  elsif generate_postgres?
113
113
  "postgres://localhost/#{app}"
114
114
  elsif generate_mysql?
115
- "mysql://localhost/#{app}"
115
+ "mysql2://localhost/#{app}"
116
116
  else
117
117
  raise "Unknown database option: #{database_option}"
118
118
  end
@@ -4,3 +4,6 @@ log/*
4
4
  public/
5
5
  node_modules/
6
6
  <%- end -%>
7
+ <%- if generate_sqlite? -%>
8
+ db/*.sqlite
9
+ <%- end -%>
@@ -6,6 +6,6 @@ module Hanami
6
6
  #
7
7
  # @api public
8
8
  # @since 2.0.0
9
- VERSION = "2.2.0.beta1"
9
+ VERSION = "2.2.0.beta2"
10
10
  end
11
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hanami-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0.beta1
4
+ version: 2.2.0.beta2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luca Guidi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-07-16 00:00:00.000000000 Z
11
+ date: 2024-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -263,7 +263,6 @@ files:
263
263
  - lib/hanami/cli/generators/app/slice/app_css.erb
264
264
  - lib/hanami/cli/generators/app/slice/app_js.erb
265
265
  - lib/hanami/cli/generators/app/slice/app_layout.erb
266
- - lib/hanami/cli/generators/app/slice/entities.erb
267
266
  - lib/hanami/cli/generators/app/slice/favicon.ico
268
267
  - lib/hanami/cli/generators/app/slice/helpers.erb
269
268
  - lib/hanami/cli/generators/app/slice/keep.erb
@@ -354,7 +353,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
354
353
  - !ruby/object:Gem::Version
355
354
  version: '0'
356
355
  requirements: []
357
- rubygems_version: 3.5.9
356
+ rubygems_version: 3.5.16
358
357
  signing_key:
359
358
  specification_version: 4
360
359
  summary: Hanami CLI
@@ -1,9 +0,0 @@
1
- # auto_register: false
2
- # frozen_string_literal: true
3
-
4
- module <%= camelized_slice_name %>
5
- module Entities
6
- end
7
- end
8
-
9
- Dir[File.join(__dir__, "entities", "*.rb")].each(&method(:require))