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