hanami-model 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -0
  3. data/README.md +54 -420
  4. data/hanami-model.gemspec +9 -6
  5. data/lib/hanami/entity.rb +107 -191
  6. data/lib/hanami/entity/schema.rb +236 -0
  7. data/lib/hanami/model.rb +52 -138
  8. data/lib/hanami/model/association.rb +37 -0
  9. data/lib/hanami/model/associations/belongs_to.rb +19 -0
  10. data/lib/hanami/model/associations/dsl.rb +29 -0
  11. data/lib/hanami/model/associations/has_many.rb +200 -0
  12. data/lib/hanami/model/configuration.rb +52 -224
  13. data/lib/hanami/model/configurator.rb +62 -0
  14. data/lib/hanami/model/entity_name.rb +35 -0
  15. data/lib/hanami/model/error.rb +37 -24
  16. data/lib/hanami/model/mapping.rb +29 -35
  17. data/lib/hanami/model/migration.rb +31 -0
  18. data/lib/hanami/model/migrator.rb +111 -88
  19. data/lib/hanami/model/migrator/adapter.rb +39 -16
  20. data/lib/hanami/model/migrator/connection.rb +23 -11
  21. data/lib/hanami/model/migrator/mysql_adapter.rb +38 -17
  22. data/lib/hanami/model/migrator/postgres_adapter.rb +20 -19
  23. data/lib/hanami/model/migrator/sqlite_adapter.rb +9 -8
  24. data/lib/hanami/model/plugins.rb +25 -0
  25. data/lib/hanami/model/plugins/mapping.rb +55 -0
  26. data/lib/hanami/model/plugins/schema.rb +55 -0
  27. data/lib/hanami/model/plugins/timestamps.rb +118 -0
  28. data/lib/hanami/model/relation_name.rb +24 -0
  29. data/lib/hanami/model/sql.rb +161 -0
  30. data/lib/hanami/model/sql/console.rb +41 -0
  31. data/lib/hanami/model/sql/consoles/abstract.rb +33 -0
  32. data/lib/hanami/model/sql/consoles/mysql.rb +63 -0
  33. data/lib/hanami/model/sql/consoles/postgresql.rb +68 -0
  34. data/lib/hanami/model/sql/consoles/sqlite.rb +46 -0
  35. data/lib/hanami/model/sql/entity/schema.rb +125 -0
  36. data/lib/hanami/model/sql/types.rb +95 -0
  37. data/lib/hanami/model/sql/types/schema/coercions.rb +198 -0
  38. data/lib/hanami/model/types.rb +99 -0
  39. data/lib/hanami/model/version.rb +1 -1
  40. data/lib/hanami/repository.rb +287 -723
  41. metadata +77 -40
  42. data/EXAMPLE.md +0 -213
  43. data/lib/hanami/entity/dirty_tracking.rb +0 -74
  44. data/lib/hanami/model/adapters/abstract.rb +0 -281
  45. data/lib/hanami/model/adapters/file_system_adapter.rb +0 -288
  46. data/lib/hanami/model/adapters/implementation.rb +0 -111
  47. data/lib/hanami/model/adapters/memory/collection.rb +0 -132
  48. data/lib/hanami/model/adapters/memory/command.rb +0 -113
  49. data/lib/hanami/model/adapters/memory/query.rb +0 -653
  50. data/lib/hanami/model/adapters/memory_adapter.rb +0 -179
  51. data/lib/hanami/model/adapters/null_adapter.rb +0 -24
  52. data/lib/hanami/model/adapters/sql/collection.rb +0 -287
  53. data/lib/hanami/model/adapters/sql/command.rb +0 -88
  54. data/lib/hanami/model/adapters/sql/console.rb +0 -33
  55. data/lib/hanami/model/adapters/sql/consoles/mysql.rb +0 -49
  56. data/lib/hanami/model/adapters/sql/consoles/postgresql.rb +0 -48
  57. data/lib/hanami/model/adapters/sql/consoles/sqlite.rb +0 -26
  58. data/lib/hanami/model/adapters/sql/query.rb +0 -788
  59. data/lib/hanami/model/adapters/sql_adapter.rb +0 -296
  60. data/lib/hanami/model/coercer.rb +0 -74
  61. data/lib/hanami/model/config/adapter.rb +0 -116
  62. data/lib/hanami/model/config/mapper.rb +0 -45
  63. data/lib/hanami/model/mapper.rb +0 -124
  64. data/lib/hanami/model/mapping/attribute.rb +0 -85
  65. data/lib/hanami/model/mapping/coercers.rb +0 -314
  66. data/lib/hanami/model/mapping/collection.rb +0 -490
  67. 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
@@ -3,6 +3,6 @@ module Hanami
3
3
  # Defines the version
4
4
  #
5
5
  # @since 0.1.0
6
- VERSION = '0.6.1'.freeze
6
+ VERSION = '0.7.0'.freeze
7
7
  end
8
8
  end
@@ -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 two adapters:
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
- # include Hanami::Repository
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
- # end.limit(limit)
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
- module Repository
122
- # Inject the public API into the hosting class.
109
+ class Repository < ROM::Repository::Root
110
+ # Mapper name.
123
111
  #
124
- # @since 0.1.0
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
- # @example
127
- # require 'hanami/model'
116
+ # @since 0.7.0
117
+ # @api private
128
118
  #
129
- # class UserRepository
130
- # include Hanami::Repository
131
- # end
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
- class_attribute :collection
138
- self.adapter = Hanami::Model::Adapters::NullAdapter.new
139
- end
140
- end
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
- module ClassMethods
143
- # Assigns an adapter.
144
- #
145
- # Hanami::Model is shipped with two adapters:
146
- #
147
- # * SqlAdapter
148
- # * MemoryAdapter
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
- # @since 0.5.0
216
- # @api private
217
- def adapter
218
- @adapter
219
- end
139
+ # Container
140
+ #
141
+ # @since 0.7.0
142
+ # @api private
143
+ def self.container
144
+ Hanami::Model.container
145
+ end
220
146
 
221
- # Creates or updates a record in the database for the given entity.
222
- #
223
- # @param entity [#id, #id=] the entity to persist
224
- #
225
- # @return [Object] a copy of the entity with `id` assigned
226
- #
227
- # @since 0.1.0
228
- #
229
- # @see Hanami::Repository#create
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
- # Creates a record in the database for the given entity.
267
- # It returns a copy of the entity with `id` assigned.
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
- # Updates a record in the database corresponding to the given entity.
307
- #
308
- # If not already persisted (`id` present) it raises an exception.
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
- # Deletes a record in the database corresponding to the given entity.
358
- #
359
- # If not already persisted (`id` present) it raises an exception.
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
- entity
405
- end
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
- # Returns all the persisted entities.
408
- #
409
- # @return [Array<Object>] the result of the query
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
- # Finds an entity by its identity.
426
- #
427
- # If used with a SQL database, it corresponds to the primary key.
428
- #
429
- # @param id [Object] the identity of the entity
430
- #
431
- # @return [Object,NilClass] the result of the query, if present
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
- # Returns the first entity in the database.
449
- #
450
- # @return [Object,nil] the result of the query
451
- #
452
- # @since 0.1.0
453
- #
454
- # @see Hanami::Repository#last
455
- #
456
- # @example With at least one persisted entity
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
- # Returns the last entity in the database.
478
- #
479
- # @return [Object,nil] the result of the query
480
- #
481
- # @since 0.1.0
482
- #
483
- # @see Hanami::Repository#last
484
- #
485
- # @example With at least one persisted entity
486
- # require 'hanami/model'
487
- #
488
- # class ArticleRepository
489
- # include Hanami::Repository
490
- # end
491
- #
492
- # ArticleRepository.last # => #<Article:0x007f8c71d98a28>
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
- # Deletes all the records from the current collection.
507
- #
508
- # If used with a SQL database it executes a `DELETE FROM <table>`.
509
- #
510
- # @since 0.1.0
511
- #
512
- # @example
513
- # require 'hanami/model'
514
- #
515
- # class ArticleRepository
516
- # include Hanami::Repository
517
- # end
518
- #
519
- # ArticleRepository.clear # deletes all the records
520
- def clear
521
- @adapter.clear(collection)
522
- end
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
- # Wraps the given block in a transaction.
525
- #
526
- # For performance reasons the block isn't in the signature of the method,
527
- # but it's yielded at the lower level.
528
- #
529
- # Please note that it's only supported by some databases.
530
- # For this reason, the accepted options may be different from adapter to
531
- # adapter.
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
- private
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
- # Executes the given raw statement on the adapter.
570
- #
571
- # Please note that it's only supported by some databases,
572
- # a `NotImplementedError` will be raised when the adapter does not
573
- # responds to the `execute` method.
574
- #
575
- # For advanced scenarios, please check the documentation of each adapter.
576
- #
577
- # @param raw [String] the raw statement to execute on the connection
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
- # Fetch raw result sets for the the given statement.
613
- #
614
- # PLEASE NOTE: The returned result set contains an array of hashes.
615
- # The columns are returned as they are from the database,
616
- # the mapper is bypassed here.
617
- #
618
- # @param raw [String] the raw statement used to fetch records
619
- # @param blk [Proc] an optional block that is yielded for each record
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
- # class Article
694
- # include Hanami::Entity
695
- # attributes :title, :body
696
- # end
284
+ # @return [Hanami::Entity] an new created entity
697
285
  #
698
- # class ArticleRepository
699
- # include Hanami::Repository
286
+ # @raise [Hanami::Model::Error] an error in case the command fails
700
287
  #
701
- # def self.titles
702
- # result = []
288
+ # @since 0.7.0
703
289
  #
704
- # fetch("SELECT s_title FROM articles") do |article|
705
- # result << article[:s_title]
706
- # end
290
+ # @example Create From Hash
291
+ # user = UserRepository.new.create(name: 'Luca')
707
292
  #
708
- # result
709
- # end
710
- # end
293
+ # @example Create From Entity
294
+ # entity = User.new(name: 'Luca')
295
+ # user = UserRepository.new.create(entity)
711
296
  #
712
- # ArticleRepository.titles # => ["Announcing Hanami v0.5.0"]
713
- def fetch(raw, &blk)
714
- @adapter.fetch(raw, &blk)
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
- # Fabricates a query and yields the given block to access the low level
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
- # @api public
747
- # @since 0.1.0
307
+ # @return [Hanami::Entity] an updated entity
748
308
  #
749
- # @see Hanami::Model::Adapters::Sql::Query
750
- # @see Hanami::Model::Adapters::Memory::Query
309
+ # @raise [Hanami::Model::Error] an error in case the command fails
751
310
  #
752
- # @example
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
- # def self.most_recent_published_by_author(author, limit = 8)
767
- # # combine .most_recent_published_by_author and .published queries
768
- # most_recent_by_author(author, limit).published
769
- # end
313
+ # @example Update From Data
314
+ # repository = UserRepository.new
315
+ # user = repository.create(name: 'Luca')
770
316
  #
771
- # def self.published
772
- # query do
773
- # where(published: true)
774
- # end
775
- # end
317
+ # user = repository.update(user.id, age: 34)
776
318
  #
777
- # def self.rank
778
- # # reuse .published, which returns a query that respond to #desc
779
- # published.desc(:comments_count)
780
- # end
319
+ # @example Update From Entity
320
+ # repository = UserRepository.new
321
+ # user = repository.create(name: 'Luca')
781
322
  #
782
- # def self.best_article_ever
783
- # # reuse .published, which returns a query that respond to #limit
784
- # rank.limit(1)
785
- # end
323
+ # entity = User.new(age: 34)
324
+ # user = repository.update(user.id, entity)
786
325
  #
787
- # def self.comments_average
788
- # query.average(:comments_count)
789
- # end
790
- # end
791
- def query(&blk)
792
- @adapter.query(collection, self, &blk)
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
- # Negates the filtering conditions of a given query with the logical
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 negated query, the type depends on the current adapter
336
+ # @return [Hanami::Entity] a deleted entity
803
337
  #
804
- # @api public
805
- # @since 0.1.0
338
+ # @raise [Hanami::Model::Error] an error in case the command fails
806
339
  #
807
- # @see Hanami::Model::Adapters::Sql::Query#negate!
340
+ # @since 0.7.0
808
341
  #
809
342
  # @example
810
- # require 'hanami/model'
811
- #
812
- # class ProjectRepository
813
- # include Hanami::Repository
814
- #
815
- # def self.cool
816
- # query do
817
- # where(language: 'ruby')
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
- # This is a method to check entity persited or not
831
- #
832
- # @param entity
833
- # @return a boolean value
834
- # @since 0.3.1
835
- def _persisted?(entity)
836
- !!entity.id
837
- end
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
- # Update timestamps
840
- #
841
- # @param entity [Object, Hanami::Entity] the entity
842
- #
843
- # @api private
844
- # @since 0.3.1
845
- def _touch(entity)
846
- now = Time.now.utc
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
- if _has_timestamp?(entity, :created_at)
849
- entity.created_at ||= now
850
- end
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
- if _has_timestamp?(entity, :updated_at)
853
- entity.updated_at = now
854
- end
855
- end
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
- # Check if the given entity has the given timestamp
858
- #
859
- # @param entity [Object, Hanami::Entity] the entity
860
- # @param timestamp [Symbol] the timestamp name
861
- #
862
- # @return [TrueClass,FalseClass]
863
- #
864
- # @api private
865
- # @since 0.3.1
866
- def _has_timestamp?(entity, timestamp)
867
- entity.respond_to?(timestamp) &&
868
- entity.respond_to?("#{ timestamp }=")
869
- end
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