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.
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