forest_admin_datasource_toolkit 1.0.0.pre.beta.21
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 +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
|