hanami-model 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -0
- data/README.md +54 -420
- data/hanami-model.gemspec +9 -6
- data/lib/hanami/entity.rb +107 -191
- data/lib/hanami/entity/schema.rb +236 -0
- data/lib/hanami/model.rb +52 -138
- data/lib/hanami/model/association.rb +37 -0
- data/lib/hanami/model/associations/belongs_to.rb +19 -0
- data/lib/hanami/model/associations/dsl.rb +29 -0
- data/lib/hanami/model/associations/has_many.rb +200 -0
- data/lib/hanami/model/configuration.rb +52 -224
- data/lib/hanami/model/configurator.rb +62 -0
- data/lib/hanami/model/entity_name.rb +35 -0
- data/lib/hanami/model/error.rb +37 -24
- data/lib/hanami/model/mapping.rb +29 -35
- data/lib/hanami/model/migration.rb +31 -0
- data/lib/hanami/model/migrator.rb +111 -88
- data/lib/hanami/model/migrator/adapter.rb +39 -16
- data/lib/hanami/model/migrator/connection.rb +23 -11
- data/lib/hanami/model/migrator/mysql_adapter.rb +38 -17
- data/lib/hanami/model/migrator/postgres_adapter.rb +20 -19
- data/lib/hanami/model/migrator/sqlite_adapter.rb +9 -8
- data/lib/hanami/model/plugins.rb +25 -0
- data/lib/hanami/model/plugins/mapping.rb +55 -0
- data/lib/hanami/model/plugins/schema.rb +55 -0
- data/lib/hanami/model/plugins/timestamps.rb +118 -0
- data/lib/hanami/model/relation_name.rb +24 -0
- data/lib/hanami/model/sql.rb +161 -0
- data/lib/hanami/model/sql/console.rb +41 -0
- data/lib/hanami/model/sql/consoles/abstract.rb +33 -0
- data/lib/hanami/model/sql/consoles/mysql.rb +63 -0
- data/lib/hanami/model/sql/consoles/postgresql.rb +68 -0
- data/lib/hanami/model/sql/consoles/sqlite.rb +46 -0
- data/lib/hanami/model/sql/entity/schema.rb +125 -0
- data/lib/hanami/model/sql/types.rb +95 -0
- data/lib/hanami/model/sql/types/schema/coercions.rb +198 -0
- data/lib/hanami/model/types.rb +99 -0
- data/lib/hanami/model/version.rb +1 -1
- data/lib/hanami/repository.rb +287 -723
- metadata +77 -40
- data/EXAMPLE.md +0 -213
- data/lib/hanami/entity/dirty_tracking.rb +0 -74
- data/lib/hanami/model/adapters/abstract.rb +0 -281
- data/lib/hanami/model/adapters/file_system_adapter.rb +0 -288
- data/lib/hanami/model/adapters/implementation.rb +0 -111
- data/lib/hanami/model/adapters/memory/collection.rb +0 -132
- data/lib/hanami/model/adapters/memory/command.rb +0 -113
- data/lib/hanami/model/adapters/memory/query.rb +0 -653
- data/lib/hanami/model/adapters/memory_adapter.rb +0 -179
- data/lib/hanami/model/adapters/null_adapter.rb +0 -24
- data/lib/hanami/model/adapters/sql/collection.rb +0 -287
- data/lib/hanami/model/adapters/sql/command.rb +0 -88
- data/lib/hanami/model/adapters/sql/console.rb +0 -33
- data/lib/hanami/model/adapters/sql/consoles/mysql.rb +0 -49
- data/lib/hanami/model/adapters/sql/consoles/postgresql.rb +0 -48
- data/lib/hanami/model/adapters/sql/consoles/sqlite.rb +0 -26
- data/lib/hanami/model/adapters/sql/query.rb +0 -788
- data/lib/hanami/model/adapters/sql_adapter.rb +0 -296
- data/lib/hanami/model/coercer.rb +0 -74
- data/lib/hanami/model/config/adapter.rb +0 -116
- data/lib/hanami/model/config/mapper.rb +0 -45
- data/lib/hanami/model/mapper.rb +0 -124
- data/lib/hanami/model/mapping/attribute.rb +0 -85
- data/lib/hanami/model/mapping/coercers.rb +0 -314
- data/lib/hanami/model/mapping/collection.rb +0 -490
- data/lib/hanami/model/mapping/collection_coercer.rb +0 -79
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'rom/types'
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
module Model
|
5
|
+
# Types definitions
|
6
|
+
#
|
7
|
+
# @since 0.7.0
|
8
|
+
module Types
|
9
|
+
include ROM::Types
|
10
|
+
|
11
|
+
# @since 0.7.0
|
12
|
+
# @api private
|
13
|
+
def self.included(mod)
|
14
|
+
mod.extend(ClassMethods)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Class level interface
|
18
|
+
#
|
19
|
+
# @since 0.7.0
|
20
|
+
module ClassMethods
|
21
|
+
# Define an array of given type
|
22
|
+
#
|
23
|
+
# @since 0.7.0
|
24
|
+
def Collection(type) # rubocop:disable Style/MethodName
|
25
|
+
type = Schema::CoercibleType.new(type) unless type.is_a?(Dry::Types::Definition)
|
26
|
+
Types::Array.member(type)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Types for schema definitions
|
31
|
+
#
|
32
|
+
# @since 0.7.0
|
33
|
+
module Schema
|
34
|
+
# Coercer for objects within custom schema definition
|
35
|
+
#
|
36
|
+
# @since 0.7.0
|
37
|
+
# @api private
|
38
|
+
class CoercibleType < Dry::Types::Definition
|
39
|
+
# Coerce given value into the wrapped object type
|
40
|
+
#
|
41
|
+
# @param value [Object] the value
|
42
|
+
#
|
43
|
+
# @return [Object] the coerced value of `object` type
|
44
|
+
#
|
45
|
+
# @raise [TypeError] if value can't be coerced
|
46
|
+
#
|
47
|
+
# @since 0.7.0
|
48
|
+
# @api private
|
49
|
+
def call(value)
|
50
|
+
if valid?(value)
|
51
|
+
coerce(value)
|
52
|
+
else
|
53
|
+
raise TypeError.new("#{value.inspect} must be coercible into #{object}")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Check if value can be coerced
|
58
|
+
#
|
59
|
+
# It is true if value is an instance of `object` type or if value
|
60
|
+
# respond to `#to_hash`.
|
61
|
+
#
|
62
|
+
# @param value [Object] the value
|
63
|
+
#
|
64
|
+
# @return [TrueClass,FalseClass] the result of the check
|
65
|
+
#
|
66
|
+
# @since 0.7.0
|
67
|
+
# @api private
|
68
|
+
def valid?(value)
|
69
|
+
value.is_a?(object) ||
|
70
|
+
value.respond_to?(:to_hash)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Coerce given value into an instance of `object` type
|
74
|
+
#
|
75
|
+
# @param value [Object] the value
|
76
|
+
#
|
77
|
+
# @return [Object] the coerced value of `object` type
|
78
|
+
def coerce(value)
|
79
|
+
case value
|
80
|
+
when object
|
81
|
+
value
|
82
|
+
else
|
83
|
+
object.new(value.to_hash)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# @since 0.7.0
|
88
|
+
# @api private
|
89
|
+
def object
|
90
|
+
result = primitive
|
91
|
+
return result unless result.respond_to?(:primitive)
|
92
|
+
|
93
|
+
result.primitive
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/hanami/model/version.rb
CHANGED
data/lib/hanami/repository.rb
CHANGED
@@ -1,5 +1,10 @@
|
|
1
|
+
require 'rom-repository'
|
2
|
+
require 'hanami/model/entity_name'
|
3
|
+
require 'hanami/model/relation_name'
|
4
|
+
require 'hanami/model/associations/dsl'
|
5
|
+
require 'hanami/model/association'
|
6
|
+
require 'hanami/utils/class'
|
1
7
|
require 'hanami/utils/class_attribute'
|
2
|
-
require 'hanami/model/adapters/null_adapter'
|
3
8
|
|
4
9
|
module Hanami
|
5
10
|
# Mediates between the entities and the persistence layer, by offering an API
|
@@ -18,25 +23,11 @@ module Hanami
|
|
18
23
|
# end
|
19
24
|
#
|
20
25
|
# # valid
|
21
|
-
# class ArticleRepository
|
22
|
-
# include Hanami::Repository
|
26
|
+
# class ArticleRepository < Hanami::Repository
|
23
27
|
# end
|
24
28
|
#
|
25
29
|
# # not valid for Article
|
26
|
-
# class PostRepository
|
27
|
-
# include Hanami::Repository
|
28
|
-
# end
|
29
|
-
#
|
30
|
-
# Repository for an entity can be configured by setting # the `#repository`
|
31
|
-
# on the mapper.
|
32
|
-
#
|
33
|
-
# @example
|
34
|
-
# # PostRepository is repository for Article
|
35
|
-
# mapper = Hanami::Model::Mapper.new do
|
36
|
-
# collection :articles do
|
37
|
-
# entity Article
|
38
|
-
# repository PostRepository
|
39
|
-
# end
|
30
|
+
# class PostRepository < Hanami::Repository
|
40
31
|
# end
|
41
32
|
#
|
42
33
|
# A repository is storage independent.
|
@@ -54,10 +45,9 @@ module Hanami
|
|
54
45
|
#
|
55
46
|
# * Isolates the persistence logic at a low level
|
56
47
|
#
|
57
|
-
# Hanami::Model is shipped with
|
48
|
+
# Hanami::Model is shipped with one adapter:
|
58
49
|
#
|
59
50
|
# * SqlAdapter
|
60
|
-
# * MemoryAdapter
|
61
51
|
#
|
62
52
|
#
|
63
53
|
#
|
@@ -82,7 +72,7 @@ module Hanami
|
|
82
72
|
# # * If we change the storage, we are forced to change the code of the
|
83
73
|
# # caller(s).
|
84
74
|
#
|
85
|
-
# ArticleRepository.where(author_id: 23).order(:published_at).limit(8)
|
75
|
+
# ArticleRepository.new.where(author_id: 23).order(:published_at).limit(8)
|
86
76
|
#
|
87
77
|
#
|
88
78
|
#
|
@@ -100,16 +90,14 @@ module Hanami
|
|
100
90
|
# #
|
101
91
|
# # * If we change the storage, the callers aren't affected.
|
102
92
|
#
|
103
|
-
# ArticleRepository.most_recent_by_author(author)
|
93
|
+
# ArticleRepository.new.most_recent_by_author(author)
|
104
94
|
#
|
105
|
-
# class ArticleRepository
|
106
|
-
#
|
107
|
-
#
|
108
|
-
# def self.most_recent_by_author(author, limit = 8)
|
109
|
-
# query do
|
95
|
+
# class ArticleRepository < Hanami::Repository
|
96
|
+
# def most_recent_by_author(author, limit = 8)
|
97
|
+
# articles.
|
110
98
|
# where(author_id: author.id).
|
111
|
-
# order(:published_at)
|
112
|
-
#
|
99
|
+
# order(:published_at).
|
100
|
+
# limit(limit)
|
113
101
|
# end
|
114
102
|
# end
|
115
103
|
#
|
@@ -118,755 +106,331 @@ module Hanami
|
|
118
106
|
# @see Hanami::Entity
|
119
107
|
# @see http://martinfowler.com/eaaCatalog/repository.html
|
120
108
|
# @see http://en.wikipedia.org/wiki/Dependency_inversion_principle
|
121
|
-
|
122
|
-
#
|
109
|
+
class Repository < ROM::Repository::Root
|
110
|
+
# Mapper name.
|
123
111
|
#
|
124
|
-
#
|
112
|
+
# With ROM mapping there is a link between the entity class and a generic
|
113
|
+
# reference for it. Example: <tt>BookRepository</tt> references <tt>Book</tt>
|
114
|
+
# as <tt>:entity</tt>.
|
125
115
|
#
|
126
|
-
# @
|
127
|
-
#
|
116
|
+
# @since 0.7.0
|
117
|
+
# @api private
|
128
118
|
#
|
129
|
-
#
|
130
|
-
#
|
131
|
-
|
132
|
-
def self.included(base)
|
133
|
-
base.class_eval do
|
134
|
-
extend ClassMethods
|
135
|
-
include Hanami::Utils::ClassAttribute
|
119
|
+
# @see Hanami::Repository.inherited
|
120
|
+
# @see Hanami::Repository.define_mapping
|
121
|
+
MAPPER_NAME = :entity
|
136
122
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
123
|
+
# Plugins for database commands
|
124
|
+
#
|
125
|
+
# @since 0.7.0
|
126
|
+
# @api private
|
127
|
+
#
|
128
|
+
# @see Hanami::Model::Plugins
|
129
|
+
COMMAND_PLUGINS = [:schema, :mapping, :timestamps].freeze
|
141
130
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
#
|
150
|
-
# @param adapter [Object] an object that implements
|
151
|
-
# `Hanami::Model::Adapters::Abstract` interface
|
152
|
-
#
|
153
|
-
# @since 0.1.0
|
154
|
-
#
|
155
|
-
# @see Hanami::Model::Adapters::SqlAdapter
|
156
|
-
# @see Hanami::Model::Adapters::MemoryAdapter
|
157
|
-
#
|
158
|
-
# @example Memory adapter
|
159
|
-
# require 'hanami/model'
|
160
|
-
# require 'hanami/model/adapters/memory_adapter'
|
161
|
-
#
|
162
|
-
# mapper = Hanami::Model::Mapper.new do
|
163
|
-
# # ...
|
164
|
-
# end
|
165
|
-
#
|
166
|
-
# adapter = Hanami::Model::Adapters::MemoryAdapter.new(mapper)
|
167
|
-
#
|
168
|
-
# class UserRepository
|
169
|
-
# include Hanami::Repository
|
170
|
-
# end
|
171
|
-
#
|
172
|
-
# UserRepository.adapter = adapter
|
173
|
-
#
|
174
|
-
#
|
175
|
-
#
|
176
|
-
# @example SQL adapter with a Sqlite database
|
177
|
-
# require 'sqlite3'
|
178
|
-
# require 'hanami/model'
|
179
|
-
# require 'hanami/model/adapters/sql_adapter'
|
180
|
-
#
|
181
|
-
# mapper = Hanami::Model::Mapper.new do
|
182
|
-
# # ...
|
183
|
-
# end
|
184
|
-
#
|
185
|
-
# adapter = Hanami::Model::Adapters::SqlAdapter.new(mapper, 'sqlite://path/to/database.db')
|
186
|
-
#
|
187
|
-
# class UserRepository
|
188
|
-
# include Hanami::Repository
|
189
|
-
# end
|
190
|
-
#
|
191
|
-
# UserRepository.adapter = adapter
|
192
|
-
#
|
193
|
-
#
|
194
|
-
#
|
195
|
-
# @example SQL adapter with a Postgres database
|
196
|
-
# require 'pg'
|
197
|
-
# require 'hanami/model'
|
198
|
-
# require 'hanami/model/adapters/sql_adapter'
|
199
|
-
#
|
200
|
-
# mapper = Hanami::Model::Mapper.new do
|
201
|
-
# # ...
|
202
|
-
# end
|
203
|
-
#
|
204
|
-
# adapter = Hanami::Model::Adapters::SqlAdapter.new(mapper, 'postgres://host:port/database')
|
205
|
-
#
|
206
|
-
# class UserRepository
|
207
|
-
# include Hanami::Repository
|
208
|
-
# end
|
209
|
-
#
|
210
|
-
# UserRepository.adapter = adapter
|
211
|
-
def adapter=(adapter)
|
212
|
-
@adapter = adapter
|
213
|
-
end
|
131
|
+
# Configuration
|
132
|
+
#
|
133
|
+
# @since 0.7.0
|
134
|
+
# @api private
|
135
|
+
def self.configuration
|
136
|
+
Hanami::Model.configuration
|
137
|
+
end
|
214
138
|
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
139
|
+
# Container
|
140
|
+
#
|
141
|
+
# @since 0.7.0
|
142
|
+
# @api private
|
143
|
+
def self.container
|
144
|
+
Hanami::Model.container
|
145
|
+
end
|
220
146
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
# @see Hanami::Repository#update
|
231
|
-
#
|
232
|
-
# @example With a non persisted entity
|
233
|
-
# require 'hanami/model'
|
234
|
-
#
|
235
|
-
# class ArticleRepository
|
236
|
-
# include Hanami::Repository
|
237
|
-
# end
|
238
|
-
#
|
239
|
-
# article = Article.new(title: 'Introducing Hanami::Model')
|
240
|
-
# article.id # => nil
|
241
|
-
#
|
242
|
-
# persisted_article = ArticleRepository.persist(article) # creates a record
|
243
|
-
# article.id # => nil
|
244
|
-
# persisted_article.id # => 23
|
245
|
-
#
|
246
|
-
# @example With a persisted entity
|
247
|
-
# require 'hanami/model'
|
248
|
-
#
|
249
|
-
# class ArticleRepository
|
250
|
-
# include Hanami::Repository
|
251
|
-
# end
|
252
|
-
#
|
253
|
-
# article = ArticleRepository.find(23)
|
254
|
-
# article.id # => 23
|
255
|
-
#
|
256
|
-
# article.title = 'Launching Hanami::Model'
|
257
|
-
# ArticleRepository.persist(article) # updates the record
|
258
|
-
#
|
259
|
-
# article = ArticleRepository.find(23)
|
260
|
-
# article.title # => "Launching Hanami::Model"
|
261
|
-
def persist(entity)
|
262
|
-
_touch(entity)
|
263
|
-
@adapter.persist(collection, entity)
|
264
|
-
end
|
147
|
+
# Define a database relation, which describes how data is fetched from the
|
148
|
+
# database.
|
149
|
+
#
|
150
|
+
# It auto-infers the underlying database table.
|
151
|
+
#
|
152
|
+
# @since 0.7.0
|
153
|
+
# @api private
|
154
|
+
def self.define_relation # rubocop:disable Metrics/MethodLength
|
155
|
+
a = @associations
|
265
156
|
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
# If already persisted (`id` present) it does nothing.
|
270
|
-
#
|
271
|
-
# @param entity [#id,#id=] the entity to create
|
272
|
-
#
|
273
|
-
# @return [Object] a copy of the entity with `id` assigned
|
274
|
-
#
|
275
|
-
# @since 0.1.0
|
276
|
-
#
|
277
|
-
# @see Hanami::Repository#persist
|
278
|
-
#
|
279
|
-
# @example
|
280
|
-
# require 'hanami/model'
|
281
|
-
#
|
282
|
-
# class ArticleRepository
|
283
|
-
# include Hanami::Repository
|
284
|
-
# end
|
285
|
-
#
|
286
|
-
# article = Article.new(title: 'Introducing Hanami::Model')
|
287
|
-
# article.id # => nil
|
288
|
-
#
|
289
|
-
# created_article = ArticleRepository.create(article) # creates a record
|
290
|
-
# article.id # => nil
|
291
|
-
# created_article.id # => 23
|
292
|
-
#
|
293
|
-
# created_article = ArticleRepository.create(article)
|
294
|
-
# created_article.id # => 24
|
295
|
-
#
|
296
|
-
# created_article = ArticleRepository.create(existing_article) # => no-op
|
297
|
-
# created_article # => nil
|
298
|
-
#
|
299
|
-
def create(entity)
|
300
|
-
unless _persisted?(entity)
|
301
|
-
_touch(entity)
|
302
|
-
@adapter.create(collection, entity)
|
157
|
+
configuration.relation(relation) do
|
158
|
+
schema(infer: true) do
|
159
|
+
associations(&a) unless a.nil?
|
303
160
|
end
|
304
|
-
end
|
305
161
|
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
#
|
310
|
-
# @param entity [#id] the entity to update
|
311
|
-
#
|
312
|
-
# @return [Object] the entity
|
313
|
-
#
|
314
|
-
# @raise [Hanami::Model::NonPersistedEntityError] if the given entity
|
315
|
-
# wasn't already persisted.
|
316
|
-
#
|
317
|
-
# @since 0.1.0
|
318
|
-
#
|
319
|
-
# @see Hanami::Repository#persist
|
320
|
-
# @see Hanami::Model::NonPersistedEntityError
|
321
|
-
#
|
322
|
-
# @example With a persisted entity
|
323
|
-
# require 'hanami/model'
|
324
|
-
#
|
325
|
-
# class ArticleRepository
|
326
|
-
# include Hanami::Repository
|
327
|
-
# end
|
328
|
-
#
|
329
|
-
# article = ArticleRepository.find(23)
|
330
|
-
# article.id # => 23
|
331
|
-
# article.title = 'Launching Hanami::Model'
|
332
|
-
#
|
333
|
-
# ArticleRepository.update(article) # updates the record
|
334
|
-
#
|
335
|
-
#
|
336
|
-
#
|
337
|
-
# @example With a non persisted entity
|
338
|
-
# require 'hanami/model'
|
339
|
-
#
|
340
|
-
# class ArticleRepository
|
341
|
-
# include Hanami::Repository
|
342
|
-
# end
|
343
|
-
#
|
344
|
-
# article = Article.new(title: 'Introducing Hanami::Model')
|
345
|
-
# article.id # => nil
|
346
|
-
#
|
347
|
-
# ArticleRepository.update(article) # raises Hanami::Model::NonPersistedEntityError
|
348
|
-
def update(entity)
|
349
|
-
if _persisted?(entity)
|
350
|
-
_touch(entity)
|
351
|
-
@adapter.update(collection, entity)
|
352
|
-
else
|
353
|
-
raise Hanami::Model::NonPersistedEntityError
|
162
|
+
# rubocop:disable Lint/NestedMethodDefinition
|
163
|
+
def by_primary_key(id)
|
164
|
+
where(primary_key => id)
|
354
165
|
end
|
166
|
+
# rubocop:enable Lint/NestedMethodDefinition
|
355
167
|
end
|
356
168
|
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
#
|
361
|
-
# @param entity [#id] the entity to delete
|
362
|
-
#
|
363
|
-
# @return [Object] the entity
|
364
|
-
#
|
365
|
-
# @raise [Hanami::Model::NonPersistedEntityError] if the given entity
|
366
|
-
# wasn't already persisted.
|
367
|
-
#
|
368
|
-
# @since 0.1.0
|
369
|
-
#
|
370
|
-
# @see Hanami::Model::NonPersistedEntityError
|
371
|
-
#
|
372
|
-
# @example With a persisted entity
|
373
|
-
# require 'hanami/model'
|
374
|
-
#
|
375
|
-
# class ArticleRepository
|
376
|
-
# include Hanami::Repository
|
377
|
-
# end
|
378
|
-
#
|
379
|
-
# article = ArticleRepository.find(23)
|
380
|
-
# article.id # => 23
|
381
|
-
#
|
382
|
-
# ArticleRepository.delete(article) # deletes the record
|
383
|
-
#
|
384
|
-
#
|
385
|
-
#
|
386
|
-
# @example With a non persisted entity
|
387
|
-
# require 'hanami/model'
|
388
|
-
#
|
389
|
-
# class ArticleRepository
|
390
|
-
# include Hanami::Repository
|
391
|
-
# end
|
392
|
-
#
|
393
|
-
# article = Article.new(title: 'Introducing Hanami::Model')
|
394
|
-
# article.id # => nil
|
395
|
-
#
|
396
|
-
# ArticleRepository.delete(article) # raises Hanami::Model::NonPersistedEntityError
|
397
|
-
def delete(entity)
|
398
|
-
if _persisted?(entity)
|
399
|
-
@adapter.delete(collection, entity)
|
400
|
-
else
|
401
|
-
raise Hanami::Model::NonPersistedEntityError
|
402
|
-
end
|
169
|
+
relations(relation)
|
170
|
+
root(relation)
|
171
|
+
end
|
403
172
|
|
404
|
-
|
405
|
-
|
173
|
+
# Defines the ampping between a database table and an entity.
|
174
|
+
#
|
175
|
+
# It's also responsible to associate table columns to entity attributes.
|
176
|
+
#
|
177
|
+
# @since 0.7.0
|
178
|
+
# @api private
|
179
|
+
#
|
180
|
+
# rubocop:disable Metrics/MethodLength
|
181
|
+
# rubocop:disable Metrics/AbcSize
|
182
|
+
def self.define_mapping
|
183
|
+
self.entity = Utils::Class.load!(entity_name)
|
184
|
+
e = entity
|
185
|
+
m = @mapping
|
406
186
|
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
# @since 0.1.0
|
412
|
-
#
|
413
|
-
# @example
|
414
|
-
# require 'hanami/model'
|
415
|
-
#
|
416
|
-
# class ArticleRepository
|
417
|
-
# include Hanami::Repository
|
418
|
-
# end
|
419
|
-
#
|
420
|
-
# ArticleRepository.all # => [ #<Article:0x007f9b19a60098> ]
|
421
|
-
def all
|
422
|
-
@adapter.all(collection)
|
187
|
+
blk = lambda do |_|
|
188
|
+
model e
|
189
|
+
register_as MAPPER_NAME
|
190
|
+
instance_exec(&m) unless m.nil?
|
423
191
|
end
|
424
192
|
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
#
|
433
|
-
# @since 0.1.0
|
434
|
-
#
|
435
|
-
# @example
|
436
|
-
# require 'hanami/model'
|
437
|
-
#
|
438
|
-
# class ArticleRepository
|
439
|
-
# include Hanami::Repository
|
440
|
-
# end
|
441
|
-
#
|
442
|
-
# ArticleRepository.find(23) # => #<Article:0x007f9b19a60098>
|
443
|
-
# ArticleRepository.find(9999) # => nil
|
444
|
-
def find(id)
|
445
|
-
@adapter.find(collection, id)
|
446
|
-
end
|
193
|
+
root = self.root
|
194
|
+
configuration.mappers { define(root, &blk) }
|
195
|
+
configuration.define_mappings(root, &blk)
|
196
|
+
configuration.register_entity(relation, entity_name.underscore, e)
|
197
|
+
end
|
198
|
+
# rubocop:enable Metrics/AbcSize
|
199
|
+
# rubocop:enable Metrics/MethodLength
|
447
200
|
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
# require 'hanami/model'
|
458
|
-
#
|
459
|
-
# class ArticleRepository
|
460
|
-
# include Hanami::Repository
|
461
|
-
# end
|
462
|
-
#
|
463
|
-
# ArticleRepository.first # => #<Article:0x007f8c71d98a28>
|
464
|
-
#
|
465
|
-
# @example With an empty collection
|
466
|
-
# require 'hanami/model'
|
467
|
-
#
|
468
|
-
# class ArticleRepository
|
469
|
-
# include Hanami::Repository
|
470
|
-
# end
|
471
|
-
#
|
472
|
-
# ArticleRepository.first # => nil
|
473
|
-
def first
|
474
|
-
@adapter.first(collection)
|
475
|
-
end
|
201
|
+
# It defines associations, by adding relations to the repository
|
202
|
+
#
|
203
|
+
# @since 0.7.0
|
204
|
+
# @api private
|
205
|
+
#
|
206
|
+
# @see Hanami::Model::Associations::Dsl
|
207
|
+
def self.define_associations
|
208
|
+
Model::Associations::Dsl.new(self, &@associations) unless @associations.nil?
|
209
|
+
end
|
476
210
|
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
#
|
494
|
-
# @example With an empty collection
|
495
|
-
# require 'hanami/model'
|
496
|
-
#
|
497
|
-
# class ArticleRepository
|
498
|
-
# include Hanami::Repository
|
499
|
-
# end
|
500
|
-
#
|
501
|
-
# ArticleRepository.last # => nil
|
502
|
-
def last
|
503
|
-
@adapter.last(collection)
|
504
|
-
end
|
211
|
+
# Declare associations for the repository
|
212
|
+
#
|
213
|
+
# NOTE: This is an experimental feature
|
214
|
+
#
|
215
|
+
# @since 0.7.0
|
216
|
+
# @api private
|
217
|
+
#
|
218
|
+
# @example
|
219
|
+
# class BookRepository < Hanami::Repository
|
220
|
+
# associations do
|
221
|
+
# has_many :books
|
222
|
+
# end
|
223
|
+
# end
|
224
|
+
def self.associations(&blk)
|
225
|
+
@associations = blk
|
226
|
+
end
|
505
227
|
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
228
|
+
# Declare mapping between database columns and entity's attributes
|
229
|
+
#
|
230
|
+
# NOTE: This should be used **only** when there is a name mismatch (eg. in legacy databases).
|
231
|
+
#
|
232
|
+
# @since 0.7.0
|
233
|
+
#
|
234
|
+
# @example
|
235
|
+
# class BookRepository < Hanami::Repository
|
236
|
+
# self.relation = :t_operator
|
237
|
+
#
|
238
|
+
# mapping do
|
239
|
+
# attribute :id, from: :operator_id
|
240
|
+
# attribute :name, from: :s_name
|
241
|
+
# end
|
242
|
+
# end
|
243
|
+
def self.mapping(&blk)
|
244
|
+
@mapping = blk
|
245
|
+
end
|
523
246
|
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
# For advanced scenarios, please check the documentation of each adapter.
|
534
|
-
#
|
535
|
-
# @param options [Hash] options for transaction
|
536
|
-
#
|
537
|
-
# @see Hanami::Model::Adapters::SqlAdapter#transaction
|
538
|
-
# @see Hanami::Model::Adapters::MemoryAdapter#transaction
|
539
|
-
#
|
540
|
-
# @since 0.2.3
|
541
|
-
#
|
542
|
-
# @example Basic usage with SQL adapter
|
543
|
-
# require 'hanami/model'
|
544
|
-
#
|
545
|
-
# class Article
|
546
|
-
# include Hanami::Entity
|
547
|
-
# attributes :title, :body
|
548
|
-
# end
|
549
|
-
#
|
550
|
-
# class ArticleRepository
|
551
|
-
# include Hanami::Repository
|
552
|
-
# end
|
553
|
-
#
|
554
|
-
# article = Article.new(title: 'Introducing transactions',
|
555
|
-
# body: 'lorem ipsum')
|
556
|
-
#
|
557
|
-
# ArticleRepository.transaction do
|
558
|
-
# ArticleRepository.dangerous_operation!(article) # => RuntimeError
|
559
|
-
# # !!! ROLLBACK !!!
|
560
|
-
# end
|
561
|
-
def transaction(options = {})
|
562
|
-
@adapter.transaction(options) do
|
563
|
-
yield
|
564
|
-
end
|
565
|
-
end
|
247
|
+
# Define relations, mapping and associations
|
248
|
+
#
|
249
|
+
# @since 0.7.0
|
250
|
+
# @api private
|
251
|
+
def self.load!
|
252
|
+
define_relation
|
253
|
+
define_mapping
|
254
|
+
define_associations
|
255
|
+
end
|
566
256
|
|
567
|
-
|
257
|
+
# @since 0.7.0
|
258
|
+
# @api private
|
259
|
+
def self.inherited(klass) # rubocop:disable Metrics/MethodLength
|
260
|
+
klass.class_eval do
|
261
|
+
include Utils::ClassAttribute
|
568
262
|
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
# @return [NilClass]
|
580
|
-
#
|
581
|
-
# @raise [NotImplementedError] if current Hanami::Model adapter doesn't
|
582
|
-
# implement `execute`.
|
583
|
-
#
|
584
|
-
# @raise [Hanami::Model::InvalidCommandError] if the raw statement is invalid
|
585
|
-
#
|
586
|
-
# @see Hanami::Model::Adapters::Abstract#execute
|
587
|
-
# @see Hanami::Model::Adapters::SqlAdapter#execute
|
588
|
-
#
|
589
|
-
# @since 0.3.1
|
590
|
-
#
|
591
|
-
# @example Basic usage with SQL adapter
|
592
|
-
# require 'hanami/model'
|
593
|
-
#
|
594
|
-
# class Article
|
595
|
-
# include Hanami::Entity
|
596
|
-
# attributes :title, :body
|
597
|
-
# end
|
598
|
-
#
|
599
|
-
# class ArticleRepository
|
600
|
-
# include Hanami::Repository
|
601
|
-
#
|
602
|
-
# def self.reset_comments_count
|
603
|
-
# execute "UPDATE articles SET comments_count = 0"
|
604
|
-
# end
|
605
|
-
# end
|
606
|
-
#
|
607
|
-
# ArticleRepository.reset_comments_count
|
608
|
-
def execute(raw)
|
609
|
-
@adapter.execute(raw)
|
263
|
+
class_attribute :entity
|
264
|
+
|
265
|
+
class_attribute :entity_name
|
266
|
+
self.entity_name = Model::EntityName.new(name)
|
267
|
+
|
268
|
+
class_attribute :relation
|
269
|
+
self.relation = Model::RelationName.new(name)
|
270
|
+
|
271
|
+
commands :create, update: :by_primary_key, delete: :by_primary_key, mapper: MAPPER_NAME, use: COMMAND_PLUGINS
|
272
|
+
prepend Commands
|
610
273
|
end
|
611
274
|
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
#
|
620
|
-
#
|
621
|
-
# @return [Enumerable<Hash>,Array<Hash>] the collection of raw records
|
622
|
-
#
|
623
|
-
# @raise [NotImplementedError] if current Hanami::Model adapter doesn't
|
624
|
-
# implement `fetch`.
|
625
|
-
#
|
626
|
-
# @raise [Hanami::Model::InvalidQueryError] if the raw statement is invalid
|
627
|
-
#
|
628
|
-
# @since 0.5.0
|
629
|
-
#
|
630
|
-
# @example Basic Usage
|
631
|
-
# require 'hanami/model'
|
632
|
-
#
|
633
|
-
# mapping do
|
634
|
-
# collection :articles do
|
635
|
-
# attribute :id, Integer, as: :s_id
|
636
|
-
# attribute :title, String, as: :s_title
|
637
|
-
# end
|
638
|
-
# end
|
639
|
-
#
|
640
|
-
# class Article
|
641
|
-
# include Hanami::Entity
|
642
|
-
# attributes :title, :body
|
643
|
-
# end
|
644
|
-
#
|
645
|
-
# class ArticleRepository
|
646
|
-
# include Hanami::Repository
|
647
|
-
#
|
648
|
-
# def self.all_raw
|
649
|
-
# fetch("SELECT * FROM articles")
|
650
|
-
# end
|
651
|
-
# end
|
652
|
-
#
|
653
|
-
# ArticleRepository.all_raw
|
654
|
-
# # => [{:_id=>1, :user_id=>nil, :s_title=>"Art 1", :comments_count=>nil, :umapped_column=>nil}]
|
655
|
-
#
|
656
|
-
# @example Map A Value From Result Set
|
657
|
-
# require 'hanami/model'
|
658
|
-
#
|
659
|
-
# mapping do
|
660
|
-
# collection :articles do
|
661
|
-
# attribute :id, Integer, as: :s_id
|
662
|
-
# attribute :title, String, as: :s_title
|
663
|
-
# end
|
664
|
-
# end
|
665
|
-
#
|
666
|
-
# class Article
|
667
|
-
# include Hanami::Entity
|
668
|
-
# attributes :title, :body
|
669
|
-
# end
|
670
|
-
#
|
671
|
-
# class ArticleRepository
|
672
|
-
# include Hanami::Repository
|
673
|
-
#
|
674
|
-
# def self.titles
|
675
|
-
# fetch("SELECT s_title FROM articles").map do |article|
|
676
|
-
# article[:s_title]
|
677
|
-
# end
|
678
|
-
# end
|
679
|
-
# end
|
680
|
-
#
|
681
|
-
# ArticleRepository.titles # => ["Announcing Hanami v0.5.0"]
|
682
|
-
#
|
683
|
-
# @example Passing A Block
|
684
|
-
# require 'hanami/model'
|
685
|
-
#
|
686
|
-
# mapping do
|
687
|
-
# collection :articles do
|
688
|
-
# attribute :id, Integer, as: :s_id
|
689
|
-
# attribute :title, String, as: :s_title
|
690
|
-
# end
|
691
|
-
# end
|
275
|
+
Hanami::Model.repositories << klass
|
276
|
+
end
|
277
|
+
|
278
|
+
# Extend commands from ROM::Repository with error management
|
279
|
+
#
|
280
|
+
# @since 0.7.0
|
281
|
+
module Commands
|
282
|
+
# Create a new record
|
692
283
|
#
|
693
|
-
#
|
694
|
-
# include Hanami::Entity
|
695
|
-
# attributes :title, :body
|
696
|
-
# end
|
284
|
+
# @return [Hanami::Entity] an new created entity
|
697
285
|
#
|
698
|
-
#
|
699
|
-
# include Hanami::Repository
|
286
|
+
# @raise [Hanami::Model::Error] an error in case the command fails
|
700
287
|
#
|
701
|
-
#
|
702
|
-
# result = []
|
288
|
+
# @since 0.7.0
|
703
289
|
#
|
704
|
-
#
|
705
|
-
#
|
706
|
-
# end
|
290
|
+
# @example Create From Hash
|
291
|
+
# user = UserRepository.new.create(name: 'Luca')
|
707
292
|
#
|
708
|
-
#
|
709
|
-
#
|
710
|
-
#
|
293
|
+
# @example Create From Entity
|
294
|
+
# entity = User.new(name: 'Luca')
|
295
|
+
# user = UserRepository.new.create(entity)
|
711
296
|
#
|
712
|
-
#
|
713
|
-
|
714
|
-
|
297
|
+
# user.id # => 23
|
298
|
+
# entity.id # => nil - It doesn't mutate original entity
|
299
|
+
def create(*args)
|
300
|
+
super
|
301
|
+
rescue => e
|
302
|
+
raise Hanami::Model::Error.for(e)
|
715
303
|
end
|
716
304
|
|
717
|
-
#
|
718
|
-
# APIs exposed by the query itself.
|
719
|
-
#
|
720
|
-
# This is a Ruby private method, because we wanted to prevent outside
|
721
|
-
# objects to query directly the database. However, this is a public API
|
722
|
-
# method, and this is the only way to filter entities.
|
723
|
-
#
|
724
|
-
# The returned query SHOULD be lazy: the entities should be fetched by
|
725
|
-
# the database only when needed.
|
726
|
-
#
|
727
|
-
# The returned query SHOULD refer to the entire collection by default.
|
728
|
-
#
|
729
|
-
# Queries can be reused and combined together. See the example below.
|
730
|
-
# IMPORTANT: This feature works only with the Sql adapter.
|
731
|
-
#
|
732
|
-
# A repository is storage independent.
|
733
|
-
# All the queries are delegated to the current adapter, which is
|
734
|
-
# responsible to implement a querying API.
|
735
|
-
#
|
736
|
-
# Hanami::Model is shipped with two adapters:
|
737
|
-
#
|
738
|
-
# * SqlAdapter, which yields a Hanami::Model::Adapters::Sql::Query
|
739
|
-
# * MemoryAdapter, which yields a Hanami::Model::Adapters::Memory::Query
|
740
|
-
#
|
741
|
-
# @param blk [Proc] a block of code that is executed in the context of a
|
742
|
-
# query
|
743
|
-
#
|
744
|
-
# @return a query, the type depends on the current adapter
|
305
|
+
# Update a record
|
745
306
|
#
|
746
|
-
# @
|
747
|
-
# @since 0.1.0
|
307
|
+
# @return [Hanami::Entity] an updated entity
|
748
308
|
#
|
749
|
-
# @
|
750
|
-
# @see Hanami::Model::Adapters::Memory::Query
|
309
|
+
# @raise [Hanami::Model::Error] an error in case the command fails
|
751
310
|
#
|
752
|
-
# @
|
753
|
-
# require 'hanami/model'
|
754
|
-
#
|
755
|
-
# class ArticleRepository
|
756
|
-
# include Hanami::Repository
|
757
|
-
#
|
758
|
-
# def self.most_recent_by_author(author, limit = 8)
|
759
|
-
# query do
|
760
|
-
# where(author_id: author.id).
|
761
|
-
# desc(:published_at).
|
762
|
-
# limit(limit)
|
763
|
-
# end
|
764
|
-
# end
|
311
|
+
# @since 0.7.0
|
765
312
|
#
|
766
|
-
#
|
767
|
-
#
|
768
|
-
#
|
769
|
-
# end
|
313
|
+
# @example Update From Data
|
314
|
+
# repository = UserRepository.new
|
315
|
+
# user = repository.create(name: 'Luca')
|
770
316
|
#
|
771
|
-
#
|
772
|
-
# query do
|
773
|
-
# where(published: true)
|
774
|
-
# end
|
775
|
-
# end
|
317
|
+
# user = repository.update(user.id, age: 34)
|
776
318
|
#
|
777
|
-
#
|
778
|
-
#
|
779
|
-
#
|
780
|
-
# end
|
319
|
+
# @example Update From Entity
|
320
|
+
# repository = UserRepository.new
|
321
|
+
# user = repository.create(name: 'Luca')
|
781
322
|
#
|
782
|
-
#
|
783
|
-
#
|
784
|
-
# rank.limit(1)
|
785
|
-
# end
|
323
|
+
# entity = User.new(age: 34)
|
324
|
+
# user = repository.update(user.id, entity)
|
786
325
|
#
|
787
|
-
#
|
788
|
-
#
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
326
|
+
# user.age # => 34
|
327
|
+
# entity.id # => nil - It doesn't mutate original entity
|
328
|
+
def update(*args)
|
329
|
+
super
|
330
|
+
rescue => e
|
331
|
+
raise Hanami::Model::Error.for(e)
|
793
332
|
end
|
794
333
|
|
795
|
-
#
|
796
|
-
# opposite operator.
|
797
|
-
#
|
798
|
-
# This is only supported by the SqlAdapter.
|
799
|
-
#
|
800
|
-
# @param query [Object] a query
|
334
|
+
# Delete a record
|
801
335
|
#
|
802
|
-
# @return a
|
336
|
+
# @return [Hanami::Entity] a deleted entity
|
803
337
|
#
|
804
|
-
# @
|
805
|
-
# @since 0.1.0
|
338
|
+
# @raise [Hanami::Model::Error] an error in case the command fails
|
806
339
|
#
|
807
|
-
# @
|
340
|
+
# @since 0.7.0
|
808
341
|
#
|
809
342
|
# @example
|
810
|
-
#
|
811
|
-
#
|
812
|
-
#
|
813
|
-
#
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
# end
|
819
|
-
# end
|
820
|
-
#
|
821
|
-
# def self.not_cool
|
822
|
-
# exclude cool
|
823
|
-
# end
|
824
|
-
# end
|
825
|
-
def exclude(query)
|
826
|
-
query.negate!
|
827
|
-
query
|
343
|
+
# repository = UserRepository.new
|
344
|
+
# user = repository.create(name: 'Luca')
|
345
|
+
#
|
346
|
+
# user = repository.delete(user.id)
|
347
|
+
def delete(*args)
|
348
|
+
super
|
349
|
+
rescue => e
|
350
|
+
raise Hanami::Model::Error.for(e)
|
828
351
|
end
|
352
|
+
end
|
829
353
|
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
354
|
+
# Initialize a new instance
|
355
|
+
#
|
356
|
+
# @return [Hanami::Repository] the new instance
|
357
|
+
#
|
358
|
+
# @since 0.7.0
|
359
|
+
def initialize
|
360
|
+
super(self.class.container)
|
361
|
+
end
|
838
362
|
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
363
|
+
# Find by primary key
|
364
|
+
#
|
365
|
+
# @return [Hanami::Entity,NilClass] the entity, if found
|
366
|
+
#
|
367
|
+
# @since 0.7.0
|
368
|
+
#
|
369
|
+
# @example
|
370
|
+
# repository = UserRepository.new
|
371
|
+
# user = repository.create(name: 'Luca')
|
372
|
+
#
|
373
|
+
# user = repository.find(user.id)
|
374
|
+
def find(id)
|
375
|
+
root.by_primary_key(id).as(:entity).one
|
376
|
+
end
|
847
377
|
|
848
|
-
|
849
|
-
|
850
|
-
|
378
|
+
# Return all the records for the relation
|
379
|
+
#
|
380
|
+
# @return [Array<Hanami::Entity>] all the entities
|
381
|
+
#
|
382
|
+
# @since 0.7.0
|
383
|
+
#
|
384
|
+
# @example
|
385
|
+
# UserRepository.new.all
|
386
|
+
def all
|
387
|
+
root.as(:entity).to_a
|
388
|
+
end
|
851
389
|
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
390
|
+
# Returns the first record for the relation
|
391
|
+
#
|
392
|
+
# @return [Hanami::Entity,NilClass] first entity, if any
|
393
|
+
#
|
394
|
+
# @since 0.7.0
|
395
|
+
#
|
396
|
+
# @example
|
397
|
+
# UserRepository.new.first
|
398
|
+
def first
|
399
|
+
root.as(:entity).first
|
400
|
+
end
|
856
401
|
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
402
|
+
# Returns the last record for the relation
|
403
|
+
#
|
404
|
+
# @return [Hanami::Entity,NilClass] last entity, if any
|
405
|
+
#
|
406
|
+
# @since 0.7.0
|
407
|
+
#
|
408
|
+
# @example
|
409
|
+
# UserRepository.new.last
|
410
|
+
def last
|
411
|
+
root.order(Model::Sql.desc(root.primary_key)).as(:entity).first
|
412
|
+
end
|
413
|
+
|
414
|
+
# Deletes all the records from the relation
|
415
|
+
#
|
416
|
+
# @since 0.7.0
|
417
|
+
#
|
418
|
+
# @example
|
419
|
+
# UserRepository.new.clear
|
420
|
+
def clear
|
421
|
+
root.delete
|
422
|
+
end
|
423
|
+
|
424
|
+
private
|
425
|
+
|
426
|
+
# Returns an association
|
427
|
+
#
|
428
|
+
# NOTE: This is an experimental feature
|
429
|
+
#
|
430
|
+
# @since 0.7.0
|
431
|
+
# @api private
|
432
|
+
def assoc(target, subject = nil)
|
433
|
+
Hanami::Model::Association.build(self, target, subject)
|
870
434
|
end
|
871
435
|
end
|
872
436
|
end
|