forest_admin_datasource_toolkit 1.0.0.pre.beta.21
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/README.md +31 -0
- data/Rakefile +12 -0
- data/forest_admin_datasource_toolkit.gemspec +37 -0
- data/lib/forest_admin_datasource_toolkit/collection.rb +50 -0
- data/lib/forest_admin_datasource_toolkit/components/caller.rb +31 -0
- data/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract.rb +51 -0
- data/lib/forest_admin_datasource_toolkit/components/contracts/datasource_contract.rb +27 -0
- data/lib/forest_admin_datasource_toolkit/components/query/aggregation.rb +23 -0
- data/lib/forest_admin_datasource_toolkit/components/query/condition_tree/condition_tree_equivalent.rb +66 -0
- data/lib/forest_admin_datasource_toolkit/components/query/condition_tree/condition_tree_factory.rb +123 -0
- data/lib/forest_admin_datasource_toolkit/components/query/condition_tree/nodes/condition_tree.rb +70 -0
- data/lib/forest_admin_datasource_toolkit/components/query/condition_tree/nodes/condition_tree_branch.rb +68 -0
- data/lib/forest_admin_datasource_toolkit/components/query/condition_tree/nodes/condition_tree_leaf.rb +144 -0
- data/lib/forest_admin_datasource_toolkit/components/query/condition_tree/operators.rb +100 -0
- data/lib/forest_admin_datasource_toolkit/components/query/condition_tree/transforms/comparisons.rb +123 -0
- data/lib/forest_admin_datasource_toolkit/components/query/condition_tree/transforms/pattern.rb +48 -0
- data/lib/forest_admin_datasource_toolkit/components/query/condition_tree/transforms/times.rb +112 -0
- data/lib/forest_admin_datasource_toolkit/components/query/filter.rb +45 -0
- data/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb +158 -0
- data/lib/forest_admin_datasource_toolkit/components/query/page.rb +14 -0
- data/lib/forest_admin_datasource_toolkit/components/query/projection.rb +42 -0
- data/lib/forest_admin_datasource_toolkit/components/query/projection_factory.rb +30 -0
- data/lib/forest_admin_datasource_toolkit/datasource.rb +29 -0
- data/lib/forest_admin_datasource_toolkit/exceptions/forest_exception.rb +10 -0
- data/lib/forest_admin_datasource_toolkit/schema/column_schema.rb +34 -0
- data/lib/forest_admin_datasource_toolkit/schema/primitive_type.rb +31 -0
- data/lib/forest_admin_datasource_toolkit/schema/relation_schema.rb +13 -0
- data/lib/forest_admin_datasource_toolkit/schema/relations/many_to_many_schema.rb +26 -0
- data/lib/forest_admin_datasource_toolkit/schema/relations/many_to_one_schema.rb +16 -0
- data/lib/forest_admin_datasource_toolkit/schema/relations/one_to_many_schema.rb +16 -0
- data/lib/forest_admin_datasource_toolkit/schema/relations/one_to_one_schema.rb +16 -0
- data/lib/forest_admin_datasource_toolkit/utils/collection.rb +162 -0
- data/lib/forest_admin_datasource_toolkit/utils/record.rb +20 -0
- data/lib/forest_admin_datasource_toolkit/utils/schema.rb +42 -0
- data/lib/forest_admin_datasource_toolkit/version.rb +3 -0
- data/lib/forest_admin_datasource_toolkit.rb +10 -0
- data/sig/forest_admin_datasource_toolkit/collection.rbs +26 -0
- data/sig/forest_admin_datasource_toolkit/components/contracts/collection_contract.rbs +29 -0
- data/sig/forest_admin_datasource_toolkit/components/contracts/datasource_contract.rbs +17 -0
- data/sig/forest_admin_datasource_toolkit/datasource.rbs +12 -0
- data/sig/forest_admin_datasource_toolkit/schema/column_schema.rbs +15 -0
- data/sig/forest_admin_datasource_toolkit/schema/relation_schema.rbs +8 -0
- data/sig/forest_admin_datasource_toolkit/schema/relations/many_relation_schema.rbs +10 -0
- data/sig/forest_admin_datasource_toolkit/schema/relations/many_to_many_schema.rbs +11 -0
- data/sig/forest_admin_datasource_toolkit/schema/relations/single_relation_schema.rbs +10 -0
- data/sig/forest_admin_datasource_toolkit.rbs +4 -0
- metadata +126 -0
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
require 'active_support/core_ext/numeric/time'
|
3
|
+
|
4
|
+
module ForestAdminDatasourceToolkit
|
5
|
+
module Components
|
6
|
+
module Query
|
7
|
+
class FilterFactory
|
8
|
+
include ForestAdminDatasourceToolkit::Schema
|
9
|
+
include ForestAdminDatasourceToolkit::Schema::Relations
|
10
|
+
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
|
11
|
+
|
12
|
+
def self.get_previous_period_filter(filter, timezone)
|
13
|
+
filter.override(condition_tree: filter.condition_tree.replace_leafs do |leaf|
|
14
|
+
case leaf.operator
|
15
|
+
when Operators::YESTERDAY
|
16
|
+
get_previous_period_by_unit(leaf.field, 'Day', timezone)
|
17
|
+
when Operators::PREVIOUS_WEEK
|
18
|
+
get_previous_period_by_unit(leaf.field, 'Week', timezone)
|
19
|
+
when Operators::PREVIOUS_MONTH
|
20
|
+
get_previous_period_by_unit(leaf.field, 'Month', timezone)
|
21
|
+
when Operators::PREVIOUS_QUARTER
|
22
|
+
get_previous_period_by_unit(leaf.field, 'Quarter', timezone)
|
23
|
+
when Operators::PREVIOUS_YEAR
|
24
|
+
get_previous_period_by_unit(leaf.field, 'Year', timezone)
|
25
|
+
when Operators::PREVIOUS_WEEK_TO_DATE
|
26
|
+
leaf.override(operator: Operators::PREVIOUS_WEEK)
|
27
|
+
when Operators::PREVIOUS_MONTH_TO_DATE
|
28
|
+
leaf.override(operator: Operators::PREVIOUS_MONTH)
|
29
|
+
when Operators::PREVIOUS_QUARTER_TO_DATE
|
30
|
+
leaf.override(operator: Operators::PREVIOUS_QUARTER)
|
31
|
+
when Operators::PREVIOUS_YEAR_TO_DATE
|
32
|
+
leaf.override(operator: Operators::PREVIOUS_YEAR)
|
33
|
+
when Operators::TODAY
|
34
|
+
leaf.override(operator: Operators::YESTERDAY)
|
35
|
+
when Operators::PREVIOUS_X_DAYS
|
36
|
+
get_previous_x_days_period(leaf, timezone, 'Previous_X_Days')
|
37
|
+
when Operators::PREVIOUS_X_DAYS_TO_DATE
|
38
|
+
get_previous_x_days_period(leaf, timezone, 'Previous_X_Days_To_Date')
|
39
|
+
else
|
40
|
+
leaf
|
41
|
+
end
|
42
|
+
end)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.get_previous_condition_tree(field, start_period, end_period)
|
46
|
+
ConditionTreeFactory.intersect([
|
47
|
+
Nodes::ConditionTreeLeaf.new(field, Operators::GREATER_THAN,
|
48
|
+
start_period.strftime('%Y-%m-%d %H:%M:%S')),
|
49
|
+
Nodes::ConditionTreeLeaf.new(field, Operators::LESS_THAN,
|
50
|
+
end_period.strftime('%Y-%m-%d %H:%M:%S'))
|
51
|
+
])
|
52
|
+
end
|
53
|
+
|
54
|
+
# Given a collection and a OneToMany/ManyToMany relation, generate a filter which
|
55
|
+
# - match only children of the provided recordId
|
56
|
+
# - can apply on the target collection of the relation
|
57
|
+
def self.make_foreign_filter(collection, id, relation_name, caller, base_foreign_filter)
|
58
|
+
relation = ForestAdminDatasourceToolkit::Utils::Schema.get_to_many_relation(collection, relation_name)
|
59
|
+
origin_value = ForestAdminDatasourceToolkit::Utils::Collection.get_value(collection, caller, id,
|
60
|
+
relation.origin_key_target)
|
61
|
+
if relation.is_a?(OneToManySchema)
|
62
|
+
origin_tree = Nodes::ConditionTreeLeaf.new(relation.origin_key, Operators::EQUAL, origin_value)
|
63
|
+
else
|
64
|
+
through_collection = collection.datasource.collection(relation.through_collection)
|
65
|
+
through_tree = ConditionTreeFactory.intersect([
|
66
|
+
Nodes::ConditionTreeLeaf.new(relation.origin_key, Operators::EQUAL, origin_value),
|
67
|
+
Nodes::ConditionTreeLeaf.new(relation.foreign_key, Operators::PRESENT)
|
68
|
+
])
|
69
|
+
records = through_collection.list(
|
70
|
+
caller,
|
71
|
+
Filter.new(condition_tree: through_tree),
|
72
|
+
Projection.new([relation.foreign_key])
|
73
|
+
)
|
74
|
+
|
75
|
+
origin_tree = Nodes::ConditionTreeLeaf.new(
|
76
|
+
relation.foreign_key_target,
|
77
|
+
Operators::IN,
|
78
|
+
records.map { |record| record[relation.foreign_key] }
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
base_foreign_filter.override(condition_tree: ConditionTreeFactory.intersect([base_foreign_filter.condition_tree, origin_tree]))
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.get_previous_period_by_unit(field, unit, timezone)
|
86
|
+
unit = unit.downcase
|
87
|
+
start = "beginning_of_#{unit}"
|
88
|
+
end_ = "end_of_#{unit}"
|
89
|
+
start_period = Time.now.in_time_zone(timezone).send("prev_#{unit}").send(start)
|
90
|
+
end_period = Time.now.in_time_zone(timezone).send("prev_#{unit}").send(end_)
|
91
|
+
|
92
|
+
get_previous_condition_tree(field, start_period.to_datetime, end_period.to_datetime)
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.get_previous_x_days_period(leaf, timezone, operator)
|
96
|
+
start_period = Time.now.in_time_zone(timezone).send(:-, 2 * leaf.value.day).beginning_of_day
|
97
|
+
end_period = if operator == Operators::PREVIOUS_X_DAYS
|
98
|
+
Time.now.in_time_zone(timezone).send(:-, leaf.value.day).beginning_of_day
|
99
|
+
else
|
100
|
+
Time.now.in_time_zone(timezone).send(:-, leaf.value.day)
|
101
|
+
end
|
102
|
+
|
103
|
+
get_previous_condition_tree(leaf.field, start_period.to_datetime, end_period.to_datetime)
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.make_through_filter(collection, id, relation_name, caller, base_foreign_filter)
|
107
|
+
relation = collection.fields[relation_name]
|
108
|
+
origin_value = Utils::Collection.get_value(collection, caller, id, relation.origin_key_target)
|
109
|
+
foreign_relation = Utils::Collection.get_through_target(collection, relation_name)
|
110
|
+
|
111
|
+
# Optimization for many to many when there is not search/segment (saves one query)
|
112
|
+
if foreign_relation && base_foreign_filter.nestable?
|
113
|
+
foreign_key = collection.datasource.collection(relation.through_collection).fields[relation.foreign_key]
|
114
|
+
base_through_filter = base_foreign_filter.nest(foreign_relation)
|
115
|
+
condition_tree = ConditionTreeFactory.intersect(
|
116
|
+
[
|
117
|
+
Nodes::ConditionTreeLeaf.new(relation.origin_key, Operators::EQUAL, origin_value),
|
118
|
+
base_through_filter.condition_tree
|
119
|
+
]
|
120
|
+
)
|
121
|
+
|
122
|
+
if foreign_key.type == 'Column' && foreign_key.filter_operators.include?(Operators::PRESENT)
|
123
|
+
present = Nodes::ConditionTreeLeaf.new(relation.foreign_key, Operators::PRESENT)
|
124
|
+
condition_tree = ConditionTreeFactory.intersect([condition_tree, present])
|
125
|
+
end
|
126
|
+
|
127
|
+
return base_through_filter.override(condition_tree: condition_tree)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Otherwise we have no choice but to call the target collection so that search and segment
|
131
|
+
# are correctly apply, and then match ids in the though collection.
|
132
|
+
target = collection.datasource.collection(relation.foreign_collection)
|
133
|
+
records = target.list(
|
134
|
+
caller,
|
135
|
+
make_foreign_filter(collection, id, relation_name, caller, base_foreign_filter),
|
136
|
+
Projection.new([relation.foreign_key_target])
|
137
|
+
)
|
138
|
+
|
139
|
+
Filter.new(
|
140
|
+
condition_tree: ConditionTreeFactory.intersect(
|
141
|
+
[
|
142
|
+
# only children of parent
|
143
|
+
Nodes::ConditionTreeLeaf.new(relation.origin_key, Operators::EQUAL, origin_value),
|
144
|
+
|
145
|
+
# only the children which match the conditions in baseForeignFilter
|
146
|
+
Nodes::ConditionTreeLeaf.new(
|
147
|
+
relation.foreign_key,
|
148
|
+
Operators::IN,
|
149
|
+
records.map { |r| r[relation.foreign_key_target] }
|
150
|
+
)
|
151
|
+
]
|
152
|
+
)
|
153
|
+
)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ForestAdminDatasourceToolkit
|
2
|
+
module Components
|
3
|
+
module Query
|
4
|
+
class Projection < Array
|
5
|
+
include ForestAdminDatasourceToolkit::Utils
|
6
|
+
def with_pks(collection)
|
7
|
+
ForestAdminDatasourceToolkit::Utils::Schema.primary_keys(collection).each do |key|
|
8
|
+
push(key) unless include?(key)
|
9
|
+
end
|
10
|
+
|
11
|
+
relations.each do |relation, projection|
|
12
|
+
schema = collection.fields[relation]
|
13
|
+
association = collection.datasource.collection(schema.foreign_collection)
|
14
|
+
projection_with_pks = projection.with_pks(association).nest(prefix: relation)
|
15
|
+
|
16
|
+
projection_with_pks.each { |field| push(field) unless include?(field) }
|
17
|
+
end
|
18
|
+
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def columns
|
23
|
+
reject { |field| field.include?(':') }
|
24
|
+
end
|
25
|
+
|
26
|
+
def relations
|
27
|
+
each_with_object({}) do |path, memo|
|
28
|
+
next unless path.include?(':')
|
29
|
+
|
30
|
+
split_path = path.split(':')
|
31
|
+
relation = split_path[0]
|
32
|
+
memo[relation] = Projection.new([split_path[1]].union(memo[relation] || []))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def nest(prefix: nil)
|
37
|
+
prefix ? Projection.new(map { |path| "#{prefix}:#{path}" }) : self
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module ForestAdminDatasourceToolkit
|
2
|
+
module Components
|
3
|
+
module Query
|
4
|
+
class ProjectionFactory
|
5
|
+
include ForestAdminDatasourceToolkit::Utils
|
6
|
+
def self.all(collection)
|
7
|
+
projection_fields = collection.fields.reduce([]) do |memo, path|
|
8
|
+
column_name = path[0]
|
9
|
+
schema = path[1]
|
10
|
+
memo += [column_name] if schema.type == 'Column'
|
11
|
+
|
12
|
+
if schema.type == 'OneToOne' || schema.type == 'ManyToOne'
|
13
|
+
relation = collection.datasource.collection(schema.foreign_collection)
|
14
|
+
relation_columns = relation.fields
|
15
|
+
.select { |_column_name, relation_column| relation_column.type == 'Column' }
|
16
|
+
.keys
|
17
|
+
.map { |relation_column_name| "#{column_name}:#{relation_column_name}" }
|
18
|
+
|
19
|
+
memo += relation_columns
|
20
|
+
end
|
21
|
+
|
22
|
+
memo
|
23
|
+
end
|
24
|
+
|
25
|
+
Projection.new projection_fields
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ForestAdminDatasourceToolkit
|
2
|
+
class Datasource < Components::Contracts::DatasourceContract
|
3
|
+
attr_reader :collections, :charts
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
super
|
7
|
+
@charts = {}
|
8
|
+
@collections = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def collection(name)
|
12
|
+
raise Exceptions::ForestException, "Collection #{name} not found." unless @collections.key? name
|
13
|
+
|
14
|
+
@collections[name]
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_collection(collection)
|
18
|
+
if @collections.key? collection.name
|
19
|
+
raise Exceptions::ForestException, "Collection #{collection.name} already defined in datasource"
|
20
|
+
end
|
21
|
+
|
22
|
+
@collections[collection.name] = collection
|
23
|
+
end
|
24
|
+
|
25
|
+
def render_chart(_caller, name)
|
26
|
+
raise Exceptions::ForestException, "No chart named #{name} exists on this datasource."
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module ForestAdminDatasourceToolkit
|
2
|
+
module Schema
|
3
|
+
class ColumnSchema
|
4
|
+
attr_reader :is_primary_key, :default_value, :enum_values, :type
|
5
|
+
|
6
|
+
attr_accessor :is_read_only,
|
7
|
+
:is_sortable,
|
8
|
+
:validations,
|
9
|
+
:filter_operators,
|
10
|
+
:column_type
|
11
|
+
|
12
|
+
def initialize(
|
13
|
+
column_type:,
|
14
|
+
filter_operators: [],
|
15
|
+
is_primary_key: false,
|
16
|
+
is_read_only: false,
|
17
|
+
is_sortable: false,
|
18
|
+
default_value: nil,
|
19
|
+
enum_values: [],
|
20
|
+
validations: []
|
21
|
+
)
|
22
|
+
@column_type = column_type
|
23
|
+
@filter_operators = filter_operators
|
24
|
+
@is_primary_key = is_primary_key
|
25
|
+
@is_read_only = is_read_only
|
26
|
+
@is_sortable = is_sortable
|
27
|
+
@default_value = default_value
|
28
|
+
@enum_values = enum_values
|
29
|
+
@validations = validations
|
30
|
+
@type = 'Column'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module ForestAdminDatasourceToolkit
|
2
|
+
module Schema
|
3
|
+
class PrimitiveType
|
4
|
+
BINARY = 'Binary'.freeze
|
5
|
+
|
6
|
+
BOOLEAN = 'Boolean'.freeze
|
7
|
+
|
8
|
+
DATE = 'Date'.freeze
|
9
|
+
|
10
|
+
DATEONLY = 'Dateonly'.freeze
|
11
|
+
|
12
|
+
ENUM = 'Enum'.freeze
|
13
|
+
|
14
|
+
JSON = 'Json'.freeze
|
15
|
+
|
16
|
+
NUMBER = 'Number'.freeze
|
17
|
+
|
18
|
+
POINT = 'Point'.freeze
|
19
|
+
|
20
|
+
STRING = 'String'.freeze
|
21
|
+
|
22
|
+
TIMEONLY = 'Timeonly'.freeze
|
23
|
+
|
24
|
+
UUID = 'Uuid'.freeze
|
25
|
+
|
26
|
+
def self.all
|
27
|
+
constants
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module ForestAdminDatasourceToolkit
|
2
|
+
module Schema
|
3
|
+
class RelationSchema
|
4
|
+
attr_accessor :foreign_collection
|
5
|
+
attr_reader :type
|
6
|
+
|
7
|
+
def initialize(foreign_collection, type)
|
8
|
+
@foreign_collection = foreign_collection
|
9
|
+
@type = type
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ForestAdminDatasourceToolkit
|
2
|
+
module Schema
|
3
|
+
module Relations
|
4
|
+
class ManyToManySchema < RelationSchema
|
5
|
+
attr_accessor :origin_key, :through_collection, :foreign_key
|
6
|
+
attr_reader :origin_key_target, :foreign_key_target
|
7
|
+
|
8
|
+
def initialize(
|
9
|
+
origin_key:,
|
10
|
+
origin_key_target:,
|
11
|
+
foreign_key:,
|
12
|
+
foreign_key_target:,
|
13
|
+
foreign_collection:,
|
14
|
+
through_collection:
|
15
|
+
)
|
16
|
+
super(foreign_collection, 'ManyToMany')
|
17
|
+
@origin_key = origin_key
|
18
|
+
@origin_key_target = origin_key_target
|
19
|
+
@through_collection = through_collection
|
20
|
+
@foreign_key = foreign_key
|
21
|
+
@foreign_key_target = foreign_key_target
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ForestAdminDatasourceToolkit
|
2
|
+
module Schema
|
3
|
+
module Relations
|
4
|
+
class ManyToOneSchema < RelationSchema
|
5
|
+
attr_accessor :foreign_key
|
6
|
+
attr_reader :foreign_key_target
|
7
|
+
|
8
|
+
def initialize(foreign_key:, foreign_key_target:, foreign_collection:)
|
9
|
+
super(foreign_collection, 'ManyToOne')
|
10
|
+
@foreign_key = foreign_key
|
11
|
+
@foreign_key_target = foreign_key_target
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ForestAdminDatasourceToolkit
|
2
|
+
module Schema
|
3
|
+
module Relations
|
4
|
+
class OneToManySchema < RelationSchema
|
5
|
+
attr_accessor :origin_key
|
6
|
+
attr_reader :origin_key_target
|
7
|
+
|
8
|
+
def initialize(origin_key:, origin_key_target:, foreign_collection:)
|
9
|
+
super(foreign_collection, 'OneToMany')
|
10
|
+
@origin_key = origin_key
|
11
|
+
@origin_key_target = origin_key_target
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ForestAdminDatasourceToolkit
|
2
|
+
module Schema
|
3
|
+
module Relations
|
4
|
+
class OneToOneSchema < RelationSchema
|
5
|
+
attr_accessor :origin_key
|
6
|
+
attr_reader :origin_key_target
|
7
|
+
|
8
|
+
def initialize(origin_key:, origin_key_target:, foreign_collection:)
|
9
|
+
super(foreign_collection, 'OneToOne')
|
10
|
+
@origin_key = origin_key
|
11
|
+
@origin_key_target = origin_key_target
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
module ForestAdminDatasourceToolkit
|
2
|
+
module Utils
|
3
|
+
class Collection
|
4
|
+
include ForestAdminDatasourceToolkit::Components::Query
|
5
|
+
include ForestAdminDatasourceToolkit::Schema
|
6
|
+
include ForestAdminDatasourceToolkit::Schema::Relations
|
7
|
+
include ForestAdminDatasourceToolkit::Exceptions
|
8
|
+
|
9
|
+
def self.get_inverse_relation(collection, relation_name)
|
10
|
+
relation_field = collection.fields[relation_name]
|
11
|
+
foreign_collection = collection.datasource.collection(relation_field.foreign_collection)
|
12
|
+
|
13
|
+
inverse = foreign_collection.fields.select do |_name, field|
|
14
|
+
field.is_a?(RelationSchema) &&
|
15
|
+
field.foreign_collection == collection.name &&
|
16
|
+
(
|
17
|
+
(field.is_a?(ManyToManySchema) &&
|
18
|
+
relation_field.is_a?(ManyToManySchema) &&
|
19
|
+
many_to_many_inverse?(field, relation_field)) ||
|
20
|
+
(field.is_a?(ManyToOneSchema) &&
|
21
|
+
(relation_field.is_a?(OneToOneSchema) || relation_field.is_a?(OneToManySchema)) &&
|
22
|
+
many_to_one_inverse?(field, relation_field)) ||
|
23
|
+
((field.is_a?(OneToOneSchema) || field.is_a?(OneToManySchema)) &&
|
24
|
+
relation_field.is_a?(ManyToOneSchema) && other_inverse?(field, relation_field))
|
25
|
+
)
|
26
|
+
end.keys.first
|
27
|
+
|
28
|
+
inverse || nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.many_to_many_inverse?(field, relation_field)
|
32
|
+
field.is_a?(ManyToManySchema) &&
|
33
|
+
relation_field.is_a?(ManyToManySchema) &&
|
34
|
+
field.origin_key == relation_field.foreign_key &&
|
35
|
+
field.through_collection == relation_field.through_collection &&
|
36
|
+
field.foreign_key == relation_field.origin_key
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.many_to_one_inverse?(field, relation_field)
|
40
|
+
field.is_a?(ManyToOneSchema) &&
|
41
|
+
(relation_field.is_a?(OneToManySchema) ||
|
42
|
+
relation_field.is_a?(OneToOneSchema)) &&
|
43
|
+
field.foreign_key == relation_field.origin_key
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.other_inverse?(field, relation_field)
|
47
|
+
(field.is_a?(OneToManySchema) || field.is_a?(OneToOneSchema)) &&
|
48
|
+
relation_field.is_a?(ManyToOneSchema) &&
|
49
|
+
field.origin_key == relation_field.foreign_key
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.get_field_schema(collection, field_name)
|
53
|
+
fields = collection.fields
|
54
|
+
unless field_name.include?(':')
|
55
|
+
raise ForestException, "Column not found #{collection.name}.#{field_name}" unless fields.key?(field_name)
|
56
|
+
|
57
|
+
return fields[field_name]
|
58
|
+
end
|
59
|
+
|
60
|
+
association_name = field_name.split(':')[0]
|
61
|
+
relation_schema = fields[association_name]
|
62
|
+
|
63
|
+
raise ForestException, "Relation not found #{collection.name}.#{association_name}" unless relation_schema
|
64
|
+
|
65
|
+
if relation_schema.type != 'ManyToOne' && relation_schema.type != 'OneToOne'
|
66
|
+
raise ForestException, "Unexpected field type #{relation_schema.type}: #{collection.name}.#{association_name}"
|
67
|
+
end
|
68
|
+
|
69
|
+
get_field_schema(
|
70
|
+
collection.datasource.collection(relation_schema.foreign_collection), field_name.split(':')[1..].join(':')
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.get_value(collection, caller, id, field)
|
75
|
+
if id.is_a? Array
|
76
|
+
index = Schema.primary_keys(collection).index(field)
|
77
|
+
|
78
|
+
return id[index] if index
|
79
|
+
elsif Schema.primary_keys(collection).include?(field)
|
80
|
+
return id[field]
|
81
|
+
end
|
82
|
+
|
83
|
+
record = collection.list(
|
84
|
+
caller,
|
85
|
+
ForestAdminDatasourceToolkit::Components::Query::Filter.new(condition_tree: ConditionTree::ConditionTreeFactory.match_ids(collection, [id])),
|
86
|
+
Projection.new([field])
|
87
|
+
)
|
88
|
+
|
89
|
+
record[field]
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.get_through_target(collection, relation_name)
|
93
|
+
relation = collection.fields[relation_name]
|
94
|
+
raise ForestException, 'Relation must be many to many' unless relation.is_a?(ManyToManySchema)
|
95
|
+
|
96
|
+
through_collection = collection.datasource.collection(relation.through_collection)
|
97
|
+
through_collection.fields.select do |field_name, field|
|
98
|
+
if field.is_a?(ManyToOneSchema) &&
|
99
|
+
field.foreign_collection == relation.foreign_collection &&
|
100
|
+
field.foreign_key == relation.foreign_key &&
|
101
|
+
field.foreign_key_target == relation.foreign_key_target
|
102
|
+
return field_name
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.list_relation(collection, id, relation_name, caller, foreign_filter, projection)
|
110
|
+
relation = collection.fields[relation_name]
|
111
|
+
foreign_collection = collection.datasource.collection(relation.foreign_collection)
|
112
|
+
|
113
|
+
if relation.is_a?(ManyToManySchema) && foreign_filter.nestable?
|
114
|
+
foreign_relation = get_through_target(collection, relation_name)
|
115
|
+
|
116
|
+
if foreign_relation
|
117
|
+
through_collection = collection.datasource.collection(relation.through_collection)
|
118
|
+
records = through_collection.list(
|
119
|
+
caller,
|
120
|
+
FilterFactory.make_through_filter(collection, id, relation_name, caller, foreign_filter),
|
121
|
+
projection.nest(prefix: foreign_relation)
|
122
|
+
)
|
123
|
+
|
124
|
+
return records.map { |r| r.try(foreign_relation) }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
foreign_collection.list(
|
129
|
+
caller,
|
130
|
+
FilterFactory.make_foreign_filter(collection, id, relation_name, caller, foreign_filter),
|
131
|
+
projection
|
132
|
+
)
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.aggregate_relation(collection, id, relation_name, caller, foreign_filter, aggregation, limit = nil)
|
136
|
+
relation = collection.fields[relation_name]
|
137
|
+
foreign_collection = collection.datasource.collection(relation.foreign_collection)
|
138
|
+
|
139
|
+
if relation.is_a?(ManyToManySchema) && foreign_filter.nestable?
|
140
|
+
foreign_relation = get_through_target(collection, relation_name)
|
141
|
+
if foreign_relation
|
142
|
+
through_collection = collection.datasource.collection(relation.through_collection)
|
143
|
+
|
144
|
+
return through_collection.aggregate(
|
145
|
+
caller,
|
146
|
+
FilterFactory.make_through_filter(collection, id, relation_name, caller, foreign_filter),
|
147
|
+
aggregation,
|
148
|
+
limit
|
149
|
+
)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
foreign_collection.aggregate(
|
154
|
+
caller,
|
155
|
+
FilterFactory.make_foreign_filter(collection, id, relation_name, caller, foreign_filter),
|
156
|
+
aggregation,
|
157
|
+
limit
|
158
|
+
)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ForestAdminDatasourceToolkit
|
2
|
+
module Utils
|
3
|
+
class Record
|
4
|
+
def self.primary_keys(collection, record)
|
5
|
+
Schema.primary_keys(collection).map do |pk|
|
6
|
+
record[pk] || raise(ForestAdminDatasourceToolkit::Exceptions::ForestException, "Missing primary key: #{pk}")
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.field_value(record, field)
|
11
|
+
path = field.split(':')
|
12
|
+
current = record
|
13
|
+
|
14
|
+
current = current[path.shift] while path.length.positive? && current
|
15
|
+
|
16
|
+
path.empty? ? current : nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ForestAdminDatasourceToolkit
|
2
|
+
module Utils
|
3
|
+
class Schema
|
4
|
+
def self.foreign_key?(collection, name)
|
5
|
+
field = collection.fields[name]
|
6
|
+
|
7
|
+
field.type == 'Column' &&
|
8
|
+
collection.fields.any? do |_key, relation|
|
9
|
+
relation.type == 'ManyToOne' && relation.foreign_key == name
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.primary_key?(collection, name)
|
14
|
+
field = collection.fields[name]
|
15
|
+
|
16
|
+
field.type == 'Column' && field.is_primary_key
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.primary_keys(collection)
|
20
|
+
collection.fields.keys.select do |field_name|
|
21
|
+
field = collection.fields[field_name]
|
22
|
+
field.type == 'Column' && field.is_primary_key
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.get_to_many_relation(collection, relation_name)
|
27
|
+
unless collection.fields.key?(relation_name)
|
28
|
+
raise Exceptions::ForestException, "Relation #{relation_name} not found"
|
29
|
+
end
|
30
|
+
|
31
|
+
relation = collection.fields[relation_name]
|
32
|
+
|
33
|
+
if relation.type != 'OneToMany' && relation.type != 'ManyToMany'
|
34
|
+
raise Exceptions::ForestException,
|
35
|
+
"Relation #{relation_name} has invalid type should be one of OneToMany or ManyToMany."
|
36
|
+
end
|
37
|
+
|
38
|
+
relation
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|