Class: ForestAdminDatasourceToolkit::Utils::Collection

Inherits:
Object
  • Object
show all
Includes:
Components::Query, Exceptions, Schema, Schema::Relations
Defined in:
lib/forest_admin_datasource_toolkit/utils/collection.rb

Class Method Summary collapse

Class Method Details

.aggregate_relation(collection, primary_key_values, relation_name, caller, foreign_filter, aggregation, limit = nil) ⇒ Object



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/forest_admin_datasource_toolkit/utils/collection.rb', line 175

def self.aggregate_relation(collection, primary_key_values, relation_name, caller, foreign_filter, aggregation, limit = nil)
  relation = collection.schema[:fields][relation_name]
  foreign_collection = collection.datasource.get_collection(relation.foreign_collection)

  if relation.is_a?(ManyToManySchema) && foreign_filter.nestable?
    foreign_relation = get_through_target(collection, relation_name)
    if foreign_relation
      through_collection = collection.datasource.get_collection(relation.through_collection)

      return through_collection.aggregate(
        caller,
        FilterFactory.make_through_filter(collection, primary_key_values, relation_name, caller, foreign_filter),
        aggregation,
        limit
      )
    end
  end

  foreign_collection.aggregate(
    caller,
    FilterFactory.make_foreign_filter(collection, primary_key_values, relation_name, caller, foreign_filter),
    aggregation,
    limit
  )
end

.assert_writable_relation!(field_name, relation) ⇒ Object

A read-only relation (e.g. #379's has_one :through identity join, which has no real join column and would corrupt the foreign collection's own primary key if written to) only hides its edit control in the UI via isReadOnly -- a direct write still has to be blocked here, at every route that can write to a relation's origin_key/through_collection. ValidationError (not a bare ForestException) so this maps to a 400, not a 500 -- rejecting a write to a non-editable field is a client error, not a server failure.

respond_to? guards PolymorphicManyToOneSchema specifically: it doesn't inherit RelationSchema (unlike every other relation type), so it has no is_read_only at all -- not a versioning gap, a genuinely different type with no such concept yet.

Raises:



91
92
93
94
95
# File 'lib/forest_admin_datasource_toolkit/utils/collection.rb', line 91

def self.assert_writable_relation!(field_name, relation)
  return unless relation.respond_to?(:is_read_only) && relation.is_read_only

  raise ValidationError, "Field #{field_name} is not editable"
end

.get_field_schema(collection, field_name) ⇒ Object

Raises:



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/forest_admin_datasource_toolkit/utils/collection.rb', line 59

def self.get_field_schema(collection, field_name)
  fields = collection.schema[:fields]
  unless field_name.include?(':')
    raise ForestException, "Column not found #{collection.name}.#{field_name}" unless fields.key?(field_name)

    return fields[field_name]
  end

  association_name = field_name.split(':')[0]
  relation_schema = fields[association_name]

  raise ForestException, "Relation not found #{collection.name}.#{association_name}" unless relation_schema

  if relation_schema.type != 'ManyToOne' && relation_schema.type != 'OneToOne'
    raise ForestException, "Unexpected field type #{relation_schema.type}: #{collection.name}.#{association_name}"
  end

  get_field_schema(
    collection.datasource.get_collection(relation_schema.foreign_collection), field_name.split(':')[1..].join(':')
  )
end

.get_inverse_relation(collection, relation_name) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/forest_admin_datasource_toolkit/utils/collection.rb', line 9

def self.get_inverse_relation(collection, relation_name)
  relation_field = collection.schema[:fields][relation_name]
  foreign_collection = collection.datasource.get_collection(relation_field.foreign_collection)
  polymorphic_relations = %w[PolymorphicOneToOne PolymorphicOneToMany]

  inverse = foreign_collection.schema[:fields].select do |_name, field|
    if polymorphic_relations.include?(relation_field.type)
      field.is_a?(PolymorphicManyToOneSchema) &&
        field.foreign_key_type_field == relation_field.origin_type_field &&
        field.foreign_key == relation_field.origin_key &&
        field.foreign_collections.include?(collection.name)
    else
      field.is_a?(RelationSchema) &&
        field.foreign_collection == collection.name &&
        (
          (field.is_a?(ManyToManySchema) && relation_field.is_a?(ManyToManySchema) &&
            many_to_many_inverse?(field, relation_field)) ||
            (field.is_a?(ManyToOneSchema) &&
              (relation_field.is_a?(OneToOneSchema) || relation_field.is_a?(OneToManySchema)) &&
              many_to_one_inverse?(field, relation_field)) ||
            ((field.is_a?(OneToOneSchema) || field.is_a?(OneToManySchema)) &&
              relation_field.is_a?(ManyToOneSchema) && other_inverse?(field, relation_field))
        )
    end
  end.keys.first

  inverse || nil
end

.get_through_origin(collection, relation_name) ⇒ Object

Raises:



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/forest_admin_datasource_toolkit/utils/collection.rb', line 132

def self.get_through_origin(collection, relation_name)
  relation = collection.schema[:fields][relation_name]
  raise ForestException, 'Relation must be many to many' unless relation.is_a?(ManyToManySchema)

  through_collection = collection.datasource.get_collection(relation.through_collection)
  through_collection.schema[:fields].select do |field_name, field|
    if field.is_a?(ManyToOneSchema) &&
       field.foreign_collection == collection.name &&
       field.foreign_key == relation.origin_key &&
       field.foreign_key_target == relation.origin_key_target
      return field_name
    end
  end

  nil
end

.get_through_target(collection, relation_name) ⇒ Object

Raises:



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/forest_admin_datasource_toolkit/utils/collection.rb', line 115

def self.get_through_target(collection, relation_name)
  relation = collection.schema[:fields][relation_name]
  raise ForestException, 'Relation must be many to many' unless relation.is_a?(ManyToManySchema)

  through_collection = collection.datasource.get_collection(relation.through_collection)
  through_collection.schema[:fields].select do |field_name, field|
    if field.is_a?(ManyToOneSchema) &&
       field.foreign_collection == relation.foreign_collection &&
       field.foreign_key == relation.foreign_key &&
       field.foreign_key_target == relation.foreign_key_target
      return field_name
    end
  end

  nil
end

.get_value(collection, caller, primary_key_values, field) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/forest_admin_datasource_toolkit/utils/collection.rb', line 97

def self.get_value(collection, caller, primary_key_values, field)
  if primary_key_values.is_a? Array
    index = Schema.primary_keys(collection).index(field)

    return primary_key_values[index] if index
  elsif Schema.primary_keys(collection).include?(field)
    return primary_key_values[field]
  end

  records = collection.list(
    caller,
    ForestAdminDatasourceToolkit::Components::Query::Filter.new(condition_tree: ConditionTree::ConditionTreeFactory.match_ids(collection, [primary_key_values])),
    Projection.new([field])
  )

  records.first&.[](field)
end

.list_relation(collection, primary_key_values, relation_name, caller, foreign_filter, projection) ⇒ Object



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/forest_admin_datasource_toolkit/utils/collection.rb', line 149

def self.list_relation(collection, primary_key_values, relation_name, caller, foreign_filter, projection)
  relation = collection.schema[:fields][relation_name]
  foreign_collection = collection.datasource.get_collection(relation.foreign_collection)

  if relation.is_a?(ManyToManySchema) && foreign_filter.nestable?
    foreign_relation = get_through_target(collection, relation_name)

    if foreign_relation
      through_collection = collection.datasource.get_collection(relation.through_collection)
      records = through_collection.list(
        caller,
        FilterFactory.make_through_filter(collection, primary_key_values, relation_name, caller, foreign_filter),
        projection.nest(prefix: foreign_relation)
      )

      return records.map { |r| r[foreign_relation] }
    end
  end

  foreign_collection.list(
    caller,
    FilterFactory.make_foreign_filter(collection, primary_key_values, relation_name, caller, foreign_filter),
    projection
  )
end

.many_to_many_inverse?(field, relation_field) ⇒ Boolean



38
39
40
41
42
43
44
# File 'lib/forest_admin_datasource_toolkit/utils/collection.rb', line 38

def self.many_to_many_inverse?(field, relation_field)
  field.is_a?(ManyToManySchema) &&
    relation_field.is_a?(ManyToManySchema) &&
    field.origin_key == relation_field.foreign_key &&
    field.through_collection == relation_field.through_collection &&
    field.foreign_key == relation_field.origin_key
end

.many_to_one_inverse?(field, relation_field) ⇒ Boolean



46
47
48
49
50
51
# File 'lib/forest_admin_datasource_toolkit/utils/collection.rb', line 46

def self.many_to_one_inverse?(field, relation_field)
  field.is_a?(ManyToOneSchema) &&
    (relation_field.is_a?(OneToManySchema) ||
      relation_field.is_a?(OneToOneSchema)) &&
    field.foreign_key == relation_field.origin_key
end

.other_inverse?(field, relation_field) ⇒ Boolean



53
54
55
56
57
# File 'lib/forest_admin_datasource_toolkit/utils/collection.rb', line 53

def self.other_inverse?(field, relation_field)
  (field.is_a?(OneToManySchema) || field.is_a?(OneToOneSchema)) &&
    relation_field.is_a?(ManyToOneSchema) &&
    field.origin_key == relation_field.foreign_key
end