EatingKing commited on
Commit
71c7618
·
1 Parent(s): 2ec2a83

Upload 150 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. model-main/.github/FUNDING.yml +1 -0
  2. model-main/.github/workflows/ci.yml +76 -0
  3. model-main/.gitignore +21 -0
  4. model-main/.jrubyrc +1 -0
  5. model-main/.rspec +2 -0
  6. model-main/.rubocop.yml +18 -0
  7. model-main/.yardopts +5 -0
  8. model-main/CHANGELOG.md +316 -0
  9. model-main/Gemfile +21 -0
  10. model-main/LICENSE.md +22 -0
  11. model-main/README.md +303 -0
  12. model-main/Rakefile +14 -0
  13. model-main/hanami-model.gemspec +36 -0
  14. model-main/lib/hanami-model.rb +3 -0
  15. model-main/lib/hanami/entity.rb +217 -0
  16. model-main/lib/hanami/entity/schema.rb +269 -0
  17. model-main/lib/hanami/model.rb +111 -0
  18. model-main/lib/hanami/model/association.rb +45 -0
  19. model-main/lib/hanami/model/associations/belongs_to.rb +109 -0
  20. model-main/lib/hanami/model/associations/dsl.rb +40 -0
  21. model-main/lib/hanami/model/associations/has_many.rb +214 -0
  22. model-main/lib/hanami/model/associations/has_one.rb +167 -0
  23. model-main/lib/hanami/model/associations/many_to_many.rb +201 -0
  24. model-main/lib/hanami/model/configuration.rb +183 -0
  25. model-main/lib/hanami/model/configurator.rb +94 -0
  26. model-main/lib/hanami/model/entity_name.rb +39 -0
  27. model-main/lib/hanami/model/error.rb +133 -0
  28. model-main/lib/hanami/model/mapped_relation.rb +61 -0
  29. model-main/lib/hanami/model/mapping.rb +62 -0
  30. model-main/lib/hanami/model/migration.rb +33 -0
  31. model-main/lib/hanami/model/migrator.rb +394 -0
  32. model-main/lib/hanami/model/migrator/adapter.rb +225 -0
  33. model-main/lib/hanami/model/migrator/connection.rb +169 -0
  34. model-main/lib/hanami/model/migrator/logger.rb +35 -0
  35. model-main/lib/hanami/model/migrator/mysql_adapter.rb +98 -0
  36. model-main/lib/hanami/model/migrator/postgres_adapter.rb +125 -0
  37. model-main/lib/hanami/model/migrator/sqlite_adapter.rb +126 -0
  38. model-main/lib/hanami/model/plugins.rb +27 -0
  39. model-main/lib/hanami/model/plugins/mapping.rb +57 -0
  40. model-main/lib/hanami/model/plugins/schema.rb +57 -0
  41. model-main/lib/hanami/model/plugins/timestamps.rb +120 -0
  42. model-main/lib/hanami/model/relation_name.rb +26 -0
  43. model-main/lib/hanami/model/sql.rb +165 -0
  44. model-main/lib/hanami/model/sql/console.rb +45 -0
  45. model-main/lib/hanami/model/sql/consoles/abstract.rb +35 -0
  46. model-main/lib/hanami/model/sql/consoles/mysql.rb +65 -0
  47. model-main/lib/hanami/model/sql/consoles/postgresql.rb +83 -0
  48. model-main/lib/hanami/model/sql/consoles/sqlite.rb +48 -0
  49. model-main/lib/hanami/model/sql/entity/schema.rb +143 -0
  50. model-main/lib/hanami/model/sql/types.rb +131 -0
model-main/.github/FUNDING.yml ADDED
@@ -0,0 +1 @@
 
 
1
+ github: hanami
model-main/.github/workflows/ci.yml ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: ci
2
+
3
+ "on":
4
+ push:
5
+ paths:
6
+ - ".github/workflows/ci.yml"
7
+ - "lib/**"
8
+ - "*.gemspec"
9
+ - "spec/**"
10
+ - "Rakefile"
11
+ - "Gemfile"
12
+ - ".rubocop.yml"
13
+ - "script/ci"
14
+ pull_request:
15
+ branches:
16
+ - main
17
+ create:
18
+
19
+ jobs:
20
+ tests:
21
+ runs-on: ubuntu-latest
22
+ strategy:
23
+ fail-fast: false
24
+ matrix:
25
+ ruby:
26
+ - "2.7"
27
+ db:
28
+ - sqlite3
29
+ - mysql
30
+ - postgresql
31
+ env:
32
+ DB: ${{matrix.db}}
33
+ steps:
34
+ - uses: actions/checkout@v1
35
+ - name: Install package dependencies
36
+ run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS"
37
+ - name: Set up Ruby
38
+ uses: ruby/setup-ruby@v1
39
+ with:
40
+ bundler-cache: true
41
+ ruby-version: ${{matrix.ruby}}
42
+ - name: Run all tests
43
+ env:
44
+ HANAMI_DATABASE_USERNAME: root
45
+ HANAMI_DATABASE_PASSWORD: root
46
+ HANAMI_DATABASE_HOST: 127.0.0.1
47
+ HANAMI_DATABASE_NAME: hanami_model
48
+ run: script/ci
49
+ services:
50
+ mysql:
51
+ image: mysql:8
52
+ env:
53
+ ALLOW_EMPTY_PASSWORD: true
54
+ MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password
55
+ MYSQL_ROOT_PASSWORD: root
56
+ MYSQL_DATABASE: hanami_model
57
+ ports:
58
+ - 3306:3306
59
+ options: >-
60
+ --health-cmd="mysqladmin ping"
61
+ --health-interval=10s
62
+ --health-timeout=5s
63
+ --health-retries=3
64
+ postgres:
65
+ image: postgres:13
66
+ ports:
67
+ - 5432:5432
68
+ options: >-
69
+ --health-cmd pg_isready
70
+ --health-interval 10s
71
+ --health-timeout 5s
72
+ --health-retries 5
73
+ env:
74
+ POSTGRES_USER: root
75
+ POSTGRES_PASSWORD: root
76
+ POSTGRES_DB: hanami_model
model-main/.gitignore ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .DS_Store
6
+ .greenbar
7
+ .ruby-version
8
+ .yardoc
9
+ .rubocop-*
10
+ _yardoc
11
+ coverage
12
+ doc/
13
+ Gemfile.lock
14
+ InstalledFiles
15
+ lib/bundler/man
16
+ pkg
17
+ rdoc
18
+ spec/reports
19
+ test/tmp
20
+ test/version_tmp
21
+ tmp
model-main/.jrubyrc ADDED
@@ -0,0 +1 @@
 
 
1
+ debug.fullTrace=true
model-main/.rspec ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ --color
2
+ --require spec_helper
model-main/.rubocop.yml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Please keep AllCops, Bundler, Style, Metrics groups and then order cops
2
+ # alphabetically
3
+ inherit_from:
4
+ - https://raw.githubusercontent.com/hanami/devtools/1.3.x/.rubocop.yml
5
+ Naming/MethodParameterName:
6
+ AllowedNames:
7
+ - ci
8
+ - db
9
+ - id
10
+ - os
11
+ Layout/LineLength:
12
+ Enabled: false
13
+ Naming/RescuedExceptionsVariableName:
14
+ PreferredName: "exception"
15
+ Style/RescueStandardError:
16
+ Enabled: false
17
+ Style/DateTime:
18
+ Enabled: false
model-main/.yardopts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ --protected
2
+ --private
3
+ -
4
+ LICENSE.md
5
+ lib/**/*.rb
model-main/CHANGELOG.md ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hanami::Model
2
+ A persistence layer for Hanami
3
+
4
+ ## v1.3.3 - 2021-05-22
5
+ ### Fixed
6
+ - [Sean Collins] Specify dependency on BigDecimal v1.4
7
+ - [Adam Daniels] Use environment variables for PostgreSQL CLI tools
8
+
9
+ ## v1.3.2 - 2019-01-31
10
+ ### Fixed
11
+ - [Luca Guidi] Depend on `dry-logic` `~> 0.4.2`, `< 0.5`
12
+
13
+ ## v1.3.1 - 2019-01-18
14
+ ### Added
15
+ - [Luca Guidi] Official support for Ruby: MRI 2.6
16
+ - [Luca Guidi] Support `bundler` 2.0+
17
+
18
+ ## v1.3.0 - 2018-10-24
19
+
20
+ ## v1.3.0.beta1 - 2018-08-08
21
+ ### Fixed
22
+ - [Luca Guidi] Print meaningful error message when connection URL is misconfigured (eg. `Unknown database adapter for URL: "". Please check your database configuration (hint: ENV['DATABASE_URL']).`)
23
+ - [Ian Ker-Seymer] Reliably parse query params from connection string
24
+
25
+ ## v1.2.0 - 2018-04-11
26
+
27
+ ## v1.2.0.rc2 - 2018-04-06
28
+
29
+ ## v1.2.0.rc1 - 2018-03-30
30
+ ### Fixed
31
+ - [Marcello Rocha & Luca Guidi] Ensure repository relations to access database attributes via `#[]` (eg. `projects[:name].ilike("Hanami")`)
32
+
33
+ ## v1.2.0.beta2 - 2018-03-23
34
+
35
+ ## v1.2.0.beta1 - 2018-02-28
36
+ ### Added
37
+ - [Luca Guidi] Official support for Ruby: MRI 2.5
38
+ - [Marcello Rocha] Introduce `Hanami::Repository#command` as a factory for custom database commands. This is useful to create custom bulk operations.
39
+
40
+ ## v1.1.0 - 2017-10-25
41
+ ### Fixed
42
+ - [Luca Guidi] Ensure associations to always accept objects that are serializable into `::Hash`
43
+
44
+ ## v1.1.0.rc1 - 2017-10-16
45
+ ### Added
46
+ - [Marcello Rocha] Added support for associations aliasing via `:as` option (`has_many :users, through: :comments, as: :authors`)
47
+ - [Luca Guidi] Allow entities to be used as type in entities manual schema (`attribute :owner, Types::Entity(User)`)
48
+
49
+ ## v1.1.0.beta3 - 2017-10-04
50
+
51
+ ## v1.1.0.beta2 - 2017-10-03
52
+ ### Added
53
+ - [Alfonso Uceda] Introduce `Hanami::Model::Migrator#rollback` to provide database migrations rollback
54
+ - [Alfonso Uceda] Improve connection string for PostgreSQL in order to pass credentials as URI query string
55
+
56
+ ### Fixed
57
+ - [Marcello Rocha] One-To-Many properly destroy the associated methods
58
+
59
+ ## v1.1.0.beta1 - 2017-08-11
60
+ ### Added
61
+ - [Marcello Rocha] Many-To-One association (aka `belongs_to`)
62
+ - [Marcello Rocha] One-To-One association (aka `has_one`)
63
+ - [Marcello Rocha] Many-To-Many association (aka `has_many :through`)
64
+ - [Luca Guidi] Introduced new extra behaviors for entity manual schema: `:schema` (default), `:strict`, `:weak`, and `:permissive`
65
+
66
+ ### Fixed
67
+ - [Sean Collins] Enhanced error message for Postgres `db create` and `db drop` when `createdb` and `dropdb` aren't in `PATH`
68
+
69
+ ## v1.0.4 - 2017-10-14
70
+ ### Fixed
71
+ - [Nikita Shilnikov] Keep the dependency on `rom-sql` at `~> 1.3`, which is compatible with `dry-types` `~> 0.11.0`
72
+ - [Nikita Shilnikov] Ensure to write Postgres JSON (`PGJSON`) type for nested associated records
73
+ - [Nikita Shilnikov] Ensure `Repository#select` to work with `Hanami::Model::MappedRelation`
74
+
75
+ ## v1.0.3 - 2017-10-11
76
+ ### Fixed
77
+ - [Luca Guidi] Keep the dependency on `dry-types` at `~> 0.11.0`
78
+
79
+ ## v1.0.2 - 2017-08-04
80
+ ### Fixed
81
+ - [Maurizio De Magnis] URI escape for Postgres password
82
+ - [Marion Duprey] Ensure repository to generate timestamps values even when only one between `created_at` and `updated_at` is present
83
+ - [Paweł Świątkowski] Make Postgres JSON(B) to work with Ruby arrays
84
+ - [Luca Guidi] Don't remove migrations when running `Hanami::Model::Migrator#apply` fails to dump the database
85
+
86
+ ## v1.0.1 - 2017-06-23
87
+ ### Fixed
88
+ - [Kai Kuchenbecker & Marcello Rocha & Luca Guidi] Ensure `Hanami::Entity#initialize` to not serialize (into `Hash`) other entities passed as an argument
89
+ - [Luca Guidi] Let `Hanami::Repository.relation=` to accept strings as an argument
90
+ - [Nikita Shilnikov] Prevent stack-overflow when `Hanami::Repository#update` is called thousand times
91
+
92
+ ## v1.0.0 - 2017-04-06
93
+
94
+ ## v1.0.0.rc1 - 2017-03-31
95
+
96
+ ## v1.0.0.beta3 - 2017-03-17
97
+ ### Added
98
+ - [Luca Guidi] Introduced `Hanami::Model.disconnect` to disconnect all the active database connections
99
+
100
+ ## v1.0.0.beta2 - 2017-03-02
101
+ ### Added
102
+ - [Semyon Pupkov] Allow to define Postgres connection URL as `"postgresql:///mydb?host=localhost&port=6433&user=postgres&password=testpasswd"`
103
+
104
+ ### Fixed
105
+ - [Marcello Rocha] Fixed migrations MySQL detection of username and password
106
+ - [Luca Guidi] Fixed migrations creation/drop of a MySQL database with a dash in the name
107
+ - [Semyon Pupkov] Ensure `db console` to work when Postgres connection URL is defined with `"postgresql://"` scheme
108
+
109
+ ## v1.0.0.beta1 - 2017-02-14
110
+ ### Added
111
+ - [Luca Guidi] Official support for Ruby: MRI 2.4
112
+ - [Luca Guidi] Introduced `Repository#read` to fetch from database with raw SQL string
113
+ - [Luca Guidi] Introduced `Repository.schema` to manually configure the schema of a database table. This is useful for legacy databases where Hanami::Model autoinferring doesn't map correctly the schema.
114
+ - [Luca Guidi & Alfonso Uceda] Added `Hanami::Model::Configuration#gateway` to configure gateway and the raw connection
115
+ - [Luca Guidi] Added `Hanami::Model::Configuration#logger` to configure a logger
116
+ - [Luca Guidi] Database operations (including migrations) print informations to standard output
117
+
118
+ ### Fixed
119
+ - [Thorbjørn Hermansen] Ensure repository to not override given timestamps
120
+ - [Luca Guidi] Raise `Hanami::Model::MissingPrimaryKeyError` if `Repository#find` is ran against a database w/o a primary key
121
+ - [Alfonso Uceda] Ensure SQLite databases to be used on JRuby when the database path is in the same directory of the Ruby script (eg. `./test.sqlite`)
122
+
123
+ ### Changed
124
+ - [Luca Guidi] Automap the main relation in a repository, by removing the need of use `.as(:entity)`
125
+ - [Luca Guidi] Raise an `Hanami::Model::UnknownDatabaseTypeError` when the application is loaded and there is an unknown column type in the database
126
+
127
+ ## v0.7.0 - 2016-11-15
128
+ ### Added
129
+ - [Luca Guidi] `Hanami::Entity` defines an automatic schema for SQL databases
130
+ – [Luca Guidi] `Hanami::Entity` attributes schema
131
+ - [Luca Guidi] Experimental support for One-To-Many association (aka `has_many`)
132
+ - [Luca Guidi] Native support for PostgreSQL types like UUID, Array, JSON(B) and Money
133
+ - [Luca Guidi] Repositories instances can access all the relations (eg. `BookRepository` can access `users` relation via `#users`)
134
+ - [Luca Guidi] Automapping for SQL databases
135
+ - [Luca Guidi] Added `Hanami::Model::DatabaseError`
136
+
137
+ ### Changed
138
+ - [Luca Guidi] Entities are immutable
139
+ - [Luca Guidi] Removed support for Memory and File System adapters
140
+ - [Luca Guidi] Removed support for _dirty tracking_
141
+ - [Luca Guidi] `Hanami::Entity.attributes` method no longer accepts a list of attributes, but a block to optionally define typed attributes
142
+ - [Luca Guidi] Removed `#fetch`, `#execute` and `#transaction` from repository
143
+ - [Luca Guidi] Removed `mapping` block from `Hanami::Model.configure`
144
+ - [Luca Guidi] Changed `adapter` signature in `Hanami::Model.configure` (use `adapter :sql, ENV['DATABASE_URL']`)
145
+ - [Luca Guidi] Repositories must inherit from `Hanami::Repository` instead of including it
146
+ - [Luca Guidi] Entities must inherit from `Hanami::Entity` instead of including it
147
+ - [Pascal Betz] Repositories use instance level interface (eg. `BookRepository.new.find` instead of `BookRepository.find`)
148
+ - [Luca Guidi] Repositories now accept hashes for CRUD operations
149
+ - [Luca Guidi] `Hanami::Repository#create` now accepts: hash (or entity)
150
+ - [Luca Guidi] `Hanami::Repository#update` now accepts two arguments: primary key (`id`) and data (or entity)
151
+ - [Luca Guidi] `Hanami::Repository#delete` now accepts: primary key (`id`)
152
+ - [Luca Guidi] Drop `Hanami::Model::NonPersistedEntityError`, `Hanami::Model::InvalidMappingError`, `Hanami::Model::InvalidCommandError`, `Hanami::Model::InvalidQueryError`
153
+ - [Luca Guidi] Official support for Ruby 2.3 and JRuby 9.0.5.0
154
+ - [Luca Guidi] Drop support for Ruby 2.0, 2.1, 2.2, and JRuby 9.0.0.0
155
+ - [Luca Guidi] Drop support for `mysql` gem in favor of `mysql2`
156
+
157
+ ### Fixed
158
+ - [Luca Guidi] Ensure booleans to be correctly dumped in database
159
+ - [Luca Guidi] Ensure to respect default database schema values
160
+ - [Luca Guidi] Ensure SQL UPDATE to not override non-default primary key
161
+ - [James Hamilton] Print appropriate error message when trying to create a PostgreSQL database that is already existing
162
+
163
+ ## v0.6.2 - 2016-06-01
164
+ ### Changed
165
+ - [Kjell-Magne Øierud] Ensure inherited entities to expose attributes from base class
166
+
167
+ ## v0.6.1 - 2016-02-05
168
+ ### Changed
169
+ - [Hélio Costa e Silva & Pascal Betz] Mapping SQL Adapter's errors as `Hanami::Model` errors
170
+
171
+ ## v0.6.1 - 2016-02-05
172
+ ### Changed
173
+ - [Hélio Costa e Silva & Pascal Betz] Mapping SQL Adapter's errors as `Hanami::Model` errors
174
+
175
+ ## v0.6.0 - 2016-01-22
176
+ ### Changed
177
+ - [Luca Guidi] Renamed the project
178
+
179
+ ## v0.5.2 - 2016-01-19
180
+ ### Changed
181
+ - [Sean Collins] Improved error message for `Lotus::Model::Adapters::NoAdapterError`
182
+
183
+ ### Fixed
184
+ - [Kyle Chong & Trung Lê] Catch Sequel exceptions and re-raise as `Lotus::Model::Error`
185
+
186
+ ## v0.5.1 - 2016-01-12
187
+ ### Added
188
+ - [Taylor Finnell] Let `Lotus::Model::Configuration#adapter` to accept arbitrary options (eg. `adapter type: :sql, uri: 'jdbc:...', after_connect: Proc.new { |connection| connection.auto_commit(true) }`)
189
+
190
+ ### Changed
191
+ - [Andrey Deryabin] Improved `Entity#inspect`
192
+ - [Karim Tarek] Introduced `Lotus::Model::Error` and let all the framework exceptions to inherit from it.
193
+
194
+ ### Fixed
195
+ - [Luca Guidi] Improved error message when trying to use a repository without mapping the corresponding collections
196
+ - [Sean Collins] Improved error message when trying to create database, but it fails (eg. missing `createdb` executable)
197
+ - [Andrey Deryabin] Improved error message when trying to drop database, but a client is still connected (useful for PostgreSQL)
198
+ - [Hiếu Nguyễn] Improved error message when trying to "prepare" database, but it fails
199
+
200
+ ## v0.5.0 - 2015-09-30
201
+ ### Added
202
+ - [Brenno Costa] Official support for JRuby 9k+
203
+ - [Luca Guidi] Command/Query separation via `Repository.execute` and `Repository.fetch`
204
+ - [Luca Guidi] Custom attribute coercers for data mapper
205
+ - [Alfonso Uceda] Added `#join` and `#left_join` and `#group` to SQL adapter
206
+
207
+ ### Changed
208
+ - [Luca Guidi] `Repository.execute` no longer returns a result from the database.
209
+
210
+ ### Fixed
211
+ - [Manuel Corrales] Use `dropdb` to drop PostgreSQL database.
212
+ - [Luca Guidi & Bohdan V.] Ignore dotfiles while running migrations.
213
+
214
+ ## v0.4.1 - 2015-07-10
215
+ ### Fixed
216
+ - [Nick Coyne] Fixed database creation for PostgreSQL (now it uses `createdb`).
217
+
218
+ ## v0.4.0 - 2015-06-23
219
+ ### Added
220
+ - [Luca Guidi] Database migrations
221
+
222
+ ### Changed
223
+ - [Matthew Bellantoni] Made `Repository.execute` not callable from the outside (private Ruby method, public API).
224
+
225
+ ## v0.3.2 - 2015-05-22
226
+ ### Added
227
+ - [Dmitry Tymchuk & Luca Guidi] Fix for dirty tracking of attributes changed in place (eg. `book.tags << 'non-fiction'`)
228
+
229
+ ## v0.3.1 - 2015-05-15
230
+ ### Added
231
+ - [Dmitry Tymchuk] Dirty tracking for entities (via `Lotus::Entity::DirtyTracking` module to include)
232
+ - [My Mai] Automatic update of timestamps when an entity is persisted.
233
+ - [Peter Berkenbosch] Introduced `Lotus::Repository#execute`, to execute raw query/commands against database (eg. `BookRepository.execute "SELECT * FROM users"` or `BookRepository.execute "UPDATE users SET admin = 'f'"`)
234
+ - [Guilherme Franco] Memory and File System adapters now accept a block for `where`, `or`, `and` conditions (eg `where { age > 33 }`).
235
+
236
+ ### Fixed
237
+ - [Luca Guidi] Ensure Array coercion to preserve original data structure
238
+
239
+ ## v0.3.0 - 2015-03-23
240
+ ### Added
241
+ - [Linus Pettersson] Database console
242
+
243
+ ### Fixed
244
+ - [Alfonso Uceda Pompa] Don't send unwanted null values to the database, while coercing entities
245
+ - [Jan Lelis] Do not define top-level `Boolean`, because it is already defined by `hanami-utils`
246
+ - [Vsevolod Romashov] Fix entity class resolving in `Coercer#from_record`
247
+ - [Jason Harrelson] Add file and line to `instance_eval` in `Coercer` to make backtrace more usable
248
+
249
+ ## v0.2.4 - 2015-02-20
250
+ ### Fixed
251
+ - [Luca Guidi] When duplicate the framework don't copy over the original `Lotus::Model` configuration
252
+
253
+ ## v0.2.3 - 2015-02-13
254
+ ### Added
255
+ - [Alfonso Uceda Pompa] Added support for database transactions in repositories
256
+
257
+ ### Fixed
258
+ - [Luca Guidi] Ensure file system adapter old data is read when a new process is started
259
+
260
+ ## v0.2.2 - 2015-01-18
261
+ ### Added
262
+ - [Luca Guidi] Coerce entities when persisted
263
+
264
+ ## v0.2.1 - 2015-01-12
265
+ ### Added
266
+ - [Luca Guidi] Compatibility between Lotus::Entity and Lotus::Validations
267
+
268
+ ## v0.2.0 - 2014-12-23
269
+ ### Added
270
+ - [Luca Guidi] Introduced file system adapter
271
+ – [Benny Klotz & Trung Lê] Introduced `Entity` inheritance of attributes
272
+ - [Trung Lê] Introduced `Entity#update` for bulk update of attributes
273
+ - [Luca Guidi] Improved error when try to use a repository which wasn't configured or when the framework wasn't loaded yet
274
+ - [Trung Lê] Introduced `Entity#to_h`
275
+ - [Trung Lê] Introduced `Lotus::Model.duplicate`
276
+ - [Trung Lê] Made `Lotus::Mapper` lazy
277
+ - [Trung Lê] Introduced thread safe autoloading for adapters
278
+ - [Felipe Sere] Add support for `Symbol` coercion
279
+ - [Celso Fernandes] Add support for `BigDecimal` coercion
280
+ - [Trung Lê] Introduced `Lotus::Model.load!` as entry point for loading
281
+ - [Trung Lê] Introduced `Mapper#repository` as DSL to associate a repository to a collection
282
+ - [Trung Lê & Tao Guo] Introduced `Configuration#mapping` as DSL to configure the mapping
283
+ - [Coen Wessels] Allow `where`, `exclude` and `or` to accept blocks
284
+ - [Trung Lê & Tao Guo] Introduced `Configuration#adapter` as DSL to configure the adapter
285
+ - [Trung Lê] Introduced `Lotus::Model::Configuration`
286
+
287
+ ### Changed
288
+ - [Trung Lê] Changed `Entity.attributes=` to `Entity.attributes`
289
+ - [Trung Lê] In case of missing entity, let `Repository#find` returns `nil` instead of raise an exception
290
+
291
+ ### Fixed
292
+ - [Rik Tonnard] Ensure correct behavior of `#offset` in memory adapter
293
+ - [Benny Klotz] Ensure `Entity` to set the attributes even when the given Hash uses strings as keys
294
+ - [Ben Askins] Always return the entity from `Repository#persist`
295
+ - [Jeremy Stephens] Made `Memory::Query#where` and `#or` behave more like the SQL counter-part
296
+
297
+ ## v0.1.2 - 2014-06-26
298
+ ### Fixed
299
+ - [Stanislav Spiridonov] Ensure to require `'hanami/model/mapping/coercions'`
300
+ - [Krzysztof Zalewski] `Entity` defines `#id` accessor by default
301
+
302
+
303
+ ## v0.1.1 - 2014-06-23
304
+ ### Added
305
+ - [Luca Guidi] Introduced `Lotus::Model::Mapping::Coercions` in order to decouple from `Lotus::Utils::Kernel`
306
+ - [Luca Guidi] Official support for Ruby 2.1
307
+
308
+ ## v0.1.0 - 2014-04-23
309
+ ### Added
310
+ - [Luca Guidi] Allow to inject coercer into mapper
311
+ - [Luca Guidi] Introduced database mapping
312
+ - [Luca Guidi] Introduced `Lotus::Entity`
313
+ - [Luca Guidi] Introduced SQL adapter
314
+ - [Luca Guidi] Introduced memory adapter
315
+ – [Luca Guidi] Introduced adapters for repositories
316
+ - [Luca Guidi] Introduced `Lotus::Repository`
model-main/Gemfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+ gemspec
5
+
6
+ unless ENV["CI"]
7
+ gem "byebug", require: false, platforms: :mri
8
+ gem "yard", require: false
9
+ end
10
+
11
+ gem "hanami-utils", "~> 1.3", require: false, git: "https://github.com/hanami/utils.git", branch: "1.3.x"
12
+
13
+ gem "sqlite3", require: false, platforms: :mri, group: :sqlite
14
+ gem "pg", require: false, platforms: :mri, group: :postgres
15
+ gem "mysql2", require: false, platforms: :mri, group: :mysql
16
+
17
+ gem "jdbc-sqlite3", require: false, platforms: :jruby, group: :sqlite
18
+ gem "jdbc-postgres", require: false, platforms: :jruby, group: :postgres
19
+ gem "jdbc-mysql", require: false, platforms: :jruby, group: :mysql
20
+
21
+ gem "hanami-devtools", require: false, git: "https://github.com/hanami/devtools.git", branch: "1.3.x"
model-main/LICENSE.md ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright © 2014-2021 Luca Guidi
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
model-main/README.md ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hanami::Model
2
+
3
+ A persistence framework for [Hanami](http://hanamirb.org).
4
+
5
+ It delivers a convenient public API to execute queries and commands against a database.
6
+ The architecture eases keeping the business logic (entities) separated from details such as persistence or validations.
7
+
8
+ It implements the following concepts:
9
+
10
+ * [Entity](#entities) - A model domain object defined by its identity.
11
+ * [Repository](#repositories) - An object that mediates between the entities and the persistence layer.
12
+
13
+ Like all the other Hanami components, it can be used as a standalone framework or within a full Hanami application.
14
+
15
+ ## Version
16
+
17
+ **This branch contains the code for `hanami-model` 2.x.**
18
+
19
+ ## Status
20
+
21
+ [![Gem Version](https://badge.fury.io/rb/hanami-model.svg)](https://badge.fury.io/rb/hanami-model)
22
+ [![CI](https://github.com/hanami/model/workflows/ci/badge.svg?branch=main)](https://github.com/hanami/model/actions?query=workflow%3Aci+branch%3Amain)
23
+ [![Test Coverage](https://codecov.io/gh/hanami/model/branch/main/graph/badge.svg)](https://codecov.io/gh/hanami/model)
24
+ [![Depfu](https://badges.depfu.com/badges/3a5d3f9e72895493bb6f39402ac4f129/overview.svg)](https://depfu.com/github/hanami/model?project=Bundler)
25
+ [![Inline Docs](http://inch-ci.org/github/hanami/model.svg)](http://inch-ci.org/github/hanami/model)
26
+
27
+ ## Contact
28
+
29
+ * Home page: http://hanamirb.org
30
+ * Mailing List: http://hanamirb.org/mailing-list
31
+ * API Doc: http://rdoc.info/gems/hanami-model
32
+ * Bugs/Issues: https://github.com/hanami/model/issues
33
+ * Support: http://stackoverflow.com/questions/tagged/hanami
34
+ * Chat: https://chat.hanamirb.org
35
+
36
+ ## Rubies
37
+
38
+ __Hanami::Model__ supports Ruby (MRI) 2.6+
39
+
40
+ ## Installation
41
+
42
+ Add this line to your application's Gemfile:
43
+
44
+ ```ruby
45
+ gem 'hanami-model'
46
+ ```
47
+
48
+ And then execute:
49
+
50
+ $ bundle
51
+
52
+ Or install it yourself as:
53
+
54
+ $ gem install hanami-model
55
+
56
+ ## Usage
57
+
58
+ This class provides a DSL to configure the connection.
59
+
60
+ ```ruby
61
+ require 'hanami/model'
62
+ require 'hanami/model/sql'
63
+
64
+ class User < Hanami::Entity
65
+ end
66
+
67
+ class UserRepository < Hanami::Repository
68
+ end
69
+
70
+ Hanami::Model.configure do
71
+ adapter :sql, 'postgres://username:password@localhost/bookshelf'
72
+ end.load!
73
+
74
+ repository = UserRepository.new
75
+ user = repository.create(name: 'Luca')
76
+
77
+ puts user.id # => 1
78
+
79
+ found = repository.find(user.id)
80
+ found == user # => true
81
+
82
+ updated = repository.update(user.id, age: 34)
83
+ updated.age # => 34
84
+
85
+ repository.delete(user.id)
86
+ ```
87
+
88
+ ## Concepts
89
+
90
+ ### Entities
91
+
92
+ A model domain object that is defined by its identity.
93
+ See "Domain Driven Design" by Eric Evans.
94
+
95
+ An entity is the core of an application, where the part of the domain logic is implemented.
96
+ It's a small, cohesive object that expresses coherent and meaningful behaviors.
97
+
98
+ It deals with one and only one responsibility that is pertinent to the
99
+ domain of the application, without caring about details such as persistence
100
+ or validations.
101
+
102
+ This simplicity of design allows developers to focus on behaviors, or
103
+ message passing if you will, which is the quintessence of Object Oriented Programming.
104
+
105
+ ```ruby
106
+ require 'hanami/model'
107
+
108
+ class Person < Hanami::Entity
109
+ end
110
+ ```
111
+
112
+ ### Repositories
113
+
114
+ An object that mediates between entities and the persistence layer.
115
+ It offers a standardized API to query and execute commands on a database.
116
+
117
+ A repository is **storage independent**, all the queries and commands are
118
+ delegated to the current adapter.
119
+
120
+ This architecture has several advantages:
121
+
122
+ * Applications depend on a standard API, instead of low level details
123
+ (Dependency Inversion principle)
124
+
125
+ * Applications depend on a stable API, that doesn't change if the
126
+ storage changes
127
+
128
+ * Developers can postpone storage decisions
129
+
130
+ * Confines persistence logic at a low level
131
+
132
+ * Multiple data sources can easily coexist in an application
133
+
134
+ When a class inherits from `Hanami::Repository`, it will receive the following interface:
135
+
136
+ * `#create(data)` – Create a record for the given data (or entity)
137
+ * `#update(id, data)` – Update the record corresponding to the given id by setting the given data (or entity)
138
+ * `#delete(id)` – Delete the record corresponding to the given id
139
+ * `#all` - Fetch all the entities from the relation
140
+ * `#find` - Fetch an entity from the relation by primary key
141
+ * `#first` - Fetch the first entity from the relation
142
+ * `#last` - Fetch the last entity from the relation
143
+ * `#clear` - Delete all the records from the relation
144
+
145
+ **A relation is a homogenous set of records.**
146
+ It corresponds to a table for a SQL database or to a MongoDB collection.
147
+
148
+ **All the queries are private**.
149
+ This decision forces developers to define intention revealing API, instead of leaking storage API details outside of a repository.
150
+
151
+ Look at the following code:
152
+
153
+ ```ruby
154
+ ArticleRepository.new.where(author_id: 23).order(:published_at).limit(8)
155
+ ```
156
+
157
+ This is **bad** for a variety of reasons:
158
+
159
+ * The caller has an intimate knowledge of the internal mechanisms of the Repository.
160
+
161
+ * The caller works on several levels of abstraction.
162
+
163
+ * It doesn't express a clear intent, it's just a chain of methods.
164
+
165
+ * The caller can't be easily tested in isolation.
166
+
167
+ * If we change the storage, we are forced to change the code of the caller(s).
168
+
169
+ There is a better way:
170
+
171
+ ```ruby
172
+ require 'hanami/model'
173
+
174
+ class ArticleRepository < Hanami::Repository
175
+ def most_recent_by_author(author, limit: 8)
176
+ articles.where(author_id: author.id).
177
+ order(:published_at).
178
+ limit(limit)
179
+ end
180
+ end
181
+ ```
182
+
183
+ This is a **huge improvement**, because:
184
+
185
+ * The caller doesn't know how the repository fetches the entities.
186
+
187
+ * The caller works on a single level of abstraction. It doesn't even know about records, only works with entities.
188
+
189
+ * It expresses a clear intent.
190
+
191
+ * The caller can be easily tested in isolation. It's just a matter of stubbing this method.
192
+
193
+ * If we change the storage, the callers aren't affected.
194
+
195
+ ### Mapping
196
+
197
+ Hanami::Model can **_automap_** columns from relations and entities attributes.
198
+
199
+ When using a `sql` adapter, you must require `hanami/model/sql` before `Hanami::Model.load!` is called so the relations are loaded correctly.
200
+
201
+ However, there are cases where columns and attribute names do not match (mainly **legacy databases**).
202
+
203
+ ```ruby
204
+ require 'hanami/model'
205
+
206
+ class UserRepository < Hanami::Repository
207
+ self.relation = :t_user_archive
208
+
209
+ mapping do
210
+ attribute :id, from: :i_user_id
211
+ attribute :name, from: :s_name
212
+ attribute :age, from: :i_age
213
+ end
214
+ end
215
+ ```
216
+ **NOTE:** This feature should be used only when **_automapping_** fails because of the naming mismatch.
217
+
218
+ ### Conventions
219
+
220
+ * A repository must be named after an entity, by appending `"Repository"` to the entity class name (eg. `Article` => `ArticleRepository`).
221
+
222
+ ### Thread safety
223
+
224
+ **Hanami::Model**'s is thread safe during the runtime, but it isn't during the loading process.
225
+ The mapper compiles some code internally, so be sure to safely load it before your application starts.
226
+
227
+ ```ruby
228
+ Mutex.new.synchronize do
229
+ Hanami::Model.load!
230
+ end
231
+ ```
232
+
233
+ **This is not necessary when Hanami::Model is used within a Hanami application.**
234
+
235
+ ## Features
236
+
237
+ ### Timestamps
238
+
239
+ If an entity has the following accessors: `:created_at` and `:updated_at`, they will be automatically updated when the entity is persisted.
240
+
241
+ ```ruby
242
+ require 'hanami/model'
243
+ require 'hanami/model/sql'
244
+
245
+ class User < Hanami::Entity
246
+ end
247
+
248
+ class UserRepository < Hanami::Repository
249
+ end
250
+
251
+ Hanami::Model.configure do
252
+ adapter :sql, 'postgresql://localhost/bookshelf'
253
+ end.load!
254
+
255
+ repository = UserRepository.new
256
+
257
+ user = repository.create(name: 'Luca')
258
+
259
+ puts user.created_at.to_s # => "2016-09-19 13:40:13 UTC"
260
+ puts user.updated_at.to_s # => "2016-09-19 13:40:13 UTC"
261
+
262
+ sleep 3
263
+ user = repository.update(user.id, age: 34)
264
+ puts user.created_at.to_s # => "2016-09-19 13:40:13 UTC"
265
+ puts user.updated_at.to_s # => "2016-09-19 13:40:16 UTC"
266
+ ```
267
+
268
+ ## Configuration
269
+
270
+ ### Logging
271
+
272
+ In order to log database operations, you can configure a logger:
273
+
274
+ ```ruby
275
+ Hanami::Model.configure do
276
+ # ...
277
+ logger "log/development.log", level: :debug
278
+ end
279
+ ```
280
+
281
+ It accepts the following arguments:
282
+
283
+ * `stream`: a Ruby StringIO object - it can be `$stdout` or a path to file (eg. `"log/development.log"`) - Defaults to `$stdout`
284
+ * `:level`: logging level - it can be: `:debug`, `:info`, `:warn`, `:error`, `:fatal`, `:unknown` - Defaults to `:debug`
285
+ * `:formatter`: logging formatter - it can be: `:default` or `:json` - Defaults to `:default`
286
+
287
+ ## Versioning
288
+
289
+ __Hanami::Model__ uses [Semantic Versioning 2.0.0](http://semver.org)
290
+
291
+ ## Contributing
292
+
293
+ 1. Fork it ( https://github.com/hanami/model/fork )
294
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
295
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
296
+ 4. Push to the branch (`git push origin my-new-feature`)
297
+ 5. Create new Pull Request
298
+
299
+ ## Copyright
300
+
301
+ Copyright © 2014-2021 Luca Guidi – Released under MIT License
302
+
303
+ This project was formerly known as Lotus (`lotus-model`).
model-main/Rakefile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "rake"
4
+ require "bundler/gem_tasks"
5
+ require "rspec/core/rake_task"
6
+ require "hanami/devtools/rake_tasks"
7
+
8
+ namespace :spec do
9
+ RSpec::Core::RakeTask.new(:unit) do |task|
10
+ task.pattern = FileList["spec/**/*_spec.rb"]
11
+ end
12
+ end
13
+
14
+ task default: "spec:unit"
model-main/hanami-model.gemspec ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("../lib", __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "hanami/model/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "hanami-model"
9
+ spec.version = Hanami::Model::VERSION
10
+ spec.authors = ["Luca Guidi"]
11
+ spec.email = ["me@lucaguidi.com"]
12
+ spec.summary = "A persistence layer for Hanami"
13
+ spec.description = "A persistence framework with entities and repositories"
14
+ spec.homepage = "http://hanamirb.org"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z -- lib/* CHANGELOG.md EXAMPLE.md LICENSE.md README.md hanami-model.gemspec`.split("\x0")
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ["lib"]
21
+ spec.required_ruby_version = ">= 2.3.0", "< 3"
22
+
23
+ spec.add_runtime_dependency "hanami-utils", "~> 1.3"
24
+ spec.add_runtime_dependency "rom", "~> 3.3", ">= 3.3.3"
25
+ spec.add_runtime_dependency "rom-sql", "~> 1.3", ">= 1.3.5"
26
+ spec.add_runtime_dependency "rom-repository", "~> 1.4"
27
+ spec.add_runtime_dependency "dry-types", "~> 0.11.0"
28
+ spec.add_runtime_dependency "dry-logic", "~> 0.4.2", "< 0.5"
29
+ spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
30
+ spec.add_runtime_dependency "bigdecimal", "~> 1.4"
31
+
32
+ spec.add_development_dependency "bundler", ">= 1.6", "< 3"
33
+ spec.add_development_dependency "rake", "~> 12"
34
+ spec.add_development_dependency "rspec", "~> 3.7"
35
+ spec.add_development_dependency "rubocop", "0.81" # rubocop 0.81+ removed support for Ruby 2.3
36
+ end
model-main/lib/hanami-model.rb ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/model"
model-main/lib/hanami/entity.rb ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/model/types"
4
+
5
+ module Hanami
6
+ # An object that is defined by its identity.
7
+ # See "Domain Driven Design" by Eric Evans.
8
+ #
9
+ # An entity is the core of an application, where the part of the domain
10
+ # logic is implemented. It's a small, cohesive object that expresses coherent
11
+ # and meaningful behaviors.
12
+ #
13
+ # It deals with one and only one responsibility that is pertinent to the
14
+ # domain of the application, without caring about details such as persistence
15
+ # or validations.
16
+ #
17
+ # This simplicity of design allows developers to focus on behaviors, or
18
+ # message passing if you will, which is the quintessence of Object Oriented
19
+ # Programming.
20
+ #
21
+ # @example With Hanami::Entity
22
+ # require 'hanami/model'
23
+ #
24
+ # class Person < Hanami::Entity
25
+ # end
26
+ #
27
+ # If we expand the code above in **pure Ruby**, it would be:
28
+ #
29
+ # @example Pure Ruby
30
+ # class Person
31
+ # attr_accessor :id, :name, :age
32
+ #
33
+ # def initialize(attributes = {})
34
+ # @id, @name, @age = attributes.values_at(:id, :name, :age)
35
+ # end
36
+ # end
37
+ #
38
+ # **Hanami::Model** ships `Hanami::Entity` for developers' convenience.
39
+ #
40
+ # **Hanami::Model** depends on a narrow and well-defined interface for an
41
+ # Entity - `#id`, `#id=`, `#initialize(attributes={})`.If your object
42
+ # implements that interface then that object can be used as an Entity in the
43
+ # **Hanami::Model** framework.
44
+ #
45
+ # However, we suggest to implement this interface by including
46
+ # `Hanami::Entity`, in case that future versions of the framework will expand
47
+ # it.
48
+ #
49
+ # See Dependency Inversion Principle for more on interfaces.
50
+ #
51
+ # @since 0.1.0
52
+ #
53
+ # @see Hanami::Repository
54
+ class Entity
55
+ require "hanami/entity/schema"
56
+
57
+ # Syntactic shortcut to reference types in custom schema DSL
58
+ #
59
+ # @since 0.7.0
60
+ module Types
61
+ include Hanami::Model::Types
62
+ end
63
+
64
+ # Class level interface
65
+ #
66
+ # @since 0.7.0
67
+ # @api private
68
+ module ClassMethods
69
+ # Define manual entity schema
70
+ #
71
+ # With a SQL database this setup happens automatically and you SHOULD NOT
72
+ # use this DSL. You should use only when you want to customize the automatic
73
+ # setup.
74
+ #
75
+ # If you're working with an entity that isn't "backed" by a SQL table or
76
+ # with a schema-less database, you may want to manually setup a set of
77
+ # attributes via this DSL. If you don't do any setup, the entity accepts all
78
+ # the given attributes.
79
+ #
80
+ # @param type [Symbol] the type of schema to build
81
+ # @param blk [Proc] the block that defines the attributes
82
+ #
83
+ # @since 0.7.0
84
+ #
85
+ # @see Hanami::Entity
86
+ def attributes(type = nil, &blk)
87
+ self.schema = Schema.new(type, &blk)
88
+ @attributes = true
89
+ end
90
+
91
+ # Assign a schema
92
+ #
93
+ # @param value [Hanami::Entity::Schema] the schema
94
+ #
95
+ # @since 0.7.0
96
+ # @api private
97
+ def schema=(value)
98
+ return if defined?(@attributes)
99
+
100
+ @schema = value
101
+ end
102
+
103
+ # @since 0.7.0
104
+ # @api private
105
+ attr_reader :schema
106
+ end
107
+
108
+ # @since 0.7.0
109
+ # @api private
110
+ def self.inherited(klass)
111
+ klass.class_eval do
112
+ @schema = Schema.new
113
+ extend ClassMethods
114
+ end
115
+ end
116
+
117
+ # Instantiate a new entity
118
+ #
119
+ # @param attributes [Hash,#to_h,NilClass] data to initialize the entity
120
+ #
121
+ # @return [Hanami::Entity] the new entity instance
122
+ #
123
+ # @raise [TypeError] if the given attributes are invalid
124
+ #
125
+ # @since 0.1.0
126
+ def initialize(attributes = nil)
127
+ @attributes = self.class.schema[attributes]
128
+ freeze
129
+ end
130
+
131
+ # Entity ID
132
+ #
133
+ # @return [Object,NilClass] the ID, if present
134
+ #
135
+ # @since 0.7.0
136
+ def id
137
+ attributes.fetch(:id, nil)
138
+ end
139
+
140
+ # Handle dynamic accessors
141
+ #
142
+ # If internal attributes set has the requested key, it returns the linked
143
+ # value, otherwise it raises a <tt>NoMethodError</tt>
144
+ #
145
+ # @since 0.7.0
146
+ def method_missing(method_name, *)
147
+ attribute?(method_name) or super
148
+ attributes.fetch(method_name, nil)
149
+ end
150
+
151
+ # Implement generic equality for entities
152
+ #
153
+ # Two entities are equal if they are instances of the same class and they
154
+ # have the same id.
155
+ #
156
+ # @param other [Object] the object of comparison
157
+ #
158
+ # @return [FalseClass,TrueClass] the result of the check
159
+ #
160
+ # @since 0.1.0
161
+ def ==(other)
162
+ self.class == other.class &&
163
+ id == other.id
164
+ end
165
+
166
+ # Implement predictable hashing for hash equality
167
+ #
168
+ # @return [Integer] the object hash
169
+ #
170
+ # @since 0.7.0
171
+ def hash
172
+ [self.class, id].hash
173
+ end
174
+
175
+ # Freeze the entity
176
+ #
177
+ # @since 0.7.0
178
+ def freeze
179
+ attributes.freeze
180
+ super
181
+ end
182
+
183
+ # Serialize entity to a Hash
184
+ #
185
+ # @return [Hash] the result of serialization
186
+ #
187
+ # @since 0.1.0
188
+ def to_h
189
+ Utils::Hash.deep_dup(attributes)
190
+ end
191
+
192
+ # @since 0.7.0
193
+ alias_method :to_hash, :to_h
194
+
195
+ protected
196
+
197
+ # Check if the attribute is allowed to be read
198
+ #
199
+ # @since 0.7.0
200
+ # @api private
201
+ def attribute?(name)
202
+ self.class.schema.attribute?(name)
203
+ end
204
+
205
+ private
206
+
207
+ # @since 0.1.0
208
+ # @api private
209
+ attr_reader :attributes
210
+
211
+ # @since 0.7.0
212
+ # @api private
213
+ def respond_to_missing?(name, _include_all)
214
+ attribute?(name)
215
+ end
216
+ end
217
+ end
model-main/lib/hanami/entity/schema.rb ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/model/types"
4
+ require "hanami/utils/hash"
5
+
6
+ module Hanami
7
+ class Entity
8
+ # Entity schema is a definition of a set of typed attributes.
9
+ #
10
+ # @since 0.7.0
11
+ # @api private
12
+ #
13
+ # @example SQL Automatic Setup
14
+ # require 'hanami/model'
15
+ #
16
+ # class Account < Hanami::Entity
17
+ # end
18
+ #
19
+ # account = Account.new(name: "Acme Inc.")
20
+ # account.name # => "Hanami"
21
+ #
22
+ # account = Account.new(foo: "bar")
23
+ # account.foo # => NoMethodError
24
+ #
25
+ # @example Non-SQL Manual Setup
26
+ # require 'hanami/model'
27
+ #
28
+ # class Account < Hanami::Entity
29
+ # attributes do
30
+ # attribute :id, Types::Int
31
+ # attribute :name, Types::String
32
+ # attribute :codes, Types::Array(Types::Int)
33
+ # attribute :users, Types::Array(User)
34
+ # attribute :email, Types::String.constrained(format: /@/)
35
+ # attribute :created_at, Types::DateTime
36
+ # end
37
+ # end
38
+ #
39
+ # account = Account.new(name: "Acme Inc.")
40
+ # account.name # => "Acme Inc."
41
+ #
42
+ # account = Account.new(foo: "bar")
43
+ # account.foo # => NoMethodError
44
+ #
45
+ # @example Schemaless Entity
46
+ # require 'hanami/model'
47
+ #
48
+ # class Account < Hanami::Entity
49
+ # end
50
+ #
51
+ # account = Account.new(name: "Acme Inc.")
52
+ # account.name # => "Acme Inc."
53
+ #
54
+ # account = Account.new(foo: "bar")
55
+ # account.foo # => "bar"
56
+ class Schema
57
+ # Schemaless entities logic
58
+ #
59
+ # @since 0.7.0
60
+ # @api private
61
+ class Schemaless
62
+ # @since 0.7.0
63
+ # @api private
64
+ def initialize
65
+ freeze
66
+ end
67
+
68
+ # @param attributes [#to_hash] the attributes hash
69
+ #
70
+ # @return [Hash]
71
+ #
72
+ # @since 0.7.0
73
+ # @api private
74
+ def call(attributes)
75
+ if attributes.nil?
76
+ {}
77
+ else
78
+ Utils::Hash.deep_symbolize(attributes.to_hash.dup)
79
+ end
80
+ end
81
+
82
+ # @since 0.7.0
83
+ # @api private
84
+ def attribute?(_name)
85
+ true
86
+ end
87
+ end
88
+
89
+ # Schema definition
90
+ #
91
+ # @since 0.7.0
92
+ # @api private
93
+ class Definition
94
+ # Schema DSL
95
+ #
96
+ # @since 0.7.0
97
+ class Dsl
98
+ # @since 1.1.0
99
+ # @api private
100
+ TYPES = %i[schema strict weak permissive strict_with_defaults symbolized].freeze
101
+
102
+ # @since 1.1.0
103
+ # @api private
104
+ DEFAULT_TYPE = TYPES.first
105
+
106
+ # @since 0.7.0
107
+ # @api private
108
+ def self.build(type, &blk)
109
+ type ||= DEFAULT_TYPE
110
+ raise Hanami::Model::Error.new("Unknown schema type: `#{type.inspect}'") unless TYPES.include?(type)
111
+
112
+ attributes = new(&blk).to_h
113
+ [attributes, Hanami::Model::Types::Coercible::Hash.__send__(type, attributes)]
114
+ end
115
+
116
+ # @since 0.7.0
117
+ # @api private
118
+ def initialize(&blk)
119
+ @attributes = {}
120
+ instance_eval(&blk)
121
+ end
122
+
123
+ # Define an attribute
124
+ #
125
+ # @param name [Symbol] the attribute name
126
+ # @param type [Dry::Types::Definition] the attribute type
127
+ #
128
+ # @since 0.7.0
129
+ #
130
+ # @example
131
+ # require 'hanami/model'
132
+ #
133
+ # class Account < Hanami::Entity
134
+ # attributes do
135
+ # attribute :id, Types::Int
136
+ # attribute :name, Types::String
137
+ # attribute :codes, Types::Array(Types::Int)
138
+ # attribute :users, Types::Array(User)
139
+ # attribute :email, Types::String.constrained(format: /@/)
140
+ # attribute :created_at, Types::DateTime
141
+ # end
142
+ # end
143
+ #
144
+ # account = Account.new(name: "Acme Inc.")
145
+ # account.name # => "Acme Inc."
146
+ #
147
+ # account = Account.new(foo: "bar")
148
+ # account.foo # => NoMethodError
149
+ def attribute(name, type)
150
+ @attributes[name] = type
151
+ end
152
+
153
+ # @since 0.7.0
154
+ # @api private
155
+ def to_h
156
+ @attributes
157
+ end
158
+ end
159
+
160
+ # Instantiate a new DSL instance for an entity
161
+ #
162
+ # @param blk [Proc] the block that defines the attributes
163
+ #
164
+ # @return [Hanami::Entity::Schema::Dsl] the DSL
165
+ #
166
+ # @since 0.7.0
167
+ # @api private
168
+ def initialize(type = nil, &blk)
169
+ raise LocalJumpError unless block_given?
170
+
171
+ @attributes, @schema = Dsl.build(type, &blk)
172
+ @attributes = Hash[@attributes.map { |k, _| [k, true] }]
173
+ freeze
174
+ end
175
+
176
+ # Process attributes
177
+ #
178
+ # @param attributes [#to_hash] the attributes hash
179
+ #
180
+ # @raise [TypeError] if the process fails
181
+ # @raise [ArgumentError] if data is missing, or unknown keys are given
182
+ #
183
+ # @since 0.7.0
184
+ # @api private
185
+ def call(attributes)
186
+ schema.call(attributes)
187
+ rescue Dry::Types::SchemaError => exception
188
+ raise TypeError.new(exception.message)
189
+ rescue Dry::Types::MissingKeyError, Dry::Types::UnknownKeysError => exception
190
+ raise ArgumentError.new(exception.message)
191
+ end
192
+
193
+ # Check if the attribute is known
194
+ #
195
+ # @param name [Symbol] the attribute name
196
+ #
197
+ # @return [TrueClass,FalseClass] the result of the check
198
+ #
199
+ # @since 0.7.0
200
+ # @api private
201
+ def attribute?(name)
202
+ attributes.key?(name)
203
+ end
204
+
205
+ private
206
+
207
+ # @since 0.7.0
208
+ # @api private
209
+ attr_reader :schema
210
+
211
+ # @since 0.7.0
212
+ # @api private
213
+ attr_reader :attributes
214
+ end
215
+
216
+ # Build a new instance of Schema with the attributes defined by the given block
217
+ #
218
+ # @param blk [Proc] the optional block that defines the attributes
219
+ #
220
+ # @return [Hanami::Entity::Schema] the schema
221
+ #
222
+ # @since 0.7.0
223
+ # @api private
224
+ def initialize(type = nil, &blk)
225
+ @schema = if block_given?
226
+ Definition.new(type, &blk)
227
+ else
228
+ Schemaless.new
229
+ end
230
+ end
231
+
232
+ # Process attributes
233
+ #
234
+ # @param attributes [#to_hash] the attributes hash
235
+ #
236
+ # @raise [TypeError] if the process fails
237
+ #
238
+ # @since 0.7.0
239
+ # @api private
240
+ def call(attributes)
241
+ Utils::Hash.deep_symbolize(
242
+ schema.call(attributes)
243
+ )
244
+ end
245
+
246
+ # @since 0.7.0
247
+ # @api private
248
+ alias_method :[], :call
249
+
250
+ # Check if the attribute is known
251
+ #
252
+ # @param name [Symbol] the attribute name
253
+ #
254
+ # @return [TrueClass,FalseClass] the result of the check
255
+ #
256
+ # @since 0.7.0
257
+ # @api private
258
+ def attribute?(name)
259
+ schema.attribute?(name)
260
+ end
261
+
262
+ protected
263
+
264
+ # @since 0.7.0
265
+ # @api private
266
+ attr_reader :schema
267
+ end
268
+ end
269
+ end
model-main/lib/hanami/model.rb ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "rom"
4
+ require "concurrent"
5
+ require "hanami/entity"
6
+ require "hanami/repository"
7
+
8
+ # Hanami
9
+ #
10
+ # @since 0.1.0
11
+ module Hanami
12
+ # Hanami persistence
13
+ #
14
+ # @since 0.1.0
15
+ module Model
16
+ require "hanami/model/version"
17
+ require "hanami/model/error"
18
+ require "hanami/model/configuration"
19
+ require "hanami/model/configurator"
20
+ require "hanami/model/mapping"
21
+ require "hanami/model/plugins"
22
+
23
+ # @api private
24
+ # @since 0.7.0
25
+ @__repositories__ = Concurrent::Array.new
26
+
27
+ class << self
28
+ # @since 0.7.0
29
+ # @api private
30
+ attr_reader :config
31
+
32
+ # @since 0.7.0
33
+ # @api private
34
+ attr_reader :loaded
35
+
36
+ # @since 0.7.0
37
+ # @api private
38
+ alias_method :loaded?, :loaded
39
+ end
40
+
41
+ # Configure the framework
42
+ #
43
+ # @since 0.1.0
44
+ #
45
+ # @example
46
+ # require 'hanami/model'
47
+ #
48
+ # Hanami::Model.configure do
49
+ # adapter :sql, ENV['DATABASE_URL']
50
+ #
51
+ # migrations 'db/migrations'
52
+ # schema 'db/schema.sql'
53
+ # end
54
+ def self.configure(&block)
55
+ @config = Configurator.build(&block)
56
+ self
57
+ end
58
+
59
+ # Current configuration
60
+ #
61
+ # @since 0.1.0
62
+ def self.configuration
63
+ @configuration ||= Configuration.new(config)
64
+ end
65
+
66
+ # @since 0.7.0
67
+ # @api private
68
+ def self.repositories
69
+ @__repositories__
70
+ end
71
+
72
+ # @since 0.7.0
73
+ # @api private
74
+ def self.container
75
+ raise "Not loaded" unless loaded?
76
+
77
+ @container
78
+ end
79
+
80
+ # @since 0.1.0
81
+ def self.load!(&blk)
82
+ @container = configuration.load!(repositories, &blk)
83
+ @loaded = true
84
+ end
85
+
86
+ # Disconnect from the database
87
+ #
88
+ # This is useful for rebooting applications in production and to ensure that
89
+ # the framework prunes stale connections.
90
+ #
91
+ # @since 1.0.0
92
+ #
93
+ # @example With Full Stack Hanami Project
94
+ # # config/puma.rb
95
+ # # ...
96
+ # on_worker_boot do
97
+ # Hanami.boot
98
+ # end
99
+ #
100
+ # @example With Standalone Hanami::Model
101
+ # # config/puma.rb
102
+ # # ...
103
+ # on_worker_boot do
104
+ # Hanami::Model.disconnect
105
+ # Hanami::Model.load!
106
+ # end
107
+ def self.disconnect
108
+ configuration.connection&.disconnect
109
+ end
110
+ end
111
+ end
model-main/lib/hanami/model/association.rb ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "rom-sql"
4
+ require "hanami/model/associations/belongs_to"
5
+ require "hanami/model/associations/has_many"
6
+ require "hanami/model/associations/has_one"
7
+ require "hanami/model/associations/many_to_many"
8
+
9
+ module Hanami
10
+ module Model
11
+ # Association factory
12
+ #
13
+ # @since 0.7.0
14
+ # @api private
15
+ class Association
16
+ # Instantiate an association
17
+ #
18
+ # @since 0.7.0
19
+ # @api private
20
+ def self.build(repository, target, subject)
21
+ lookup(repository.root.associations[target])
22
+ .new(repository, repository.root.name.to_sym, target, subject)
23
+ end
24
+
25
+ # Translate ROM SQL associations into Hanami::Model associations
26
+ #
27
+ # @since 0.7.0
28
+ # @api private
29
+ def self.lookup(association)
30
+ case association
31
+ when ROM::SQL::Association::ManyToMany
32
+ Associations::ManyToMany
33
+ when ROM::SQL::Association::OneToOne
34
+ Associations::HasOne
35
+ when ROM::SQL::Association::OneToMany
36
+ Associations::HasMany
37
+ when ROM::SQL::Association::ManyToOne
38
+ Associations::BelongsTo
39
+ else
40
+ raise "Unsupported association: #{association}"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
model-main/lib/hanami/model/associations/belongs_to.rb ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/model/types"
4
+
5
+ module Hanami
6
+ module Model
7
+ module Associations
8
+ # Many-To-One association
9
+ #
10
+ # @since 1.1.0
11
+ # @api private
12
+ class BelongsTo
13
+ # @since 1.1.0
14
+ # @api private
15
+ def self.schema_type(entity)
16
+ Sql::Types::Schema::AssociationType.new(entity)
17
+ end
18
+
19
+ # @since 1.1.0
20
+ # @api private
21
+ attr_reader :repository
22
+
23
+ # @since 1.1.0
24
+ # @api private
25
+ attr_reader :source
26
+
27
+ # @since 1.1.0
28
+ # @api private
29
+ attr_reader :target
30
+
31
+ # @since 1.1.0
32
+ # @api private
33
+ attr_reader :subject
34
+
35
+ # @since 1.1.0
36
+ # @api private
37
+ attr_reader :scope
38
+
39
+ # @since 1.1.0
40
+ # @api private
41
+ def initialize(repository, source, target, subject, scope = nil)
42
+ @repository = repository
43
+ @source = source
44
+ @target = target
45
+ @subject = subject.to_hash unless subject.nil?
46
+ @scope = scope || _build_scope
47
+ freeze
48
+ end
49
+
50
+ # @since 1.1.0
51
+ # @api private
52
+ def one
53
+ scope.one
54
+ end
55
+
56
+ private
57
+
58
+ # @since 1.1.0
59
+ # @api private
60
+ def container
61
+ repository.container
62
+ end
63
+
64
+ # @since 1.1.0
65
+ # @api private
66
+ def primary_key
67
+ association_keys.first
68
+ end
69
+
70
+ # @since 1.1.0
71
+ # @api private
72
+ def relation(name)
73
+ repository.relations[Hanami::Utils::String.pluralize(name)]
74
+ end
75
+
76
+ # @since 1.1.0
77
+ # @api private
78
+ def foreign_key
79
+ association_keys.last
80
+ end
81
+
82
+ # Returns primary key and foreign key
83
+ #
84
+ # @since 1.1.0
85
+ # @api private
86
+ def association_keys
87
+ association
88
+ .__send__(:join_key_map, container.relations)
89
+ end
90
+
91
+ # Return the ROM::Associations for the source relation
92
+ #
93
+ # @since 1.1.9
94
+ # @api private
95
+ def association
96
+ relation(source).associations[target]
97
+ end
98
+
99
+ # @since 1.1.0
100
+ # @api private
101
+ def _build_scope
102
+ result = relation(association.target.to_sym)
103
+ result = result.where(foreign_key => subject.fetch(primary_key)) unless subject.nil?
104
+ result.as(Model::MappedRelation.mapper_name)
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
model-main/lib/hanami/model/associations/dsl.rb ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Model
5
+ module Associations
6
+ # Auto-infer relations linked to repository's associations
7
+ #
8
+ # @since 0.7.0
9
+ # @api private
10
+ #
11
+ class Dsl
12
+ # @since 0.7.0
13
+ # @api private
14
+ def initialize(repository, &blk)
15
+ @repository = repository
16
+ instance_eval(&blk)
17
+ end
18
+
19
+ # @since 0.7.0
20
+ # @api private
21
+ def has_many(relation, **args)
22
+ @repository.__send__(:relations, relation)
23
+ @repository.__send__(:relations, args[:through]) if args[:through]
24
+ end
25
+
26
+ # @since 1.1.0
27
+ # @api private
28
+ def has_one(relation, *)
29
+ @repository.__send__(:relations, Hanami::Utils::String.pluralize(relation).to_sym)
30
+ end
31
+
32
+ # @since 1.1.0
33
+ # @api private
34
+ def belongs_to(relation, *)
35
+ @repository.__send__(:relations, Hanami::Utils::String.pluralize(relation).to_sym)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
model-main/lib/hanami/model/associations/has_many.rb ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/model/types"
4
+
5
+ module Hanami
6
+ module Model
7
+ module Associations
8
+ # One-To-Many association
9
+ #
10
+ # @since 0.7.0
11
+ # @api private
12
+ class HasMany
13
+ # @since 0.7.0
14
+ # @api private
15
+ def self.schema_type(entity)
16
+ type = Sql::Types::Schema::AssociationType.new(entity)
17
+ Types::Strict::Array.member(type)
18
+ end
19
+
20
+ # @since 0.7.0
21
+ # @api private
22
+ attr_reader :repository
23
+
24
+ # @since 0.7.0
25
+ # @api private
26
+ attr_reader :source
27
+
28
+ # @since 0.7.0
29
+ # @api private
30
+ attr_reader :target
31
+
32
+ # @since 0.7.0
33
+ # @api private
34
+ attr_reader :subject
35
+
36
+ # @since 0.7.0
37
+ # @api private
38
+ attr_reader :scope
39
+
40
+ # @since 0.7.0
41
+ # @api private
42
+ def initialize(repository, source, target, subject, scope = nil)
43
+ @repository = repository
44
+ @source = source
45
+ @target = target
46
+ @subject = subject.to_hash unless subject.nil?
47
+ @scope = scope || _build_scope
48
+ freeze
49
+ end
50
+
51
+ # @since 0.7.0
52
+ # @api private
53
+ def create(data)
54
+ entity.new(command(:create, aggregate(target), mapper: nil, use: [:timestamps])
55
+ .call(serialize(data)))
56
+ rescue => exception
57
+ raise Hanami::Model::Error.for(exception)
58
+ end
59
+
60
+ # @since 0.7.0
61
+ # @api private
62
+ def add(data)
63
+ command(:create, relation(target), use: [:timestamps])
64
+ .call(associate(serialize(data)))
65
+ rescue => exception
66
+ raise Hanami::Model::Error.for(exception)
67
+ end
68
+
69
+ # @since 0.7.0
70
+ # @api private
71
+ def remove(id)
72
+ command(:update, relation(target), use: [:timestamps])
73
+ .by_pk(id)
74
+ .call(unassociate)
75
+ end
76
+
77
+ # @since 0.7.0
78
+ # @api private
79
+ def delete
80
+ scope.delete
81
+ end
82
+
83
+ # @since 0.7.0
84
+ # @api private
85
+ def each(&blk)
86
+ scope.each(&blk)
87
+ end
88
+
89
+ # @since 0.7.0
90
+ # @api private
91
+ def map(&blk)
92
+ to_a.map(&blk)
93
+ end
94
+
95
+ # @since 0.7.0
96
+ # @api private
97
+ def to_a
98
+ scope.to_a
99
+ end
100
+
101
+ # @since 0.7.0
102
+ # @api private
103
+ def where(condition)
104
+ __new__(scope.where(condition))
105
+ end
106
+
107
+ # @since 0.7.0
108
+ # @api private
109
+ def count
110
+ scope.count
111
+ end
112
+
113
+ private
114
+
115
+ # @since 0.7.0
116
+ # @api private
117
+ def command(target, relation, options = {})
118
+ repository.command(target, relation, options)
119
+ end
120
+
121
+ # @since 0.7.0
122
+ # @api private
123
+ def entity
124
+ repository.class.entity
125
+ end
126
+
127
+ # @since 0.7.0
128
+ # @api private
129
+ def relation(name)
130
+ repository.relations[name]
131
+ end
132
+
133
+ # @since 0.7.0
134
+ # @api private
135
+ def aggregate(name)
136
+ repository.aggregate(name)
137
+ end
138
+
139
+ # @since 0.7.0
140
+ # @api private
141
+ def association(name)
142
+ relation(target).associations[name]
143
+ end
144
+
145
+ # @since 0.7.0
146
+ # @api private
147
+ def associate(data)
148
+ relation(source)
149
+ .associations[target]
150
+ .associate(container.relations, data, subject)
151
+ end
152
+
153
+ # @since 0.7.0
154
+ # @api private
155
+ def unassociate
156
+ {foreign_key => nil}
157
+ end
158
+
159
+ # @since 0.7.0
160
+ # @api private
161
+ def container
162
+ repository.container
163
+ end
164
+
165
+ # @since 0.7.0
166
+ # @api private
167
+ def primary_key
168
+ association_keys.first
169
+ end
170
+
171
+ # @since 0.7.0
172
+ # @api private
173
+ def foreign_key
174
+ association_keys.last
175
+ end
176
+
177
+ # Returns primary key and foreign key
178
+ #
179
+ # @since 0.7.0
180
+ # @api private
181
+ def association_keys
182
+ target_association
183
+ .__send__(:join_key_map, container.relations)
184
+ end
185
+
186
+ # Returns the targeted association for a given source
187
+ #
188
+ # @since 0.7.0
189
+ # @api private
190
+ def target_association
191
+ relation(source).associations[target]
192
+ end
193
+
194
+ # @since 0.7.0
195
+ # @api private
196
+ def _build_scope
197
+ result = relation(target_association.target.to_sym)
198
+ result = result.where(foreign_key => subject.fetch(primary_key)) unless subject.nil?
199
+ result.as(Model::MappedRelation.mapper_name)
200
+ end
201
+
202
+ # @since 0.7.0
203
+ # @api private
204
+ def __new__(new_scope)
205
+ self.class.new(repository, source, target, subject, new_scope)
206
+ end
207
+
208
+ def serialize(data)
209
+ Utils::Hash.deep_serialize(data)
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
model-main/lib/hanami/model/associations/has_one.rb ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/utils/hash"
4
+
5
+ module Hanami
6
+ module Model
7
+ module Associations
8
+ # Many-To-One association
9
+ #
10
+ # @since 1.1.0
11
+ # @api private
12
+ class HasOne
13
+ # @since 1.1.0
14
+ # @api private
15
+ def self.schema_type(entity)
16
+ Sql::Types::Schema::AssociationType.new(entity)
17
+ end
18
+ #
19
+ # @since 1.1.0
20
+ # @api private
21
+ attr_reader :repository
22
+
23
+ # @since 1.1.0
24
+ # @api private
25
+ attr_reader :source
26
+
27
+ # @since 1.1.0
28
+ # @api private
29
+ attr_reader :target
30
+
31
+ # @since 1.1.0
32
+ # @api private
33
+ attr_reader :subject
34
+
35
+ # @since 1.1.0
36
+ # @api private
37
+ attr_reader :scope
38
+
39
+ # @since 1.1.0
40
+ # @api private
41
+ def initialize(repository, source, target, subject, scope = nil)
42
+ @repository = repository
43
+ @source = source
44
+ @target = target
45
+ @subject = subject.to_hash unless subject.nil?
46
+ @scope = scope || _build_scope
47
+ freeze
48
+ end
49
+
50
+ def one
51
+ scope.one
52
+ end
53
+
54
+ def create(data)
55
+ entity.new(
56
+ command(:create, aggregate(target), mapper: nil).call(serialize(data))
57
+ )
58
+ rescue => exception
59
+ raise Hanami::Model::Error.for(exception)
60
+ end
61
+
62
+ def add(data)
63
+ command(:create, relation(target), mapper: nil).call(associate(serialize(data)))
64
+ rescue => exception
65
+ raise Hanami::Model::Error.for(exception)
66
+ end
67
+
68
+ def update(data)
69
+ command(:update, relation(target), mapper: nil)
70
+ .by_pk(
71
+ one.public_send(relation(target).primary_key)
72
+ ).call(serialize(data))
73
+ rescue => exception
74
+ raise Hanami::Model::Error.for(exception)
75
+ end
76
+
77
+ def delete
78
+ scope.delete
79
+ end
80
+ alias_method :remove, :delete
81
+
82
+ def replace(data)
83
+ repository.transaction do
84
+ delete
85
+ add(serialize(data))
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ # @since 1.1.0
92
+ # @api private
93
+ def entity
94
+ repository.class.entity
95
+ end
96
+
97
+ # @since 1.1.0
98
+ # @api private
99
+ def aggregate(name)
100
+ repository.aggregate(name)
101
+ end
102
+
103
+ # @since 1.1.0
104
+ # @api private
105
+ def command(target, relation, options = {})
106
+ repository.command(target, relation, options)
107
+ end
108
+
109
+ # @since 1.1.0
110
+ # @api private
111
+ def relation(name)
112
+ repository.relations[Hanami::Utils::String.pluralize(name)]
113
+ end
114
+
115
+ # @since 1.1.0
116
+ # @api private
117
+ def container
118
+ repository.container
119
+ end
120
+
121
+ # @since 1.1.0
122
+ # @api private
123
+ def primary_key
124
+ association_keys.first
125
+ end
126
+
127
+ # @since 1.1.0
128
+ # @api private
129
+ def foreign_key
130
+ association_keys.last
131
+ end
132
+
133
+ # @since 1.1.0
134
+ # @api private
135
+ def associate(data)
136
+ relation(source)
137
+ .associations[target]
138
+ .associate(container.relations, data, subject)
139
+ end
140
+
141
+ # Returns primary key and foreign key
142
+ #
143
+ # @since 1.1.0
144
+ # @api private
145
+ def association_keys
146
+ relation(source)
147
+ .associations[target]
148
+ .__send__(:join_key_map, container.relations)
149
+ end
150
+
151
+ # @since 1.1.0
152
+ # @api private
153
+ def _build_scope
154
+ result = relation(target)
155
+ result = result.where(foreign_key => subject.fetch(primary_key)) unless subject.nil?
156
+ result.as(Model::MappedRelation.mapper_name)
157
+ end
158
+
159
+ # @since 1.1.0
160
+ # @api private
161
+ def serialize(data)
162
+ Utils::Hash.deep_serialize(data)
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
model-main/lib/hanami/model/associations/many_to_many.rb ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/utils/hash"
4
+
5
+ module Hanami
6
+ module Model
7
+ module Associations
8
+ # Many-To-Many association
9
+ #
10
+ # @since 0.7.0
11
+ # @api private
12
+ class ManyToMany
13
+ # @since 0.7.0
14
+ # @api private
15
+ def self.schema_type(entity)
16
+ type = Sql::Types::Schema::AssociationType.new(entity)
17
+ Types::Strict::Array.member(type)
18
+ end
19
+
20
+ # @since 1.1.0
21
+ # @api private
22
+ attr_reader :repository
23
+
24
+ # @since 1.1.0
25
+ # @api private
26
+ attr_reader :source
27
+
28
+ # @since 1.1.0
29
+ # @api private
30
+ attr_reader :target
31
+
32
+ # @since 1.1.0
33
+ # @api private
34
+ attr_reader :subject
35
+
36
+ # @since 1.1.0
37
+ # @api private
38
+ attr_reader :scope
39
+
40
+ # @since 1.1.0
41
+ # @api private
42
+ attr_reader :through
43
+
44
+ def initialize(repository, source, target, subject, scope = nil)
45
+ @repository = repository
46
+ @source = source
47
+ @target = target
48
+ @subject = subject.to_hash unless subject.nil?
49
+ @through = relation(source).associations[target].through.to_sym
50
+ @scope = scope || _build_scope
51
+ freeze
52
+ end
53
+
54
+ def to_a
55
+ scope.to_a
56
+ end
57
+
58
+ def map(&blk)
59
+ to_a.map(&blk)
60
+ end
61
+
62
+ def each(&blk)
63
+ scope.each(&blk)
64
+ end
65
+
66
+ def count
67
+ scope.count
68
+ end
69
+
70
+ def where(condition)
71
+ __new__(scope.where(condition))
72
+ end
73
+
74
+ # Return the association table object. Would need an aditional query to return the entity
75
+ #
76
+ # @since 1.1.0
77
+ # @api private
78
+ def add(*data)
79
+ command(:create, relation(through), use: [:timestamps])
80
+ .call(associate(serialize(data)))
81
+ rescue => exception
82
+ raise Hanami::Model::Error.for(exception)
83
+ end
84
+
85
+ # @since 1.1.0
86
+ # @api private
87
+ def delete
88
+ relation(through).where(source_foreign_key => subject.fetch(source_primary_key)).delete
89
+ end
90
+
91
+ # @since 1.1.0
92
+ # @api private
93
+ def remove(target_id)
94
+ association_record = relation(through)
95
+ .where(target_foreign_key => target_id, source_foreign_key => subject.fetch(source_primary_key))
96
+ .one
97
+
98
+ return if association_record.nil?
99
+
100
+ ar_id = association_record.public_send relation(through).primary_key
101
+ command(:delete, relation(through)).by_pk(ar_id).call
102
+ end
103
+
104
+ private
105
+
106
+ # @since 1.1.0
107
+ # @api private
108
+ def container
109
+ repository.container
110
+ end
111
+
112
+ # @since 1.1.0
113
+ # @api private
114
+ def relation(name)
115
+ repository.relations[name]
116
+ end
117
+
118
+ # @since 1.1.0
119
+ # @api private
120
+ def command(target, relation, options = {})
121
+ repository.command(target, relation, options)
122
+ end
123
+
124
+ # @since 1.1.0
125
+ # @api private
126
+ def associate(data)
127
+ relation(target)
128
+ .associations[source]
129
+ .associate(container.relations, data, subject)
130
+ end
131
+
132
+ # @since 1.1.0
133
+ # @api private
134
+ def source_primary_key
135
+ association_keys[0].first
136
+ end
137
+
138
+ # @since 1.1.0
139
+ # @api private
140
+ def source_foreign_key
141
+ association_keys[0].last
142
+ end
143
+
144
+ # @since 1.1.0
145
+ # @api private
146
+ def association_keys
147
+ relation(source)
148
+ .associations[target]
149
+ .__send__(:join_key_map, container.relations)
150
+ end
151
+
152
+ # @since 1.1.0
153
+ # @api private
154
+ def target_foreign_key
155
+ association_keys[1].first
156
+ end
157
+
158
+ # @since 1.1.0
159
+ # @api private
160
+ def target_primary_key
161
+ association_keys[1].last
162
+ end
163
+
164
+ # Return the ROM::Associations for the source relation
165
+ #
166
+ # @since 1.1.0
167
+ # @api private
168
+ def association
169
+ relation(source).associations[target]
170
+ end
171
+
172
+ # @since 1.1.0
173
+ #
174
+ # @api private
175
+ def _build_scope
176
+ result = relation(association.target.to_sym).qualified
177
+ unless subject.nil?
178
+ result = result
179
+ .join(through, target_foreign_key => target_primary_key)
180
+ .where(source_foreign_key => subject.fetch(source_primary_key))
181
+ end
182
+ result.as(Model::MappedRelation.mapper_name)
183
+ end
184
+
185
+ # @since 1.1.0
186
+ # @api private
187
+ def __new__(new_scope)
188
+ self.class.new(repository, source, target, subject, new_scope)
189
+ end
190
+
191
+ # @since 1.1.0
192
+ # @api private
193
+ def serialize(data)
194
+ data.map do |d|
195
+ Utils::Hash.deep_serialize(d)
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
model-main/lib/hanami/model/configuration.rb ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "rom/configuration"
4
+
5
+ module Hanami
6
+ module Model
7
+ # Configuration for the framework, models and adapters.
8
+ #
9
+ # Hanami::Model has its own global configuration that can be manipulated
10
+ # via `Hanami::Model.configure`.
11
+ #
12
+ # @since 0.2.0
13
+ class Configuration
14
+ # @since 0.7.0
15
+ # @api private
16
+ attr_reader :mappings
17
+
18
+ # @since 0.7.0
19
+ # @api private
20
+ attr_reader :entities
21
+
22
+ # @since 1.0.0
23
+ # @api private
24
+ attr_reader :logger
25
+
26
+ # @since 1.0.0
27
+ # @api private
28
+ attr_reader :migrations_logger
29
+
30
+ # @since 0.2.0
31
+ # @api private
32
+ def initialize(configurator)
33
+ @backend = configurator.backend
34
+ @url = configurator.url
35
+ @migrations = configurator._migrations
36
+ @schema = configurator._schema
37
+ @gateway_config = configurator._gateway
38
+ @logger = configurator._logger
39
+ @migrations_logger = configurator.migrations_logger
40
+ @mappings = {}
41
+ @entities = {}
42
+ end
43
+
44
+ # NOTE: This must be changed when we want to support several adapters at the time
45
+ #
46
+ # @since 0.7.0
47
+ # @api private
48
+ attr_reader :url
49
+
50
+ # NOTE: This must be changed when we want to support several adapters at the time
51
+ #
52
+ # @raise [Hanami::Model::UnknownDatabaseAdapterError] if `url` is blank,
53
+ # or it uses an unknown adapter.
54
+ #
55
+ # @since 0.7.0
56
+ # @api private
57
+ def connection
58
+ gateway.connection
59
+ end
60
+
61
+ # NOTE: This must be changed when we want to support several adapters at the time
62
+ #
63
+ # @raise [Hanami::Model::UnknownDatabaseAdapterError] if `url` is blank,
64
+ # or it uses an unknown adapter.
65
+ #
66
+ # @since 0.7.0
67
+ # @api private
68
+ def gateway
69
+ gateways[:default]
70
+ end
71
+
72
+ # Root directory
73
+ #
74
+ # @since 0.4.0
75
+ # @api private
76
+ def root
77
+ Hanami.respond_to?(:root) ? Hanami.root : Pathname.pwd
78
+ end
79
+
80
+ # Migrations directory
81
+ #
82
+ # @since 0.4.0
83
+ def migrations
84
+ (@migrations.nil? ? root : root.join(@migrations)).realpath
85
+ end
86
+
87
+ # Path for schema dump file
88
+ #
89
+ # @since 0.4.0
90
+ def schema
91
+ @schema.nil? ? root : root.join(@schema)
92
+ end
93
+
94
+ # @since 0.7.0
95
+ # @api private
96
+ def define_mappings(root, &blk)
97
+ @mappings[root] = Mapping.new(&blk)
98
+ end
99
+
100
+ # @since 0.7.0
101
+ # @api private
102
+ def register_entity(plural, singular, klass)
103
+ @entities[plural] = klass
104
+ @entities[singular] = klass
105
+ end
106
+
107
+ # @since 0.7.0
108
+ # @api private
109
+ def define_entities_mappings(container, repositories)
110
+ return unless defined?(Sql::Entity::Schema)
111
+
112
+ repositories.each do |r|
113
+ relation = r.relation
114
+ entity = r.entity
115
+
116
+ entity.schema = Sql::Entity::Schema.new(entities, container.relations[relation], mappings.fetch(relation))
117
+ end
118
+ end
119
+
120
+ # @since 1.0.0
121
+ # @api private
122
+ def configure_gateway
123
+ @gateway_config&.call(gateway)
124
+ end
125
+
126
+ # @since 1.0.0
127
+ # @api private
128
+ def logger=(value)
129
+ return if value.nil?
130
+
131
+ gateway.use_logger(@logger = value)
132
+ end
133
+
134
+ # @raise [Hanami::Model::UnknownDatabaseAdapterError] if `url` is blank,
135
+ # or it uses an unknown adapter.
136
+ #
137
+ # @since 1.0.0
138
+ # @api private
139
+ def rom
140
+ @rom ||= ROM::Configuration.new(@backend, @url, infer_relations: false)
141
+ rescue => exception
142
+ raise UnknownDatabaseAdapterError.new(@url) if exception.message =~ /adapters/
143
+
144
+ raise exception
145
+ end
146
+
147
+ # @raise [Hanami::Model::UnknownDatabaseAdapterError] if `url` is blank,
148
+ # or it uses an unknown adapter.
149
+ #
150
+ # @since 1.0.0
151
+ # @api private
152
+ def load!(repositories, &blk)
153
+ rom.setup.auto_registration(config.directory.to_s) unless config.directory.nil?
154
+ rom.instance_eval(&blk) if block_given?
155
+ configure_gateway
156
+ repositories.each(&:load!)
157
+ self.logger = logger
158
+
159
+ container = ROM.container(rom)
160
+ define_entities_mappings(container, repositories)
161
+ container
162
+ rescue => exception
163
+ raise Hanami::Model::Error.for(exception)
164
+ end
165
+
166
+ # @since 1.0.0
167
+ # @api private
168
+ def method_missing(method_name, *args, &blk)
169
+ if rom.respond_to?(method_name)
170
+ rom.__send__(method_name, *args, &blk)
171
+ else
172
+ super
173
+ end
174
+ end
175
+
176
+ # @since 1.1.0
177
+ # @api private
178
+ def respond_to_missing?(method_name, include_all)
179
+ rom.respond_to?(method_name, include_all)
180
+ end
181
+ end
182
+ end
183
+ end
model-main/lib/hanami/model/configurator.rb ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Model
5
+ # Configuration DSL
6
+ #
7
+ # @since 0.7.0
8
+ # @api private
9
+ class Configurator
10
+ # @since 0.7.0
11
+ # @api private
12
+ attr_reader :backend
13
+
14
+ # @since 0.7.0
15
+ # @api private
16
+ attr_reader :url
17
+
18
+ # @since 0.7.0
19
+ # @api private
20
+ attr_reader :directory
21
+
22
+ # @since 0.7.0
23
+ # @api private
24
+ attr_reader :_migrations
25
+
26
+ # @since 0.7.0
27
+ # @api private
28
+ attr_reader :_schema
29
+
30
+ # @since 1.0.0
31
+ # @api private
32
+ attr_reader :_logger
33
+
34
+ # @since 1.0.0
35
+ # @api private
36
+ attr_reader :_gateway
37
+
38
+ # @since 0.7.0
39
+ # @api private
40
+ def self.build(&block)
41
+ new.tap { |config| config.instance_eval(&block) }
42
+ end
43
+
44
+ # @since 1.0.0
45
+ # @api private
46
+ def migrations_logger(stream = $stdout)
47
+ require "hanami/model/migrator/logger"
48
+ @migrations_logger ||= Hanami::Model::Migrator::Logger.new(stream)
49
+ end
50
+
51
+ private
52
+
53
+ # @since 0.7.0
54
+ # @api private
55
+ def adapter(backend, url)
56
+ @backend = backend
57
+ @url = url
58
+ end
59
+
60
+ # @since 0.7.0
61
+ # @api private
62
+ def path(path)
63
+ @directory = path
64
+ end
65
+
66
+ # @since 0.7.0
67
+ # @api private
68
+ def migrations(path)
69
+ @_migrations = path
70
+ end
71
+
72
+ # @since 0.7.0
73
+ # @api private
74
+ def schema(path)
75
+ @_schema = path
76
+ end
77
+
78
+ # @since 1.0.0
79
+ # @api private
80
+ def logger(stream, options = {})
81
+ require "hanami/logger"
82
+
83
+ opts = options.merge(stream: stream)
84
+ @_logger = Hanami::Logger.new("hanami.model", **opts)
85
+ end
86
+
87
+ # @since 1.0.0
88
+ # @api private
89
+ def gateway(&blk)
90
+ @_gateway = blk
91
+ end
92
+ end
93
+ end
94
+ end
model-main/lib/hanami/model/entity_name.rb ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Model
5
+ # Conventional name for entities.
6
+ #
7
+ # Given a repository named <tt>SourceFileRepository</tt>, the associated
8
+ # entity will be <tt>SourceFile</tt>.
9
+ #
10
+ # @since 0.7.0
11
+ # @api private
12
+ class EntityName
13
+ # @since 0.7.0
14
+ # @api private
15
+ SUFFIX = /Repository\z/.freeze
16
+
17
+ # @param name [Class,String] the class or its name
18
+ # @return [String] the entity name
19
+ #
20
+ # @since 0.7.0
21
+ # @api private
22
+ def initialize(name)
23
+ @name = name.sub(SUFFIX, "")
24
+ end
25
+
26
+ # @since 0.7.0
27
+ # @api private
28
+ def underscore
29
+ Utils::String.underscore(@name).to_sym
30
+ end
31
+
32
+ # @since 0.7.0
33
+ # @api private
34
+ def to_s
35
+ @name
36
+ end
37
+ end
38
+ end
39
+ end
model-main/lib/hanami/model/error.rb ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module Hanami
6
+ module Model
7
+ # Default Error class
8
+ #
9
+ # @since 0.5.1
10
+ class Error < ::StandardError
11
+ # @api private
12
+ # @since 0.7.0
13
+ @__mapping__ = Concurrent::Map.new
14
+
15
+ # @api private
16
+ # @since 0.7.0
17
+ def self.for(exception)
18
+ mapping.fetch(exception.class, self).new(exception)
19
+ end
20
+
21
+ # @api private
22
+ # @since 0.7.0
23
+ def self.register(external, internal)
24
+ mapping.put_if_absent(external, internal)
25
+ end
26
+
27
+ # @api private
28
+ # @since 0.7.0
29
+ def self.mapping
30
+ @__mapping__
31
+ end
32
+ end
33
+
34
+ # Generic database error
35
+ #
36
+ # @since 0.7.0
37
+ class DatabaseError < Error
38
+ end
39
+
40
+ # Error for invalid raw command syntax
41
+ #
42
+ # @since 0.5.0
43
+ class InvalidCommandError < Error
44
+ # @since 0.5.0
45
+ # @api private
46
+ def initialize(message = "Invalid command")
47
+ super
48
+ end
49
+ end
50
+
51
+ # Error for Constraint Violation
52
+ #
53
+ # @since 0.7.0
54
+ class ConstraintViolationError < Error
55
+ # @since 0.7.0
56
+ # @api private
57
+ def initialize(message = "Constraint has been violated")
58
+ super
59
+ end
60
+ end
61
+
62
+ # Error for Unique Constraint Violation
63
+ #
64
+ # @since 0.6.1
65
+ class UniqueConstraintViolationError < ConstraintViolationError
66
+ # @since 0.6.1
67
+ # @api private
68
+ def initialize(message = "Unique constraint has been violated")
69
+ super
70
+ end
71
+ end
72
+
73
+ # Error for Foreign Key Constraint Violation
74
+ #
75
+ # @since 0.6.1
76
+ class ForeignKeyConstraintViolationError < ConstraintViolationError
77
+ # @since 0.6.1
78
+ # @api private
79
+ def initialize(message = "Foreign key constraint has been violated")
80
+ super
81
+ end
82
+ end
83
+
84
+ # Error for Not Null Constraint Violation
85
+ #
86
+ # @since 0.6.1
87
+ class NotNullConstraintViolationError < ConstraintViolationError
88
+ # @since 0.6.1
89
+ # @api private
90
+ def initialize(message = "NOT NULL constraint has been violated")
91
+ super
92
+ end
93
+ end
94
+
95
+ # Error for Check Constraint Violation raised by Sequel
96
+ #
97
+ # @since 0.6.1
98
+ class CheckConstraintViolationError < ConstraintViolationError
99
+ # @since 0.6.1
100
+ # @api private
101
+ def initialize(message = "Check constraint has been violated")
102
+ super
103
+ end
104
+ end
105
+
106
+ # Unknown database type error for repository auto-mapping
107
+ #
108
+ # @since 1.0.0
109
+ class UnknownDatabaseTypeError < Error
110
+ end
111
+
112
+ # Unknown primary key error
113
+ #
114
+ # @since 1.0.0
115
+ class MissingPrimaryKeyError < Error
116
+ end
117
+
118
+ # Unknown attribute error
119
+ #
120
+ # @since 1.2.0
121
+ class UnknownAttributeError < Error
122
+ end
123
+
124
+ # Unknown database adapter error
125
+ #
126
+ # @since 1.2.1
127
+ class UnknownDatabaseAdapterError < Error
128
+ def initialize(url)
129
+ super("Unknown database adapter for URL: #{url.inspect}. Please check your database configuration (hint: ENV['DATABASE_URL']).")
130
+ end
131
+ end
132
+ end
133
+ end
model-main/lib/hanami/model/mapped_relation.rb ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Model
5
+ # Mapped proxy for ROM relations.
6
+ #
7
+ # It eliminates the need to use #as for repository queries
8
+ #
9
+ # @since 1.0.0
10
+ # @api private
11
+ class MappedRelation < SimpleDelegator
12
+ # Mapper name.
13
+ #
14
+ # With ROM mapping there is a link between the entity class and a generic
15
+ # reference for it. Example: <tt>BookRepository</tt> references <tt>Book</tt>
16
+ # as <tt>:entity</tt>.
17
+ #
18
+ # @since 1.0.0
19
+ # @api private
20
+ MAPPER_NAME = :entity
21
+
22
+ # @since 1.0.0
23
+ # @api private
24
+ def self.mapper_name
25
+ MAPPER_NAME
26
+ end
27
+
28
+ # @since 1.0.0
29
+ # @api private
30
+ def initialize(relation)
31
+ @relation = relation
32
+ super(relation.as(self.class.mapper_name))
33
+ end
34
+
35
+ # Access low level relation's attribute
36
+ #
37
+ # @param attribute [Symbol] the attribute name
38
+ #
39
+ # @return [ROM::SQL::Attribute] the attribute
40
+ #
41
+ # @raise [Hanami::Model::UnknownAttributeError] if the attribute cannot be found
42
+ #
43
+ # @since 1.2.0
44
+ #
45
+ # @example
46
+ # class UserRepository < Hanami::Repository
47
+ # def by_matching_name(name)
48
+ # users
49
+ # .where(users[:name].ilike(name))
50
+ # .map_to(User)
51
+ # .to_a
52
+ # end
53
+ # end
54
+ def [](attribute)
55
+ @relation[attribute]
56
+ rescue KeyError => exception
57
+ raise UnknownAttributeError.new(exception.message)
58
+ end
59
+ end
60
+ end
61
+ end
model-main/lib/hanami/model/mapping.rb ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "transproc/all"
4
+
5
+ module Hanami
6
+ module Model
7
+ # Mapping
8
+ #
9
+ # @since 0.1.0
10
+ # @api private
11
+ class Mapping
12
+ extend Transproc::Registry
13
+
14
+ import Transproc::HashTransformations
15
+
16
+ # @since 0.1.0
17
+ # @api private
18
+ def initialize(&blk)
19
+ @attributes = {}
20
+ @r_attributes = {}
21
+ instance_eval(&blk)
22
+ @processor = @attributes.empty? ? ::Hash : t(:rename_keys, @attributes)
23
+ end
24
+
25
+ # @api private
26
+ def t(name, *args)
27
+ self.class[name, *args]
28
+ end
29
+
30
+ # @api private
31
+ def model(entity)
32
+ end
33
+
34
+ # @api private
35
+ def register_as(name)
36
+ end
37
+
38
+ # @api private
39
+ def attribute(name, options)
40
+ from = options.fetch(:from, name)
41
+
42
+ @attributes[name] = from
43
+ @r_attributes[from] = name
44
+ end
45
+
46
+ # @api private
47
+ def process(input)
48
+ @processor[input]
49
+ end
50
+
51
+ # @api private
52
+ def reverse?
53
+ @r_attributes.any?
54
+ end
55
+
56
+ # @api private
57
+ def translate(attribute)
58
+ @r_attributes.fetch(attribute)
59
+ end
60
+ end
61
+ end
62
+ end
model-main/lib/hanami/model/migration.rb ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Model
5
+ # Database migration
6
+ #
7
+ # @since 0.7.0
8
+ # @api private
9
+ class Migration
10
+ # @since 0.7.0
11
+ # @api private
12
+ attr_reader :gateway
13
+
14
+ # @since 0.7.0
15
+ # @api private
16
+ attr_reader :migration
17
+
18
+ # @since 0.7.0
19
+ # @api private
20
+ def initialize(gateway, &block)
21
+ @gateway = gateway
22
+ @migration = gateway.migration(&block)
23
+ freeze
24
+ end
25
+
26
+ # @since 0.7.0
27
+ # @api private
28
+ def run(direction = :up)
29
+ migration.apply(gateway.connection, direction)
30
+ end
31
+ end
32
+ end
33
+ end
model-main/lib/hanami/model/migrator.rb ADDED
@@ -0,0 +1,394 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "sequel"
4
+ require "sequel/extensions/migration"
5
+
6
+ module Hanami
7
+ module Model
8
+ # Migration error
9
+ #
10
+ # @since 0.4.0
11
+ class MigrationError < Hanami::Model::Error
12
+ end
13
+
14
+ # Database schema migrator
15
+ #
16
+ # @since 0.4.0
17
+ class Migrator
18
+ require "hanami/model/migrator/connection"
19
+ require "hanami/model/migrator/adapter"
20
+
21
+ # Create database defined by current configuration.
22
+ #
23
+ # It's only implemented for the following databases:
24
+ #
25
+ # * SQLite3
26
+ # * PostgreSQL
27
+ # * MySQL
28
+ #
29
+ # @raise [Hanami::Model::MigrationError] if an error occurs
30
+ #
31
+ # @since 0.4.0
32
+ #
33
+ # @see Hanami::Model::Configuration#adapter
34
+ #
35
+ # @example
36
+ # require 'hanami/model'
37
+ # require 'hanami/model/migrator'
38
+ #
39
+ # Hanami::Model.configure do
40
+ # # ...
41
+ # adapter :sql, 'postgres://localhost/foo'
42
+ # end
43
+ #
44
+ # Hanami::Model::Migrator.create # Creates `foo' database
45
+ #
46
+ # NOTE: Class level interface SHOULD be removed in Hanami 2.0
47
+ def self.create
48
+ new.create
49
+ end
50
+
51
+ # Drop database defined by current configuration.
52
+ #
53
+ # It's only implemented for the following databases:
54
+ #
55
+ # * SQLite3
56
+ # * PostgreSQL
57
+ # * MySQL
58
+ #
59
+ # @raise [Hanami::Model::MigrationError] if an error occurs
60
+ #
61
+ # @since 0.4.0
62
+ #
63
+ # @see Hanami::Model::Configuration#adapter
64
+ #
65
+ # @example
66
+ # require 'hanami/model'
67
+ # require 'hanami/model/migrator'
68
+ #
69
+ # Hanami::Model.configure do
70
+ # # ...
71
+ # adapter :sql, 'postgres://localhost/foo'
72
+ # end
73
+ #
74
+ # Hanami::Model::Migrator.drop # Drops `foo' database
75
+ #
76
+ # NOTE: Class level interface SHOULD be removed in Hanami 2.0
77
+ def self.drop
78
+ new.drop
79
+ end
80
+
81
+ # Migrate database schema
82
+ #
83
+ # It's possible to migrate "down" by specifying a version
84
+ # (eg. <tt>"20150610133853"</tt>)
85
+ #
86
+ # @param version [String,NilClass] target version
87
+ #
88
+ # @raise [Hanami::Model::MigrationError] if an error occurs
89
+ #
90
+ # @since 0.4.0
91
+ #
92
+ # @see Hanami::Model::Configuration#adapter
93
+ # @see Hanami::Model::Configuration#migrations
94
+ # @see Hanami::Model::Configuration#rollback
95
+ #
96
+ # @example Migrate Up
97
+ # require 'hanami/model'
98
+ # require 'hanami/model/migrator'
99
+ #
100
+ # Hanami::Model.configure do
101
+ # # ...
102
+ # adapter :sql, 'postgres://localhost/foo'
103
+ # migrations 'db/migrations'
104
+ # end
105
+ #
106
+ # # Reads all files from "db/migrations" and apply them
107
+ # Hanami::Model::Migrator.migrate
108
+ #
109
+ # @example Migrate Down
110
+ # require 'hanami/model'
111
+ # require 'hanami/model/migrator'
112
+ #
113
+ # Hanami::Model.configure do
114
+ # # ...
115
+ # adapter :sql, 'postgres://localhost/foo'
116
+ # migrations 'db/migrations'
117
+ # end
118
+ #
119
+ # # Reads all files from "db/migrations" and apply them
120
+ # Hanami::Model::Migrator.migrate
121
+ #
122
+ # # Migrate to a specific version
123
+ # Hanami::Model::Migrator.migrate(version: "20150610133853")
124
+ #
125
+ # NOTE: Class level interface SHOULD be removed in Hanami 2.0
126
+ def self.migrate(version: nil)
127
+ new.migrate(version: version)
128
+ end
129
+
130
+ # Rollback database schema
131
+ #
132
+ # @param steps [Number,NilClass] number of versions to rollback
133
+ #
134
+ # @raise [Hanami::Model::MigrationError] if an error occurs
135
+ #
136
+ # @since 1.1.0
137
+ #
138
+ # @see Hanami::Model::Configuration#adapter
139
+ # @see Hanami::Model::Configuration#migrations
140
+ # @see Hanami::Model::Configuration#migrate
141
+ #
142
+ # @example Rollback
143
+ # require 'hanami/model'
144
+ # require 'hanami/model/migrator'
145
+ #
146
+ # Hanami::Model.configure do
147
+ # # ...
148
+ # adapter :sql, 'postgres://localhost/foo'
149
+ # migrations 'db/migrations'
150
+ # end
151
+ #
152
+ # # Reads all files from "db/migrations" and apply them
153
+ # Hanami::Model::Migrator.migrate
154
+ #
155
+ # # By default only rollback one version
156
+ # Hanami::Model::Migrator.rollback
157
+ #
158
+ # # Use a hash passing a number of versions to rollback, it will rollbacks those versions
159
+ # Hanami::Model::Migrator.rollback(versions: 2)
160
+ #
161
+ # NOTE: Class level interface SHOULD be removed in Hanami 2.0
162
+ def self.rollback(steps: 1)
163
+ new.rollback(steps: steps)
164
+ end
165
+
166
+ # Migrate, dump schema, delete migrations.
167
+ #
168
+ # This is an experimental feature.
169
+ # It may change or be removed in the future.
170
+ #
171
+ # Actively developed applications accumulate tons of migrations.
172
+ # In the long term they are hard to maintain and slow to execute.
173
+ #
174
+ # "Apply" feature solves this problem.
175
+ #
176
+ # It keeps an updated SQL file with the structure of the database.
177
+ # This file can be used to create fresh databases for developer machines
178
+ # or during testing. This is faster than to run dozen or hundred migrations.
179
+ #
180
+ # When we use "apply", it eliminates all the migrations that are no longer
181
+ # necessary.
182
+ #
183
+ # @raise [Hanami::Model::MigrationError] if an error occurs
184
+ #
185
+ # @since 0.4.0
186
+ #
187
+ # @see Hanami::Model::Configuration#adapter
188
+ # @see Hanami::Model::Configuration#migrations
189
+ #
190
+ # @example Apply Migrations
191
+ # require 'hanami/model'
192
+ # require 'hanami/model/migrator'
193
+ #
194
+ # Hanami::Model.configure do
195
+ # # ...
196
+ # adapter :sql, 'postgres://localhost/foo'
197
+ # migrations 'db/migrations'
198
+ # schema 'db/schema.sql'
199
+ # end
200
+ #
201
+ # # Reads all files from "db/migrations" and apply and delete them.
202
+ # # It generates an updated version of "db/schema.sql"
203
+ # Hanami::Model::Migrator.apply
204
+ #
205
+ # NOTE: Class level interface SHOULD be removed in Hanami 2.0
206
+ def self.apply
207
+ new.apply
208
+ end
209
+
210
+ # Prepare database: drop, create, load schema (if any), migrate.
211
+ #
212
+ # This is designed for development machines and testing mode.
213
+ # It works faster if used with <tt>apply</tt>.
214
+ #
215
+ # @raise [Hanami::Model::MigrationError] if an error occurs
216
+ #
217
+ # @since 0.4.0
218
+ #
219
+ # @see Hanami::Model::Migrator.apply
220
+ #
221
+ # @example Prepare Database
222
+ # require 'hanami/model'
223
+ # require 'hanami/model/migrator'
224
+ #
225
+ # Hanami::Model.configure do
226
+ # # ...
227
+ # adapter :sql, 'postgres://localhost/foo'
228
+ # migrations 'db/migrations'
229
+ # end
230
+ #
231
+ # Hanami::Model::Migrator.prepare # => creates `foo' and runs migrations
232
+ #
233
+ # @example Prepare Database (with schema dump)
234
+ # require 'hanami/model'
235
+ # require 'hanami/model/migrator'
236
+ #
237
+ # Hanami::Model.configure do
238
+ # # ...
239
+ # adapter :sql, 'postgres://localhost/foo'
240
+ # migrations 'db/migrations'
241
+ # schema 'db/schema.sql'
242
+ # end
243
+ #
244
+ # Hanami::Model::Migrator.apply # => updates schema dump
245
+ # Hanami::Model::Migrator.prepare # => creates `foo', load schema and run pending migrations (if any)
246
+ #
247
+ # NOTE: Class level interface SHOULD be removed in Hanami 2.0
248
+ def self.prepare
249
+ new.prepare
250
+ end
251
+
252
+ # Return current database version timestamp
253
+ #
254
+ # If no migrations were ran, it returns <tt>nil</tt>.
255
+ #
256
+ # @return [String,NilClass] current version, if previously migrated
257
+ #
258
+ # @since 0.4.0
259
+ #
260
+ # @example
261
+ # # Given last migrations is:
262
+ # # 20150610133853_create_books.rb
263
+ #
264
+ # Hanami::Model::Migrator.version # => "20150610133853"
265
+ #
266
+ # NOTE: Class level interface SHOULD be removed in Hanami 2.0
267
+ def self.version
268
+ new.version
269
+ end
270
+
271
+ # Instantiate a new migrator
272
+ #
273
+ # @param configuration [Hanami::Model::Configuration] framework configuration
274
+ #
275
+ # @return [Hanami::Model::Migrator] a new instance
276
+ #
277
+ # @since 0.7.0
278
+ # @api private
279
+ def initialize(configuration: self.class.configuration)
280
+ @configuration = configuration
281
+ @adapter = Adapter.for(configuration)
282
+ end
283
+
284
+ # @since 0.7.0
285
+ # @api private
286
+ #
287
+ # @see Hanami::Model::Migrator.create
288
+ def create
289
+ adapter.create
290
+ end
291
+
292
+ # @since 0.7.0
293
+ # @api private
294
+ #
295
+ # @see Hanami::Model::Migrator.drop
296
+ def drop
297
+ adapter.drop
298
+ end
299
+
300
+ # @since 0.7.0
301
+ # @api private
302
+ #
303
+ # @see Hanami::Model::Migrator.migrate
304
+ def migrate(version: nil)
305
+ adapter.migrate(migrations, version) if migrations?
306
+ end
307
+
308
+ # @since 1.1.0
309
+ # @api private
310
+ #
311
+ # @see Hanami::Model::Migrator.rollback
312
+ def rollback(steps: 1)
313
+ adapter.rollback(migrations, steps.abs) if migrations?
314
+ end
315
+
316
+ # @since 0.7.0
317
+ # @api private
318
+ #
319
+ # @see Hanami::Model::Migrator.apply
320
+ def apply
321
+ migrate
322
+ adapter.dump
323
+ delete_migrations
324
+ end
325
+
326
+ # @since 0.7.0
327
+ # @api private
328
+ #
329
+ # @see Hanami::Model::Migrator.prepare
330
+ def prepare
331
+ drop
332
+ rescue # rubocop:disable Lint/SuppressedException
333
+ ensure
334
+ create
335
+ adapter.load
336
+ migrate
337
+ end
338
+
339
+ # @since 0.7.0
340
+ # @api private
341
+ #
342
+ # @see Hanami::Model::Migrator.version
343
+ def version
344
+ adapter.version
345
+ end
346
+
347
+ # Hanami::Model configuration
348
+ #
349
+ # @since 0.4.0
350
+ # @api private
351
+ def self.configuration
352
+ Model.configuration
353
+ end
354
+
355
+ private
356
+
357
+ # @since 0.7.0
358
+ # @api private
359
+ attr_reader :configuration
360
+
361
+ # @since 0.7.0
362
+ # @api private
363
+ attr_reader :connection
364
+
365
+ # @since 0.7.0
366
+ # @api private
367
+ attr_reader :adapter
368
+
369
+ # Migrations directory
370
+ #
371
+ # @since 0.7.0
372
+ # @api private
373
+ def migrations
374
+ configuration.migrations
375
+ end
376
+
377
+ # Check if there are migrations
378
+ #
379
+ # @since 0.7.0
380
+ # @api private
381
+ def migrations?
382
+ Dir["#{migrations}/*.rb"].any?
383
+ end
384
+
385
+ # Delete all the migrations
386
+ #
387
+ # @since 0.7.0
388
+ # @api private
389
+ def delete_migrations
390
+ migrations.each_child(&:delete)
391
+ end
392
+ end
393
+ end
394
+ end
model-main/lib/hanami/model/migrator/adapter.rb ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "shellwords"
5
+ require "open3"
6
+
7
+ module Hanami
8
+ module Model
9
+ class Migrator
10
+ # Migrator base adapter
11
+ #
12
+ # @since 0.4.0
13
+ # @api private
14
+ class Adapter
15
+ # Migrations table to store migrations metadata.
16
+ #
17
+ # @since 0.4.0
18
+ # @api private
19
+ MIGRATIONS_TABLE = :schema_migrations
20
+
21
+ # Migrations table version column
22
+ #
23
+ # @since 0.4.0
24
+ # @api private
25
+ MIGRATIONS_TABLE_VERSION_COLUMN = :filename
26
+
27
+ # Loads and returns a specific adapter for the given connection.
28
+ #
29
+ # @since 0.4.0
30
+ # @api private
31
+ def self.for(configuration)
32
+ connection = Connection.new(configuration)
33
+
34
+ case connection.database_type
35
+ when :sqlite
36
+ require "hanami/model/migrator/sqlite_adapter"
37
+ SQLiteAdapter
38
+ when :postgres
39
+ require "hanami/model/migrator/postgres_adapter"
40
+ PostgresAdapter
41
+ when :mysql
42
+ require "hanami/model/migrator/mysql_adapter"
43
+ MySQLAdapter
44
+ else
45
+ self
46
+ end.new(connection)
47
+ end
48
+
49
+ # Initialize an adapter
50
+ #
51
+ # @since 0.4.0
52
+ # @api private
53
+ def initialize(connection)
54
+ @connection = connection
55
+ end
56
+
57
+ # Create database.
58
+ # It must be implemented by subclasses.
59
+ #
60
+ # @since 0.4.0
61
+ # @api private
62
+ #
63
+ # @see Hanami::Model::Migrator.create
64
+ def create
65
+ raise MigrationError.new("Current adapter (#{connection.database_type}) doesn't support create.")
66
+ end
67
+
68
+ # Drop database.
69
+ # It must be implemented by subclasses.
70
+ #
71
+ # @since 0.4.0
72
+ # @api private
73
+ #
74
+ # @see Hanami::Model::Migrator.drop
75
+ def drop
76
+ raise MigrationError.new("Current adapter (#{connection.database_type}) doesn't support drop.")
77
+ end
78
+
79
+ # @since 0.4.0
80
+ # @api private
81
+ def migrate(migrations, version)
82
+ version = Integer(version) unless version.nil?
83
+
84
+ Sequel::Migrator.run(connection.raw, migrations, target: version, allow_missing_migration_files: true)
85
+ rescue Sequel::Migrator::Error => exception
86
+ raise MigrationError.new(exception.message)
87
+ end
88
+
89
+ # @since 1.1.0
90
+ # @api private
91
+ def rollback(migrations, steps)
92
+ table = migrations_table_dataset
93
+ version = version_to_rollback(table, steps)
94
+
95
+ Sequel::Migrator.run(connection.raw, migrations, target: version, allow_missing_migration_files: true)
96
+ rescue Sequel::Migrator::Error => exception
97
+ raise MigrationError.new(exception.message)
98
+ end
99
+
100
+ # Load database schema.
101
+ # It must be implemented by subclasses.
102
+ #
103
+ # @since 0.4.0
104
+ # @api private
105
+ #
106
+ # @see Hanami::Model::Migrator.prepare
107
+ def load
108
+ raise MigrationError.new("Current adapter (#{connection.database_type}) doesn't support load.")
109
+ end
110
+
111
+ # Database version.
112
+ #
113
+ # @since 0.4.0
114
+ # @api private
115
+ def version
116
+ table = migrations_table_dataset
117
+ return if table.nil?
118
+
119
+ record = table.order(MIGRATIONS_TABLE_VERSION_COLUMN).last
120
+ return if record.nil?
121
+
122
+ record.fetch(MIGRATIONS_TABLE_VERSION_COLUMN).scan(MIGRATIONS_FILE_NAME_PATTERN).first.to_s
123
+ end
124
+
125
+ private
126
+
127
+ # @since 1.1.0
128
+ # @api private
129
+ MIGRATIONS_FILE_NAME_PATTERN = /\A[\d]{14}/.freeze
130
+
131
+ # @since 1.1.0
132
+ # @api private
133
+ def version_to_rollback(table, steps)
134
+ record = table.order(Sequel.desc(MIGRATIONS_TABLE_VERSION_COLUMN)).all[steps]
135
+ return 0 unless record
136
+
137
+ record.fetch(MIGRATIONS_TABLE_VERSION_COLUMN).scan(MIGRATIONS_FILE_NAME_PATTERN).first.to_i
138
+ end
139
+
140
+ # @since 1.1.0
141
+ # @api private
142
+ def migrations_table_dataset
143
+ connection.table(MIGRATIONS_TABLE)
144
+ end
145
+
146
+ # @since 0.5.0
147
+ # @api private
148
+ attr_reader :connection
149
+
150
+ # @since 0.4.0
151
+ # @api private
152
+ def schema
153
+ connection.schema
154
+ end
155
+
156
+ # Returns a database connection
157
+ #
158
+ # Given a DB connection URI we can connect to a specific database or not, we need this when creating
159
+ # or dropping a database. Important to notice that we can't always open a _global_ DB connection,
160
+ # because most of the times application's DB user has no rights to do so.
161
+ #
162
+ # @param global [Boolean] determine whether or not a connection should specify a database.
163
+ #
164
+ # @since 0.5.0
165
+ # @api private
166
+ def new_connection(global: false)
167
+ uri = global ? connection.global_uri : connection.uri
168
+
169
+ Sequel.connect(uri)
170
+ end
171
+
172
+ # @since 0.4.0
173
+ # @api private
174
+ def database
175
+ escape connection.database
176
+ end
177
+
178
+ # @since 0.4.0
179
+ # @api private
180
+ def port
181
+ escape connection.port
182
+ end
183
+
184
+ # @since 0.4.0
185
+ # @api private
186
+ def host
187
+ escape connection.host
188
+ end
189
+
190
+ # @since 0.4.0
191
+ # @api private
192
+ def username
193
+ escape connection.user
194
+ end
195
+
196
+ # @since 0.4.0
197
+ # @api private
198
+ def password
199
+ escape connection.password
200
+ end
201
+
202
+ # @since 0.4.0
203
+ # @api private
204
+ def migrations_table
205
+ escape MIGRATIONS_TABLE
206
+ end
207
+
208
+ # @since 0.4.0
209
+ # @api private
210
+ def escape(string)
211
+ Shellwords.escape(string) unless string.nil?
212
+ end
213
+
214
+ # @since 1.0.2
215
+ # @api private
216
+ def execute(command, env: {}, error: ->(err) { raise MigrationError.new(err) })
217
+ Open3.popen3(env, command) do |_, stdout, stderr, wait_thr|
218
+ error.call(stderr.read) unless wait_thr.value.success?
219
+ yield stdout if block_given?
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
model-main/lib/hanami/model/migrator/connection.rb ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module Hanami
6
+ module Model
7
+ class Migrator
8
+ # Sequel connection wrapper
9
+ #
10
+ # Normalize external adapters interfaces
11
+ #
12
+ # @since 0.5.0
13
+ # @api private
14
+ class Connection
15
+ # @since 0.5.0
16
+ # @api private
17
+ def initialize(configuration)
18
+ @configuration = configuration
19
+ end
20
+
21
+ # @since 0.7.0
22
+ # @api private
23
+ def raw
24
+ @raw ||= begin
25
+ Sequel.connect(
26
+ configuration.url,
27
+ loggers: [configuration.migrations_logger]
28
+ )
29
+ rescue Sequel::AdapterNotFound
30
+ raise MigrationError.new("Current adapter (#{configuration.adapter.type}) doesn't support SQL database operations.")
31
+ end
32
+ end
33
+
34
+ # Returns DB connection host
35
+ #
36
+ # Even when adapter doesn't provide it explicitly it tries to parse
37
+ #
38
+ # @since 0.5.0
39
+ # @api private
40
+ def host
41
+ @host ||= parsed_uri.host || parsed_opt("host")
42
+ end
43
+
44
+ # Returns DB connection port
45
+ #
46
+ # Even when adapter doesn't provide it explicitly it tries to parse
47
+ #
48
+ # @since 0.5.0
49
+ # @api private
50
+ def port
51
+ @port ||= parsed_uri.port || parsed_opt("port").to_i.nonzero?
52
+ end
53
+
54
+ # Returns DB name from conenction
55
+ #
56
+ # Even when adapter doesn't provide it explicitly it tries to parse
57
+ #
58
+ # @since 0.5.0
59
+ # @api private
60
+ def database
61
+ @database ||= parsed_uri.path[1..-1]
62
+ end
63
+
64
+ # Returns DB type
65
+ #
66
+ # @example
67
+ # connection.database_type
68
+ # # => 'postgres'
69
+ #
70
+ # @since 0.5.0
71
+ # @api private
72
+ def database_type
73
+ case uri
74
+ when /sqlite/
75
+ :sqlite
76
+ when /postgres/
77
+ :postgres
78
+ when /mysql/
79
+ :mysql
80
+ end
81
+ end
82
+
83
+ # Returns user from DB connection
84
+ #
85
+ # Even when adapter doesn't provide it explicitly it tries to parse
86
+ #
87
+ # @since 0.5.0
88
+ # @api private
89
+ def user
90
+ @user ||= parsed_opt("user") || parsed_uri.user
91
+ end
92
+
93
+ # Returns user from DB connection
94
+ #
95
+ # Even when adapter doesn't provide it explicitly it tries to parse
96
+ #
97
+ # @since 0.5.0
98
+ # @api private
99
+ def password
100
+ @password ||= parsed_opt("password") || parsed_uri.password
101
+ end
102
+
103
+ # Returns DB connection URI directly from adapter
104
+ #
105
+ # @since 0.5.0
106
+ # @api private
107
+ def uri
108
+ @configuration.url
109
+ end
110
+
111
+ # Returns DB connection wihout specifying database name
112
+ #
113
+ # @since 0.5.0
114
+ # @api private
115
+ def global_uri
116
+ uri.sub(parsed_uri.select(:path).first, "")
117
+ end
118
+
119
+ # Returns a boolean telling if a DB connection is from JDBC or not
120
+ #
121
+ # @since 0.5.0
122
+ # @api private
123
+ def jdbc?
124
+ !uri.scan("jdbc:").empty?
125
+ end
126
+
127
+ # Returns database connection URI instance without JDBC namespace
128
+ #
129
+ # @since 0.5.0
130
+ # @api private
131
+ def parsed_uri
132
+ @parsed_uri ||= URI.parse(uri.sub("jdbc:", ""))
133
+ end
134
+
135
+ # @api private
136
+ def schema
137
+ configuration.schema
138
+ end
139
+
140
+ # Return the database table for the given name
141
+ #
142
+ # @since 0.7.0
143
+ # @api private
144
+ def table(name)
145
+ raw[name] if raw.tables.include?(name)
146
+ end
147
+
148
+ private
149
+
150
+ # @since 1.0.0
151
+ # @api private
152
+ attr_reader :configuration
153
+
154
+ # Returns a value of a given query string param
155
+ #
156
+ # @param option [String] which option from database connection will be extracted from URI
157
+ #
158
+ # @since 0.5.0
159
+ # @api private
160
+ def parsed_opt(option, query: parsed_uri.query)
161
+ return if query.nil?
162
+
163
+ @parsed_query_opts ||= CGI.parse(query)
164
+ @parsed_query_opts[option].to_a.last
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
model-main/lib/hanami/model/migrator/logger.rb ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/logger"
4
+
5
+ module Hanami
6
+ module Model
7
+ class Migrator
8
+ # Automatic logger for migrations
9
+ #
10
+ # @since 1.0.0
11
+ # @api private
12
+ class Logger < Hanami::Logger
13
+ # Formatter for migrations logger
14
+ #
15
+ # @since 1.0.0
16
+ # @api private
17
+ class Formatter < Hanami::Logger::Formatter
18
+ private
19
+
20
+ # @since 1.0.0
21
+ # @api private
22
+ def _format(hash)
23
+ "[hanami] [#{hash.fetch(:severity)}] #{hash.fetch(:message)}\n"
24
+ end
25
+ end
26
+
27
+ # @since 1.0.0
28
+ # @api private
29
+ def initialize(stream)
30
+ super(nil, stream: stream, formatter: Formatter.new)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
model-main/lib/hanami/model/migrator/mysql_adapter.rb ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Model
5
+ class Migrator
6
+ # MySQL adapter
7
+ #
8
+ # @since 0.4.0
9
+ # @api private
10
+ class MySQLAdapter < Adapter
11
+ # @since 0.7.0
12
+ # @api private
13
+ PASSWORD = "MYSQL_PWD"
14
+
15
+ # @since 1.3.3
16
+ # @api private
17
+ DEFAULT_PORT = 3306
18
+
19
+ # @since 1.0.0
20
+ # @api private
21
+ DB_CREATION_ERROR = "Database creation failed. If the database exists, " \
22
+ "then its console may be open. See this issue for more details: " \
23
+ "https://github.com/hanami/model/issues/250"
24
+
25
+ # @since 0.4.0
26
+ # @api private
27
+ def create
28
+ new_connection(global: true).run %(CREATE DATABASE `#{database}`;)
29
+ rescue Sequel::DatabaseError => exception
30
+ message = if exception.message.match(/database exists/)
31
+ DB_CREATION_ERROR
32
+ else
33
+ exception.message
34
+ end
35
+
36
+ raise MigrationError.new(message)
37
+ end
38
+
39
+ # @since 0.4.0
40
+ # @api private
41
+ def drop
42
+ new_connection(global: true).run %(DROP DATABASE `#{database}`;)
43
+ rescue Sequel::DatabaseError => exception
44
+ message = if exception.message.match(/doesn\'t exist/)
45
+ "Cannot find database: #{database}"
46
+ else
47
+ exception.message
48
+ end
49
+
50
+ raise MigrationError.new(message)
51
+ end
52
+
53
+ # @since 0.4.0
54
+ # @api private
55
+ def dump
56
+ dump_structure
57
+ dump_migrations_data
58
+ end
59
+
60
+ # @since 0.4.0
61
+ # @api private
62
+ def load
63
+ load_structure
64
+ end
65
+
66
+ private
67
+
68
+ # @since 0.7.0
69
+ # @api private
70
+ def password
71
+ connection.password
72
+ end
73
+
74
+ def port
75
+ super || DEFAULT_PORT
76
+ end
77
+
78
+ # @since 0.4.0
79
+ # @api private
80
+ def dump_structure
81
+ execute "mysqldump --host=#{host} --port=#{port} --user=#{username} --no-data --skip-comments --ignore-table=#{database}.#{migrations_table} #{database} > #{schema}", env: {PASSWORD => password}
82
+ end
83
+
84
+ # @since 0.4.0
85
+ # @api private
86
+ def load_structure
87
+ execute("mysql --host=#{host} --port=#{port} --user=#{username} #{database} < #{escape(schema)}", env: {PASSWORD => password}) if schema.exist?
88
+ end
89
+
90
+ # @since 0.4.0
91
+ # @api private
92
+ def dump_migrations_data
93
+ execute "mysqldump --host=#{host} --port=#{port} --user=#{username} --skip-comments #{database} #{migrations_table} >> #{schema}", env: {PASSWORD => password}
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
model-main/lib/hanami/model/migrator/postgres_adapter.rb ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/utils/blank"
4
+
5
+ module Hanami
6
+ module Model
7
+ class Migrator
8
+ # PostgreSQL adapter
9
+ #
10
+ # @since 0.4.0
11
+ # @api private
12
+ class PostgresAdapter < Adapter
13
+ # @since 0.4.0
14
+ # @api private
15
+ HOST = "PGHOST"
16
+
17
+ # @since 0.4.0
18
+ # @api private
19
+ PORT = "PGPORT"
20
+
21
+ # @since 0.4.0
22
+ # @api private
23
+ USER = "PGUSER"
24
+
25
+ # @since 0.4.0
26
+ # @api private
27
+ PASSWORD = "PGPASSWORD"
28
+
29
+ # @since 1.0.0
30
+ # @api private
31
+ DB_CREATION_ERROR = "createdb: database creation failed. If the database exists, " \
32
+ "then its console may be open. See this issue for more details: " \
33
+ "https://github.com/hanami/model/issues/250"
34
+
35
+ # @since 0.4.0
36
+ # @api private
37
+ def create
38
+ call_db_command("createdb")
39
+ end
40
+
41
+ # @since 0.4.0
42
+ # @api private
43
+ def drop
44
+ call_db_command("dropdb")
45
+ end
46
+
47
+ # @since 0.4.0
48
+ # @api private
49
+ def dump
50
+ dump_structure
51
+ dump_migrations_data
52
+ end
53
+
54
+ # @since 0.4.0
55
+ # @api private
56
+ def load
57
+ load_structure
58
+ end
59
+
60
+ private
61
+
62
+ # @since 1.3.3
63
+ # @api private
64
+ def environment_variables
65
+ {}.tap do |env|
66
+ env[HOST] = host unless host.nil?
67
+ env[PORT] = port.to_s unless port.nil?
68
+ env[PASSWORD] = password unless password.nil?
69
+ env[USER] = username unless username.nil?
70
+ end
71
+ end
72
+
73
+ # @since 0.4.0
74
+ # @api private
75
+ def dump_structure
76
+ execute "pg_dump -s -x -O -T #{migrations_table} -f #{escape(schema)} #{database}", env: environment_variables
77
+ end
78
+
79
+ # @since 0.4.0
80
+ # @api private
81
+ def load_structure
82
+ return unless schema.exist?
83
+
84
+ execute "psql -X -q -f #{escape(schema)} #{database}", env: environment_variables
85
+ end
86
+
87
+ # @since 0.4.0
88
+ # @api private
89
+ def dump_migrations_data
90
+ error = ->(err) { raise MigrationError.new(err) unless err =~ /no matching tables/i }
91
+ execute "pg_dump -t #{migrations_table} #{database} >> #{escape(schema)}", error: error, env: environment_variables
92
+ end
93
+
94
+ # @since 0.5.1
95
+ # @api private
96
+ def call_db_command(command)
97
+ require "open3"
98
+
99
+ begin
100
+ Open3.popen3(environment_variables, command, database) do |_stdin, _stdout, stderr, wait_thr|
101
+ raise MigrationError.new(modified_message(stderr.read)) unless wait_thr.value.success? # wait_thr.value is the exit status
102
+ end
103
+ rescue SystemCallError => exception
104
+ raise MigrationError.new(modified_message(exception.message))
105
+ end
106
+ end
107
+
108
+ # @since 1.1.0
109
+ # @api private
110
+ def modified_message(original_message)
111
+ case original_message
112
+ when /already exists/
113
+ DB_CREATION_ERROR
114
+ when /does not exist/
115
+ "Cannot find database: #{database}"
116
+ when /No such file or directory/
117
+ "Could not find executable in your PATH: `#{original_message.split.last}`"
118
+ else
119
+ original_message
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
model-main/lib/hanami/model/migrator/sqlite_adapter.rb ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "hanami/utils"
5
+ require "English"
6
+
7
+ module Hanami
8
+ module Model
9
+ class Migrator
10
+ # SQLite3 Migrator
11
+ #
12
+ # @since 0.4.0
13
+ # @api private
14
+ class SQLiteAdapter < Adapter
15
+ # No-op for in-memory databases
16
+ #
17
+ # @since 0.4.0
18
+ # @api private
19
+ module Memory
20
+ # @since 0.4.0
21
+ # @api private
22
+ def create
23
+ end
24
+
25
+ # @since 0.4.0
26
+ # @api private
27
+ def drop
28
+ end
29
+ end
30
+
31
+ # Initialize adapter
32
+ #
33
+ # @since 0.4.0
34
+ # @api private
35
+ def initialize(configuration)
36
+ super
37
+ extend Memory if memory?
38
+ end
39
+
40
+ # @since 0.4.0
41
+ # @api private
42
+ def create
43
+ path.dirname.mkpath
44
+ FileUtils.touch(path)
45
+ rescue Errno::EACCES, Errno::EPERM
46
+ raise MigrationError.new("Permission denied: #{path.sub(/\A\/\//, '')}")
47
+ end
48
+
49
+ # @since 0.4.0
50
+ # @api private
51
+ def drop
52
+ path.delete
53
+ rescue Errno::ENOENT
54
+ raise MigrationError.new("Cannot find database: #{path.sub(/\A\/\//, '')}")
55
+ end
56
+
57
+ # @since 0.4.0
58
+ # @api private
59
+ def dump
60
+ dump_structure
61
+ dump_migrations_data
62
+ end
63
+
64
+ # @since 0.4.0
65
+ # @api private
66
+ def load
67
+ load_structure
68
+ end
69
+
70
+ private
71
+
72
+ # @since 0.4.0
73
+ # @api private
74
+ def path
75
+ root.join(
76
+ @connection.uri.sub(/\A(jdbc:sqlite:\/\/|sqlite:\/\/)/, "")
77
+ )
78
+ end
79
+
80
+ # @since 0.4.0
81
+ # @api private
82
+ def root
83
+ Hanami::Model.configuration.root
84
+ end
85
+
86
+ # @since 0.4.0
87
+ # @api private
88
+ def memory?
89
+ uri = path.to_s
90
+ uri.match(/sqlite\:\/\z/) ||
91
+ uri.match(/\:memory\:/)
92
+ end
93
+
94
+ # @since 0.4.0
95
+ # @api private
96
+ def dump_structure
97
+ execute "sqlite3 #{escape(path)} .schema > #{escape(schema)}"
98
+ end
99
+
100
+ # @since 0.4.0
101
+ # @api private
102
+ def load_structure
103
+ execute "sqlite3 #{escape(path)} < #{escape(schema)}" if schema.exist?
104
+ end
105
+
106
+ # @since 0.4.0
107
+ # @api private
108
+ #
109
+ def dump_migrations_data
110
+ execute "sqlite3 #{escape(path)} .dump" do |stdout|
111
+ begin
112
+ contents = stdout.read.split($INPUT_RECORD_SEPARATOR)
113
+ contents = contents.grep(/^INSERT INTO "?#{migrations_table}"?/)
114
+
115
+ ::File.open(schema, ::File::CREAT | ::File::BINARY | ::File::WRONLY | ::File::APPEND) do |file|
116
+ file.write(contents.join($INPUT_RECORD_SEPARATOR))
117
+ end
118
+ rescue => exception
119
+ raise MigrationError.new(exception.message)
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
model-main/lib/hanami/model/plugins.rb ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Model
5
+ # Plugins to extend read/write operations from/to the database
6
+ #
7
+ # @since 0.7.0
8
+ # @api private
9
+ module Plugins
10
+ # Wrapping input
11
+ #
12
+ # @since 0.7.0
13
+ # @api private
14
+ class WrappingInput
15
+ # @since 0.7.0
16
+ # @api private
17
+ def initialize(_relation, input)
18
+ @input = input || Hash
19
+ end
20
+ end
21
+
22
+ require "hanami/model/plugins/mapping"
23
+ require "hanami/model/plugins/schema"
24
+ require "hanami/model/plugins/timestamps"
25
+ end
26
+ end
27
+ end
model-main/lib/hanami/model/plugins/mapping.rb ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Model
5
+ module Plugins
6
+ # Transform output into model domain types (entities).
7
+ #
8
+ # @since 0.7.0
9
+ # @api private
10
+ module Mapping
11
+ # Takes the output and applies the transformations
12
+ #
13
+ # @since 0.7.0
14
+ # @api private
15
+ class InputWithMapping < WrappingInput
16
+ # @since 0.7.0
17
+ # @api private
18
+ def initialize(relation, input)
19
+ super
20
+ @mapping = Hanami::Model.configuration.mappings[relation.name.to_sym]
21
+ end
22
+
23
+ # Processes the output
24
+ #
25
+ # @since 0.7.0
26
+ # @api private
27
+ def [](value)
28
+ @input[@mapping.process(value)]
29
+ end
30
+ end
31
+
32
+ # Class interface
33
+ #
34
+ # @since 0.7.0
35
+ # @api private
36
+ module ClassMethods
37
+ # Builds the output processor
38
+ #
39
+ # @since 0.7.0
40
+ # @api private
41
+ def build(relation, options = {})
42
+ wrapped_input = InputWithMapping.new(relation, options.fetch(:input) { input })
43
+ super(relation, options.merge(input: wrapped_input))
44
+ end
45
+ end
46
+
47
+ # @since 0.7.0
48
+ # @api private
49
+ def self.included(klass)
50
+ super
51
+
52
+ klass.extend ClassMethods
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
model-main/lib/hanami/model/plugins/schema.rb ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Model
5
+ module Plugins
6
+ # Transform input values into database specific types (primitives).
7
+ #
8
+ # @since 0.7.0
9
+ # @api private
10
+ module Schema
11
+ # Takes the input and applies the values transformations.
12
+ #
13
+ # @since 0.7.0
14
+ # @api private
15
+ class InputWithSchema < WrappingInput
16
+ # @since 0.7.0
17
+ # @api private
18
+ def initialize(relation, input)
19
+ super
20
+ @schema = relation.input_schema
21
+ end
22
+
23
+ # Processes the input
24
+ #
25
+ # @since 0.7.0
26
+ # @api private
27
+ def [](value)
28
+ @schema[@input[value]]
29
+ end
30
+ end
31
+
32
+ # Class interface
33
+ #
34
+ # @since 0.7.0
35
+ # @api private
36
+ module ClassMethods
37
+ # Builds the input processor
38
+ #
39
+ # @since 0.7.0
40
+ # @api private
41
+ def build(relation, options = {})
42
+ wrapped_input = InputWithSchema.new(relation, options.fetch(:input) { input })
43
+ super(relation, options.merge(input: wrapped_input))
44
+ end
45
+ end
46
+
47
+ # @since 0.7.0
48
+ # @api private
49
+ def self.included(klass)
50
+ super
51
+
52
+ klass.extend ClassMethods
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
model-main/lib/hanami/model/plugins/timestamps.rb ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Model
5
+ module Plugins
6
+ # Automatically set/update timestamp columns for create/update commands
7
+ #
8
+ # @since 0.7.0
9
+ # @api private
10
+ module Timestamps
11
+ # Takes the input and applies the timestamp transformation.
12
+ # This is an "abstract class", please look at the subclasses for
13
+ # specific behaviors.
14
+ #
15
+ # @since 0.7.0
16
+ # @api private
17
+ class InputWithTimestamp < WrappingInput
18
+ # Conventional timestamp names
19
+ #
20
+ # @since 0.7.0
21
+ # @api private
22
+ TIMESTAMPS = %i[created_at updated_at].freeze
23
+
24
+ # @since 0.7.0
25
+ # @api private
26
+ def initialize(relation, input)
27
+ super
28
+ @timestamps = relation.columns & TIMESTAMPS
29
+ end
30
+
31
+ # Processes the input
32
+ #
33
+ # @since 0.7.0
34
+ # @api private
35
+ def [](value)
36
+ return @input[value] unless timestamps?
37
+
38
+ _touch(@input[value], Time.now)
39
+ end
40
+
41
+ protected
42
+
43
+ # @since 0.7.0
44
+ # @api private
45
+ def _touch(_value)
46
+ raise NoMethodError
47
+ end
48
+
49
+ private
50
+
51
+ # @since 0.7.0
52
+ # @api private
53
+ def timestamps?
54
+ !@timestamps.empty?
55
+ end
56
+ end
57
+
58
+ # Updates <tt>updated_at</tt> timestamp for update commands
59
+ #
60
+ # @since 0.7.0
61
+ # @api private
62
+ class InputWithUpdateTimestamp < InputWithTimestamp
63
+ protected
64
+
65
+ # @since 0.7.0
66
+ # @api private
67
+ def _touch(value, now)
68
+ value[:updated_at] ||= now if @timestamps.include?(:updated_at)
69
+ value
70
+ end
71
+ end
72
+
73
+ # Sets <tt>created_at</tt> and <tt>updated_at</tt> timestamps for create commands
74
+ #
75
+ # @since 0.7.0
76
+ # @api private
77
+ class InputWithCreateTimestamp < InputWithUpdateTimestamp
78
+ protected
79
+
80
+ # @since 0.7.0
81
+ # @api private
82
+ def _touch(value, now)
83
+ super
84
+ value[:created_at] ||= now if @timestamps.include?(:created_at)
85
+ value
86
+ end
87
+ end
88
+
89
+ # Class interface
90
+ #
91
+ # @since 0.7.0
92
+ # @api private
93
+ module ClassMethods
94
+ # Build an input processor according to the current command (create or update).
95
+ #
96
+ # @since 0.7.0
97
+ # @api private
98
+ def build(relation, options = {})
99
+ plugin = if self < ROM::Commands::Create
100
+ InputWithCreateTimestamp
101
+ else
102
+ InputWithUpdateTimestamp
103
+ end
104
+
105
+ wrapped_input = plugin.new(relation, options.fetch(:input) { input })
106
+ super(relation, options.merge(input: wrapped_input))
107
+ end
108
+ end
109
+
110
+ # @since 0.7.0
111
+ # @api private
112
+ def self.included(klass)
113
+ super
114
+
115
+ klass.extend ClassMethods
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
model-main/lib/hanami/model/relation_name.rb ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "entity_name"
4
+ require "hanami/utils/string"
5
+
6
+ module Hanami
7
+ module Model
8
+ # Conventional name for relations.
9
+ #
10
+ # Given a repository named <tt>SourceFileRepository</tt>, the associated
11
+ # relation will be <tt>:source_files</tt>.
12
+ #
13
+ # @since 0.7.0
14
+ # @api private
15
+ class RelationName < EntityName
16
+ # @param name [Class,String] the class or its name
17
+ # @return [String] the relation name
18
+ #
19
+ # @since 0.7.0
20
+ # @api private
21
+ def self.new(name)
22
+ Utils::String.transform(super, :underscore, :pluralize)
23
+ end
24
+ end
25
+ end
26
+ end
model-main/lib/hanami/model/sql.rb ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "rom-sql"
4
+ require "hanami/utils"
5
+
6
+ module Hanami
7
+ # Hanami::Model migrations
8
+ module Model
9
+ require "hanami/model/error"
10
+ require "hanami/model/association"
11
+ require "hanami/model/migration"
12
+
13
+ # Define a migration
14
+ #
15
+ # It must define an up/down strategy to write schema changes (up) and to
16
+ # rollback them (down).
17
+ #
18
+ # We can use <tt>up</tt> and <tt>down</tt> blocks for custom strategies, or
19
+ # only one <tt>change</tt> block that automatically implements "down" strategy.
20
+ #
21
+ # @param blk [Proc] a block that defines up/down or change database migration
22
+ #
23
+ # @since 0.4.0
24
+ #
25
+ # @example Use up/down blocks
26
+ # Hanami::Model.migration do
27
+ # up do
28
+ # create_table :books do
29
+ # primary_key :id
30
+ # column :book, String
31
+ # end
32
+ # end
33
+ #
34
+ # down do
35
+ # drop_table :books
36
+ # end
37
+ # end
38
+ #
39
+ # @example Use change block
40
+ # Hanami::Model.migration do
41
+ # change do
42
+ # create_table :books do
43
+ # primary_key :id
44
+ # column :book, String
45
+ # end
46
+ # end
47
+ #
48
+ # # DOWN strategy is automatically generated
49
+ # end
50
+ def self.migration(&blk)
51
+ Migration.new(configuration.gateways[:default], &blk)
52
+ end
53
+
54
+ # SQL adapter
55
+ #
56
+ # @since 0.7.0
57
+ module Sql
58
+ require "hanami/model/sql/types"
59
+ require "hanami/model/sql/entity/schema"
60
+
61
+ # Returns a SQL fragment that references a database function by the given name
62
+ # This is useful for database migrations
63
+ #
64
+ # @param name [String,Symbol] the function name
65
+ # @return [String] the SQL fragment
66
+ #
67
+ # @since 0.7.0
68
+ #
69
+ # @example
70
+ # Hanami::Model.migration do
71
+ # up do
72
+ # execute 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'
73
+ #
74
+ # create_table :source_files do
75
+ # column :id, 'uuid', primary_key: true, default: Hanami::Model::Sql.function(:uuid_generate_v4)
76
+ # # ...
77
+ # end
78
+ # end
79
+ #
80
+ # down do
81
+ # drop_table :source_files
82
+ # execute 'DROP EXTENSION "uuid-ossp"'
83
+ # end
84
+ # end
85
+ def self.function(name)
86
+ Sequel.function(name)
87
+ end
88
+
89
+ # Returns a literal SQL fragment for the given SQL fragment.
90
+ # This is useful for database migrations
91
+ #
92
+ # @param string [String] the SQL fragment
93
+ # @return [String] the literal SQL fragment
94
+ #
95
+ # @since 0.7.0
96
+ #
97
+ # @example
98
+ # Hanami::Model.migration do
99
+ # up do
100
+ # execute %{
101
+ # CREATE TYPE inventory_item AS (
102
+ # name text,
103
+ # supplier_id integer,
104
+ # price numeric
105
+ # );
106
+ # }
107
+ #
108
+ # create_table :items do
109
+ # column :item, 'inventory_item', default: Hanami::Model::Sql.literal("ROW('fuzzy dice', 42, 1.99)")
110
+ # # ...
111
+ # end
112
+ # end
113
+ #
114
+ # down do
115
+ # drop_table :items
116
+ # execute 'DROP TYPE inventory_item'
117
+ # end
118
+ # end
119
+ def self.literal(string)
120
+ Sequel.lit(string)
121
+ end
122
+
123
+ # Returns SQL fragment for ascending order for the given column
124
+ #
125
+ # @param column [Symbol] the column name
126
+ # @return [String] the SQL fragment
127
+ #
128
+ # @since 0.7.0
129
+ def self.asc(column)
130
+ Sequel.asc(column)
131
+ end
132
+
133
+ # Returns SQL fragment for descending order for the given column
134
+ #
135
+ # @param column [Symbol] the column name
136
+ # @return [String] the SQL fragment
137
+ #
138
+ # @since 0.7.0
139
+ def self.desc(column)
140
+ Sequel.desc(column)
141
+ end
142
+ end
143
+
144
+ Error.register(ROM::SQL::DatabaseError, DatabaseError)
145
+ Error.register(ROM::SQL::ConstraintError, ConstraintViolationError)
146
+ Error.register(ROM::SQL::NotNullConstraintError, NotNullConstraintViolationError)
147
+ Error.register(ROM::SQL::UniqueConstraintError, UniqueConstraintViolationError)
148
+ Error.register(ROM::SQL::CheckConstraintError, CheckConstraintViolationError)
149
+ Error.register(ROM::SQL::ForeignKeyConstraintError, ForeignKeyConstraintViolationError)
150
+ Error.register(ROM::SQL::UnknownDBTypeError, UnknownDatabaseTypeError)
151
+ Error.register(ROM::SQL::MissingPrimaryKeyError, MissingPrimaryKeyError)
152
+
153
+ Error.register(Java::JavaSql::SQLException, DatabaseError) if Utils.jruby?
154
+ end
155
+ end
156
+
157
+ Sequel.default_timezone = :utc
158
+
159
+ ROM.plugins do
160
+ adapter :sql do
161
+ register :mapping, Hanami::Model::Plugins::Mapping, type: :command
162
+ register :schema, Hanami::Model::Plugins::Schema, type: :command
163
+ register :timestamps, Hanami::Model::Plugins::Timestamps, type: :command
164
+ end
165
+ end
model-main/lib/hanami/model/sql/console.rb ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Hanami
6
+ module Model
7
+ module Sql
8
+ # SQL console
9
+ #
10
+ # @since 0.7.0
11
+ # @api private
12
+ class Console
13
+ extend Forwardable
14
+
15
+ # @since 0.7.0
16
+ # @api private
17
+ def_delegator :console, :connection_string
18
+
19
+ # @since 0.7.0
20
+ # @api private
21
+ def initialize(uri)
22
+ @uri = URI.parse(uri)
23
+ end
24
+
25
+ private
26
+
27
+ # @since 0.7.0
28
+ # @api private
29
+ def console
30
+ case @uri.scheme
31
+ when "sqlite"
32
+ require "hanami/model/sql/consoles/sqlite"
33
+ Sql::Consoles::Sqlite.new(@uri)
34
+ when "postgres", "postgresql"
35
+ require "hanami/model/sql/consoles/postgresql"
36
+ Sql::Consoles::Postgresql.new(@uri)
37
+ when "mysql", "mysql2"
38
+ require "hanami/model/sql/consoles/mysql"
39
+ Sql::Consoles::Mysql.new(@uri)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
model-main/lib/hanami/model/sql/consoles/abstract.rb ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Model
5
+ module Sql
6
+ module Consoles
7
+ # Abstract adapter
8
+ #
9
+ # @since 0.7.0
10
+ # @api private
11
+ class Abstract
12
+ # @since 0.7.0
13
+ # @api private
14
+ def initialize(uri)
15
+ @uri = uri
16
+ end
17
+
18
+ private
19
+
20
+ # @since 0.7.0
21
+ # @api private
22
+ def database_name
23
+ @uri.path.sub(/^\//, "")
24
+ end
25
+
26
+ # @since 0.7.0
27
+ # @api private
28
+ def concat(*tokens)
29
+ tokens.join
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
model-main/lib/hanami/model/sql/consoles/mysql.rb ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "abstract"
4
+
5
+ module Hanami
6
+ module Model
7
+ module Sql
8
+ module Consoles
9
+ # MySQL adapter
10
+ #
11
+ # @since 0.7.0
12
+ # @api private
13
+ class Mysql < Abstract
14
+ # @since 0.7.0
15
+ # @api private
16
+ COMMAND = "mysql"
17
+
18
+ # @since 0.7.0
19
+ # @api private
20
+ def connection_string
21
+ concat(command, host, database, port, username, password)
22
+ end
23
+
24
+ private
25
+
26
+ # @since 0.7.0
27
+ # @api private
28
+ def command
29
+ COMMAND
30
+ end
31
+
32
+ # @since 0.7.0
33
+ # @api private
34
+ def host
35
+ " -h #{@uri.host}"
36
+ end
37
+
38
+ # @since 0.7.0
39
+ # @api private
40
+ def database
41
+ " -D #{database_name}"
42
+ end
43
+
44
+ # @since 0.7.0
45
+ # @api private
46
+ def port
47
+ " -P #{@uri.port}" unless @uri.port.nil?
48
+ end
49
+
50
+ # @since 0.7.0
51
+ # @api private
52
+ def username
53
+ " -u #{@uri.user}" unless @uri.user.nil?
54
+ end
55
+
56
+ # @since 0.7.0
57
+ # @api private
58
+ def password
59
+ " -p #{@uri.password}" unless @uri.password.nil?
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
model-main/lib/hanami/model/sql/consoles/postgresql.rb ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "abstract"
4
+ require "cgi"
5
+
6
+ module Hanami
7
+ module Model
8
+ module Sql
9
+ module Consoles
10
+ # PostgreSQL adapter
11
+ #
12
+ # @since 0.7.0
13
+ # @api private
14
+ class Postgresql < Abstract
15
+ # @since 0.7.0
16
+ # @api private
17
+ COMMAND = "psql"
18
+
19
+ # @since 0.7.0
20
+ # @api private
21
+ PASSWORD = "PGPASSWORD"
22
+
23
+ # @since 0.7.0
24
+ # @api private
25
+ def connection_string
26
+ configure_password
27
+ concat(command, host, database, port, username)
28
+ end
29
+
30
+ private
31
+
32
+ # @since 0.7.0
33
+ # @api private
34
+ def command
35
+ COMMAND
36
+ end
37
+
38
+ # @since 0.7.0
39
+ # @api private
40
+ def host
41
+ " -h #{query['host'] || @uri.host}"
42
+ end
43
+
44
+ # @since 0.7.0
45
+ # @api private
46
+ def database
47
+ " -d #{database_name}"
48
+ end
49
+
50
+ # @since 0.7.0
51
+ # @api private
52
+ def port
53
+ port = query["port"] || @uri.port
54
+ " -p #{port}" if port
55
+ end
56
+
57
+ # @since 0.7.0
58
+ # @api private
59
+ def username
60
+ username = query["user"] || @uri.user
61
+ " -U #{username}" if username
62
+ end
63
+
64
+ # @since 0.7.0
65
+ # @api private
66
+ def configure_password
67
+ password = query["password"] || @uri.password
68
+ ENV[PASSWORD] = CGI.unescape(query["password"] || @uri.password) if password
69
+ end
70
+
71
+ # @since 1.1.0
72
+ # @api private
73
+ def query
74
+ return {} if @uri.query.nil? || @uri.query.empty?
75
+
76
+ parsed_query = @uri.query.split("&").map { |a| a.split("=") }
77
+ @query ||= Hash[parsed_query]
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
model-main/lib/hanami/model/sql/consoles/sqlite.rb ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "abstract"
4
+ require "shellwords"
5
+
6
+ module Hanami
7
+ module Model
8
+ module Sql
9
+ module Consoles
10
+ # SQLite adapter
11
+ #
12
+ # @since 0.7.0
13
+ # @api private
14
+ class Sqlite < Abstract
15
+ # @since 0.7.0
16
+ # @api private
17
+ COMMAND = "sqlite3"
18
+
19
+ # @since 0.7.0
20
+ # @api private
21
+ def connection_string
22
+ concat(command, " ", host, database)
23
+ end
24
+
25
+ private
26
+
27
+ # @since 0.7.0
28
+ # @api private
29
+ def command
30
+ COMMAND
31
+ end
32
+
33
+ # @since 0.7.0
34
+ # @api private
35
+ def host
36
+ @uri.host unless @uri.host.nil?
37
+ end
38
+
39
+ # @since 0.7.0
40
+ # @api private
41
+ def database
42
+ Shellwords.escape(@uri.path)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
model-main/lib/hanami/model/sql/entity/schema.rb ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/entity/schema"
4
+ require "hanami/model/types"
5
+ require "hanami/model/association"
6
+
7
+ module Hanami
8
+ module Model
9
+ module Sql
10
+ module Entity
11
+ # SQL Entity schema
12
+ #
13
+ # This schema setup is automatic.
14
+ #
15
+ # Hanami looks at the database columns, associations and potentially to
16
+ # the mapping in the repository (optional, only for legacy databases).
17
+ #
18
+ # @since 0.7.0
19
+ # @api private
20
+ #
21
+ # @see Hanami::Entity::Schema
22
+ class Schema < Hanami::Entity::Schema
23
+ # Build a new instance of Schema according to database columns,
24
+ # associations and potentially to mapping defined by the repository.
25
+ #
26
+ # @param registry [Hash] a registry that keeps reference between
27
+ # entities class and their underscored names
28
+ # @param relation [ROM::Relation] the database relation
29
+ # @param mapping [Hanami::Model::Mapping] the optional repository
30
+ # mapping
31
+ #
32
+ # @return [Hanami::Model::Sql::Entity::Schema] the schema
33
+ #
34
+ # @since 0.7.0
35
+ # @api private
36
+ def initialize(registry, relation, mapping)
37
+ attributes = build(registry, relation, mapping)
38
+ @schema = Types::Coercible::Hash.schema(attributes)
39
+ @attributes = Hash[attributes.map { |k, _| [k, true] }]
40
+ freeze
41
+ end
42
+
43
+ # Process attributes
44
+ #
45
+ # @param attributes [#to_hash] the attributes hash
46
+ #
47
+ # @raise [TypeError] if the process fails
48
+ #
49
+ # @since 1.0.1
50
+ # @api private
51
+ def call(attributes)
52
+ schema.call(attributes)
53
+ end
54
+
55
+ # @since 1.0.1
56
+ # @api private
57
+ alias_method :[], :call
58
+
59
+ # Check if the attribute is known
60
+ #
61
+ # @param name [Symbol] the attribute name
62
+ #
63
+ # @return [TrueClass,FalseClass] the result of the check
64
+ #
65
+ # @since 0.7.0
66
+ # @api private
67
+ def attribute?(name)
68
+ attributes.key?(name)
69
+ end
70
+
71
+ private
72
+
73
+ # @since 0.7.0
74
+ # @api private
75
+ attr_reader :attributes
76
+
77
+ # Build the schema
78
+ #
79
+ # @param registry [Hash] a registry that keeps reference between
80
+ # entities class and their underscored names
81
+ # @param relation [ROM::Relation] the database relation
82
+ # @param mapping [Hanami::Model::Mapping] the optional repository
83
+ # mapping
84
+ #
85
+ # @return [Dry::Types::Constructor] the inner schema
86
+ #
87
+ # @since 0.7.0
88
+ # @api private
89
+ def build(registry, relation, mapping)
90
+ build_attributes(relation, mapping).merge(
91
+ build_associations(registry, relation.associations)
92
+ )
93
+ end
94
+
95
+ # Extract a set of attributes from the database table or from the
96
+ # optional repository mapping.
97
+ #
98
+ # @param relation [ROM::Relation] the database relation
99
+ # @param mapping [Hanami::Model::Mapping] the optional repository
100
+ # mapping
101
+ #
102
+ # @return [Hash] a set of attributes
103
+ #
104
+ # @since 0.7.0
105
+ # @api private
106
+ def build_attributes(relation, mapping)
107
+ schema = relation.schema.to_h
108
+ schema.each_with_object({}) do |(attribute, type), result|
109
+ attribute = mapping.translate(attribute) if mapping.reverse?
110
+ result[attribute] = coercible(type)
111
+ end
112
+ end
113
+
114
+ # Merge attributes and associations
115
+ #
116
+ # @param registry [Hash] a registry that keeps reference between
117
+ # entities class and their underscored names
118
+ # @param associations [ROM::AssociationSet] a set of associations for
119
+ # the current relation
120
+ #
121
+ # @return [Hash] attributes with associations
122
+ #
123
+ # @since 0.7.0
124
+ # @api private
125
+ def build_associations(registry, associations)
126
+ associations.each_with_object({}) do |(name, association), result|
127
+ target = registry.fetch(association.target.to_sym)
128
+ result[name] = Association.lookup(association).schema_type(target)
129
+ end
130
+ end
131
+
132
+ # Converts given ROM type into coercible type for entity attribute
133
+ #
134
+ # @since 0.7.0
135
+ # @api private
136
+ def coercible(type)
137
+ Types::Schema.coercible(type)
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
model-main/lib/hanami/model/sql/types.rb ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/model/types"
4
+ require "rom/types"
5
+
6
+ module Hanami
7
+ module Model
8
+ module Sql
9
+ # Types definitions for SQL databases
10
+ #
11
+ # @since 0.7.0
12
+ module Types
13
+ include Dry::Types.module
14
+
15
+ # Types for schema definitions
16
+ #
17
+ # @since 0.7.0
18
+ module Schema
19
+ require "hanami/model/sql/types/schema/coercions"
20
+
21
+ String = Types::Optional::Coercible::String
22
+
23
+ Int = Types::Strict::Nil | Types::Int.constructor(Coercions.method(:int))
24
+ Float = Types::Strict::Nil | Types::Float.constructor(Coercions.method(:float))
25
+ Decimal = Types::Strict::Nil | Types::Float.constructor(Coercions.method(:decimal))
26
+
27
+ Bool = Types::Strict::Nil | Types::Strict::Bool
28
+
29
+ Date = Types::Strict::Nil | Types::Date.constructor(Coercions.method(:date))
30
+ DateTime = Types::Strict::Nil | Types::DateTime.constructor(Coercions.method(:datetime))
31
+ Time = Types::Strict::Nil | Types::Time.constructor(Coercions.method(:time))
32
+
33
+ Array = Types::Strict::Nil | Types::Array.constructor(Coercions.method(:array))
34
+ Hash = Types::Strict::Nil | Types::Hash.constructor(Coercions.method(:hash))
35
+
36
+ PG_JSON = Types::Strict::Nil | Types::Any.constructor(Coercions.method(:pg_json))
37
+
38
+ # @since 0.7.0
39
+ # @api private
40
+ MAPPING = {
41
+ Types::String.pristine => Schema::String,
42
+ Types::Int.pristine => Schema::Int,
43
+ Types::Float.pristine => Schema::Float,
44
+ Types::Decimal.pristine => Schema::Decimal,
45
+ Types::Bool.pristine => Schema::Bool,
46
+ Types::Date.pristine => Schema::Date,
47
+ Types::DateTime.pristine => Schema::DateTime,
48
+ Types::Time.pristine => Schema::Time,
49
+ Types::Array.pristine => Schema::Array,
50
+ Types::Hash.pristine => Schema::Hash,
51
+ Types::String.optional.pristine => Schema::String,
52
+ Types::Int.optional.pristine => Schema::Int,
53
+ Types::Float.optional.pristine => Schema::Float,
54
+ Types::Decimal.optional.pristine => Schema::Decimal,
55
+ Types::Bool.optional.pristine => Schema::Bool,
56
+ Types::Date.optional.pristine => Schema::Date,
57
+ Types::DateTime.optional.pristine => Schema::DateTime,
58
+ Types::Time.optional.pristine => Schema::Time,
59
+ Types::Array.optional.pristine => Schema::Array,
60
+ Types::Hash.optional.pristine => Schema::Hash
61
+ }.freeze
62
+
63
+ # Convert given type into coercible
64
+ #
65
+ # @since 0.7.0
66
+ # @api private
67
+ def self.coercible(attribute)
68
+ return attribute if attribute.constrained?
69
+
70
+ type = attribute.type
71
+ unwrapped = type.optional? ? type.right : type
72
+
73
+ # NOTE: In the future rom-sql should be able to always return Ruby
74
+ # types instead of Sequel types. When that will happen we can get
75
+ # rid of this logic in the block and fall back to:
76
+ #
77
+ # MAPPING.fetch(unwrapped.pristine, attribute)
78
+ MAPPING.fetch(unwrapped.pristine) do
79
+ if pg_json?(unwrapped.pristine)
80
+ Schema::PG_JSON
81
+ else
82
+ attribute
83
+ end
84
+ end
85
+ end
86
+
87
+ # @since 1.0.4
88
+ # @api private
89
+ def self.pg_json_pristines
90
+ @pg_json_pristines ||= ::Hash.new do |hash, type|
91
+ hash[type] = (ROM::SQL::Types::PG.const_get(type).pristine if defined?(ROM::SQL::Types::PG))
92
+ end
93
+ end
94
+
95
+ # @since 1.0.2
96
+ # @api private
97
+ def self.pg_json?(pristine)
98
+ pristine == pg_json_pristines["JSONB"] || # rubocop:disable Style/MultipleComparison
99
+ pristine == pg_json_pristines["JSON"]
100
+ end
101
+
102
+ private_class_method :pg_json?
103
+
104
+ # Coercer for SQL associations target
105
+ #
106
+ # @since 0.7.0
107
+ # @api private
108
+ class AssociationType < Hanami::Model::Types::Schema::CoercibleType
109
+ # Check if value can be coerced
110
+ #
111
+ # @param value [Object] the value
112
+ #
113
+ # @return [TrueClass,FalseClass] the result of the check
114
+ #
115
+ # @since 0.7.0
116
+ # @api private
117
+ def valid?(value)
118
+ value.inspect =~ /\[#{primitive}\]/ || super
119
+ end
120
+
121
+ # @since 0.7.0
122
+ # @api private
123
+ def success(*args)
124
+ result(Dry::Types::Result::Success, primitive.new(args.first.to_h))
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end