dbee 1.0.1 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|