Commit ·
71c7618
1
Parent(s): 2ec2a83
Upload 150 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- model-main/.github/FUNDING.yml +1 -0
- model-main/.github/workflows/ci.yml +76 -0
- model-main/.gitignore +21 -0
- model-main/.jrubyrc +1 -0
- model-main/.rspec +2 -0
- model-main/.rubocop.yml +18 -0
- model-main/.yardopts +5 -0
- model-main/CHANGELOG.md +316 -0
- model-main/Gemfile +21 -0
- model-main/LICENSE.md +22 -0
- model-main/README.md +303 -0
- model-main/Rakefile +14 -0
- model-main/hanami-model.gemspec +36 -0
- model-main/lib/hanami-model.rb +3 -0
- model-main/lib/hanami/entity.rb +217 -0
- model-main/lib/hanami/entity/schema.rb +269 -0
- model-main/lib/hanami/model.rb +111 -0
- model-main/lib/hanami/model/association.rb +45 -0
- model-main/lib/hanami/model/associations/belongs_to.rb +109 -0
- model-main/lib/hanami/model/associations/dsl.rb +40 -0
- model-main/lib/hanami/model/associations/has_many.rb +214 -0
- model-main/lib/hanami/model/associations/has_one.rb +167 -0
- model-main/lib/hanami/model/associations/many_to_many.rb +201 -0
- model-main/lib/hanami/model/configuration.rb +183 -0
- model-main/lib/hanami/model/configurator.rb +94 -0
- model-main/lib/hanami/model/entity_name.rb +39 -0
- model-main/lib/hanami/model/error.rb +133 -0
- model-main/lib/hanami/model/mapped_relation.rb +61 -0
- model-main/lib/hanami/model/mapping.rb +62 -0
- model-main/lib/hanami/model/migration.rb +33 -0
- model-main/lib/hanami/model/migrator.rb +394 -0
- model-main/lib/hanami/model/migrator/adapter.rb +225 -0
- model-main/lib/hanami/model/migrator/connection.rb +169 -0
- model-main/lib/hanami/model/migrator/logger.rb +35 -0
- model-main/lib/hanami/model/migrator/mysql_adapter.rb +98 -0
- model-main/lib/hanami/model/migrator/postgres_adapter.rb +125 -0
- model-main/lib/hanami/model/migrator/sqlite_adapter.rb +126 -0
- model-main/lib/hanami/model/plugins.rb +27 -0
- model-main/lib/hanami/model/plugins/mapping.rb +57 -0
- model-main/lib/hanami/model/plugins/schema.rb +57 -0
- model-main/lib/hanami/model/plugins/timestamps.rb +120 -0
- model-main/lib/hanami/model/relation_name.rb +26 -0
- model-main/lib/hanami/model/sql.rb +165 -0
- model-main/lib/hanami/model/sql/console.rb +45 -0
- model-main/lib/hanami/model/sql/consoles/abstract.rb +35 -0
- model-main/lib/hanami/model/sql/consoles/mysql.rb +65 -0
- model-main/lib/hanami/model/sql/consoles/postgresql.rb +83 -0
- model-main/lib/hanami/model/sql/consoles/sqlite.rb +48 -0
- model-main/lib/hanami/model/sql/entity/schema.rb +143 -0
- 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 |
+
[](https://badge.fury.io/rb/hanami-model)
|
| 22 |
+
[](https://github.com/hanami/model/actions?query=workflow%3Aci+branch%3Amain)
|
| 23 |
+
[](https://codecov.io/gh/hanami/model)
|
| 24 |
+
[](https://depfu.com/github/hanami/model?project=Bundler)
|
| 25 |
+
[](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
|