dbee 2.0.2 → 3.0.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 +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +14 -10
- data/.ruby-version +1 -1
- data/.travis.yml +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +257 -43
- data/dbee.gemspec +17 -6
- data/exe/.gitkeep +0 -0
- data/lib/dbee.rb +10 -10
- data/lib/dbee/base.rb +7 -49
- data/lib/dbee/constant_resolver.rb +34 -0
- data/lib/dbee/dsl/association.rb +8 -19
- data/lib/dbee/dsl_schema_builder.rb +86 -0
- data/lib/dbee/key_chain.rb +11 -0
- data/lib/dbee/model.rb +50 -38
- data/lib/dbee/model/relationships.rb +24 -0
- data/lib/dbee/model/relationships/basic.rb +47 -0
- data/lib/dbee/query.rb +30 -24
- data/lib/dbee/query/field.rb +35 -5
- data/lib/dbee/schema.rb +66 -0
- data/lib/dbee/schema_creator.rb +107 -0
- data/lib/dbee/schema_from_tree_based_model.rb +47 -0
- data/lib/dbee/util/make_keyed_by.rb +50 -0
- data/lib/dbee/version.rb +1 -1
- data/spec/dbee/base_spec.rb +9 -72
- data/spec/dbee/constant_resolver_spec.rb +58 -0
- data/spec/dbee/dsl_schema_builder_spec.rb +106 -0
- data/spec/dbee/key_chain_spec.rb +24 -0
- data/spec/dbee/model/constraints_spec.rb +6 -7
- data/spec/dbee/model_spec.rb +62 -59
- data/spec/dbee/query/field_spec.rb +54 -6
- data/spec/dbee/query/filters_spec.rb +16 -17
- data/spec/dbee/query_spec.rb +55 -62
- data/spec/dbee/schema_creator_spec.rb +163 -0
- data/spec/dbee/schema_from_tree_based_model_spec.rb +31 -0
- data/spec/dbee/schema_spec.rb +62 -0
- data/spec/dbee_spec.rb +17 -37
- data/spec/fixtures/models.yaml +254 -56
- data/spec/spec_helper.rb +7 -0
- metadata +83 -18
@@ -10,6 +10,31 @@
|
|
10
10
|
require 'spec_helper'
|
11
11
|
|
12
12
|
describe Dbee::Query::Field do
|
13
|
+
let(:config) do
|
14
|
+
{
|
15
|
+
display: 'd',
|
16
|
+
key_path: 'a.b.c'
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:config_with_aggregation_and_filters) do
|
21
|
+
config.merge(
|
22
|
+
aggregator: :ave,
|
23
|
+
filters: [
|
24
|
+
{
|
25
|
+
key_path: 'a.b',
|
26
|
+
value: 'something'
|
27
|
+
}
|
28
|
+
]
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
subject { described_class.new(config_with_aggregation_and_filters) }
|
33
|
+
|
34
|
+
let(:subject_without_aggregation_and_filters) do
|
35
|
+
described_class.new(config)
|
36
|
+
end
|
37
|
+
|
13
38
|
it 'should act as hashable' do
|
14
39
|
expect(described_class).to respond_to(:make)
|
15
40
|
expect(described_class).to respond_to(:array)
|
@@ -24,16 +49,19 @@ describe Dbee::Query::Field do
|
|
24
49
|
end
|
25
50
|
|
26
51
|
context 'equality' do
|
27
|
-
|
52
|
+
specify '#hash produces same output as [aggregator, key_path, and display]' do
|
53
|
+
expected = [
|
54
|
+
subject.aggregator,
|
55
|
+
subject.display,
|
56
|
+
subject.filters,
|
57
|
+
subject.key_path
|
58
|
+
].hash
|
28
59
|
|
29
|
-
|
30
|
-
|
31
|
-
specify '#hash produces same output as concatenated string hash of key_path and display' do
|
32
|
-
expect(subject.hash).to eq("#{config[:key_path]}#{config[:display]}".hash)
|
60
|
+
expect(subject.hash).to eq(expected)
|
33
61
|
end
|
34
62
|
|
35
63
|
specify '#== and #eql? compare attributes' do
|
36
|
-
object2 = described_class.new(
|
64
|
+
object2 = described_class.new(config_with_aggregation_and_filters)
|
37
65
|
|
38
66
|
expect(subject).to eq(object2)
|
39
67
|
expect(subject).to eql(object2)
|
@@ -44,4 +72,24 @@ describe Dbee::Query::Field do
|
|
44
72
|
expect(subject).not_to eq(nil)
|
45
73
|
end
|
46
74
|
end
|
75
|
+
|
76
|
+
describe '#aggregator?' do
|
77
|
+
it 'returns true if not nil' do
|
78
|
+
expect(subject.aggregator?).to be true
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'returns false if nil' do
|
82
|
+
expect(subject_without_aggregation_and_filters.aggregator?).to be false
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe '#filters?' do
|
87
|
+
it 'returns true if at least one filter' do
|
88
|
+
expect(subject.filters?).to be true
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'returns false if nil' do
|
92
|
+
expect(subject_without_aggregation_and_filters.filters?).to be false
|
93
|
+
end
|
94
|
+
end
|
47
95
|
end
|
@@ -9,24 +9,23 @@
|
|
9
9
|
|
10
10
|
require 'spec_helper'
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
Dbee::Query::Filters::StartsWith => FILTERS_CONFIG.merge(type: :starts_with)
|
28
|
-
}.freeze
|
12
|
+
FILTERS_CONFIG = { key_path: 'a.b.c', value: :d }.freeze
|
13
|
+
FILTER_FACTORIES = {
|
14
|
+
Dbee::Query::Filters::Contains => FILTERS_CONFIG.merge(type: :contains),
|
15
|
+
Dbee::Query::Filters::Equals => FILTERS_CONFIG.merge(type: :equals),
|
16
|
+
Dbee::Query::Filters::GreaterThanOrEqualTo => FILTERS_CONFIG.merge(
|
17
|
+
type: :greater_than_or_equal_to
|
18
|
+
),
|
19
|
+
Dbee::Query::Filters::GreaterThan => FILTERS_CONFIG.merge(type: :greater_than),
|
20
|
+
Dbee::Query::Filters::LessThanOrEqualTo => FILTERS_CONFIG.merge(type: :less_than_or_equal_to),
|
21
|
+
Dbee::Query::Filters::LessThan => FILTERS_CONFIG.merge(type: :less_than),
|
22
|
+
Dbee::Query::Filters::NotContain => FILTERS_CONFIG.merge(type: :not_contain),
|
23
|
+
Dbee::Query::Filters::NotEquals => FILTERS_CONFIG.merge(type: :not_equals),
|
24
|
+
Dbee::Query::Filters::NotStartWith => FILTERS_CONFIG.merge(type: :not_start_with),
|
25
|
+
Dbee::Query::Filters::StartsWith => FILTERS_CONFIG.merge(type: :starts_with)
|
26
|
+
}.freeze
|
29
27
|
|
28
|
+
describe Dbee::Query::Filters do
|
30
29
|
FILTER_FACTORIES.each_pair do |constant, config|
|
31
30
|
it "should instantiate #{constant} objects" do
|
32
31
|
expect(described_class.make(config)).to be_a(constant)
|
data/spec/dbee/query_spec.rb
CHANGED
@@ -9,9 +9,63 @@
|
|
9
9
|
|
10
10
|
require 'spec_helper'
|
11
11
|
|
12
|
+
README_EXAMPLES = {
|
13
|
+
'Get all practices' => {
|
14
|
+
fields: [
|
15
|
+
{ key_path: 'id' },
|
16
|
+
{ key_path: 'active' },
|
17
|
+
{ key_path: 'name' }
|
18
|
+
]
|
19
|
+
},
|
20
|
+
'Get all practices, limit to 10, and sort by name (descending) then id (ascending)' => {
|
21
|
+
fields: [
|
22
|
+
{ key_path: 'id' },
|
23
|
+
{ key_path: 'active' },
|
24
|
+
{ key_path: 'name' }
|
25
|
+
],
|
26
|
+
sorters: [
|
27
|
+
{ key_path: 'name', direction: :descending },
|
28
|
+
{ key_path: 'id' }
|
29
|
+
],
|
30
|
+
limit: 10
|
31
|
+
},
|
32
|
+
"Get top 5 active practices and patient whose name start with 'Sm':" => {
|
33
|
+
fields: [
|
34
|
+
{ key_path: 'name', display: 'Practice Name' },
|
35
|
+
{ key_path: 'patients.first', display: 'Patient First Name' },
|
36
|
+
{ key_path: 'patients.middle', display: 'Patient Middle Name' },
|
37
|
+
{ key_path: 'patients.last', display: 'Patient Last Name' }
|
38
|
+
],
|
39
|
+
filters: [
|
40
|
+
{ type: :equals, key_path: 'active', value: true },
|
41
|
+
{ type: :starts_with, key_path: 'patients.last', value: 'Sm' }
|
42
|
+
],
|
43
|
+
limit: 5
|
44
|
+
},
|
45
|
+
'Get practice IDs, patient IDs, names, and cell phone numbers that starts with 555' => {
|
46
|
+
fields: [
|
47
|
+
{ key_path: 'id', display: 'Practice ID #' },
|
48
|
+
{ key_path: 'patients.id', display: 'Patient ID #' },
|
49
|
+
{ key_path: 'patients.first', display: 'Patient First Name' },
|
50
|
+
{ key_path: 'patients.middle', display: 'Patient Middle Name' },
|
51
|
+
{ key_path: 'patients.last', display: 'Patient Last Name' },
|
52
|
+
{ key_path: 'patients.cell_phone_numbers.phone_number', display: 'Patient Cell #' }
|
53
|
+
],
|
54
|
+
filters: [
|
55
|
+
{ type: :equals, key_path: 'active', value: true },
|
56
|
+
{
|
57
|
+
type: :starts_with,
|
58
|
+
key_path: 'patients.cell_phone_numbers.phone_number',
|
59
|
+
value: '555'
|
60
|
+
}
|
61
|
+
]
|
62
|
+
}
|
63
|
+
}.freeze
|
64
|
+
|
12
65
|
describe Dbee::Query do
|
13
66
|
let(:config) do
|
14
67
|
{
|
68
|
+
from: 'my_model',
|
15
69
|
fields: [
|
16
70
|
{ key_path: 'matt.nick.sam', display: 'some display' },
|
17
71
|
{ key_path: 'katie' },
|
@@ -36,14 +90,6 @@ describe Dbee::Query do
|
|
36
90
|
end
|
37
91
|
|
38
92
|
describe '#initialize' do
|
39
|
-
it 'should raise an ArgumentError if fields keyword is missing' do
|
40
|
-
expect { described_class.new }.to raise_error(ArgumentError)
|
41
|
-
end
|
42
|
-
|
43
|
-
it 'should raise a NoFieldsError if no fields were passed in' do
|
44
|
-
expect { described_class.new(fields: []) }.to raise_error(Dbee::Query::NoFieldsError)
|
45
|
-
end
|
46
|
-
|
47
93
|
it 'should remove duplicate filters (keep first instance)' do
|
48
94
|
query_hash = {
|
49
95
|
fields: [
|
@@ -124,60 +170,7 @@ describe Dbee::Query do
|
|
124
170
|
end
|
125
171
|
|
126
172
|
context 'README examples' do
|
127
|
-
|
128
|
-
'Get all practices' => {
|
129
|
-
fields: [
|
130
|
-
{ key_path: 'id' },
|
131
|
-
{ key_path: 'active' },
|
132
|
-
{ key_path: 'name' }
|
133
|
-
]
|
134
|
-
},
|
135
|
-
'Get all practices, limit to 10, and sort by name (descending) then id (ascending)' => {
|
136
|
-
fields: [
|
137
|
-
{ key_path: 'id' },
|
138
|
-
{ key_path: 'active' },
|
139
|
-
{ key_path: 'name' }
|
140
|
-
],
|
141
|
-
sorters: [
|
142
|
-
{ key_path: 'name', direction: :descending },
|
143
|
-
{ key_path: 'id' }
|
144
|
-
],
|
145
|
-
limit: 10
|
146
|
-
},
|
147
|
-
"Get top 5 active practices and patient whose name start with 'Sm':" => {
|
148
|
-
fields: [
|
149
|
-
{ key_path: 'name', display: 'Practice Name' },
|
150
|
-
{ key_path: 'patients.first', display: 'Patient First Name' },
|
151
|
-
{ key_path: 'patients.middle', display: 'Patient Middle Name' },
|
152
|
-
{ key_path: 'patients.last', display: 'Patient Last Name' }
|
153
|
-
],
|
154
|
-
filters: [
|
155
|
-
{ type: :equals, key_path: 'active', value: true },
|
156
|
-
{ type: :starts_with, key_path: 'patients.last', value: 'Sm' }
|
157
|
-
],
|
158
|
-
limit: 5
|
159
|
-
},
|
160
|
-
'Get practice IDs, patient IDs, names, and cell phone numbers that starts with 555' => {
|
161
|
-
fields: [
|
162
|
-
{ key_path: 'id', display: 'Practice ID #' },
|
163
|
-
{ key_path: 'patients.id', display: 'Patient ID #' },
|
164
|
-
{ key_path: 'patients.first', display: 'Patient First Name' },
|
165
|
-
{ key_path: 'patients.middle', display: 'Patient Middle Name' },
|
166
|
-
{ key_path: 'patients.last', display: 'Patient Last Name' },
|
167
|
-
{ key_path: 'patients.cell_phone_numbers.phone_number', display: 'Patient Cell #' }
|
168
|
-
],
|
169
|
-
filters: [
|
170
|
-
{ type: :equals, key_path: 'active', value: true },
|
171
|
-
{
|
172
|
-
type: :starts_with,
|
173
|
-
key_path: 'patients.cell_phone_numbers.phone_number',
|
174
|
-
value: '555'
|
175
|
-
}
|
176
|
-
]
|
177
|
-
}
|
178
|
-
}.freeze
|
179
|
-
|
180
|
-
EXAMPLES.each_pair do |name, query|
|
173
|
+
README_EXAMPLES.each_pair do |name, query|
|
181
174
|
specify name do
|
182
175
|
expect(described_class.make(query)).to be_a(described_class)
|
183
176
|
end
|
@@ -0,0 +1,163 @@
|
|
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
|
+
require 'fixtures/models'
|
12
|
+
|
13
|
+
describe Dbee::SchemaCreator do
|
14
|
+
let(:model_hash_graph) do
|
15
|
+
{
|
16
|
+
'model1' => {
|
17
|
+
relationships: {
|
18
|
+
model2: {
|
19
|
+
constraints: [
|
20
|
+
{
|
21
|
+
type: 'reference',
|
22
|
+
parent: 'id',
|
23
|
+
name: 'model1_id'
|
24
|
+
}
|
25
|
+
]
|
26
|
+
}
|
27
|
+
}
|
28
|
+
},
|
29
|
+
'model2' => nil
|
30
|
+
}
|
31
|
+
end
|
32
|
+
let(:schema) { Dbee::Schema.new(model_hash_graph) }
|
33
|
+
|
34
|
+
let(:model_hash_tree) do
|
35
|
+
{
|
36
|
+
name: 'model1',
|
37
|
+
models: [
|
38
|
+
{
|
39
|
+
name: 'model2',
|
40
|
+
constraints: [
|
41
|
+
{
|
42
|
+
type: 'reference',
|
43
|
+
parent: 'id',
|
44
|
+
name: 'model1_id'
|
45
|
+
}
|
46
|
+
]
|
47
|
+
}
|
48
|
+
]
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
let(:model_tree) { Dbee::Model.make(model_hash_tree) }
|
53
|
+
|
54
|
+
let(:query_hash) do
|
55
|
+
{
|
56
|
+
from: 'model1',
|
57
|
+
fields: [
|
58
|
+
{ key_path: :a }
|
59
|
+
]
|
60
|
+
}
|
61
|
+
end
|
62
|
+
let(:query) { Dbee::Query.make(query_hash) }
|
63
|
+
|
64
|
+
let(:query_hash_no_from) { query_hash.slice(:fields) }
|
65
|
+
let(:query_no_from) { Dbee::Query.make(query_hash_no_from) }
|
66
|
+
|
67
|
+
describe 'query parsing' do
|
68
|
+
it 'passes through Dbee::Query instances' do
|
69
|
+
expect(described_class.new(schema, query).query).to eq query
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'creates a Dbee::Query from a query hash' do
|
73
|
+
expect(described_class.new(schema, query_hash).query).to eq query
|
74
|
+
end
|
75
|
+
|
76
|
+
describe 'the "from" field' do
|
77
|
+
it 'raises an error when nil' do
|
78
|
+
expect do
|
79
|
+
described_class.new(schema, query_hash_no_from)
|
80
|
+
end.to raise_error(ArgumentError, 'query requires a from model name')
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'raises an error when the empty string' do
|
84
|
+
expect do
|
85
|
+
described_class.new(schema, query_hash_no_from.merge(from: ''))
|
86
|
+
end.to raise_error(ArgumentError, 'query requires a from model name')
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe 'tree based models' do
|
92
|
+
it 'creates a schema from a Dbee::Model' do
|
93
|
+
expect(described_class.new(model_tree, query).schema).to eq schema
|
94
|
+
end
|
95
|
+
|
96
|
+
describe 'hash detection' do
|
97
|
+
it 'creates a schema from a hash model' do
|
98
|
+
expect(described_class.new(model_hash_tree, query).schema).to eq schema
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'creates a schema from a minimal hash model with no child models' do
|
102
|
+
minimal_tree_hash = { name: :practice }
|
103
|
+
minimal_schema = Dbee::Schema.new(practice: nil)
|
104
|
+
|
105
|
+
expect(described_class.new(minimal_tree_hash, {}).schema).to eq minimal_schema
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe 'query "from" field' do
|
110
|
+
it 'is set using name of the root model of the tree' do
|
111
|
+
expect(described_class.new(model_tree, query_no_from).query).to eq query
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'raises an error the query "from" does not equal the root model name' do
|
115
|
+
query_hash_different_from = query_hash_no_from.merge(from: :bogus_model)
|
116
|
+
|
117
|
+
expect do
|
118
|
+
described_class.new(model_tree, query_hash_different_from)
|
119
|
+
end.to raise_error(
|
120
|
+
ArgumentError,
|
121
|
+
"expected from model to be 'model1' but got 'bogus_model'"
|
122
|
+
)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe 'graph based models' do
|
128
|
+
it 'creates a schema from a hash schema' do
|
129
|
+
expect(described_class.new(model_hash_graph, query).schema).to eq schema
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'passes through Dbee::Schema instances' do
|
133
|
+
expect(described_class.new(schema, query).schema).to eq schema
|
134
|
+
end
|
135
|
+
|
136
|
+
describe 'when given a Dbee::Base class' do
|
137
|
+
let(:dsl_model_class) { PartitionerExamples::Dog }
|
138
|
+
|
139
|
+
it 'creates a schema' do
|
140
|
+
expected_config = yaml_fixture('models.yaml')['Partitioner Example 1']
|
141
|
+
expected_schema = Dbee::Schema.new(expected_config)
|
142
|
+
dog_query = Dbee::Query.make(query_hash_no_from.merge(from: :dog))
|
143
|
+
|
144
|
+
expect(described_class.new(dsl_model_class, dog_query).schema).to eq expected_schema
|
145
|
+
end
|
146
|
+
|
147
|
+
it "can infer the query's 'from' field" do
|
148
|
+
expect(described_class.new(dsl_model_class, query_no_from).query.from).to eq 'dog'
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'raises an error the query "from" does not equal the DSL model name' do
|
152
|
+
cat_query = Dbee::Query.make(query_hash_no_from.merge(from: :cat))
|
153
|
+
|
154
|
+
expect do
|
155
|
+
described_class.new(dsl_model_class, cat_query)
|
156
|
+
end.to raise_error(
|
157
|
+
ArgumentError,
|
158
|
+
"expected from model to be 'dog' but got 'cat'"
|
159
|
+
)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,31 @@
|
|
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
|
+
require 'fixtures/models'
|
12
|
+
|
13
|
+
describe Dbee::SchemaFromTreeBasedModel do
|
14
|
+
let(:tree_config) do
|
15
|
+
Dbee::Model.make(yaml_fixture('models.yaml')['Theaters, Members, and Movies Tree Based'])
|
16
|
+
end
|
17
|
+
let(:schema_config) do
|
18
|
+
yaml_fixture('models.yaml')['Theaters, Members, and Movies from Tree']
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'converts the theaters model' do
|
22
|
+
expected_schema = Dbee::Schema.new(schema_config)
|
23
|
+
expect(described_class.convert(tree_config)).to eq expected_schema
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'does not raise any errors when converting the readme model' do
|
27
|
+
tree_config = Dbee::Model.make(yaml_fixture('models.yaml')['Readme Tree Based'])
|
28
|
+
|
29
|
+
expect { described_class.convert(tree_config) }.not_to raise_error
|
30
|
+
end
|
31
|
+
end
|