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.
@@ -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
- class NoFieldsError < StandardError; end
25
-
26
- attr_reader :fields, :filters, :limit, :sorters
27
-
28
- def_delegator :fields, :sort, :sorted_fields
29
-
30
- def_delegator :filters, :sort, :sorted_filters
31
-
32
- def_delegator :sorters, :sort, :sorted_sorters
33
-
34
- def initialize(fields:, filters: [], limit: nil, sorters: [])
35
- @fields = Field.array(fields)
36
-
37
- # If no fields were passed into a query then we will have no data to return.
38
- # Let's raise a hard error here and let the consumer deal with it since this may
39
- # have implications in downstream SQL generators.
40
- raise NoFieldsError if @fields.empty?
41
-
42
- @filters = Filters.array(filters).uniq
43
- @limit = limit.to_s.empty? ? nil : limit.to_i
44
- @sorters = Sorters.array(sorters).uniq
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
- (fields.map(&:key_path) + filters.map(&:key_path) + sorters.map(&:key_path))
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
@@ -15,24 +15,50 @@ module Dbee
15
15
  class Field
16
16
  acts_as_hashable
17
17
 
18
- attr_reader :key_path, :display
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
- @key_path = KeyPath.get(key_path)
24
- @display = (display.to_s.empty? ? key_path : display).to_s
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
- "#{key_path}#{display}".hash
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
@@ -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