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
@@ -0,0 +1,24 @@
|
|
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 'relationships/basic'
|
11
|
+
require_relative '../util/make_keyed_by'
|
12
|
+
|
13
|
+
module Dbee
|
14
|
+
class Model
|
15
|
+
# Top-level class that allows for the making of relationships.
|
16
|
+
class Relationships
|
17
|
+
acts_as_hashable_factory
|
18
|
+
extend Dbee::Util::MakeKeyedBy
|
19
|
+
|
20
|
+
register 'basic', Basic
|
21
|
+
register '', Basic # When type is not present this will be the default
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,47 @@
|
|
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 Model
|
12
|
+
class Relationships
|
13
|
+
# A relationship from one model to another.
|
14
|
+
class Basic
|
15
|
+
acts_as_hashable
|
16
|
+
|
17
|
+
attr_reader :constraints, :model, :name
|
18
|
+
|
19
|
+
def initialize(name:, constraints: [], model: nil)
|
20
|
+
@name = name
|
21
|
+
raise ArgumentError, 'name is required' if name.to_s.empty?
|
22
|
+
|
23
|
+
@constraints = Constraints.array(constraints || []).uniq
|
24
|
+
@model = model
|
25
|
+
|
26
|
+
freeze
|
27
|
+
end
|
28
|
+
|
29
|
+
def model_name
|
30
|
+
model || name
|
31
|
+
end
|
32
|
+
|
33
|
+
def hash
|
34
|
+
[self.class.hash, name.hash, constraints.hash, model.hash].hash
|
35
|
+
end
|
36
|
+
|
37
|
+
def ==(other)
|
38
|
+
other.instance_of?(self.class) &&
|
39
|
+
other.name == name &&
|
40
|
+
other.constraints == constraints &&
|
41
|
+
other.model == model
|
42
|
+
end
|
43
|
+
alias eql? ==
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/dbee/query.rb
CHANGED
@@ -21,37 +21,39 @@ module Dbee
|
|
21
21
|
extend Forwardable
|
22
22
|
acts_as_hashable
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
def_delegator :
|
31
|
-
|
32
|
-
def_delegator :sorters,
|
33
|
-
|
34
|
-
def initialize(
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
@filters
|
43
|
-
@
|
44
|
-
@
|
24
|
+
attr_reader :fields,
|
25
|
+
:filters,
|
26
|
+
:from,
|
27
|
+
:limit,
|
28
|
+
:sorters
|
29
|
+
|
30
|
+
def_delegator :fields, :sort, :sorted_fields
|
31
|
+
def_delegator :filters, :sort, :sorted_filters
|
32
|
+
def_delegator :sorters, :sort, :sorted_sorters
|
33
|
+
|
34
|
+
def initialize(
|
35
|
+
fields: [],
|
36
|
+
from: nil,
|
37
|
+
filters: [],
|
38
|
+
limit: nil,
|
39
|
+
sorters: []
|
40
|
+
)
|
41
|
+
@fields = Field.array(fields)
|
42
|
+
@filters = Filters.array(filters).uniq
|
43
|
+
@from = from.to_s
|
44
|
+
@limit = limit.to_s.empty? ? nil : limit.to_i
|
45
|
+
@sorters = Sorters.array(sorters).uniq
|
45
46
|
|
46
47
|
freeze
|
47
48
|
end
|
48
49
|
|
49
50
|
def ==(other)
|
50
51
|
other.instance_of?(self.class) &&
|
52
|
+
other.limit == limit &&
|
53
|
+
other.from == from &&
|
51
54
|
other.sorted_fields == sorted_fields &&
|
52
55
|
other.sorted_filters == sorted_filters &&
|
53
|
-
other.sorted_sorters == sorted_sorters
|
54
|
-
other.limit == limit
|
56
|
+
other.sorted_sorters == sorted_sorters
|
55
57
|
end
|
56
58
|
alias eql? ==
|
57
59
|
|
@@ -62,7 +64,11 @@ module Dbee
|
|
62
64
|
private
|
63
65
|
|
64
66
|
def key_paths
|
65
|
-
(
|
67
|
+
(
|
68
|
+
fields.flat_map(&:key_paths) +
|
69
|
+
filters.map(&:key_path) +
|
70
|
+
sorters.map(&:key_path)
|
71
|
+
)
|
66
72
|
end
|
67
73
|
end
|
68
74
|
end
|
data/lib/dbee/query/field.rb
CHANGED
@@ -15,24 +15,50 @@ module Dbee
|
|
15
15
|
class Field
|
16
16
|
acts_as_hashable
|
17
17
|
|
18
|
-
|
18
|
+
module Aggregator
|
19
|
+
AVE = :ave
|
20
|
+
COUNT = :count
|
21
|
+
MAX = :max
|
22
|
+
MIN = :min
|
23
|
+
SUM = :sum
|
24
|
+
end
|
25
|
+
include Aggregator
|
26
|
+
|
27
|
+
attr_reader :aggregator, :display, :filters, :key_path
|
19
28
|
|
20
|
-
def initialize(key_path:, display: nil)
|
29
|
+
def initialize(key_path:, aggregator: nil, display: nil, filters: [])
|
21
30
|
raise ArgumentError, 'key_path is required' if key_path.to_s.empty?
|
22
31
|
|
23
|
-
@
|
24
|
-
@display
|
32
|
+
@aggregator = aggregator ? Aggregator.const_get(aggregator.to_s.upcase.to_sym) : nil
|
33
|
+
@display = (display.to_s.empty? ? key_path : display).to_s
|
34
|
+
@filters = Filters.array(filters).uniq
|
35
|
+
@key_path = KeyPath.get(key_path)
|
25
36
|
|
26
37
|
freeze
|
27
38
|
end
|
28
39
|
|
40
|
+
def filters?
|
41
|
+
filters.any?
|
42
|
+
end
|
43
|
+
|
44
|
+
def aggregator?
|
45
|
+
!aggregator.nil?
|
46
|
+
end
|
47
|
+
|
29
48
|
def hash
|
30
|
-
|
49
|
+
[
|
50
|
+
aggregator,
|
51
|
+
display,
|
52
|
+
filters,
|
53
|
+
key_path
|
54
|
+
].hash
|
31
55
|
end
|
32
56
|
|
33
57
|
def ==(other)
|
34
58
|
other.instance_of?(self.class) &&
|
59
|
+
other.aggregator == aggregator &&
|
35
60
|
other.key_path == key_path &&
|
61
|
+
other.filters == filters &&
|
36
62
|
other.display == display
|
37
63
|
end
|
38
64
|
alias eql? ==
|
@@ -40,6 +66,10 @@ module Dbee
|
|
40
66
|
def <=>(other)
|
41
67
|
"#{key_path}#{display}" <=> "#{other.key_path}#{other.display}"
|
42
68
|
end
|
69
|
+
|
70
|
+
def key_paths
|
71
|
+
[key_path] + filters.map(&:key_path)
|
72
|
+
end
|
43
73
|
end
|
44
74
|
end
|
45
75
|
end
|
data/lib/dbee/schema.rb
ADDED
@@ -0,0 +1,66 @@
|
|
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
|
+
# A schema represents an entire graph of related models.
|
12
|
+
class Schema
|
13
|
+
attr_reader :models
|
14
|
+
|
15
|
+
extend Forwardable
|
16
|
+
def initialize(schema_config)
|
17
|
+
@models_by_name = Model.make_keyed_by(:name, schema_config)
|
18
|
+
|
19
|
+
freeze
|
20
|
+
end
|
21
|
+
|
22
|
+
# Given a Dbee::Model and Dbee::KeyPath, this returns a list of
|
23
|
+
# Dbee::Relationship and Dbee::Model tuples that lie on the key path.
|
24
|
+
# The returned list is a two dimensional array in
|
25
|
+
# the form of <tt>[[relationship, model], [relationship2, model2]]</tt>,
|
26
|
+
# etc. The relationships and models correspond to each ancestor part of the
|
27
|
+
# key path.
|
28
|
+
#
|
29
|
+
# The key_path argument can be either a Dbee::KeyPath or an array of
|
30
|
+
# string ancestor names.
|
31
|
+
#
|
32
|
+
# An exception is raised of the provided key_path contains relationship
|
33
|
+
# names that do not exist in this schema.
|
34
|
+
def expand_query_path(model, key_path, query_path = [])
|
35
|
+
ancestors = key_path.respond_to?(:ancestor_names) ? key_path.ancestor_names : key_path
|
36
|
+
relationship_name = ancestors.first
|
37
|
+
return query_path unless relationship_name
|
38
|
+
|
39
|
+
relationship = relationship_for_name!(model, relationship_name)
|
40
|
+
join_model = model_for_name!(relationship.model_name)
|
41
|
+
expand_query_path(
|
42
|
+
join_model,
|
43
|
+
ancestors.drop(1),
|
44
|
+
query_path + [[relationship_for_name!(model, relationship_name), join_model]]
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
def model_for_name!(model_name)
|
49
|
+
models_by_name[model_name.to_s] || raise(Model::ModelNotFoundError, model_name)
|
50
|
+
end
|
51
|
+
|
52
|
+
def ==(other)
|
53
|
+
other.instance_of?(self.class) && other.send(:models_by_name) == models_by_name
|
54
|
+
end
|
55
|
+
alias eql? ==
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
attr_reader :models_by_name
|
60
|
+
|
61
|
+
def relationship_for_name!(model, rel_name)
|
62
|
+
model.relationship_for_name(rel_name) ||
|
63
|
+
raise("model '#{model.name}' does not have a '#{rel_name}' relationship")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,107 @@
|
|
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 creates a Dbee::Schema from a variety of different inputs:
|
12
|
+
#
|
13
|
+
# 1. The hash representation of a schema.
|
14
|
+
# 2. A Dbee::Base subclass (code based models).
|
15
|
+
#
|
16
|
+
# For backward compatibility, tree based models are also supported in the
|
17
|
+
# following formats:
|
18
|
+
#
|
19
|
+
# 3. The hash representation of a tree based model.
|
20
|
+
# 4. Dbee::Model instances in tree based form (using the deprecated constraints and models
|
21
|
+
# attributes).
|
22
|
+
class SchemaCreator # :nodoc:
|
23
|
+
attr_reader :schema
|
24
|
+
|
25
|
+
# An ArgumentError is raised if the query "from" attribute differs from the name of the root
|
26
|
+
# model of a tree based model or if the "from" attribute is blank.
|
27
|
+
def initialize(schema_or_model, query)
|
28
|
+
@orig_query = Query.make(query) || raise(ArgumentError, 'query is required')
|
29
|
+
raise ArgumentError, 'a schema or model is required' unless schema_or_model
|
30
|
+
|
31
|
+
@schema = make_schema(schema_or_model)
|
32
|
+
|
33
|
+
# Note that for backward compatibility reasons, this validation does not
|
34
|
+
# exist in the DBee::Query class. This allows continued support for
|
35
|
+
# old callers who depend on the "from" field being inferred from the root
|
36
|
+
# tree model name.
|
37
|
+
raise ArgumentError, 'query requires a from model name' if expected_from_model.empty?
|
38
|
+
|
39
|
+
validate_query_from_model!
|
40
|
+
|
41
|
+
freeze
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns a Dbee::Query instance with a "from" attribute which is
|
45
|
+
# sometimes derived for tree based models.
|
46
|
+
def query
|
47
|
+
return orig_query if expected_from_model == orig_query.from
|
48
|
+
|
49
|
+
Query.new(
|
50
|
+
from: expected_from_model,
|
51
|
+
fields: orig_query.fields,
|
52
|
+
filters: orig_query.filters,
|
53
|
+
sorters: orig_query.sorters,
|
54
|
+
limit: orig_query.limit
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
attr_reader :orig_query, :expected_from_model
|
61
|
+
|
62
|
+
def make_schema(input)
|
63
|
+
@expected_from_model = orig_query.from
|
64
|
+
|
65
|
+
return input if input.is_a?(Dbee::Schema)
|
66
|
+
|
67
|
+
if input.respond_to?(:to_schema)
|
68
|
+
@expected_from_model = input.inflected_class_name
|
69
|
+
return input.to_schema(orig_query.key_chain)
|
70
|
+
end
|
71
|
+
|
72
|
+
model_or_schema = to_object(input)
|
73
|
+
if model_or_schema.is_a?(Model)
|
74
|
+
@expected_from_model = model_or_schema.name.to_s
|
75
|
+
SchemaFromTreeBasedModel.convert(model_or_schema)
|
76
|
+
else
|
77
|
+
model_or_schema
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_object(input)
|
82
|
+
return input unless input.is_a?(Hash)
|
83
|
+
|
84
|
+
if tree_based_hash?(input)
|
85
|
+
Model.make(input)
|
86
|
+
else
|
87
|
+
Schema.new(input)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def validate_query_from_model!
|
92
|
+
!orig_query.from.empty? && expected_from_model.to_s != orig_query.from.to_s && \
|
93
|
+
raise(
|
94
|
+
ArgumentError,
|
95
|
+
"expected from model to be '#{expected_from_model}' but got '#{orig_query.from}'"
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
def tree_based_hash?(hash)
|
100
|
+
name = hash[:name] || hash['name']
|
101
|
+
|
102
|
+
# In the unlikely event that schema based hash had a model called "name",
|
103
|
+
# its value would either be nil or a hash.
|
104
|
+
name.is_a?(String) || name.is_a?(Symbol)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,47 @@
|
|
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
|
+
# Converts a tree based model to a Schema. Note that this results in a
|
12
|
+
# different but compatible schema compared to the result of generating a
|
13
|
+
# Dbee::Schema by calling to_schema on a Dbee::Base class. This is
|
14
|
+
# because converting from a tree based model is a lossy.
|
15
|
+
class SchemaFromTreeBasedModel # :nodoc:
|
16
|
+
class << self
|
17
|
+
def convert(tree_model)
|
18
|
+
Schema.new(to_graph_based_models(tree_model))
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def to_graph_based_models(tree_model, parent_model = nil, models_attrs_by_name = {})
|
24
|
+
models_attrs_by_name[tree_model.name] = {
|
25
|
+
name: tree_model.name,
|
26
|
+
table: tree_model.table,
|
27
|
+
partitioners: tree_model.partitioners,
|
28
|
+
relationships: {}
|
29
|
+
}
|
30
|
+
|
31
|
+
parent_model && add_relationship_to_parent_model(
|
32
|
+
tree_model, models_attrs_by_name[parent_model.name]
|
33
|
+
)
|
34
|
+
|
35
|
+
tree_model.models.each do |sub_model|
|
36
|
+
to_graph_based_models(sub_model, tree_model, models_attrs_by_name)
|
37
|
+
end
|
38
|
+
|
39
|
+
models_attrs_by_name
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_relationship_to_parent_model(model, graph_model_attrs)
|
43
|
+
graph_model_attrs[:relationships][model.name] = { constraints: model.constraints }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,50 @@
|
|
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
|
+
module Util
|
12
|
+
# Provides a "make_keyed_by" method which extends the Hashable gem's
|
13
|
+
# concept of a "make" method.
|
14
|
+
module MakeKeyedBy # :nodoc:
|
15
|
+
# Given a hash of hashes or a hash of values of instances of this class,
|
16
|
+
# a hash is returned where all of the values are instances of this class
|
17
|
+
# and the keys are the string versions of the original hash.
|
18
|
+
#
|
19
|
+
# An ArgumentError is raised if the value's <tt>key_attrib</tt> is not
|
20
|
+
# equal to the top level hash key. This ensures that the
|
21
|
+
# <tt>key_attrib</tt> is the same in the incoming hash and the value.
|
22
|
+
#
|
23
|
+
# This is useful for cases where it makes sense in the configuration
|
24
|
+
# (YAML) specification to represent certain objects in a hash structure
|
25
|
+
# instead of a list.
|
26
|
+
def make_keyed_by(key_attrib, spec_hash)
|
27
|
+
# Once Ruby 2.5 support is dropped, this can just use the block form of
|
28
|
+
# #to_h.
|
29
|
+
spec_hash.map do |key, spec|
|
30
|
+
string_key = key.to_s
|
31
|
+
[string_key, make_value_checking_key_attib!(key_attrib, string_key, spec)]
|
32
|
+
end.to_h
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def make_value_checking_key_attib!(key_attrib, key, spec)
|
38
|
+
if spec.is_a?(self)
|
39
|
+
if spec.send(key_attrib).to_s != key
|
40
|
+
err_msg = "expected a #{key_attrib} of '#{key}' but got '#{spec.send(key_attrib)}'"
|
41
|
+
raise ArgumentError, err_msg
|
42
|
+
end
|
43
|
+
spec
|
44
|
+
else
|
45
|
+
make((spec || {}).merge(key_attrib => key))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|