hanami-cli 2.2.0.beta1 → 2.2.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
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))