dbee 1.0.1 → 1.0.2

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: bc7bf9701c14a96bfe5c83893637a1e10545e70f268c674851279d150b57bcc4
4
- data.tar.gz: 1fbdb14e73fe98629a2cf41db0906b543eb33d16e044d5c8552916c76d4e9e8d
3
+ metadata.gz: 3e07996f0622fbf3eb19b8fa7bdd6ebfe963d3edc18745ccdf89f2a87f7a16dd
4
+ data.tar.gz: 0e25d666b9cd0a099741220caf423a670e832149bac26f27283c7a43987c14c5
5
5
  SHA512:
6
- metadata.gz: 6ecaedc494e15776f1716de8891dd528fcbbbb76e44d6fe4d3056ab70674ede17f029f5316c171680c9265074624de8d78cf8856957fa5242371c26e41297089
7
- data.tar.gz: e091625f6c6281673c48df689a1ec3ec692898799f9ef76d28991c90c519526bcd3c09d19d0cd1ce72a1df35bc98b5f81b46b01ec25934b4aa7ea86635549d1e
6
+ metadata.gz: 5290d4c90695c68b7d558c35cf4ca47cd24d60e50fc776ce2c4138a74d469e86bc9021d66a9645ddcb0e25924cad80d1dca239df610a1c01a41c02523159062e
7
+ data.tar.gz: b9023ad0e9a82db914a0c0c40b918fd52a87572718aadc31e82eb1ba96adbd7a8a50932d9d1968b72d73de9689f26d4ca71374e4a7c2fcba416fd5f4e992c696
data/CHANGELOG.md CHANGED
@@ -1,8 +1,19 @@
1
+ # 1.0.2 (August 26th, 2019)
2
+
3
+ Fixes:
4
+
5
+ * Dbee::Base subclasses can now support cycles to N-depth, where N is limited by the Query. The recursion will go as far as the Query specifies it has to go.
6
+
7
+ Additions:
8
+
9
+ * Equals filter is now the default type when type is omitted.
10
+ * model can now be a string so it can be lazy evaluated at run-time instead of at script evaluation time.
11
+
1
12
  # 1.0.1 (August 26th, 2019)
2
13
 
3
14
  Fixes:
4
15
 
5
- * Dbee::Base sub-classes can now declare self-referential associations. Note that it will stop the hierarchy once the cycle is detected.
16
+ * Dbee::Base subclasses can now declare self-referential associations. Note that it will stop the hierarchy once the cycle is detected.
6
17
 
7
18
  Additions:
8
19
 
data/lib/dbee/base.rb CHANGED
@@ -24,11 +24,23 @@ module Dbee
24
24
  self
25
25
  end
26
26
 
27
- def to_model(name = nil, constraints = [], from = nil)
28
- name = derive_name(name)
29
- key = [name, constraints, from]
30
-
31
- to_models[key] ||= Model.make(model_config(name, constraints, from))
27
+ # This method is cycle-resistant due to the fact that it is a requirement to send in a
28
+ # key_chain. That means each model produced using to_model is specific to a set of desired
29
+ # fields. Basically, you cannot derive a Model from a Base subclass without the context
30
+ # of a Query. This is not true for configuration-first Model definitions because, in that
31
+ # case, cycles do not exist since the nature of the configuration is flat.
32
+ def to_model(key_chain, name = nil, constraints = [], path_parts = [])
33
+ derived_name = derive_name(name)
34
+ key = [key_chain, derived_name, constraints, path_parts]
35
+
36
+ to_models[key] ||= Model.make(
37
+ model_config(
38
+ key_chain,
39
+ derived_name,
40
+ constraints,
41
+ path_parts + [name]
42
+ )
43
+ )
32
44
  end
33
45
 
34
46
  def table_name
@@ -63,10 +75,10 @@ module Dbee
63
75
  subclasses.reverse
64
76
  end
65
77
 
66
- def model_config(name, constraints, from)
78
+ def model_config(key_chain, name, constraints, path_parts)
67
79
  {
68
80
  constraints: constraints,
69
- models: associations(from),
81
+ models: associations(key_chain, path_parts),
70
82
  name: name,
71
83
  table: derive_table
72
84
  }
@@ -82,15 +94,15 @@ module Dbee
82
94
  inherited_table.empty? ? inflected_name : inherited_table
83
95
  end
84
96
 
85
- def associations(from)
97
+ def associations(key_chain, path_parts)
86
98
  inherited_associations_by_name.values
87
- .reject { |c| c[:name].to_s == from.to_s }
99
+ .select { |c| key_chain.ancestor_path?(path_parts, c[:name]) }
88
100
  .each_with_object([]) do |config, memo|
89
- model_klass = config[:model]
101
+ model_constant = constantize(config[:model])
90
102
  associated_constraints = config[:constraints]
91
103
  name = config[:name]
92
104
 
93
- memo << model_klass.to_model(name, associated_constraints, name)
105
+ memo << model_constant.to_model(key_chain, name, associated_constraints, path_parts)
94
106
  end
95
107
  end
96
108
 
@@ -106,6 +118,10 @@ module Dbee
106
118
  .tr('-', '_')
107
119
  .downcase
108
120
  end
121
+
122
+ def constantize(value)
123
+ value.is_a?(String) || value.is_a?(Symbol) ? Object.const_get(value) : value
124
+ end
109
125
  end
110
126
  end
111
127
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require_relative 'key_path'
11
+
12
+ module Dbee
13
+ # A KeyChain is a collection of KeyPath objects. It knows how to deal with aggregate methods,
14
+ # such as equality of a set of KeyPath objects and finding an ancestor path in all the
15
+ # KeyPath objects' ancestor paths. You can pass in either KeyPath instances or strings,
16
+ # which will be coerced to KeyPath objects. Duplicates will also be removed.
17
+ class KeyChain
18
+ attr_reader :key_path_set, :ancestor_path_set
19
+
20
+ def initialize(key_paths = [])
21
+ @key_path_set = key_paths.map { |k| KeyPath.get(k) }.to_set
22
+ @ancestor_path_set = @key_path_set.map(&:ancestor_paths).flatten.to_set
23
+
24
+ freeze
25
+ end
26
+
27
+ def hash
28
+ key_path_set.hash
29
+ end
30
+
31
+ def ==(other)
32
+ key_path_set == other.key_path_set
33
+ end
34
+ alias eql? ==
35
+
36
+ def ancestor_path?(*parts)
37
+ path = parts.flatten.compact.join(KeyPath::SPLIT_CHAR)
38
+
39
+ ancestor_path_set.include?(path)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Dbee
11
+ # This class represents a relative path from a model to a column. For example:
12
+ # Say we have a model called "users" which is represented by a "users" table.
13
+ # The "users" table also has a one-to-many relationship with a "phone_numbers" table, which
14
+ # is modeled as a nested model under "users" as "phone_numbers". Then, to get the column:
15
+ # "area_code", you would use: "phone_numbers.area_code".
16
+ # Say the column "name" is located on "users", you could use the key path: "name".
17
+ # This also works for deeper nested columns in the same fashion.
18
+ class KeyPath
19
+ extend Forwardable
20
+
21
+ class << self
22
+ def get(obj)
23
+ obj.is_a?(self.class) ? obj : new(obj)
24
+ end
25
+ end
26
+
27
+ SPLIT_CHAR = '.'
28
+
29
+ attr_reader :value, :ancestor_names, :column_name
30
+
31
+ def_delegators :value, :to_s
32
+
33
+ def initialize(value)
34
+ raise 'Value is required' if value.to_s.empty?
35
+
36
+ @value = value.to_s
37
+ @ancestor_names = value.to_s.split(SPLIT_CHAR)
38
+ @column_name = @ancestor_names.pop
39
+
40
+ freeze
41
+ end
42
+
43
+ def hash
44
+ value.hash
45
+ end
46
+
47
+ def ==(other)
48
+ other.to_s == to_s
49
+ end
50
+ alias eql? ==
51
+
52
+ def ancestor_paths
53
+ ancestor_names.each_with_object([]) do |ancestor, memo|
54
+ memo << [memo.last, ancestor].compact.join(SPLIT_CHAR)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -23,7 +23,7 @@ module Dbee
23
23
  end
24
24
 
25
25
  def <=>(other)
26
- other.name <=> name
26
+ name <=> other.name
27
27
  end
28
28
 
29
29
  def hash
data/lib/dbee/model.rb CHANGED
@@ -21,7 +21,7 @@ module Dbee
21
21
 
22
22
  class ModelNotFoundError < StandardError; end
23
23
 
24
- attr_reader :constraints, :name
24
+ attr_reader :constraints, :name, :table
25
25
 
26
26
  def initialize(name:, constraints: [], models: [], table: '')
27
27
  raise ArgumentError, 'name is required' if name.to_s.empty?
@@ -29,7 +29,7 @@ module Dbee
29
29
  @name = name.to_s
30
30
  @constraints = Constraints.array(constraints)
31
31
  @models_by_name = name_hash(Model.array(models))
32
- @table = table.to_s
32
+ @table = table.to_s.empty? ? @name : table.to_s
33
33
 
34
34
  freeze
35
35
  end
@@ -38,10 +38,6 @@ module Dbee
38
38
  array.map { |a| [a.name, a] }.to_h
39
39
  end
40
40
 
41
- def table
42
- @table.to_s.empty? ? name : @table
43
- end
44
-
45
41
  def models
46
42
  models_by_name.values
47
43
  end
@@ -75,7 +71,7 @@ module Dbee
75
71
  alias eql? ==
76
72
 
77
73
  def <=>(other)
78
- other.name <=> name
74
+ name <=> other.name
79
75
  end
80
76
 
81
77
  private
@@ -7,8 +7,6 @@
7
7
  # LICENSE file in the root directory of this source tree.
8
8
  #
9
9
 
10
- require_relative 'key_path'
11
-
12
10
  module Dbee
13
11
  class Query
14
12
  # This class is an abstraction of the SELECT part of a SQL statement.
@@ -36,6 +34,10 @@ module Dbee
36
34
  other.key_path == key_path && other.display == display
37
35
  end
38
36
  alias eql? ==
37
+
38
+ def <=>(other)
39
+ "#{key_path}#{display}" <=> "#{other.key_path}#{other.display}"
40
+ end
39
41
  end
40
42
  end
41
43
  end
@@ -33,6 +33,10 @@ module Dbee
33
33
  other.key_path == key_path && other.value == value
34
34
  end
35
35
  alias eql? ==
36
+
37
+ def <=>(other)
38
+ "#{key_path}#{value}" <=> "#{other.key_path}#{other.value}"
39
+ end
36
40
  end
37
41
  end
38
42
  end
@@ -25,6 +25,7 @@ module Dbee
25
25
  class Filters
26
26
  acts_as_hashable_factory
27
27
 
28
+ register '', Equals # Default if type is blank.
28
29
  register 'contains', Contains
29
30
  register 'equals', Equals
30
31
  register 'greater_than_or_equal_to', GreaterThanOrEqualTo
@@ -7,8 +7,6 @@
7
7
  # LICENSE file in the root directory of this source tree.
8
8
  #
9
9
 
10
- require_relative 'key_path'
11
-
12
10
  module Dbee
13
11
  class Query
14
12
  # Abstract representation of the ORDER BY part of a SQL statement.
@@ -48,6 +46,10 @@ module Dbee
48
46
  other.key_path == key_path && other.direction == direction
49
47
  end
50
48
  alias eql? ==
49
+
50
+ def <=>(other)
51
+ "#{key_path}#{direction}" <=> "#{other.key_path}#{other.direction}"
52
+ end
51
53
  end
52
54
  end
53
55
  end
data/lib/dbee/query.rb CHANGED
@@ -40,11 +40,21 @@ module Dbee
40
40
  end
41
41
 
42
42
  def ==(other)
43
- other.fields == fields &&
44
- other.filters == filters &&
43
+ other.fields.sort == fields.sort &&
44
+ other.filters.sort == filters.sort &&
45
45
  other.limit == limit &&
46
- other.sorters == sorters
46
+ other.sorters.sort == sorters.sort
47
47
  end
48
48
  alias eql? ==
49
+
50
+ def key_chain
51
+ KeyChain.new(key_paths)
52
+ end
53
+
54
+ private
55
+
56
+ def key_paths
57
+ (fields.map(&:key_path) + filters.map(&:key_path) + sorters.map(&:key_path))
58
+ end
49
59
  end
50
60
  end
data/lib/dbee/version.rb CHANGED
@@ -8,5 +8,5 @@
8
8
  #
9
9
 
10
10
  module Dbee
11
- VERSION = '1.0.1'
11
+ VERSION = '1.0.2'
12
12
  end
data/lib/dbee.rb CHANGED
@@ -11,6 +11,8 @@ require 'acts_as_hashable'
11
11
  require 'forwardable'
12
12
 
13
13
  require_relative 'dbee/base'
14
+ require_relative 'dbee/key_chain'
15
+ require_relative 'dbee/key_path'
14
16
  require_relative 'dbee/model'
15
17
  require_relative 'dbee/query'
16
18
  require_relative 'dbee/providers'
@@ -19,8 +21,13 @@ require_relative 'dbee/providers'
19
21
  module Dbee
20
22
  class << self
21
23
  def sql(model, query, provider)
22
- model = model.is_a?(Hash) || model.is_a?(Model) ? Model.make(model) : model.to_model
23
24
  query = Query.make(query)
25
+ model =
26
+ if model.is_a?(Hash) || model.is_a?(Model)
27
+ Model.make(model)
28
+ else
29
+ model.to_model(query.key_chain)
30
+ end
24
31
 
25
32
  provider.sql(model, query)
26
33
  end
@@ -11,13 +11,53 @@ require 'spec_helper'
11
11
  require 'fixtures/models'
12
12
 
13
13
  describe Dbee::Base do
14
- it 'compiles to Model instance correctly' do
15
- model_name = 'Theaters, Members, and Movies'
16
- expected_config = yaml_fixture('models.yaml')[model_name]
14
+ describe '#to_model' do
15
+ it 'compiles correctly' do
16
+ model_name = 'Theaters, Members, and Movies'
17
+ expected_config = yaml_fixture('models.yaml')[model_name]
17
18
 
18
- expected_model = Dbee::Model.make(expected_config)
19
+ expected_model = Dbee::Model.make(expected_config)
19
20
 
20
- expect(Models::Theaters.to_model).to eq(expected_model)
21
+ key_paths = %w[
22
+ members.demos.phone_numbers.a
23
+ members.movies.b
24
+ members.favorite_comic_movies.c
25
+ members.favorite_mystery_movies.d
26
+ members.favorite_comedy_movies.e
27
+ parent_theater.members.demos.phone_numbers.f
28
+ parent_theater.members.movies.g
29
+ parent_theater.members.favorite_comic_movies.h
30
+ parent_theater.members.favorite_mystery_movies.i
31
+ parent_theater.members.favorite_comedy_movies.j
32
+ ]
33
+
34
+ key_chain = Dbee::KeyChain.new(key_paths)
35
+
36
+ actual_model = Models::Theaters.to_model(key_chain)
37
+
38
+ expect(actual_model).to eq(expected_model)
39
+ end
40
+
41
+ it 'honors key_chain to flatten cyclic references' do
42
+ model_name = 'Cycle Example'
43
+ expected_config = yaml_fixture('models.yaml')[model_name]
44
+
45
+ expected_model = Dbee::Model.make(expected_config)
46
+
47
+ key_paths = %w[
48
+ b1.c.a.z
49
+ b1.d.a.y
50
+ b2.c.a.x
51
+ b2.d.a.w
52
+ b2.d.a.b1.c.z
53
+ ]
54
+
55
+ key_chain = Dbee::KeyChain.new(key_paths)
56
+
57
+ actual_model = Cycles::A.to_model(key_chain)
58
+
59
+ expect(actual_model).to eq(expected_model)
60
+ end
21
61
  end
22
62
 
23
63
  context 'inheritance' do
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require 'spec_helper'
11
+
12
+ describe Dbee::KeyChain do
13
+ let(:key_paths) do
14
+ [
15
+ 'z.b.c.d.e',
16
+ 'b.c.s',
17
+ 'a',
18
+ 'b.c.s',
19
+ 123,
20
+ Dbee::KeyPath.get('44.55.66'),
21
+ 'a'
22
+ ]
23
+ end
24
+
25
+ let(:out_of_order_key_paths) do
26
+ [
27
+ Dbee::KeyPath.get('44.55.66'),
28
+ Dbee::KeyPath.get('44.55.66'),
29
+ Dbee::KeyPath.get('44.55.66'),
30
+ Dbee::KeyPath.get('b.c.s'),
31
+ Dbee::KeyPath.get(123),
32
+ Dbee::KeyPath.get('z.b.c.d.e'),
33
+ Dbee::KeyPath.get('44.55.66'),
34
+ Dbee::KeyPath.get('a')
35
+ ]
36
+ end
37
+
38
+ subject { described_class.new(key_paths) }
39
+
40
+ describe '#initialize' do
41
+ it 'sets key_path_set correctly' do
42
+ expect(subject.key_path_set).to eq(out_of_order_key_paths.to_set)
43
+ end
44
+
45
+ it 'sets ancestor_path_set correctly' do
46
+ expected = out_of_order_key_paths.map(&:ancestor_paths).flatten.to_set
47
+
48
+ expect(subject.ancestor_path_set).to eq(expected)
49
+ end
50
+ end
51
+
52
+ specify 'equality compares unique key_paths' do
53
+ chain1 = described_class.new(key_paths)
54
+ chain2 = described_class.new(out_of_order_key_paths)
55
+
56
+ expect(chain1).to eq(chain2)
57
+ expect(chain1).to eql(chain2)
58
+ end
59
+
60
+ specify '#hash compares unique key_path hashes' do
61
+ chain1 = described_class.new(key_paths)
62
+ chain2 = described_class.new(out_of_order_key_paths)
63
+
64
+ expect(chain1.hash).to eq(chain2.hash)
65
+ expect(chain1.hash).to eql(chain2.hash)
66
+ end
67
+
68
+ describe '#ancestor_path?' do
69
+ it 'returns true when passed in ancestor_path exists' do
70
+ %w[z z.b z.b.c z.b.c.d b b.c 44 44.55].each do |example|
71
+ expect(subject.ancestor_path?(example)).to be true
72
+ end
73
+ end
74
+
75
+ it 'returns false when passed in ancestor_path does not exist' do
76
+ %w[z.b.c.d.e matt nick sam.nick A Z].each do |example|
77
+ expect(subject.ancestor_path?(example)).to be false
78
+ end
79
+ end
80
+ end
81
+ end
@@ -9,7 +9,7 @@
9
9
 
10
10
  require 'spec_helper'
11
11
 
12
- describe Dbee::Query::KeyPath do
12
+ describe Dbee::KeyPath do
13
13
  let(:key_path_string) { 'contacts.demographics.first' }
14
14
 
15
15
  subject { described_class.get(key_path_string) }
@@ -32,4 +32,16 @@ describe Dbee::Query::KeyPath do
32
32
  expect(subject).to eq(key_path_string)
33
33
  expect(subject).to eql(key_path_string)
34
34
  end
35
+
36
+ describe '#ancestor_paths' do
37
+ let(:key_path_string) { 'a.b.c.d.e' }
38
+
39
+ subject { described_class.get(key_path_string) }
40
+
41
+ it 'should include all left-inclusive paths' do
42
+ expected = %w[a a.b a.b.c a.b.c.d]
43
+
44
+ expect(subject.ancestor_paths).to eq(expected)
45
+ end
46
+ end
35
47
  end
@@ -103,11 +103,20 @@ describe Dbee::Model do
103
103
 
104
104
  context 'README examples' do
105
105
  specify 'code-first and configuration-first models are equal' do
106
- config = yaml_fixture('models.yaml')['Readme']
106
+ config = yaml_fixture('models.yaml')['Readme']
107
+ config_model = described_class.make(config)
107
108
 
108
- config_model = described_class.make(config)
109
+ key_chain = Dbee::KeyChain.new(%w[
110
+ patients.a
111
+ patients.notes.b
112
+ patients.work_phone_number.c
113
+ patients.cell_phone_number.d
114
+ patients.fax_phone_number.e
115
+ ])
109
116
 
110
- expect(config_model).to eq(ReadmeDataModels::Practices.to_model)
117
+ code_model = ReadmeDataModels::Practices.to_model(key_chain)
118
+
119
+ expect(config_model).to eq(code_model)
111
120
  end
112
121
  end
113
122
  end
@@ -21,6 +21,10 @@ describe Dbee::Query do
21
21
  { key_path: :sort_me },
22
22
  { key_path: :sort_me_too, direction: :descending }
23
23
  ],
24
+ filters: [
25
+ { key_path: 'filter_me.a_little', value: 'something' },
26
+ { key_path: 'matt.nick.sam', value: 'something' }
27
+ ],
24
28
  limit: 125
25
29
  }
26
30
  end
@@ -41,6 +45,19 @@ describe Dbee::Query do
41
45
  end
42
46
  end
43
47
 
48
+ describe '#key_chain' do
49
+ it 'should include filter, sorter, and field key_paths' do
50
+ key_paths =
51
+ config[:fields].map { |f| f[:key_path].to_s } +
52
+ config[:filters].map { |f| f[:key_path].to_s } +
53
+ config[:sorters].map { |s| s[:key_path].to_s }
54
+
55
+ expected_key_chain = Dbee::KeyChain.new(key_paths)
56
+
57
+ expect(subject.key_chain).to eq(expected_key_chain)
58
+ end
59
+ end
60
+
44
61
  context 'README examples' do
45
62
  EXAMPLES = {
46
63
  'Get all practices' => {
data/spec/dbee_spec.rb CHANGED
@@ -14,47 +14,58 @@ describe Dbee do
14
14
  describe '#sql' do
15
15
  let(:provider) { Dbee::Providers::NullProvider.new }
16
16
 
17
- let(:query) { { fields: [{ key_path: :a }] } }
17
+ let(:model_hash) do
18
+ {
19
+ name: 'something'
20
+ }
21
+ end
18
22
 
19
- it 'accepts a hash as a model and passes a Model instance to provider#sql' do
20
- model = { name: 'something' }
23
+ let(:model) { Dbee::Model.make(model_hash) }
21
24
 
22
- expect(provider).to receive(:sql).with(Dbee::Model.make(model), Dbee::Query.make(query))
25
+ let(:query_hash) do
26
+ {
27
+ fields: [
28
+ { key_path: :a }
29
+ ]
30
+ }
31
+ end
23
32
 
24
- described_class.sql(model, query, provider)
33
+ let(:query) { Dbee::Query.make(query_hash) }
34
+
35
+ it 'accepts a hash as a model and passes a Model instance to provider#sql' do
36
+ expect(provider).to receive(:sql).with(model, query)
37
+
38
+ described_class.sql(model_hash, query, provider)
25
39
  end
26
40
 
27
41
  it 'accepts a Dbee::Model instance as a model and passes a Model instance to provider#sql' do
28
- model = Dbee::Model.make(name: 'something')
29
-
30
- expect(provider).to receive(:sql).with(Dbee::Model.make(model), Dbee::Query.make(query))
42
+ expect(provider).to receive(:sql).with(model, query)
31
43
 
32
44
  described_class.sql(model, query, provider)
33
45
  end
34
46
 
35
47
  it 'accepts a Dbee::Base constant as a model and passes a Model instance to provider#sql' do
36
- model = Models::Theaters
48
+ model_constant = Models::Theaters
37
49
 
38
- expect(provider).to receive(:sql).with(model.to_model, Dbee::Query.make(query))
50
+ expect(provider).to receive(:sql).with(model_constant.to_model(query.key_chain), query)
39
51
 
40
- described_class.sql(model, query, provider)
52
+ described_class.sql(model_constant, query, provider)
41
53
  end
42
54
 
43
55
  it 'accepts a Dbee::Query instance as a query and passes a Query instance to provider#sql' do
44
- model = Models::Theaters
45
- query = Dbee::Query.make(query)
56
+ model = Models::Theaters.to_model(query.key_chain)
46
57
 
47
- expect(provider).to receive(:sql).with(model.to_model, Dbee::Query.make(query))
58
+ expect(provider).to receive(:sql).with(model, query)
48
59
 
49
60
  described_class.sql(model, query, provider)
50
61
  end
51
62
 
52
63
  it 'accepts a hash as a query and passes a Query instance to provider#sql' do
53
- model = Models::Theaters
64
+ model = Models::Theaters.to_model(query.key_chain)
54
65
 
55
- expect(provider).to receive(:sql).with(model.to_model, Dbee::Query.make(query))
66
+ expect(provider).to receive(:sql).with(model, query)
56
67
 
57
- described_class.sql(model, query, provider)
68
+ described_class.sql(model, query_hash, provider)
58
69
  end
59
70
  end
60
71
  end
@@ -15,7 +15,7 @@ module Models
15
15
  end
16
16
 
17
17
  class Demographics < Dbee::Base
18
- association :phone_numbers, model: PhoneNumbers,
18
+ association :phone_numbers, model: 'Models::PhoneNumbers',
19
19
  constraints: {
20
20
  name: :demographic_id,
21
21
  parent: :id
@@ -56,7 +56,7 @@ module Models
56
56
 
57
57
  class Theaters < TheatersBase
58
58
  association :parent_theater, model: self, constraints: [
59
- { type: :reference, name: :parent_theater_id, parent: :id }
59
+ { type: :reference, name: :id, parent: :parent_theater_id }
60
60
  ]
61
61
  end
62
62
 
@@ -115,3 +115,27 @@ module ReadmeDataModels
115
115
  }
116
116
  end
117
117
  end
118
+
119
+ module Cycles
120
+ class BaseA < Dbee::Base
121
+ association :b1, model: 'Cycles::B'
122
+ end
123
+
124
+ class A < BaseA
125
+ association :b2, model: 'Cycles::B'
126
+ end
127
+
128
+ class B < Dbee::Base
129
+ association :c, model: 'Cycles::C'
130
+
131
+ association :d, model: 'Cycles::D'
132
+ end
133
+
134
+ class C < Dbee::Base
135
+ association :a, model: 'Cycles::A'
136
+ end
137
+
138
+ class D < Dbee::Base
139
+ association :a, model: 'Cycles::A'
140
+ end
141
+ end
@@ -60,8 +60,8 @@ Theaters, Members, and Movies:
60
60
  table: theaters
61
61
  constraints:
62
62
  - type: reference
63
- name: parent_theater_id
64
- parent: id
63
+ name: id
64
+ parent: parent_theater_id
65
65
  models:
66
66
  - name: members
67
67
  table: members
@@ -160,3 +160,29 @@ Readme:
160
160
  - type: static
161
161
  name: phone_number_type
162
162
  value: fax
163
+ Cycle Example:
164
+ name: a
165
+ models:
166
+ - name: b1
167
+ table: b
168
+ models:
169
+ - name: c
170
+ models:
171
+ - name: a
172
+ - name: d
173
+ models:
174
+ - name: a
175
+ - name: b2
176
+ table: b
177
+ models:
178
+ - name: c
179
+ models:
180
+ - name: a
181
+ - name: d
182
+ models:
183
+ - name: a
184
+ models:
185
+ - name: b1
186
+ table: b
187
+ models:
188
+ - name: c
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbee
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Ruggio
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-26 00:00:00.000000000 Z
11
+ date: 2019-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: acts_as_hashable
@@ -155,6 +155,8 @@ files:
155
155
  - dbee.gemspec
156
156
  - lib/dbee.rb
157
157
  - lib/dbee/base.rb
158
+ - lib/dbee/key_chain.rb
159
+ - lib/dbee/key_path.rb
158
160
  - lib/dbee/model.rb
159
161
  - lib/dbee/model/constraints.rb
160
162
  - lib/dbee/model/constraints/base.rb
@@ -176,10 +178,11 @@ files:
176
178
  - lib/dbee/query/filters/not_equals.rb
177
179
  - lib/dbee/query/filters/not_start_with.rb
178
180
  - lib/dbee/query/filters/starts_with.rb
179
- - lib/dbee/query/key_path.rb
180
181
  - lib/dbee/query/sorter.rb
181
182
  - lib/dbee/version.rb
182
183
  - spec/dbee/base_spec.rb
184
+ - spec/dbee/key_chain_spec.rb
185
+ - spec/dbee/key_path_spec.rb
183
186
  - spec/dbee/model/constraints/base_spec.rb
184
187
  - spec/dbee/model/constraints/reference_spec.rb
185
188
  - spec/dbee/model/constraints/static_spec.rb
@@ -189,7 +192,6 @@ files:
189
192
  - spec/dbee/query/field_spec.rb
190
193
  - spec/dbee/query/filters/base_spec.rb
191
194
  - spec/dbee/query/filters_spec.rb
192
- - spec/dbee/query/key_path_spec.rb
193
195
  - spec/dbee/query/sorter_spec.rb
194
196
  - spec/dbee/query_spec.rb
195
197
  - spec/dbee_spec.rb
@@ -221,6 +223,8 @@ specification_version: 4
221
223
  summary: Adhoc Reporting SQL Generator
222
224
  test_files:
223
225
  - spec/dbee/base_spec.rb
226
+ - spec/dbee/key_chain_spec.rb
227
+ - spec/dbee/key_path_spec.rb
224
228
  - spec/dbee/model/constraints/base_spec.rb
225
229
  - spec/dbee/model/constraints/reference_spec.rb
226
230
  - spec/dbee/model/constraints/static_spec.rb
@@ -230,7 +234,6 @@ test_files:
230
234
  - spec/dbee/query/field_spec.rb
231
235
  - spec/dbee/query/filters/base_spec.rb
232
236
  - spec/dbee/query/filters_spec.rb
233
- - spec/dbee/query/key_path_spec.rb
234
237
  - spec/dbee/query/sorter_spec.rb
235
238
  - spec/dbee/query_spec.rb
236
239
  - spec/dbee_spec.rb
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
- #
6
- # This source code is licensed under the MIT license found in the
7
- # LICENSE file in the root directory of this source tree.
8
- #
9
-
10
- module Dbee
11
- class Query
12
- # This class represents a relative path from a model to a column. For example:
13
- # Say we have a model called "users" which is represented by a "users" table.
14
- # The "users" table also has a one-to-many relationship with a "phone_numbers" table, which
15
- # is modeled as a nested model under "users" as "phone_numbers". Then, to get the column:
16
- # "area_code", you would use: "phone_numbers.area_code".
17
- # Say the column "name" is located on "users", you could use the key path: "name".
18
- # This also works for deeper nested columns in the same fashion.
19
- class KeyPath
20
- extend Forwardable
21
-
22
- class << self
23
- def get(obj)
24
- obj.is_a?(self.class) ? obj : new(obj)
25
- end
26
- end
27
-
28
- SPLIT_CHAR = '.'
29
-
30
- private_constant :SPLIT_CHAR
31
-
32
- attr_reader :value, :ancestor_names, :column_name
33
-
34
- def_delegators :value, :to_s
35
-
36
- def initialize(value)
37
- raise 'Value is required' if value.to_s.empty?
38
-
39
- @value = value.to_s
40
- @ancestor_names = value.to_s.split(SPLIT_CHAR)
41
- @column_name = @ancestor_names.pop
42
-
43
- freeze
44
- end
45
-
46
- def hash
47
- value.hash
48
- end
49
-
50
- def ==(other)
51
- other.to_s == to_s
52
- end
53
- alias eql? ==
54
- end
55
- end
56
- end