dbee 2.0.2 → 3.0.0

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