dbee 2.1.0.pre.alpha → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +71 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +6 -17
- data/.tool-versions +1 -0
- data/CHANGELOG.md +21 -1
- data/README.md +154 -62
- data/dbee.gemspec +6 -10
- data/lib/dbee/base.rb +7 -49
- data/lib/dbee/dsl_schema_builder.rb +86 -0
- data/lib/dbee/key_chain.rb +11 -0
- data/lib/dbee/model/relationships/basic.rb +47 -0
- data/lib/dbee/model/relationships.rb +24 -0
- data/lib/dbee/model.rb +50 -38
- data/lib/dbee/query/field.rb +5 -6
- data/lib/dbee/query.rb +32 -20
- 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/lib/dbee.rb +9 -10
- data/spec/dbee/base_spec.rb +9 -72
- data/spec/dbee/constant_resolver_spec.rb +17 -12
- 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/filters_spec.rb +16 -17
- data/spec/dbee/query_spec.rb +56 -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 +86 -19
- data/.travis.yml +0 -24
@@ -0,0 +1,86 @@
|
|
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
|
+
# Builds a Dbee::Schema given a Dbee::Base (DSL) model. Note that this class
|
12
|
+
# does not exist in the "Dsl" module as it is not used for defining the DSL
|
13
|
+
# itself.
|
14
|
+
class DslSchemaBuilder # :nodoc:
|
15
|
+
attr_reader :dsl_model, :key_chain
|
16
|
+
|
17
|
+
def initialize(dsl_model, key_chain)
|
18
|
+
@dsl_model = dsl_model || ArgumentError('dsl_model is required')
|
19
|
+
@key_chain = key_chain || ArgumentError('key_chain is required')
|
20
|
+
|
21
|
+
freeze
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_schema
|
25
|
+
schema_spec = { dsl_model.inflected_class_name => model_config(dsl_model) }
|
26
|
+
|
27
|
+
ancestor_paths(key_chain).each do |key_path|
|
28
|
+
start_model = dsl_model
|
29
|
+
|
30
|
+
key_path.ancestor_names.each do |association_name|
|
31
|
+
start_model = append_model_and_relationship(schema_spec, start_model, association_name)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
Schema.new(schema_spec)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def model_config(dsl_model) # :nodoc:
|
41
|
+
{
|
42
|
+
name: dsl_model.inflected_class_name,
|
43
|
+
partitioners: dsl_model.inherited_partitioners,
|
44
|
+
table: dsl_model.inherited_table_name,
|
45
|
+
relationships: {}
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def append_model_and_relationship(schema_spec, base_model, association_name)
|
50
|
+
association = find_association!(base_model, association_name)
|
51
|
+
target_model = association.model_constant
|
52
|
+
|
53
|
+
schema_spec[target_model.inflected_class_name] ||= model_config(target_model)
|
54
|
+
|
55
|
+
schema_spec[base_model.inflected_class_name][:relationships][association.name] = {
|
56
|
+
constraints: association.constraints,
|
57
|
+
model: relationship_model_name(association, target_model)
|
58
|
+
}
|
59
|
+
|
60
|
+
target_model
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns a unique list of ancestor paths from a key_chain. Omits any
|
64
|
+
# fields on the base model.
|
65
|
+
def ancestor_paths(key_chain)
|
66
|
+
key_chain.to_unique_ancestors.key_path_set.select do |key_path|
|
67
|
+
key_path.ancestor_names.any?
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def find_association!(base_model, association_name)
|
72
|
+
base_model.inherited_associations.find { |assoc| assoc.name == association_name } \
|
73
|
+
||
|
74
|
+
raise(
|
75
|
+
ArgumentError,
|
76
|
+
"no association #{association_name} exists on model #{base_model.name}"
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def relationship_model_name(association, target_model)
|
81
|
+
return nil if association.name == target_model.inflected_class_name
|
82
|
+
|
83
|
+
target_model.inflected_class_name
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/dbee/key_chain.rb
CHANGED
@@ -38,5 +38,16 @@ module Dbee
|
|
38
38
|
|
39
39
|
ancestor_path_set.include?(path)
|
40
40
|
end
|
41
|
+
|
42
|
+
# Returns a unique set of ancestors by considering all column names to be the same.
|
43
|
+
def to_unique_ancestors # :nodoc:
|
44
|
+
normalized_paths = key_path_set.map do |kp|
|
45
|
+
KeyPath.new((kp.ancestor_names + COLUMN_PLACEHOLDER).join(KeyPath::SPLIT_CHAR))
|
46
|
+
end
|
47
|
+
self.class.new(normalized_paths.uniq)
|
48
|
+
end
|
49
|
+
|
50
|
+
COLUMN_PLACEHOLDER = ['any_column'].freeze
|
51
|
+
private_constant :COLUMN_PLACEHOLDER
|
41
52
|
end
|
42
53
|
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
|
@@ -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
|
data/lib/dbee/model.rb
CHANGED
@@ -9,65 +9,53 @@
|
|
9
9
|
|
10
10
|
require_relative 'model/constraints'
|
11
11
|
require_relative 'model/partitioner'
|
12
|
+
require_relative 'model/relationships'
|
13
|
+
require_relative 'util/make_keyed_by'
|
12
14
|
|
13
15
|
module Dbee
|
14
16
|
# In DB terms, a Model is usually a table, but it does not have to be. You can also re-model
|
15
17
|
# your DB schema using Dbee::Models.
|
16
18
|
class Model
|
17
|
-
extend Forwardable
|
18
19
|
acts_as_hashable
|
20
|
+
extend Dbee::Util::MakeKeyedBy
|
21
|
+
extend Forwardable
|
19
22
|
|
20
23
|
class ModelNotFoundError < StandardError; end
|
21
24
|
|
22
|
-
attr_reader :constraints, :filters, :name, :partitioners, :table
|
25
|
+
attr_reader :constraints, :filters, :name, :partitioners, :relationships, :table
|
23
26
|
|
24
27
|
def_delegator :models_by_name, :values, :models
|
25
28
|
def_delegator :models, :sort, :sorted_models
|
26
29
|
def_delegator :constraints, :sort, :sorted_constraints
|
27
30
|
def_delegator :partitioners, :sort, :sorted_partitioners
|
28
31
|
|
29
|
-
def initialize(
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
32
|
+
def initialize(
|
33
|
+
name:,
|
34
|
+
constraints: [], # Exists here for tree based model backward compatibility.
|
35
|
+
relationships: [],
|
36
|
+
models: [], # Exists here for tree based model backward compatibility.
|
37
|
+
partitioners: [],
|
38
|
+
table: ''
|
39
|
+
)
|
40
|
+
@name = name
|
41
|
+
@constraints = Constraints.array(constraints || []).uniq
|
42
|
+
@relationships = Relationships.make_keyed_by(:name, relationships)
|
34
43
|
@models_by_name = name_hash(Model.array(models))
|
35
44
|
@partitioners = Partitioner.array(partitioners).uniq
|
36
45
|
@table = table.to_s.empty? ? @name : table.to_s
|
37
46
|
|
47
|
+
ensure_input_is_valid
|
48
|
+
|
38
49
|
freeze
|
39
50
|
end
|
40
51
|
|
41
|
-
|
42
|
-
|
43
|
-
# The hash key will be an array of strings (model names) and the value will be the
|
44
|
-
# identified model.
|
45
|
-
def ancestors!(parts = [], visited_parts = [], found = {})
|
46
|
-
return found if Array(parts).empty?
|
47
|
-
|
48
|
-
# Take the first entry in parts
|
49
|
-
model_name = parts.first.to_s
|
50
|
-
|
51
|
-
# Ensure we have it registered as a child, or raise error
|
52
|
-
model = assert_model(model_name, visited_parts)
|
53
|
-
|
54
|
-
# Push onto visited list
|
55
|
-
visited_parts += [model_name]
|
56
|
-
|
57
|
-
# Add found model to flattened structure
|
58
|
-
found[visited_parts] = model
|
59
|
-
|
60
|
-
# Recursively call for next parts in the chain
|
61
|
-
model.ancestors!(parts[1..-1], visited_parts, found)
|
52
|
+
def relationship_for_name(relationship_name)
|
53
|
+
relationships[relationship_name]
|
62
54
|
end
|
63
55
|
|
64
56
|
def ==(other)
|
65
57
|
other.instance_of?(self.class) &&
|
66
|
-
other.name == name &&
|
67
|
-
other.table == table &&
|
68
|
-
other.sorted_constraints == sorted_constraints &&
|
69
|
-
other.sorted_partitioners == sorted_partitioners &&
|
70
|
-
other.sorted_models == sorted_models
|
58
|
+
other.name == name && other.table == table && children_are_equal(other)
|
71
59
|
end
|
72
60
|
alias eql? ==
|
73
61
|
|
@@ -75,17 +63,41 @@ module Dbee
|
|
75
63
|
name <=> other.name
|
76
64
|
end
|
77
65
|
|
66
|
+
def hash
|
67
|
+
[
|
68
|
+
name.hash,
|
69
|
+
table.hash,
|
70
|
+
relationships.hash,
|
71
|
+
sorted_constraints.hash,
|
72
|
+
sorted_partitioners.hash,
|
73
|
+
sorted_models.hash
|
74
|
+
].hash
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_s
|
78
|
+
name
|
79
|
+
end
|
80
|
+
|
78
81
|
private
|
79
82
|
|
80
83
|
attr_reader :models_by_name
|
81
84
|
|
82
|
-
def assert_model(model_name, visited_parts)
|
83
|
-
models_by_name[model_name] ||
|
84
|
-
raise(ModelNotFoundError, "Missing: #{model_name}, after: #{visited_parts}")
|
85
|
-
end
|
86
|
-
|
87
85
|
def name_hash(array)
|
88
86
|
array.map { |a| [a.name, a] }.to_h
|
89
87
|
end
|
88
|
+
|
89
|
+
def children_are_equal(other)
|
90
|
+
other.relationships == relationships &&
|
91
|
+
other.sorted_constraints == sorted_constraints &&
|
92
|
+
other.sorted_partitioners == sorted_partitioners &&
|
93
|
+
other.sorted_models == sorted_models
|
94
|
+
end
|
95
|
+
|
96
|
+
def ensure_input_is_valid
|
97
|
+
raise ArgumentError, 'name is required' if name.to_s.empty?
|
98
|
+
|
99
|
+
constraints&.any? && relationships&.any? && \
|
100
|
+
raise(ArgumentError, 'constraints and relationships are mutually exclusive')
|
101
|
+
end
|
90
102
|
end
|
91
103
|
end
|
data/lib/dbee/query/field.rb
CHANGED
@@ -26,12 +26,7 @@ module Dbee
|
|
26
26
|
|
27
27
|
attr_reader :aggregator, :display, :filters, :key_path
|
28
28
|
|
29
|
-
def initialize(
|
30
|
-
aggregator: nil,
|
31
|
-
display: nil,
|
32
|
-
filters: [],
|
33
|
-
key_path:
|
34
|
-
)
|
29
|
+
def initialize(key_path:, aggregator: nil, display: nil, filters: [])
|
35
30
|
raise ArgumentError, 'key_path is required' if key_path.to_s.empty?
|
36
31
|
|
37
32
|
@aggregator = aggregator ? Aggregator.const_get(aggregator.to_s.upcase.to_sym) : nil
|
@@ -71,6 +66,10 @@ module Dbee
|
|
71
66
|
def <=>(other)
|
72
67
|
"#{key_path}#{display}" <=> "#{other.key_path}#{other.display}"
|
73
68
|
end
|
69
|
+
|
70
|
+
def key_paths
|
71
|
+
[key_path] + filters.map(&:key_path)
|
72
|
+
end
|
74
73
|
end
|
75
74
|
end
|
76
75
|
end
|
data/lib/dbee/query.rb
CHANGED
@@ -21,35 +21,43 @@ module Dbee
|
|
21
21
|
extend Forwardable
|
22
22
|
acts_as_hashable
|
23
23
|
|
24
|
-
|
24
|
+
attr_reader :fields,
|
25
|
+
:filters,
|
26
|
+
:from,
|
27
|
+
:limit,
|
28
|
+
:offset,
|
29
|
+
:sorters
|
25
30
|
|
26
|
-
|
31
|
+
def_delegator :fields, :sort, :sorted_fields
|
32
|
+
def_delegator :filters, :sort, :sorted_filters
|
33
|
+
def_delegator :sorters, :sort, :sorted_sorters
|
27
34
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
@
|
41
|
-
@
|
42
|
-
@sorters = Sorters.array(sorters).uniq
|
35
|
+
def initialize(
|
36
|
+
fields: [],
|
37
|
+
from: nil,
|
38
|
+
filters: [],
|
39
|
+
limit: nil,
|
40
|
+
offset: nil,
|
41
|
+
sorters: []
|
42
|
+
)
|
43
|
+
@fields = Field.array(fields)
|
44
|
+
@filters = Filters.array(filters).uniq
|
45
|
+
@from = from.to_s
|
46
|
+
@limit = limit.to_s.empty? ? nil : limit.to_i
|
47
|
+
@offset = offset.to_s.empty? ? nil : offset.to_i
|
48
|
+
@sorters = Sorters.array(sorters).uniq
|
43
49
|
|
44
50
|
freeze
|
45
51
|
end
|
46
52
|
|
47
53
|
def ==(other)
|
48
54
|
other.instance_of?(self.class) &&
|
55
|
+
other.limit == limit &&
|
56
|
+
other.offset == offset &&
|
57
|
+
other.from == from &&
|
49
58
|
other.sorted_fields == sorted_fields &&
|
50
59
|
other.sorted_filters == sorted_filters &&
|
51
|
-
other.sorted_sorters == sorted_sorters
|
52
|
-
other.limit == limit
|
60
|
+
other.sorted_sorters == sorted_sorters
|
53
61
|
end
|
54
62
|
alias eql? ==
|
55
63
|
|
@@ -60,7 +68,11 @@ module Dbee
|
|
60
68
|
private
|
61
69
|
|
62
70
|
def key_paths
|
63
|
-
(
|
71
|
+
(
|
72
|
+
fields.flat_map(&:key_paths) +
|
73
|
+
filters.map(&:key_path) +
|
74
|
+
sorters.map(&:key_path)
|
75
|
+
)
|
64
76
|
end
|
65
77
|
end
|
66
78
|
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
|
data/lib/dbee/version.rb
CHANGED