vorpal 1.0.0 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2fdca5b34f5a1ac2b9b228849c53b8322e542ab9bc75a5c15a0c8003f1f1fd94
4
- data.tar.gz: aebdb05efdfd853644afa49bf101d845082a7d627e47c50bf609830be46901f0
3
+ metadata.gz: cf920ae279d48b7b9db84a79c77eeb521602a885c8fe88e8f2121e47a5a46e41
4
+ data.tar.gz: 9e97596434cf65e598279005a0bb325cadde5d7a6d3f2180ea5a828432f89760
5
5
  SHA512:
6
- metadata.gz: 1416691727efd279769865a4725949ff41965d120d6431a3737880936838e6760d857c3a5546b5b73774a1ea24cb1ef742d6f7cce6dfa0497de34739ba67b22a
7
- data.tar.gz: 117da60ac019fa9b373d7f45ad6893b24de5611533d348034ef4f4d978adc81468d3228d55adf8107482084b219c47509cb8fef69516ae0ccdea31f73e3a0390
6
+ metadata.gz: d7c07c613b24c7d342be0361e33bda6db6cff0270222db1b7ca8018cf141fb623b2583e49a0d39db51d53389225e527d6a5fd74f133edc6f7653309e86644be4
7
+ data.tar.gz: 492d62d23506de0e26ffdc8e525169dcb78d6c1060dc9309c9828e9865f2341e8f01cc84fe95a059d01d89462c62292a67ebcfc4148ce4ace770884a56e5e04c
data/README.md CHANGED
@@ -1,16 +1,13 @@
1
- # Vorpal [![Build Status](https://travis-ci.org/nulogy/vorpal.svg?branch=master)](https://travis-ci.org/nulogy/vorpal) [![Code Climate](https://codeclimate.com/github/nulogy/vorpal/badges/gpa.svg)](https://codeclimate.com/github/nulogy/vorpal)
1
+ # Vorpal [![Build Status](https://travis-ci.com/nulogy/vorpal.svg?branch=main)](https://travis-ci.com/nulogy/vorpal) [![Code Climate](https://codeclimate.com/github/nulogy/vorpal/badges/gpa.svg)](https://codeclimate.com/github/nulogy/vorpal) [![Code Coverage](https://codecov.io/gh/nulogy/vorpal/branch/main/graph/badge.svg)](https://codecov.io/gh/nulogy/vorpal/branch/main)
2
2
 
3
3
  Separate your domain model from your persistence mechanism. Some problems call for a really sharp tool.
4
4
 
5
-
6
- > One, two! One, two! and through and through
7
-
8
- > The vorpal blade went snicker-snack!
9
-
10
- > He left it dead, and with its head
11
-
5
+ > One, two! One, two! and through and through<br/>
6
+ > The vorpal blade went snicker-snack!<br/>
7
+ > He left it dead, and with its head<br/>
12
8
  > He went galumphing back.
13
9
 
10
+ \- [Jabberwocky](https://www.poetryfoundation.org/poems/42916/jabberwocky) by Lewis Carroll
14
11
 
15
12
  ## Overview
16
13
  Vorpal is a [Data Mapper](http://martinfowler.com/eaaCatalog/dataMapper.html)-style ORM (object relational mapper) framelet that persists POROs (plain old Ruby objects) to a relational DB. It has been heavily influenced by concepts from [Domain Driven Design](http://www.infoq.com/minibooks/domain-driven-design-quickly).
@@ -48,27 +45,21 @@ Or install it yourself as:
48
45
  Start with a domain model of POROs and AR::Base objects that form an aggregate:
49
46
 
50
47
  ```ruby
51
- class Tree; end
52
-
53
48
  class Branch
54
- include Virtus.model
55
-
56
- attribute :id, Integer
57
- attribute :length, Decimal
58
- attribute :diameter, Decimal
59
- attribute :tree, Tree
49
+ attr_accessor :id
50
+ attr_accessor :length
51
+ attr_accessor :diameter
52
+ attr_accessor :tree
60
53
  end
61
54
 
62
- class Gardener < ActiveRecord::Base
55
+ class Gardener
63
56
  end
64
57
 
65
58
  class Tree
66
- include Virtus.model
67
-
68
- attribute :id, Integer
69
- attribute :name, String
70
- attribute :gardener, Gardener
71
- attribute :branches, Array[Branch]
59
+ attr_accessor :id
60
+ attr_accessor :name
61
+ attr_accessor :gardener
62
+ attr_accessor :branches
72
63
  end
73
64
  ```
74
65
 
@@ -143,7 +134,8 @@ module TreeRepository
143
134
  end
144
135
  ```
145
136
 
146
- Here we've used the `owned` flag on the `belongs_to` from the Tree to the Gardener to show that the Gardener is on the aggregate boundary.
137
+ Here we've used the `owned: false` flag on the `belongs_to` from the Tree to the Gardener to show
138
+ that the Gardener is on the aggregate boundary.
147
139
 
148
140
  And use it:
149
141
 
@@ -164,9 +156,35 @@ TreeRepository.destroy(dead_tree)
164
156
  TreeRepository.destroy_by_id(dead_tree_id)
165
157
  ```
166
158
 
159
+ ### Ids
160
+
161
+ Vorpal by default will use auto-incrementing Integers from a DB sequence for ids. However, UUID v4 ids are also
162
+ supported:
163
+
164
+ ```ruby
165
+ Vorpal.define do
166
+ # UUID v4 id!
167
+ map Tree, primary_key_type: :uuid do
168
+ # ..
169
+ end
170
+
171
+ # Also a UUID v4 id, the Rails Way!
172
+ map Trunk, id: :uuid do
173
+ # ..
174
+ end
175
+
176
+ # If you feel the need to specify an auto-incrementing integer id.
177
+ map Branch, primary_key_type: :serial do
178
+ # ..
179
+ end
180
+ end
181
+ ```
182
+
183
+ CAVEAT: Vorpal currently does NOT SUPPORT anyone but Vorpal setting the id of an entity!
184
+
167
185
  ## API Documentation
168
186
 
169
- http://rubydoc.info/github/nulogy/vorpal/master/frames
187
+ http://rubydoc.info/github/nulogy/vorpal/main/frames
170
188
 
171
189
  ## Caveats
172
190
  It also does not do some things that you might expect from other ORMs:
@@ -174,7 +192,7 @@ It also does not do some things that you might expect from other ORMs:
174
192
  1. No lazy loading of associations. This might sound like a big deal, but with [correctly designed aggregates](http://dddcommunity.org/library/vernon_2011/) it turns out not to be.
175
193
  1. No managing of transactions. It is the strong opinion of the authors that managing transactions is an application-level concern.
176
194
  1. No support for validations. Validations are not a persistence concern.
177
- 1. No AR-style callbacks. Use Infrastructure, Application, or Domain [services](http://martinfowler.com/bliki/EvansClassification.html) instead.
195
+ 1. No AR-style callbacks. Use [Infrastructure, Application, or Domain services](http://martinfowler.com/bliki/EvansClassification.html) instead.
178
196
  1. No has-many-through associations. Use two has-many associations to a join entity instead.
179
197
  1. The `id` attribute is reserved for database primary keys. If you have a natural key/id on your domain model, name it something that makes sense for your domain. It is the strong opinion of the authors that using natural keys as foreign keys is a bad idea. This mixes domain and persistence concerns.
180
198
 
@@ -183,13 +201,10 @@ It also does not do some things that you might expect from other ORMs:
183
201
  1. Only supports PostgreSQL.
184
202
 
185
203
  ## Future Enhancements
186
- * Aggregate updated_at.
187
- * Support for other DBMSs (no MySQL support until ids can be generated without inserting into a table!)
188
- * Support for other ORMs.
189
- * Value objects.
190
- * Remove dependency on ActiveRecord (optimistic locking? updated_at, created_at support? Data type conversions? TimeZone support?)
191
- * More efficient updates (use fewer queries.)
204
+ * Support for clients to set UUID-based ids.
192
205
  * Nicer DSL for specifying attributes that have different names in the domain model than in the DB.
206
+ * Aggregate updated_at.
207
+ * Better support for value objects.
193
208
 
194
209
  ## FAQ
195
210
 
@@ -205,11 +220,12 @@ It also does not do some things that you might expect from other ORMs:
205
220
 
206
221
  **A.** Create a method on a [Repository](http://martinfowler.com/eaaCatalog/repository.html)! They have full access to the DB/ORM so you can use [Arel](https://github.com/rails/arel) and go [crazy](http://asciicasts.com/episodes/239-activerecord-relation-walkthrough) or use direct SQL if you want.
207
222
 
208
- For example:
223
+ For example, use the [#query](https://rubydoc.info/github/nulogy/vorpal/main/Vorpal/AggregateMapper#query-instance_method) method on the [AggregateMapper](https://rubydoc.info/github/nulogy/vorpal/main/Vorpal/AggregateMapper) to access the underyling [ActiveRecordRelation](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html):
209
224
 
210
225
  ```ruby
211
- def find_all
212
- @mapper.query.load_all # use the mapper to load all the aggregates
226
+ def find_special_ones
227
+ # use `load_all` or `load_one` to convert from ActiveRecord objects to domain POROs.
228
+ @mapper.query.where(special: true).load_all
213
229
  end
214
230
  ```
215
231
 
@@ -231,19 +247,9 @@ For example:
231
247
 
232
248
  **A.** You can use [ActiveModel::Serialization](http://api.rubyonrails.org/classes/ActiveModel/Serialization.html) or [ActiveModel::Serializers](https://github.com/rails-api/active_model_serializers) but they are not heartily recommended. The former is too coupled to the model and the latter is too coupled to Rails controllers. Vorpal uses [SimpleSerializer](https://github.com/nulogy/simple_serializer) for this purpose.
233
249
 
234
- ## Running Tests
250
+ **Q.** Are `updated_at` and `created_at` supported?
235
251
 
236
- 1. Start a PostgreSQL server.
237
- 2. Either:
238
- * Create a DB user called `vorpal` with password `pass`. OR:
239
- * Modify `spec/helpers/db_helpers.rb`.
240
- 3. Run `rake` from the terminal.
241
-
242
- ## Contributors
243
-
244
- * [Sean Kirby](https://github.com/sskirby)
245
- * [Paul Sobocinski](https://github.com/psobocinski)
246
- * [Jason Cheong-Kee-You](https://github.com/jchunky)
252
+ **A.** Yes. If they exist on your database tables, they will behave exactly as if you were using vanilla ActiveRecord.
247
253
 
248
254
  ## Contributing
249
255
 
@@ -252,3 +258,35 @@ For example:
252
258
  3. Commit your changes (`git commit -am 'Add some feature'`)
253
259
  4. Push to the branch (`git push origin my-new-feature`)
254
260
  5. Create a new Pull Request
261
+
262
+ ## OSX Environment setup
263
+
264
+ 1. Install [Homebrew](https://brew.sh/)
265
+ 2. Install [rbenv](https://github.com/rbenv/rbenv#installation) ([RVM](https://rvm.io/) can work too)
266
+ 3. Install [DirEnv](https://direnv.net/docs/installation.html) (`brew install direnv`)
267
+ 4. Install Docker Desktop Community Edition (`brew cask install docker`)
268
+ 5. Start Docker Desktop Community Edition (`CMD+space docker ENTER`)
269
+ 6. Install Ruby (`rbenv install 2.7.0`)
270
+ 7. Install PostgreSQL (`brew install postgresql`)
271
+ 8. Clone the repo (`git clone git@github.com:nulogy/vorpal.git`) and `cd` to the project root.
272
+ 8. Copy the contents of `gemfiles/rails_<version>.gemfile.lock` into a `Gemfile.lock` file
273
+ at the root of the project. (`cp gemfiles/rails_6_0.gemfile.lock gemfile.lock`)
274
+ 9. `bundle`
275
+
276
+ ### Running Tests
277
+
278
+ 1. Start a PostgreSQL server using `docker-compose up`
279
+ 3. Run `rake` from the terminal to run all specs or `rspec <path to spec file>` to
280
+ run a single spec.
281
+
282
+ ### Running Tests for a specific version of Rails
283
+
284
+ 1. Start a PostgreSQL server using `docker-compose up`
285
+ 2. Run `appraisal rails-5-2 rake` from the terminal to run all specs or
286
+ `appraisal rails-5-2 rspec <path to spec file>` to run a single spec.
287
+
288
+ Please see the [Appraisal gem docs](https://github.com/thoughtbot/appraisal) for more information.
289
+
290
+ ## Contributors
291
+
292
+ See who's [contributed](https://github.com/nulogy/vorpal/graphs/contributors)!
@@ -84,6 +84,17 @@ module Vorpal
84
84
  @engine
85
85
  end
86
86
 
87
+ # Returns a 'Vorpal-aware' [ActiveRecord::Relation](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html)
88
+ # for the ActiveRecord object underlying the domain entity mapped by this mapper.
89
+ #
90
+ # This method allows you to easily access the power of ActiveRecord::Relation to do more complex
91
+ # queries in your repositories.
92
+ #
93
+ # The ActiveRecord::Relation is 'Vorpal-aware' because it has the {#load_one} and {#load_many} methods
94
+ # mixed in so that you can get the POROs from your domain model instead of the ActiveRecord
95
+ # objects normally returned by ActiveRecord::Relation.
96
+ #
97
+ # @return [ActiveRecord::Relation]
87
98
  def query
88
99
  @engine.query(@domain_class)
89
100
  end
@@ -0,0 +1,71 @@
1
+ require 'equalizer'
2
+
3
+ module Vorpal
4
+ module Config
5
+ # @private
6
+ class ClassConfig
7
+ include Equalizer.new(:domain_class, :db_class)
8
+ attr_reader :serializer, :deserializer, :domain_class, :db_class, :primary_key_type, :local_association_configs
9
+ attr_accessor :has_manys, :belongs_tos, :has_ones
10
+
11
+ ALLOWED_PRIMARY_KEY_TYPE_OPTIONS = [:serial, :uuid]
12
+
13
+ def initialize(attrs)
14
+ @has_manys = []
15
+ @belongs_tos = []
16
+ @has_ones = []
17
+ @local_association_configs = []
18
+
19
+ @serializer = attrs[:serializer]
20
+ @deserializer = attrs[:deserializer]
21
+ @domain_class = attrs[:domain_class]
22
+ @db_class = attrs[:db_class]
23
+ @primary_key_type = attrs[:primary_key_type]
24
+ raise "Invalid primary_key_type: '#{@primary_key_type}'" unless ALLOWED_PRIMARY_KEY_TYPE_OPTIONS.include?(@primary_key_type)
25
+ end
26
+
27
+ def build_db_object(attributes)
28
+ db_class.new(attributes)
29
+ end
30
+
31
+ def set_db_object_attributes(db_object, attributes)
32
+ db_object.attributes = attributes
33
+ end
34
+
35
+ def get_db_object_attributes(db_object)
36
+ symbolize_keys(db_object.attributes)
37
+ end
38
+
39
+ def serialization_required?
40
+ domain_class.superclass.name != 'ActiveRecord::Base'
41
+ end
42
+
43
+ def serialize(object)
44
+ serializer.serialize(object)
45
+ end
46
+
47
+ def deserialize(db_object)
48
+ attributes = get_db_object_attributes(db_object)
49
+ serialization_required? ? deserializer.deserialize(domain_class.new, attributes) : db_object
50
+ end
51
+
52
+ def set_attribute(db_object, attribute, value)
53
+ db_object.send("#{attribute}=", value)
54
+ end
55
+
56
+ def get_attribute(db_object, attribute)
57
+ db_object.send(attribute)
58
+ end
59
+
60
+ private
61
+
62
+ def symbolize_keys(hash)
63
+ result = {}
64
+ hash.each_key do |key|
65
+ result[key.to_sym] = hash[key]
66
+ end
67
+ result
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,13 +1,13 @@
1
1
  require 'vorpal/util/hash_initialization'
2
2
  require 'vorpal/exceptions'
3
+ require 'vorpal/config/class_config'
3
4
  require 'equalizer'
4
5
 
5
6
  module Vorpal
6
7
  # @private
7
- class MasterConfig
8
- def initialize(class_configs)
9
- @class_configs = class_configs
10
- initialize_association_configs
8
+ class MainConfig
9
+ def initialize
10
+ @class_configs = []
11
11
  end
12
12
 
13
13
  def config_for(clazz)
@@ -20,7 +20,9 @@ module Vorpal
20
20
  @class_configs.detect { |conf| conf.db_class == db_object.class }
21
21
  end
22
22
 
23
- private
23
+ def add_class_config(class_config)
24
+ @class_configs << class_config
25
+ end
24
26
 
25
27
  def initialize_association_configs
26
28
  association_configs = {}
@@ -50,6 +52,8 @@ module Vorpal
50
52
  end
51
53
  end
52
54
 
55
+ private
56
+
53
57
  def build_association_config(association_configs, local_config, fk, fk_type)
54
58
  association_config = AssociationConfig.new(local_config, fk, fk_type)
55
59
  if association_configs[association_config]
@@ -126,67 +130,6 @@ module Vorpal
126
130
  end
127
131
  end
128
132
 
129
- # @private
130
- class ClassConfig
131
- include Equalizer.new(:domain_class, :db_class)
132
- attr_reader :serializer, :deserializer, :domain_class, :db_class, :local_association_configs
133
- attr_accessor :has_manys, :belongs_tos, :has_ones
134
-
135
- def initialize(attrs)
136
- @has_manys = []
137
- @belongs_tos = []
138
- @has_ones = []
139
- @local_association_configs = []
140
-
141
- attrs.each do |k,v|
142
- instance_variable_set("@#{k}", v)
143
- end
144
- end
145
-
146
- def build_db_object(attributes)
147
- db_class.new(attributes)
148
- end
149
-
150
- def set_db_object_attributes(db_object, attributes)
151
- db_object.attributes = attributes
152
- end
153
-
154
- def get_db_object_attributes(db_object)
155
- symbolize_keys(db_object.attributes)
156
- end
157
-
158
- def serialization_required?
159
- domain_class.superclass.name != 'ActiveRecord::Base'
160
- end
161
-
162
- def serialize(object)
163
- serializer.serialize(object)
164
- end
165
-
166
- def deserialize(db_object)
167
- attributes = get_db_object_attributes(db_object)
168
- serialization_required? ? deserializer.deserialize(domain_class.new, attributes) : db_object
169
- end
170
-
171
- def set_attribute(db_object, attribute, value)
172
- db_object.send("#{attribute}=", value)
173
- end
174
-
175
- def get_attribute(db_object, attribute)
176
- db_object.send(attribute)
177
- end
178
-
179
- private
180
-
181
- def symbolize_keys(hash)
182
- result = {}
183
- hash.each_key do |key|
184
- result[key.to_sym] = hash[key]
185
- end
186
- result
187
- end
188
- end
189
-
190
133
  # @private
191
134
  class ForeignKeyInfo
192
135
  include Equalizer.new(:fk_column, :fk_type_column, :fk_type)
@@ -9,7 +9,12 @@ module Vorpal
9
9
  end
10
10
 
11
11
  def insert(db_class, db_objects)
12
- if defined? ActiveRecord::Import
12
+ if ActiveRecord::VERSION::MAJOR >= 6
13
+ return if db_objects.empty?
14
+
15
+ update_timestamps_on_create(db_class, db_objects)
16
+ db_class.insert_all!(db_objects.map(&:attributes))
17
+ elsif defined? ActiveRecord::Import
13
18
  db_class.import(db_objects, validate: false)
14
19
  else
15
20
  db_objects.each do |db_object|
@@ -19,13 +24,20 @@ module Vorpal
19
24
  end
20
25
 
21
26
  def update(db_class, db_objects)
22
- db_objects.each do |db_object|
23
- db_object.save!(validate: false)
27
+ if ActiveRecord::VERSION::MAJOR >= 6
28
+ return if db_objects.empty?
29
+
30
+ update_timestamps_on_update(db_class, db_objects)
31
+ db_class.upsert_all(db_objects.map(&:attributes))
32
+ else
33
+ db_objects.each do |db_object|
34
+ db_object.save!(validate: false)
35
+ end
24
36
  end
25
37
  end
26
38
 
27
39
  def destroy(db_class, ids)
28
- db_class.delete_all(id: ids)
40
+ db_class.where(id: ids).delete_all
29
41
  end
30
42
 
31
43
  # Loads instances of the given class by primary key.
@@ -100,6 +112,30 @@ module Vorpal
100
112
 
101
113
  private
102
114
 
115
+ # Adapted from https://github.com/rails/rails/blob/614580270d7789e5275defc3da020ce27b3b2302/activerecord/lib/active_record/timestamp.rb#L99
116
+ def update_timestamps_on_create(db_class, db_objects)
117
+ return unless db_class.record_timestamps
118
+
119
+ current_time = db_class.current_time_from_proper_timezone
120
+ db_objects.each do |db_object|
121
+ db_class.all_timestamp_attributes_in_model.each do |column|
122
+ db_object.write_attribute(column, current_time) unless db_object.read_attribute(column)
123
+ end
124
+ end
125
+ end
126
+
127
+ #Adapted from https://github.com/rails/rails/blob/614580270d7789e5275defc3da020ce27b3b2302/activerecord/lib/active_record/timestamp.rb#L111
128
+ def update_timestamps_on_update(db_class, db_objects)
129
+ return unless db_class.record_timestamps
130
+
131
+ current_time = db_class.current_time_from_proper_timezone
132
+ db_objects.each do |db_object|
133
+ db_class.timestamp_attributes_for_update_in_model.each do |column|
134
+ db_object.write_attribute(column, current_time)
135
+ end
136
+ end
137
+ end
138
+
103
139
  def sequence_name(db_class)
104
140
  @sequence_names[db_class] ||= execute(
105
141
  "SELECT substring(column_default from '''(.*)''') FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = $1 AND column_name = 'id' LIMIT 1",