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 +4 -4
- data/CHANGELOG.md +12 -1
- data/lib/dbee/base.rb +27 -11
- data/lib/dbee/key_chain.rb +42 -0
- data/lib/dbee/key_path.rb +58 -0
- data/lib/dbee/model/constraints/base.rb +1 -1
- data/lib/dbee/model.rb +3 -7
- data/lib/dbee/query/field.rb +4 -2
- data/lib/dbee/query/filters/base.rb +4 -0
- data/lib/dbee/query/filters.rb +1 -0
- data/lib/dbee/query/sorter.rb +4 -2
- data/lib/dbee/query.rb +13 -3
- data/lib/dbee/version.rb +1 -1
- data/lib/dbee.rb +8 -1
- data/spec/dbee/base_spec.rb +45 -5
- data/spec/dbee/key_chain_spec.rb +81 -0
- data/spec/dbee/{query/key_path_spec.rb → key_path_spec.rb} +13 -1
- data/spec/dbee/model_spec.rb +12 -3
- data/spec/dbee/query_spec.rb +17 -0
- data/spec/dbee_spec.rb +28 -17
- data/spec/fixtures/models.rb +26 -2
- data/spec/fixtures/models.yaml +28 -2
- metadata +8 -5
- data/lib/dbee/query/key_path.rb +0 -56
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3e07996f0622fbf3eb19b8fa7bdd6ebfe963d3edc18745ccdf89f2a87f7a16dd
|
4
|
+
data.tar.gz: 0e25d666b9cd0a099741220caf423a670e832149bac26f27283c7a43987c14c5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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,
|
78
|
+
def model_config(key_chain, name, constraints, path_parts)
|
67
79
|
{
|
68
80
|
constraints: constraints,
|
69
|
-
models: associations(
|
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(
|
97
|
+
def associations(key_chain, path_parts)
|
86
98
|
inherited_associations_by_name.values
|
87
|
-
.
|
99
|
+
.select { |c| key_chain.ancestor_path?(path_parts, c[:name]) }
|
88
100
|
.each_with_object([]) do |config, memo|
|
89
|
-
|
101
|
+
model_constant = constantize(config[:model])
|
90
102
|
associated_constraints = config[:constraints]
|
91
103
|
name = config[:name]
|
92
104
|
|
93
|
-
memo <<
|
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
|
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
|
-
|
74
|
+
name <=> other.name
|
79
75
|
end
|
80
76
|
|
81
77
|
private
|
data/lib/dbee/query/field.rb
CHANGED
@@ -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
|
data/lib/dbee/query/filters.rb
CHANGED
data/lib/dbee/query/sorter.rb
CHANGED
@@ -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
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
|
data/spec/dbee/base_spec.rb
CHANGED
@@ -11,13 +11,53 @@ require 'spec_helper'
|
|
11
11
|
require 'fixtures/models'
|
12
12
|
|
13
13
|
describe Dbee::Base do
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
19
|
+
expected_model = Dbee::Model.make(expected_config)
|
19
20
|
|
20
|
-
|
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::
|
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
|
data/spec/dbee/model_spec.rb
CHANGED
@@ -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
|
106
|
+
config = yaml_fixture('models.yaml')['Readme']
|
107
|
+
config_model = described_class.make(config)
|
107
108
|
|
108
|
-
|
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
|
-
|
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
|
data/spec/dbee/query_spec.rb
CHANGED
@@ -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(:
|
17
|
+
let(:model_hash) do
|
18
|
+
{
|
19
|
+
name: 'something'
|
20
|
+
}
|
21
|
+
end
|
18
22
|
|
19
|
-
|
20
|
-
model = { name: 'something' }
|
23
|
+
let(:model) { Dbee::Model.make(model_hash) }
|
21
24
|
|
22
|
-
|
25
|
+
let(:query_hash) do
|
26
|
+
{
|
27
|
+
fields: [
|
28
|
+
{ key_path: :a }
|
29
|
+
]
|
30
|
+
}
|
31
|
+
end
|
23
32
|
|
24
|
-
|
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
|
-
|
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
|
-
|
48
|
+
model_constant = Models::Theaters
|
37
49
|
|
38
|
-
expect(provider).to receive(:sql).with(
|
50
|
+
expect(provider).to receive(:sql).with(model_constant.to_model(query.key_chain), query)
|
39
51
|
|
40
|
-
described_class.sql(
|
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
|
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
|
66
|
+
expect(provider).to receive(:sql).with(model, query)
|
56
67
|
|
57
|
-
described_class.sql(model,
|
68
|
+
described_class.sql(model, query_hash, provider)
|
58
69
|
end
|
59
70
|
end
|
60
71
|
end
|
data/spec/fixtures/models.rb
CHANGED
@@ -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: :
|
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
|
data/spec/fixtures/models.yaml
CHANGED
@@ -60,8 +60,8 @@ Theaters, Members, and Movies:
|
|
60
60
|
table: theaters
|
61
61
|
constraints:
|
62
62
|
- type: reference
|
63
|
-
name:
|
64
|
-
parent:
|
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.
|
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-
|
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
|
data/lib/dbee/query/key_path.rb
DELETED
@@ -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
|