horza 0.3.9 → 0.4.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
  SHA1:
3
- metadata.gz: 05dec4f4ed4ea42ae1721a8b519496ec70f2a803
4
- data.tar.gz: 3fbd9ab81b756c4e4666a5334d7dda52b44a6fca
3
+ metadata.gz: 85bbbb02cee6bd4f2fe40c56bd9c37d16307b22f
4
+ data.tar.gz: 0398edae9ed16b2bb8d13650b1869741e8213a2c
5
5
  SHA512:
6
- metadata.gz: 3afecde1421ecf764dfb2b8ebe1a3f7450526632488fc052376e46c1f0f35d7b3fd8dec25f2465bd3a1db3f65dd3dbef2041064ef7f53c59f03e0f7d88b13179
7
- data.tar.gz: 42e7183c12b8733dd2db997ebccb2df9741c3ef5e5dbf5b0f57c7ef01c1f33684b280698594e52ae5c53b57c41c14c30ad8831678474cf8d7e3b8ee958728640
6
+ metadata.gz: f890a99acceefb96521d07a8b0ef41a290438f4672f917bfefd68cc0085379de73dd050de8eff44b442bb0f91daa7e4a39f409893200bc2b2dd4d82dd4560347
7
+ data.tar.gz: 98f821f258db1e72ce67439eb7aff920098a5c941954cb5fc8192ff491b68a7b12bd8247ce3aee336db9c14ed405a22ccb17b8e96d25195f70f58e767820aa59
data/README.md CHANGED
@@ -16,7 +16,8 @@ end
16
16
  **Get Adapter for your ORM Object**
17
17
  ```ruby
18
18
  # ActiveRecord Example
19
- user = Horza.adapt(User)
19
+ # Don't worry, We don't actually call things horza_users in our codebase, this is just for emphasis
20
+ horza_user = Horza.adapt(User)
20
21
 
21
22
  # Examples
22
23
  user.get(id) # Find by id - Return nil on fail
@@ -59,29 +60,48 @@ user.find_all(conditions: conditions, offset: 50)
59
60
 
60
61
  # Eager loading associations
61
62
  employer.association(target: :users, eager_load: true)
62
- ```
63
-
64
- ## Options
65
-
66
- **Base Options**
67
63
 
68
- Key | Type | Details
69
- --- | ---- | -------
70
- `conditions` | Hash | Key value pairs for the query
71
- `order` | Hash | { `field` => `:asc`/`:desc` }
72
- `limit` | Integer | Number of records to return
73
- `offset` | Integer | Number of records to offset
74
- `id` | Integer | The id of the root object (associations only)
75
- `target` | Symbol | The target of the association - ie. employer.users would have a target of :users (associations only)
76
- `eager_load` | Boolean | Whether to eager_load the association (associations only)
77
-
78
- **Association Options**
79
-
80
- Key | Type | Details
81
- --- | ---- | -------
82
- `id` | Integer | The id of the root object
83
- `target` | Symbol | The target of the association - ie. employer.users would have a target of :users
84
- `eager_load` | Boolean | Whether to eager_load the association
64
+ # Joins are slightly more complex
65
+ join_params = {
66
+ with: :employers,
67
+ on: { employer_id: :id }, # field for adapted model => field for join model
68
+ fields: {
69
+ users: [:first_name, :last_name, :email],
70
+ employers: [:company_name, :address, :phone],
71
+ },
72
+ conditions: {
73
+ users: { last_name: 'Turner' },
74
+ employers: { company_name: 'Corporation ltd.' }
75
+ },
76
+ limit: 20,
77
+ offset: 5,
78
+ }
79
+ horza_user.join(join_params)
80
+
81
+ # You can join on multiple fields by passing an array
82
+ join_params = {
83
+ with: :employers,
84
+ on: [
85
+ { employer_id: :id }, # field for adapted model => field for join model
86
+ { email: :email }, # field for adapted model => field for join model
87
+ ]
88
+ }
89
+ horza_user.join(join_params)
90
+
91
+ # You can also alias field names
92
+ join_params = {
93
+ with: :employers,
94
+ on: [
95
+ { employer_id: :id }, # field for adapted model => field for join model
96
+ { email: :email }, # field for adapted model => field for join model
97
+ ],
98
+ fields: {
99
+ users: [:id, :last_name, :email],
100
+ employers: [{id: :employer_id}], # Fieldname in db => alias for output
101
+ },
102
+ }
103
+ horza_user.join(join_params)
104
+ ```
85
105
 
86
106
  ## Outputs
87
107
 
@@ -91,7 +111,7 @@ Collection entities behave like arrays.
91
111
 
92
112
  ```ruby
93
113
  # Singular Entity
94
- result = user.find_first(first_name: 'Blake')
114
+ result = horza_user.find_first(first_name: 'Blake')
95
115
 
96
116
  result # => {"id"=>1, "first_name"=>"Blake", "last_name"=>"Turner", "employer_id"=>1}
97
117
  result.class.name # => "Horza::Entities::Single"
@@ -100,7 +120,7 @@ result.id # => 1
100
120
  result.id? # => true
101
121
 
102
122
  # Collection Entity
103
- result = user.find_all(last_name: 'Turner')
123
+ result = horza_user.find_all(last_name: 'Turner')
104
124
 
105
125
  result.class.name # => "Horza::Entities::Collection"
106
126
  result.length # => 1
@@ -2,7 +2,8 @@ require 'horza/adapters/class_methods'
2
2
  require 'horza/adapters/instance_methods'
3
3
  require 'horza/adapters/options'
4
4
  require 'horza/adapters/abstract_adapter'
5
- require 'horza/adapters/active_record'
5
+ require 'horza/adapters/active_record/active_record'
6
+ require 'horza/adapters/active_record/arel_join'
6
7
  require 'horza/core_extensions/string'
7
8
  require 'horza/entities/single'
8
9
  require 'horza/entities/collection'
@@ -29,6 +29,10 @@ module Horza
29
29
  not_implemented_error
30
30
  end
31
31
 
32
+ def join(options = {})
33
+ not_implemented_error
34
+ end
35
+
32
36
  def create!(options = {})
33
37
  not_implemented_error
34
38
  end
@@ -32,6 +32,13 @@ module Horza
32
32
  run_and_convert_exceptions { entity_class(query(options)) }
33
33
  end
34
34
 
35
+ def join(options = {})
36
+ run_and_convert_exceptions do
37
+ sql = ArelJoin.sql(self.context, options)
38
+ entity_class(::ActiveRecord::Base.connection.exec_query(sql).to_a)
39
+ end
40
+ end
41
+
35
42
  def create!(options = {})
36
43
  run_and_convert_exceptions do
37
44
  record = @context.new(options)
@@ -0,0 +1,80 @@
1
+ module Horza
2
+ module Adapters
3
+ class ArelJoin < Options
4
+
5
+ class << self
6
+ def sql(context, options)
7
+ new(context, options).query.to_sql
8
+ end
9
+ end
10
+
11
+ def initialize(context, options)
12
+ @options = options
13
+
14
+ @base_table_key = context.table_name.to_sym
15
+ @join_table_key = options[:with]
16
+
17
+ @base_table = Arel::Table.new(@base_table_key)
18
+ @join_table = Arel::Table.new(@join_table_key)
19
+ end
20
+
21
+ def query
22
+ join = @base_table.project(fields).join(@join_table).on(predicates)
23
+ join = join.where(where_clause(@base_table, @base_table_key)) if conditions_for_table?(@base_table_key)
24
+ join = join.where(where_clause(@join_table, @join_table_key)) if conditions_for_table?(@join_table_key)
25
+ join = join.take(@options[:limit]) if @options[:limit]
26
+ join = join.skip(@options[:offset]) if @options[:offset]
27
+ join
28
+ end
29
+
30
+ private
31
+
32
+ def fields
33
+ return [@base_table[:id], Arel.star] unless @options[:fields] && @options[:fields].present?
34
+
35
+ @options[:fields].map do |table, fields|
36
+ fields.map do |field|
37
+ case table
38
+ when @base_table_key
39
+ alias_field(@base_table, field)
40
+ when @join_table_key
41
+ alias_field(@join_table, field)
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def alias_field(table, field)
48
+ return table[field.to_s] unless field.is_a?(Hash)
49
+ table[field.keys.first.to_s].as(field.values.first.to_s)
50
+ end
51
+
52
+ def predicates
53
+ @options[:on] = [@options[:on]] unless @options[:on].is_a?(Array)
54
+
55
+ predicate_list = @options[:on].map { |on| predicate(on) }.flatten
56
+ chain_with_method(predicate_list, :and)
57
+ end
58
+
59
+ def predicate(on)
60
+ on.map { |base_field, join_field| @base_table[base_field].eq(@join_table[join_field]) }
61
+ end
62
+
63
+ def conditions_for_table?(table_key)
64
+ @options[:conditions].present? && @options[:conditions][table_key].present?
65
+ end
66
+
67
+ def where_clause(table, table_key)
68
+ clauses = @options[:conditions][table_key].map { |key, value| table[key].eq(value) }
69
+ chain_with_method(clauses, :and)
70
+ end
71
+
72
+ def chain_with_method(statements, method)
73
+ statements.reduce(nil) do |chained, statement|
74
+ next statement if chained.nil?
75
+ chained.send(method, statement)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -1,8 +1,8 @@
1
1
  module Horza
2
2
  module Entities
3
3
  class Collection
4
- def initialize(collection)
5
- @collection = collection
4
+ def initialize(collection, singular_entity = nil)
5
+ @collection, @singular_entity = collection, singular_entity
6
6
  end
7
7
 
8
8
  def [](index)
@@ -28,8 +28,8 @@ module Horza
28
28
  end
29
29
 
30
30
  def singular_entity(record)
31
- adapter = Horza.adapter.new(record)
32
- singular_entity_class(record).new(adapter.to_hash)
31
+ attributes = record.respond_to?(:to_hash) ? record.to_hash : Horza.adapter.new(record).to_hash
32
+ singular_entity_class(record).new(attributes)
33
33
  end
34
34
 
35
35
  # Collection classes have the form Horza::Entities::Users
@@ -7,8 +7,8 @@ else
7
7
 
8
8
  ActiveRecord::Migration.suppress_messages do
9
9
  ActiveRecord::Schema.define(:version => 0) do
10
- create_table(:employers, force: true) {|t| t.string :name }
11
- create_table(:users, force: true) {|t| t.string :first_name; t.string :last_name; t.references :employer; }
10
+ create_table(:employers, force: true) {|t| t.string :name; t.string :boss_email }
11
+ create_table(:users, force: true) {|t| t.string :first_name; t.string :last_name; t.string :email; t.references :employer; }
12
12
  create_table(:customers, force: true) {|t| t.string :first_name; t.string :last_name; }
13
13
  create_table(:sports_cars, force: true) {|t| t.string :make; t.references :employer; }
14
14
  create_table(:dummy_models, force: true) {|t| t.string :key }
@@ -65,6 +65,8 @@ describe Horza do
65
65
  after do
66
66
  HorzaSpec::User.delete_all
67
67
  HorzaSpec::Employer.delete_all
68
+ HorzaSpec::Customer.delete_all
69
+ HorzaSpec::SportsCar.delete_all
68
70
  end
69
71
 
70
72
  context '#context_for_entity' do
@@ -515,6 +517,156 @@ describe Horza do
515
517
  end
516
518
  end
517
519
  end
520
+
521
+ context '#join' do
522
+ let(:simple) do
523
+ {
524
+ with: :employers,
525
+ on: { employer_id: :id } # field for adapted model => field for join model
526
+ }
527
+ end
528
+
529
+ let(:complex_predicate) do
530
+ {
531
+ with: :employers,
532
+ on: [
533
+ { employer_id: :id }, # field for adapted model => field for join model
534
+ { email: :boss_email }, # field for adapted model => field for join model
535
+ ],
536
+ fields: {
537
+ users: [:id, :email],
538
+ employers: [:boss_email]
539
+ }
540
+ }
541
+ end
542
+
543
+ let(:fields) do
544
+ {
545
+ fields: {
546
+ users: [:id, :first_name, :last_name],
547
+ employers: [{id: :employer_id}, :name]
548
+ }
549
+ }.merge(simple)
550
+ end
551
+
552
+ let(:conditions) do
553
+ {
554
+ conditions: {
555
+ users: { last_name: 'Turner' },
556
+ employers: { name: 'Corporation ltd.' }
557
+ }
558
+ }.merge(fields)
559
+ end
560
+
561
+ context 'without conditions' do
562
+ context 'when one join record exists' do
563
+ let!(:employer) { HorzaSpec::Employer.create(name: 'Corporation ltd.', boss_email: 'boss@boss.com') }
564
+ let!(:user) { HorzaSpec::User.create(employer: employer, first_name: 'John', last_name: 'Turner', email: 'email@email.com') }
565
+
566
+ context 'without fields' do
567
+ it 'returns joined record' do
568
+ result = user_adapter.join(simple)
569
+ expect(result.length).to eq 1
570
+
571
+ expect(result.first.first_name).to eq user.first_name
572
+ expect(result.first.last_name).to eq user.last_name
573
+ expect(result.first.email).to eq user.email
574
+ expect(result.first.boss_email).to eq employer.boss_email
575
+ expect(result.first.name).to eq employer.name
576
+ end
577
+ end
578
+
579
+ context 'with fields' do
580
+ it 'returns joined record and the specified fields' do
581
+ result = user_adapter.join(fields)
582
+ expect(result.length).to eq 1
583
+
584
+ record = result.first
585
+
586
+ expect(record.id).to eq user.id
587
+ expect(record.first_name).to eq user.first_name
588
+ expect(record.last_name).to eq user.last_name
589
+ expect(record.name).to eq employer.name
590
+ expect(record.employer_id).to eq employer.id
591
+
592
+ expect(record.respond_to?(:email)).to be false
593
+ end
594
+ end
595
+
596
+ context 'complex predicate' do
597
+ let!(:match_user) { HorzaSpec::User.create(employer: employer, first_name: 'John', last_name: 'Turner', email: 'boss@boss.com') }
598
+ let!(:match_user2) { HorzaSpec::User.create(employer: employer, first_name: 'Helen', last_name: 'Jones', email: 'boss@boss.com') }
599
+ it 'returns joined records and the specified fields' do
600
+ result = user_adapter.join(complex_predicate)
601
+ expect(result.length).to eq 2
602
+
603
+ expect(result.first.id).to eq match_user.id
604
+ expect(result.first.email).to eq match_user.email
605
+ expect(result.first.boss_email).to eq employer.boss_email
606
+ end
607
+
608
+ end
609
+ end
610
+
611
+ context 'when no join record exists' do
612
+ let!(:employer) { HorzaSpec::Employer.create(name: 'Corporation ltd.') }
613
+ let!(:user) { HorzaSpec::User.create(employer_id: 9999, first_name: 'John', last_name: 'Turner', email: 'email@email.com') }
614
+
615
+ it 'returns an empty collection' do
616
+ result = user_adapter.join(simple)
617
+ expect(result.length).to eq 0
618
+ end
619
+ end
620
+
621
+ context 'when multiple join records exists' do
622
+ let!(:employer) { HorzaSpec::Employer.create(name: 'Corporation ltd.') }
623
+ let!(:user) { HorzaSpec::User.create(employer: employer, first_name: 'John', last_name: 'Turner', email: 'email@turner.com') }
624
+ let!(:user2) { HorzaSpec::User.create(employer: employer, first_name: 'Adam', last_name: 'Boots', email: 'email@boots.com') }
625
+ let!(:user3) { HorzaSpec::User.create(employer: employer, first_name: 'Tim', last_name: 'Socks', email: 'email@socks.com') }
626
+
627
+ it 'returns an empty collection' do
628
+ result = user_adapter.join(simple)
629
+ expect(result.length).to eq 3
630
+ end
631
+ end
632
+ end
633
+
634
+ context 'with conditions' do
635
+ let!(:employer) { HorzaSpec::Employer.create(name: 'Corporation ltd.') }
636
+ let!(:employer2) { HorzaSpec::Employer.create(name: 'BigBucks ltd.') }
637
+ let!(:user) { HorzaSpec::User.create(employer: employer, first_name: 'John', last_name: 'Turner', email: 'email@turner.com') }
638
+ let!(:user2) { HorzaSpec::User.create(employer: employer, first_name: 'Adam', last_name: 'Boots', email: 'email@boots.com') }
639
+ let!(:user3) { HorzaSpec::User.create(employer: employer2, first_name: 'Tim', last_name: 'Socks', email: 'email@socks.com') }
640
+
641
+ it 'returns only the joins that match the conditions' do
642
+ result = user_adapter.join(conditions)
643
+ expect(result.length).to eq 1
644
+ expect(result.first.id).to eq user.id
645
+ end
646
+ end
647
+
648
+ context 'limits/offset' do
649
+ let!(:employer) { HorzaSpec::Employer.create(name: 'Corporation ltd.') }
650
+ let!(:employer2) { HorzaSpec::Employer.create(name: 'BigBucks ltd.') }
651
+ let!(:user) { HorzaSpec::User.create(employer: employer, first_name: 'John', last_name: 'Turner', email: 'email@turner.com') }
652
+ let!(:user2) { HorzaSpec::User.create(employer: employer, first_name: 'Adam', last_name: 'Turner', email: 'email@boots.com') }
653
+ let!(:user3) { HorzaSpec::User.create(employer: employer2, first_name: 'Tim', last_name: 'Socks', email: 'email@socks.com') }
654
+
655
+ it 'limits the joins that match the conditions' do
656
+ params = conditions.merge(limit: 1)
657
+ result = user_adapter.join(params)
658
+ expect(result.length).to eq 1
659
+ expect(result.first.id).to eq user.id
660
+ end
661
+
662
+ it 'offsets the joins that match the conditions' do
663
+ params = conditions.merge(offset: 1)
664
+ result = user_adapter.join(params)
665
+ expect(result.length).to eq 1
666
+ expect(result.first.id).to eq user2.id
667
+ end
668
+ end
669
+ end
518
670
  end
519
671
 
520
672
  describe 'Entities' do
@@ -76,7 +76,7 @@ describe Horza::Adapters::Options do
76
76
  offset: 10,
77
77
  target: :sports_cars,
78
78
  via: [:employer],
79
- eager_load: true
79
+ eager_load: true,
80
80
  }
81
81
  end
82
82
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: horza
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.9
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Blake Turner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-07-01 00:00:00.000000000 Z
11
+ date: 2015-07-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hashie
@@ -105,7 +105,8 @@ files:
105
105
  - README.md
106
106
  - lib/horza.rb
107
107
  - lib/horza/adapters/abstract_adapter.rb
108
- - lib/horza/adapters/active_record.rb
108
+ - lib/horza/adapters/active_record/active_record.rb
109
+ - lib/horza/adapters/active_record/arel_join.rb
109
110
  - lib/horza/adapters/class_methods.rb
110
111
  - lib/horza/adapters/instance_methods.rb
111
112
  - lib/horza/adapters/options.rb