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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/README.md +31 -0
  4. data/Rakefile +12 -0
  5. data/forest_admin_datasource_toolkit.gemspec +37 -0
  6. data/lib/forest_admin_datasource_toolkit/collection.rb +50 -0
  7. data/lib/forest_admin_datasource_toolkit/components/caller.rb +31 -0
  8. data/lib/forest_admin_datasource_toolkit/components/contracts/collection_contract.rb +51 -0
  9. data/lib/forest_admin_datasource_toolkit/components/contracts/datasource_contract.rb +27 -0
  10. data/lib/forest_admin_datasource_toolkit/components/query/aggregation.rb +23 -0
  11. data/lib/forest_admin_datasource_toolkit/components/query/condition_tree/condition_tree_equivalent.rb +66 -0
  12. data/lib/forest_admin_datasource_toolkit/components/query/condition_tree/condition_tree_factory.rb +123 -0
  13. data/lib/forest_admin_datasource_toolkit/components/query/condition_tree/nodes/condition_tree.rb +70 -0
  14. data/lib/forest_admin_datasource_toolkit/components/query/condition_tree/nodes/condition_tree_branch.rb +68 -0
  15. data/lib/forest_admin_datasource_toolkit/components/query/condition_tree/nodes/condition_tree_leaf.rb +144 -0
  16. data/lib/forest_admin_datasource_toolkit/components/query/condition_tree/operators.rb +100 -0
  17. data/lib/forest_admin_datasource_toolkit/components/query/condition_tree/transforms/comparisons.rb +123 -0
  18. data/lib/forest_admin_datasource_toolkit/components/query/condition_tree/transforms/pattern.rb +48 -0
  19. data/lib/forest_admin_datasource_toolkit/components/query/condition_tree/transforms/times.rb +112 -0
  20. data/lib/forest_admin_datasource_toolkit/components/query/filter.rb +45 -0
  21. data/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb +158 -0
  22. data/lib/forest_admin_datasource_toolkit/components/query/page.rb +14 -0
  23. data/lib/forest_admin_datasource_toolkit/components/query/projection.rb +42 -0
  24. data/lib/forest_admin_datasource_toolkit/components/query/projection_factory.rb +30 -0
  25. data/lib/forest_admin_datasource_toolkit/datasource.rb +29 -0
  26. data/lib/forest_admin_datasource_toolkit/exceptions/forest_exception.rb +10 -0
  27. data/lib/forest_admin_datasource_toolkit/schema/column_schema.rb +34 -0
  28. data/lib/forest_admin_datasource_toolkit/schema/primitive_type.rb +31 -0
  29. data/lib/forest_admin_datasource_toolkit/schema/relation_schema.rb +13 -0
  30. data/lib/forest_admin_datasource_toolkit/schema/relations/many_to_many_schema.rb +26 -0
  31. data/lib/forest_admin_datasource_toolkit/schema/relations/many_to_one_schema.rb +16 -0
  32. data/lib/forest_admin_datasource_toolkit/schema/relations/one_to_many_schema.rb +16 -0
  33. data/lib/forest_admin_datasource_toolkit/schema/relations/one_to_one_schema.rb +16 -0
  34. data/lib/forest_admin_datasource_toolkit/utils/collection.rb +162 -0
  35. data/lib/forest_admin_datasource_toolkit/utils/record.rb +20 -0
  36. data/lib/forest_admin_datasource_toolkit/utils/schema.rb +42 -0
  37. data/lib/forest_admin_datasource_toolkit/version.rb +3 -0
  38. data/lib/forest_admin_datasource_toolkit.rb +10 -0
  39. data/sig/forest_admin_datasource_toolkit/collection.rbs +26 -0
  40. data/sig/forest_admin_datasource_toolkit/components/contracts/collection_contract.rbs +29 -0
  41. data/sig/forest_admin_datasource_toolkit/components/contracts/datasource_contract.rbs +17 -0
  42. data/sig/forest_admin_datasource_toolkit/datasource.rbs +12 -0
  43. data/sig/forest_admin_datasource_toolkit/schema/column_schema.rbs +15 -0
  44. data/sig/forest_admin_datasource_toolkit/schema/relation_schema.rbs +8 -0
  45. data/sig/forest_admin_datasource_toolkit/schema/relations/many_relation_schema.rbs +10 -0
  46. data/sig/forest_admin_datasource_toolkit/schema/relations/many_to_many_schema.rbs +11 -0
  47. data/sig/forest_admin_datasource_toolkit/schema/relations/single_relation_schema.rbs +10 -0
  48. data/sig/forest_admin_datasource_toolkit.rbs +4 -0
  49. 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,14 @@
1
+ module ForestAdminDatasourceToolkit
2
+ module Components
3
+ module Query
4
+ class Page
5
+ attr_reader :offset, :limit
6
+
7
+ def initialize(offset:, limit:)
8
+ @offset = offset
9
+ @limit = limit
10
+ end
11
+ end
12
+ end
13
+ end
14
+ 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,10 @@
1
+ module ForestAdminDatasourceToolkit
2
+ module Exceptions
3
+ class ForestException < RuntimeError
4
+ def initialize(msg = '')
5
+ msg = "🌳🌳🌳 #{msg}"
6
+ super msg
7
+ end
8
+ end
9
+ end
10
+ 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
@@ -0,0 +1,3 @@
1
+ module ForestAdminDatasourceToolkit
2
+ VERSION = "1.0.0-beta.21"
3
+ end