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.
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