forest_liana 8.0.16 → 8.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/app/deserializers/forest_liana/resource_deserializer.rb +6 -1
  3. data/app/helpers/forest_liana/query_helper.rb +22 -5
  4. data/app/serializers/forest_liana/serializer_factory.rb +17 -1
  5. data/app/services/forest_liana/apimap_sorter.rb +1 -0
  6. data/app/services/forest_liana/base_getter.rb +13 -6
  7. data/app/services/forest_liana/belongs_to_updater.rb +10 -1
  8. data/app/services/forest_liana/has_many_getter.rb +14 -8
  9. data/app/services/forest_liana/schema_adapter.rb +37 -1
  10. data/app/services/forest_liana/schema_utils.rb +26 -3
  11. data/app/services/forest_liana/stat_getter.rb +8 -0
  12. data/app/services/forest_liana/value_stat_getter.rb +7 -6
  13. data/lib/forest_liana/bootstrapper.rb +6 -1
  14. data/lib/forest_liana/schema_file_updater.rb +1 -0
  15. data/lib/forest_liana/version.rb +1 -1
  16. data/lib/tasks/send_apimap.rake +1 -1
  17. data/spec/dummy/app/models/address.rb +5 -0
  18. data/spec/dummy/app/models/user.rb +1 -0
  19. data/spec/dummy/db/migrate/20231117084236_create_addresses.rb +12 -0
  20. data/spec/dummy/db/schema.rb +12 -1
  21. data/spec/helpers/forest_liana/query_helper_spec.rb +25 -1
  22. data/spec/lib/forest_liana/bootstrapper_spec.rb +1 -0
  23. data/spec/lib/forest_liana/schema_file_updater_spec.rb +35 -0
  24. data/spec/requests/resources_spec.rb +84 -0
  25. data/spec/services/forest_liana/line_stat_getter_spec.rb +13 -0
  26. data/spec/services/forest_liana/pie_stat_getter_spec.rb +90 -67
  27. data/spec/services/forest_liana/schema_adapter_spec.rb +58 -0
  28. data/spec/services/forest_liana/value_stat_getter_spec.rb +78 -55
  29. data/spec/spec_helper.rb +2 -0
  30. metadata +6 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d66f322c9f51d6beebaa2eb338b8f568f7d12bc781d71c4b47a8c2c3e6503c73
4
- data.tar.gz: 37db17c392bac96b35c43595c03ef702e840ae40e420538151d36edc2a347c68
3
+ metadata.gz: e051d1446dd0f16749a9d078bb0caee6ff1b93c37557b5fdc0571fca710137f8
4
+ data.tar.gz: 90db1924c8d20ef02b3525eb84bba26ed03025fe042b4c6405d83ebaf3ac2e23
5
5
  SHA512:
6
- metadata.gz: 9466f6b82dc0ffbfae007bfc0024e5b831172dacdffcf00e51c58a8ca2333dbbefb165227a3d36b704954b1cdb75ecdfb9a5f2605840572220e612226e6cb39d
7
- data.tar.gz: 5c313811aead7c0617d1e5d5d9c424f66e56b0482a46dc8354a1714b59f61490a728cc1cacb9224553110b0577b68712ba62366e8b214d6c7bddb2daffde4c7d
6
+ metadata.gz: 87d40012a5f767ebc6faf75c5cb9826d50afc536837daad7f578f85ecf8b6e4c82f20d2c4eb246a4a3197e7e8d0ef6ad294362dccd5100b1194ce3897fda8044
7
+ data.tar.gz: 3d24e8527ae65ec0945ebd309a4958257ee4b12792d78f41e72427baea073333f4630756dc48254fef347ad467976a5bddeddf85d823432c94f34a4f8e465eb5
@@ -63,7 +63,12 @@ module ForestLiana
63
63
  # ActionController::Parameters do not inherit from Hash anymore
64
64
  # since Rails 5.
65
65
  if (data.is_a?(Hash) || data.is_a?(ActionController::Parameters)) && data[:id]
66
- @attributes[name] = association.klass.find(data[:id])
66
+ if (SchemaUtils.polymorphic?(association))
67
+ @attributes[association.foreign_key] = data[:id]
68
+ @attributes[association.foreign_type] = data[:type]
69
+ else
70
+ @attributes[name] = association.klass.find(data[:id])
71
+ end
67
72
  elsif data.blank?
68
73
  @attributes[name] = nil
69
74
  end
@@ -1,8 +1,16 @@
1
1
  module ForestLiana
2
2
  module QueryHelper
3
3
  def self.get_one_associations(resource)
4
- SchemaUtils.one_associations(resource)
5
- .select { |association| SchemaUtils.model_included?(association.klass) }
4
+ associations = SchemaUtils.one_associations(resource)
5
+ .select do |association|
6
+ if SchemaUtils.polymorphic?(association)
7
+ SchemaUtils.polymorphic_models(association).all? { |model| SchemaUtils.model_included?(model) }
8
+ else
9
+ SchemaUtils.model_included?(association.klass)
10
+ end
11
+ end
12
+
13
+ associations
6
14
  end
7
15
 
8
16
  def self.get_one_association_names_symbol(resource)
@@ -18,10 +26,19 @@ module ForestLiana
18
26
  associations_has_one = self.get_one_associations(resource)
19
27
 
20
28
  associations_has_one.each do |association|
21
- if tables_associated_to_relations_name[association.table_name].nil?
22
- tables_associated_to_relations_name[association.table_name] = []
29
+ if SchemaUtils.polymorphic?(association)
30
+ SchemaUtils.polymorphic_models(association).each do |model|
31
+ if tables_associated_to_relations_name[model.table_name].nil?
32
+ tables_associated_to_relations_name[model.table_name] = []
33
+ end
34
+ tables_associated_to_relations_name[model.table_name] << association.name
35
+ end
36
+ else
37
+ if tables_associated_to_relations_name[association.try(:table_name)].nil?
38
+ tables_associated_to_relations_name[association.table_name] = []
39
+ end
40
+ tables_associated_to_relations_name[association.table_name] << association.name
23
41
  end
24
- tables_associated_to_relations_name[association.table_name] << association.name
25
42
  end
26
43
 
27
44
  tables_associated_to_relations_name
@@ -265,7 +265,19 @@ module ForestLiana
265
265
 
266
266
  SchemaUtils.associations(active_record_class).each do |a|
267
267
  begin
268
- if SchemaUtils.model_included?(a.klass)
268
+ if SchemaUtils.polymorphic?(a)
269
+ serializer.send(serializer_association(a), a.name) {
270
+ if [:has_one, :belongs_to].include?(a.macro)
271
+ begin
272
+ object.send(a.name)
273
+ rescue ActiveRecord::RecordNotFound
274
+ nil
275
+ end
276
+ else
277
+ []
278
+ end
279
+ }
280
+ elsif SchemaUtils.model_included?(a.klass)
269
281
  serializer.send(serializer_association(a), a.name) {
270
282
  if [:has_one, :belongs_to].include?(a.macro)
271
283
  begin
@@ -369,6 +381,7 @@ module ForestLiana
369
381
 
370
382
  def attributes(active_record_class)
371
383
  return [] if @is_smart_collection
384
+
372
385
  active_record_class.column_names.select do |column_name|
373
386
  !association?(active_record_class, column_name)
374
387
  end
@@ -410,6 +423,9 @@ module ForestLiana
410
423
  def foreign_keys(active_record_class)
411
424
  begin
412
425
  SchemaUtils.belongs_to_associations(active_record_class).map(&:foreign_key)
426
+ SchemaUtils.belongs_to_associations(active_record_class)
427
+ .select { |association| !SchemaUtils.polymorphic?(association) }
428
+ .map(&:foreign_key)
413
429
  rescue => err
414
430
  # Association foreign_key triggers an error. Put the stacktrace and
415
431
  # returns no foreign keys.
@@ -29,6 +29,7 @@ module ForestLiana
29
29
  'relationship',
30
30
  'widget',
31
31
  'validations',
32
+ 'polymorphic_referenced_models',
32
33
  ]
33
34
  KEYS_ACTION = [
34
35
  'name',
@@ -33,16 +33,23 @@ module ForestLiana
33
33
  def optimize_record_loading(resource, records)
34
34
  instance_dependent_associations = instance_dependent_associations(resource)
35
35
 
36
+ polymorphic = []
36
37
  preload_loads = @includes.select do |name|
37
- targetModelConnection = resource.reflect_on_association(name).klass.connection
38
- targetModelDatabase = targetModelConnection.current_database if targetModelConnection.respond_to? :current_database
39
- resourceConnection = resource.connection
40
- resourceDatabase = resourceConnection.current_database if resourceConnection.respond_to? :current_database
38
+ association = resource.reflect_on_association(name)
39
+ if SchemaUtils.polymorphic?(association)
40
+ polymorphic << association.name
41
+ false
42
+ else
43
+ targetModelConnection = association.klass.connection
44
+ targetModelDatabase = targetModelConnection.current_database if targetModelConnection.respond_to? :current_database
45
+ resourceConnection = resource.connection
46
+ resourceDatabase = resourceConnection.current_database if resourceConnection.respond_to? :current_database
41
47
 
42
- targetModelDatabase != resourceDatabase
48
+ targetModelDatabase != resourceDatabase
49
+ end
43
50
  end + instance_dependent_associations
44
51
 
45
- result = records.eager_load(@includes - preload_loads)
52
+ result = records.eager_load(@includes - preload_loads - polymorphic)
46
53
 
47
54
  # Rails 7 can mix `eager_load` and `preload` in the same scope
48
55
  # Rails 6 cannot mix `eager_load` and `preload` in the same scope
@@ -13,7 +13,16 @@ module ForestLiana
13
13
  def perform
14
14
  begin
15
15
  @record = @resource.find(@params[:id])
16
- new_value = @association.klass.find(@data[:id]) if @data && @data[:id]
16
+ if (SchemaUtils.polymorphic?(@association))
17
+ if @data.nil?
18
+ new_value = nil
19
+ else
20
+ association_klass = SchemaUtils.polymorphic_models(@association).select { |a| a.name.downcase == @data[:type] }.first
21
+ new_value = association_klass.find(@data[:id]) if @data && @data[:id]
22
+ end
23
+ else
24
+ new_value = @association.klass.find(@data[:id]) if @data && @data[:id]
25
+ end
17
26
  @record.send("#{@association.name}=", new_value)
18
27
 
19
28
  @record.save
@@ -40,17 +40,23 @@ module ForestLiana
40
40
  @includes = @association.klass
41
41
  .reflect_on_all_associations
42
42
  .select do |association|
43
- inclusion = !association.options[:polymorphic] &&
44
- SchemaUtils.model_included?(association.klass) &&
45
- [:belongs_to, :has_and_belongs_to_many].include?(association.macro)
46
43
 
47
- if @field_names_requested
48
- inclusion && @field_names_requested.include?(association.name)
44
+ if SchemaUtils.polymorphic?(association)
45
+ inclusion = SchemaUtils.polymorphic_models(association)
46
+ .all? { |model| SchemaUtils.model_included?(model) } &&
47
+ [:belongs_to, :has_and_belongs_to_many].include?(association.macro)
49
48
  else
50
- inclusion
49
+ inclusion = SchemaUtils.model_included?(association.klass) &&
50
+ [:belongs_to, :has_and_belongs_to_many].include?(association.macro)
51
51
  end
52
- end
53
- .map { |association| association.name.to_s }
52
+
53
+ if @field_names_requested
54
+ inclusion && @field_names_requested.include?(association.name)
55
+ else
56
+ inclusion
57
+ end
58
+ end
59
+ .map { |association| association.name }
54
60
  end
55
61
 
56
62
  def field_names_requested
@@ -241,8 +241,33 @@ module ForestLiana
241
241
  def add_associations
242
242
  SchemaUtils.associations(@model).each do |association|
243
243
  begin
244
+ if SchemaUtils.polymorphic?(association) &&
245
+ (ENV['ENABLE_SUPPORT_POLYMORPHISM'].present? && ENV['ENABLE_SUPPORT_POLYMORPHISM'].downcase == 'true')
246
+
247
+ collection.fields << {
248
+ field: association.name.to_s,
249
+ type: get_type_for_association(association),
250
+ relationship: get_relationship_type(association),
251
+ reference: "#{association.name.to_s}.id",
252
+ inverse_of: @model.name.demodulize.underscore,
253
+ is_filterable: false,
254
+ is_sortable: true,
255
+ is_read_only: false,
256
+ is_required: false,
257
+ is_virtual: false,
258
+ default_value: nil,
259
+ integration: nil,
260
+ relationships: nil,
261
+ widget: nil,
262
+ validations: [],
263
+ polymorphic_referenced_models: get_polymorphic_types(association)
264
+ }
265
+
266
+ collection.fields = collection.fields.reject do |field|
267
+ field[:field] == association.foreign_key || field[:field] == association.foreign_type
268
+ end
244
269
  # NOTICE: Delete the association if the targeted model is excluded.
245
- if !SchemaUtils.model_included?(association.klass)
270
+ elsif !SchemaUtils.model_included?(association.klass)
246
271
  field = collection.fields.find do |x|
247
272
  x[:field] == association.foreign_key
248
273
  end
@@ -275,6 +300,17 @@ module ForestLiana
275
300
  automatic_inverse_of(association)
276
301
  end
277
302
 
303
+ def get_polymorphic_types(relation)
304
+ types = []
305
+ ForestLiana.models.each do |model|
306
+ unless model.reflect_on_all_associations.select { |association| association.options[:as] == relation.name.to_sym }.empty?
307
+ types << model.name
308
+ end
309
+ end
310
+
311
+ types
312
+ end
313
+
278
314
  def automatic_inverse_of(association)
279
315
  name = association.active_record.name.demodulize.underscore
280
316
 
@@ -4,7 +4,12 @@ module ForestLiana
4
4
  def self.associations(active_record_class)
5
5
  active_record_class.reflect_on_all_associations.select do |association|
6
6
  begin
7
- !polymorphic?(association) && !is_active_type?(association.klass)
7
+ if (ENV['ENABLE_SUPPORT_POLYMORPHISM'].present? && ENV['ENABLE_SUPPORT_POLYMORPHISM'].downcase == 'true')
8
+ polymorphic?(association) ? true : !is_active_type?(association.klass)
9
+ else
10
+ !polymorphic?(association) && !is_active_type?(association.klass)
11
+ end
12
+
8
13
  rescue
9
14
  FOREST_LOGGER.warn "Unknown association #{association.name} on class #{active_record_class.name}"
10
15
  false
@@ -53,12 +58,30 @@ module ForestLiana
53
58
  ActiveRecord::Base.connection.tables
54
59
  end
55
60
 
56
- private
57
-
58
61
  def self.polymorphic?(association)
59
62
  association.options[:polymorphic]
60
63
  end
61
64
 
65
+ def self.klass(association)
66
+ return association.klass unless polymorphic?(association)
67
+
68
+
69
+ end
70
+
71
+ def self.polymorphic_models(relation)
72
+ models = []
73
+ ForestLiana.models.each do |model|
74
+ unless model.reflect_on_all_associations.select { |association| association.options[:as] == relation.name.to_sym }.empty?
75
+ models << model
76
+ end
77
+ end
78
+
79
+ models
80
+ end
81
+
82
+
83
+ private
84
+
62
85
  def self.find_model_from_abstract_class(abstract_class, collection_name)
63
86
  abstract_class.subclasses.find do |subclass|
64
87
  if subclass.abstract_class?
@@ -6,9 +6,17 @@ module ForestLiana
6
6
  @resource = resource
7
7
  @params = params
8
8
  @user = forest_user
9
+
10
+ validate_params
9
11
  compute_includes
10
12
  end
11
13
 
14
+ def validate_params
15
+ if @params.key?(:aggregator) && !%w[count sum avg max min].include?(@params[:aggregator].downcase)
16
+ raise ForestLiana::Errors::HTTP422Error.new('Invalid aggregate function')
17
+ end
18
+ end
19
+
12
20
  def get_resource
13
21
  super
14
22
  @resource.reorder('')
@@ -19,24 +19,25 @@ module ForestLiana
19
19
  end
20
20
 
21
21
  @record = Model::Stat.new(value: {
22
- countCurrent: count(resource),
23
- countPrevious: previous_value ? count(previous_value) : nil
22
+ countCurrent: aggregate(resource),
23
+ countPrevious: previous_value ? aggregate(previous_value) : nil
24
24
  })
25
25
  end
26
26
 
27
27
  private
28
28
 
29
- def count(value)
30
- uniq = @params[:aggregator].downcase == 'count'
29
+ def aggregate(value)
30
+ aggregator = @params[:aggregator].downcase
31
+ uniq = aggregator == 'count'
31
32
 
32
33
  if Rails::VERSION::MAJOR >= 4
33
34
  if uniq
34
35
  # NOTICE: uniq is deprecated since Rails 5.0
35
36
  value = Rails::VERSION::MAJOR >= 5 ? value.distinct : value.uniq
36
37
  end
37
- value.send(@params[:aggregator].downcase, aggregate_field)
38
+ value.send(aggregator, aggregate_field)
38
39
  else
39
- value.send(@params[:aggregator].downcase, aggregate_field, distinct: uniq)
40
+ value.send(aggregator, aggregate_field, distinct: uniq)
40
41
  end
41
42
  end
42
43
 
@@ -5,7 +5,12 @@ module ForestLiana
5
5
  class Bootstrapper
6
6
  SCHEMA_FILENAME = File.join(Dir.pwd, '.forestadmin-schema.json')
7
7
 
8
- def initialize
8
+ def initialize(reset_api_map = false)
9
+ if reset_api_map
10
+ ForestLiana.apimap = []
11
+ ForestLiana.models = []
12
+ end
13
+
9
14
  @integration_stripe_valid = false
10
15
  @integration_intercom_valid = false
11
16
 
@@ -35,6 +35,7 @@ module ForestLiana
35
35
  'relationship',
36
36
  'widget',
37
37
  'validations',
38
+ 'polymorphic_referenced_models',
38
39
  ]
39
40
  KEYS_VALIDATION = [
40
41
  'message',
@@ -1,3 +1,3 @@
1
1
  module ForestLiana
2
- VERSION = "8.0.16"
2
+ VERSION = "8.1.0"
3
3
  end
@@ -3,7 +3,7 @@ namespace :forest do
3
3
  task(:send_apimap).clear
4
4
  task send_apimap: :environment do
5
5
  if ForestLiana.env_secret
6
- bootstrapper = ForestLiana::Bootstrapper.new
6
+ bootstrapper = ForestLiana::Bootstrapper.new(true)
7
7
  bootstrapper.synchronize(true)
8
8
  else
9
9
  puts 'Cannot send the Apimap, Forest cannot find your env_secret'
@@ -0,0 +1,5 @@
1
+ class Address < ActiveRecord::Base
2
+ self.table_name = 'addresses'
3
+
4
+ belongs_to :addressable, polymorphic: true
5
+ end
@@ -1,6 +1,7 @@
1
1
  class User < ActiveRecord::Base
2
2
  has_many :trees_owned, class_name: 'Tree', inverse_of: :owner
3
3
  has_many :trees_cut, class_name: 'Tree', inverse_of: :cutter
4
+ has_many :addresses, as: :addressable
4
5
 
5
6
  enum title: [ :king, :villager, :outlaw ]
6
7
  end
@@ -0,0 +1,12 @@
1
+ class CreateAddresses < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :addresses do |t|
4
+ t.string :line1
5
+ t.string :city
6
+ t.string :zipcode
7
+ t.references :addressable, polymorphic: true, null: false
8
+
9
+ t.timestamps
10
+ end
11
+ end
12
+ end
@@ -10,7 +10,18 @@
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- ActiveRecord::Schema.define(version: 2022_07_27_114930) do
13
+ ActiveRecord::Schema.define(version: 2023_11_17_084236) do
14
+
15
+ create_table "addresses", force: :cascade do |t|
16
+ t.string "line1"
17
+ t.string "city"
18
+ t.string "zipcode"
19
+ t.string "addressable_type", null: false
20
+ t.integer "addressable_id", null: false
21
+ t.datetime "created_at", precision: 6, null: false
22
+ t.datetime "updated_at", precision: 6, null: false
23
+ t.index ["addressable_type", "addressable_id"], name: "index_addresses_on_addressable_type_and_addressable_id"
24
+ end
14
25
 
15
26
  create_table "isle", force: :cascade do |t|
16
27
  t.string "name"
@@ -3,6 +3,7 @@ module ForestLiana
3
3
  before(:all) do
4
4
  Tree.connection
5
5
  User.connection
6
+ Address.connection
6
7
  Island.connection
7
8
  end
8
9
 
@@ -14,6 +15,21 @@ module ForestLiana
14
15
  end
15
16
  end
16
17
 
18
+ context 'on a model having 1 polymorphic association' do
19
+ it 'should return the association' do
20
+ associations = QueryHelper.get_one_associations(Address)
21
+ expect(associations).to eq(Address.reflect_on_all_associations(:belongs_to))
22
+ end
23
+
24
+ it 'should return 0 association when one of referenced model was excluded' do
25
+ allow(ForestLiana).to receive(:excluded_models).and_return(['User'])
26
+ associations = QueryHelper.get_one_associations(Address)
27
+
28
+ expect(associations.length).to eq(0)
29
+ end
30
+ end
31
+
32
+
17
33
  context 'on a model having some belongsTo associations' do
18
34
  let(:expected_association_attributes) do
19
35
  [
@@ -75,7 +91,15 @@ module ForestLiana
75
91
  expect(tables_associated_to_relations_name['isle'].second).to eq(:eponymous_island)
76
92
  end
77
93
  end
78
- end
79
94
 
95
+ context 'on a model having polymorphic association' do
96
+ tables_associated_to_relations_name =
97
+ QueryHelper.get_tables_associated_to_relations_name(Address)
98
+
99
+ it 'should return the one-one associations' do
100
+ expect(tables_associated_to_relations_name.keys.length).to eq(1)
101
+ end
102
+ end
103
+ end
80
104
  end
81
105
  end
@@ -23,6 +23,7 @@ module ForestLiana
23
23
 
24
24
  let(:expected_application_models) do
25
25
  [
26
+ Address,
26
27
  Island,
27
28
  Location,
28
29
  Manufacturer,
@@ -10,6 +10,41 @@ module ForestLiana
10
10
  end
11
11
 
12
12
  describe "with a given collection" do
13
+ describe "when the collection has a polymorphic relation" do
14
+ it "should save the relation" do
15
+ collections = [
16
+ {
17
+ "name" => "Address",
18
+ "fields" => [
19
+ {
20
+ "field" => "addressable",
21
+ "type" => "Number",
22
+ "relationship" => "BelongsTo",
23
+ "reference" => "addressable.id",
24
+ "inverse_of" => "address",
25
+ "is_filterable" => false,
26
+ "is_sortable" => true,
27
+ "is_read_only" => false,
28
+ "is_required" => false,
29
+ "is_virtual" => false,
30
+ "default_value" => nil,
31
+ "integration" => nil,
32
+ "relationships" => nil,
33
+ "widget" => nil,
34
+ "validations" => [],
35
+ "polymorphic_referenced_models" => ["User"]
36
+ },
37
+ ],
38
+ "actions" => [],
39
+ "segments" => []
40
+ }
41
+ ]
42
+ schema_file_updater = ForestLiana::SchemaFileUpdater.new("test.txt", collections, {})
43
+ expect(schema_file_updater.instance_variable_get(:@collections))
44
+ .to eq(collections)
45
+ end
46
+ end
47
+
13
48
  describe "when the collection has a smart action action" do
14
49
  it "should save the smart action" do
15
50
  collections = [{
@@ -154,3 +154,87 @@ describe 'Requesting Tree resources', :type => :request do
154
154
  end
155
155
  end
156
156
  end
157
+
158
+ describe 'Requesting Address resources', :type => :request do
159
+
160
+ before do
161
+ user = User.create(name: 'Michel')
162
+ address = Address.create(line1: '10 Downing Street', city: 'London', zipcode: '2AB', addressable: user)
163
+
164
+ Rails.cache.write('forest.users', {'1' => { 'id' => 1, 'roleId' => 1, 'rendering_id' => '1' }})
165
+ Rails.cache.write('forest.has_permission', true)
166
+ Rails.cache.write(
167
+ 'forest.collections',
168
+ {
169
+ 'Address' => {
170
+ 'browse' => [1],
171
+ 'read' => [1],
172
+ 'edit' => [1],
173
+ 'add' => [1],
174
+ 'delete' => [1],
175
+ 'export' => [1],
176
+ 'actions' => {}
177
+ }
178
+ }
179
+ )
180
+
181
+ allow(ForestLiana::IpWhitelist).to receive(:retrieve) { true }
182
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_whitelist_retrieved) { true }
183
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_valid) { true }
184
+ allow(ForestLiana::ScopeManager).to receive(:fetch_scopes).and_return({})
185
+ end
186
+
187
+ after do
188
+ User.destroy_all
189
+ Address.destroy_all
190
+ end
191
+
192
+ token = JWT.encode({
193
+ id: 1,
194
+ email: 'michael.kelso@that70.show',
195
+ first_name: 'Michael',
196
+ last_name: 'Kelso',
197
+ team: 'Operations',
198
+ rendering_id: 16,
199
+ exp: Time.now.to_i + 2.weeks.to_i,
200
+ permission_level: 'admin'
201
+ }, ForestLiana.auth_secret, 'HS256')
202
+
203
+ headers = {
204
+ 'Accept' => 'application/json',
205
+ 'Content-Type' => 'application/json',
206
+ 'Authorization' => "Bearer #{token}"
207
+ }
208
+
209
+ describe 'index' do
210
+ params = {
211
+ fields: { 'Address' => 'id,line1,city,zip_code,addressable' },
212
+ page: { 'number' => '1', 'size' => '10' },
213
+ searchExtended: '0',
214
+ sort: '-id',
215
+ timezone: 'Europe/Paris'
216
+ }
217
+
218
+ it 'should respond the address data' do
219
+ get '/forest/Address', params: params, headers: headers
220
+
221
+ expect(JSON.parse(response.body)).to include(
222
+ "data" => [
223
+ {
224
+ "type" => "Address",
225
+ "id" => "1",
226
+ "attributes" => {
227
+ "id" => 1,
228
+ "line1" => "10 Downing Street",
229
+ "city" => "London"
230
+ },
231
+ "links" => { "self" => "/forest/address/1" },
232
+ "relationships" => {
233
+ "addressable" => { "links" => { "related" => {} }, "data" => { "type" => "User", "id" => "1" } }
234
+ }
235
+ }
236
+ ]
237
+ )
238
+ end
239
+ end
240
+ end
@@ -9,6 +9,19 @@ module ForestLiana
9
9
  allow(ForestLiana::ScopeManager).to receive(:fetch_scopes).and_return(scopes)
10
10
  end
11
11
 
12
+ describe 'with not allowed aggregator' do
13
+ it 'should raise an error' do
14
+ expect {
15
+ LineStatGetter.new(Owner, {
16
+ timezone: "Europe/Paris",
17
+ aggregator: "eval",
18
+ timeRange: "Week",
19
+ groupByFieldName: "`ls`",
20
+ }, user)
21
+ }.to raise_error(ForestLiana::Errors::HTTP422Error, 'Invalid aggregate function')
22
+ end
23
+ end
24
+
12
25
  describe 'Check client_timezone function' do
13
26
  describe 'with a SQLite database' do
14
27
  it 'should return false' do
@@ -23,90 +23,113 @@ module ForestLiana
23
23
  }
24
24
  end
25
25
 
26
- let(:model) { Tree }
27
- let(:collection) { 'trees' }
28
- let(:params) {
29
- {
30
- type: 'Pie',
31
- sourceCollectionName: collection,
32
- timezone: 'Europe/Paris',
33
- aggregator: 'Count',
34
- groupByFieldName: groupByFieldName
26
+ describe 'with not allowed aggregator' do
27
+ let(:scopes) { { } }
28
+ let(:model) { Tree }
29
+ let(:collection) { 'trees' }
30
+ let(:params) {
31
+ {
32
+ type: 'Pie',
33
+ sourceCollectionName: collection,
34
+ timezone: 'Europe/Paris',
35
+ aggregator: 'eval',
36
+ groupByFieldName: '`ls`'
37
+ }
35
38
  }
36
- }
37
39
 
38
- subject { PieStatGetter.new(model, params, user) }
40
+ it 'should raise an error' do
41
+ expect {
42
+ PieStatGetter.new(model, params, user)
43
+ }.to raise_error(ForestLiana::Errors::HTTP422Error, 'Invalid aggregate function')
44
+ end
45
+ end
39
46
 
40
- describe 'with empty scopes' do
41
- let(:scopes) { { } }
47
+ describe 'with valid aggregate function' do
48
+ let(:model) { Tree }
49
+ let(:collection) { 'trees' }
50
+ let(:params) {
51
+ {
52
+ type: 'Pie',
53
+ sourceCollectionName: collection,
54
+ timezone: 'Europe/Paris',
55
+ aggregator: 'Count',
56
+ groupByFieldName: groupByFieldName
57
+ }
58
+ }
42
59
 
43
- describe 'with an aggregate on the name field' do
44
- let(:groupByFieldName) { 'name' }
45
-
46
- it 'should be as many categories as records count' do
47
- subject.perform
48
- expect(subject.record.value).to match_array([
49
- {:key => "Old Tree n1", :value => 1},
50
- {:key => "Old Tree n2", :value => 1},
51
- {:key => "Old Tree n3", :value => 1},
52
- {:key => "Old Tree n4", :value => 1},
53
- {:key => "Young Tree n1", :value => 1},
54
- {:key => "Young Tree n2", :value => 1},
55
- {:key => "Young Tree n3", :value => 1},
56
- {:key => "Young Tree n4", :value => 1},
57
- {:key => "Young Tree n5", :value => 1}
58
- ])
60
+ subject { PieStatGetter.new(model, params, user) }
61
+
62
+ describe 'with empty scopes' do
63
+ let(:scopes) { { } }
64
+
65
+ describe 'with an aggregate on the name field' do
66
+ let(:groupByFieldName) { 'name' }
67
+
68
+ it 'should be as many categories as records count' do
69
+ subject.perform
70
+ expect(subject.record.value).to match_array([
71
+ {:key => "Old Tree n1", :value => 1},
72
+ {:key => "Old Tree n2", :value => 1},
73
+ {:key => "Old Tree n3", :value => 1},
74
+ {:key => "Old Tree n4", :value => 1},
75
+ {:key => "Young Tree n1", :value => 1},
76
+ {:key => "Young Tree n2", :value => 1},
77
+ {:key => "Young Tree n3", :value => 1},
78
+ {:key => "Young Tree n4", :value => 1},
79
+ {:key => "Young Tree n5", :value => 1}
80
+ ])
81
+ end
59
82
  end
60
- end
61
83
 
62
- describe 'with an aggregate on the age field' do
63
- let(:groupByFieldName) { 'age' }
84
+ describe 'with an aggregate on the age field' do
85
+ let(:groupByFieldName) { 'age' }
64
86
 
65
- it 'should be as many categories as different ages among records' do
66
- subject.perform
67
- expect(subject.record.value).to eq [{ :key => 3, :value => 5}, { :key => 15, :value => 4 }]
87
+ it 'should be as many categories as different ages among records' do
88
+ subject.perform
89
+ expect(subject.record.value).to eq [{ :key => 3, :value => 5}, { :key => 15, :value => 4 }]
90
+ end
68
91
  end
69
92
  end
70
- end
71
93
 
72
- describe 'with scopes' do
73
- let(:scopes) {
74
- {
75
- 'Tree' => {
76
- 'scope'=> {
77
- 'filter'=> {
78
- 'aggregator' => 'and',
79
- 'conditions' => [
80
- { 'field' => 'age', 'operator' => 'less_than', 'value' => 10 }
81
- ]
82
- },
83
- 'dynamicScopesValues' => { }
94
+ describe 'with scopes' do
95
+ let(:scopes) {
96
+ {
97
+ 'Tree' => {
98
+ 'scope'=> {
99
+ 'filter'=> {
100
+ 'aggregator' => 'and',
101
+ 'conditions' => [
102
+ { 'field' => 'age', 'operator' => 'less_than', 'value' => 10 }
103
+ ]
104
+ },
105
+ 'dynamicScopesValues' => { }
106
+ }
84
107
  }
85
108
  }
86
109
  }
87
- }
88
110
 
89
- describe 'with an aggregate on the name field' do
90
- let(:groupByFieldName) { 'name' }
91
-
92
- it 'should be as many categories as records inside the scope' do
93
- subject.perform
94
- expect(subject.record.value).to match_array([
95
- {:key => "Young Tree n1", :value => 1},
96
- {:key => "Young Tree n2", :value => 1},
97
- {:key => "Young Tree n3", :value => 1},
98
- {:key => "Young Tree n4", :value => 1},
99
- {:key => "Young Tree n5", :value => 1}
100
- ])
111
+ describe 'with an aggregate on the name field' do
112
+ let(:groupByFieldName) { 'name' }
113
+
114
+ it 'should be as many categories as records inside the scope' do
115
+ subject.perform
116
+ expect(subject.record.value).to match_array([
117
+ {:key => "Young Tree n1", :value => 1},
118
+ {:key => "Young Tree n2", :value => 1},
119
+ {:key => "Young Tree n3", :value => 1},
120
+ {:key => "Young Tree n4", :value => 1},
121
+ {:key => "Young Tree n5", :value => 1}
122
+ ])
123
+ end
101
124
  end
102
- end
103
125
 
104
- describe 'with an aggregate on the age field' do
105
- let(:groupByFieldName) { 'age' }
126
+ describe 'with an aggregate on the age field' do
127
+ let(:groupByFieldName) { 'age' }
106
128
 
107
- it 'should be only one category' do
108
- subject.perform
109
- expect(subject.record.value).to eq [{ :key => 3, :value => 5}]
129
+ it 'should be only one category' do
130
+ subject.perform
131
+ expect(subject.record.value).to eq [{ :key => 3, :value => 5}]
132
+ end
110
133
  end
111
134
  end
112
135
  end
@@ -1,6 +1,64 @@
1
1
  module ForestLiana
2
2
  describe SchemaAdapter do
3
3
  describe 'perform' do
4
+ context 'with polymorphic association' do
5
+ it 'should define the association with the referenced models' do
6
+ collection = ForestLiana.apimap.find do |object|
7
+ object.name.to_s == ForestLiana.name_for(Address)
8
+ end
9
+ field = collection.fields.find { |field| field[:field] == 'addressable' }
10
+
11
+ expect(field).to eq(
12
+ {
13
+ field: "addressable",
14
+ type: "Number",
15
+ relationship: "BelongsTo",
16
+ reference: "addressable.id",
17
+ inverse_of: "address",
18
+ is_filterable: false,
19
+ is_sortable: true,
20
+ is_read_only: false,
21
+ is_required: false,
22
+ is_virtual: false,
23
+ default_value: nil,
24
+ integration: nil,
25
+ relationships: nil,
26
+ widget: nil,
27
+ validations: [],
28
+ polymorphic_referenced_models: ['User']
29
+ }
30
+ )
31
+ end
32
+
33
+ it 'should remove the polymorphic attributes(_id and _type)' do
34
+ collection = ForestLiana.apimap.find do |object|
35
+ object.name.to_s == ForestLiana.name_for(Address)
36
+ end
37
+ removed_fields = collection.fields.select do
38
+ |field| field[:field] == 'addressable_id' || field[:field] == 'addressable_type'
39
+ end
40
+
41
+ expect(removed_fields).to be_empty
42
+ end
43
+
44
+ context 'when the polymorphic support was disabled' do
45
+ it 'should not define the association' do
46
+ ENV['ENABLE_SUPPORT_POLYMORPHISM'] = 'false'
47
+ Bootstrapper.new(true)
48
+ collection = ForestLiana.apimap.find do |object|
49
+ object.name.to_s == ForestLiana.name_for(Address)
50
+ end
51
+ association = collection.fields.find { |field| field[:field] == 'addressable' }
52
+ fields = collection.fields.select do |field|
53
+ field[:field] == 'addressable_id' || field[:field] == 'addressable_type'
54
+ end
55
+
56
+ expect(association).to be_nil
57
+ expect(fields.size).to eq(2)
58
+ end
59
+ end
60
+ end
61
+
4
62
  context 'with an "unhandled" column types (binary, postgis geography, ...)' do
5
63
  it 'should not define theses column in the schema' do
6
64
  collection = ForestLiana.apimap.find do |object|
@@ -15,80 +15,103 @@ module ForestLiana
15
15
  Tree.create!(name: 'Tree n3', age: 4, island: island, owner: king, cutter: villager)
16
16
  end
17
17
 
18
- let(:params) {
19
- {
20
- type: "Value",
21
- sourceCollectionName: sourceCollectionName,
22
- timezone: "Europe/Paris",
23
- aggregator: "Count",
24
- filter: filter
18
+ describe 'with not allowed aggregator' do
19
+ let(:model) { User }
20
+ let(:collection) { 'users' }
21
+ let(:scopes) { { } }
22
+ let(:params) {
23
+ {
24
+ type: "Value",
25
+ sourceCollectionName: collection,
26
+ timezone: "Europe/Paris",
27
+ aggregator: "eval",
28
+ aggregateFieldName: "`ls`"
29
+ }
25
30
  }
26
- }
27
31
 
28
- subject { ValueStatGetter.new(model, params, user) }
32
+ it 'should raise an error' do
33
+ expect {
34
+ ValueStatGetter.new(model, params, user)
35
+ }.to raise_error(ForestLiana::Errors::HTTP422Error, 'Invalid aggregate function')
36
+ end
37
+ end
29
38
 
30
- describe 'with empty scopes' do
31
- let(:scopes) { { } }
39
+ describe 'with valid aggregate function' do
40
+ let(:params) {
41
+ {
42
+ type: "Value",
43
+ sourceCollectionName: sourceCollectionName,
44
+ timezone: "Europe/Paris",
45
+ aggregator: "Count",
46
+ filter: filter
47
+ }
48
+ }
49
+
50
+ subject { ValueStatGetter.new(model, params, user) }
32
51
 
33
- describe 'with a simple filter matching no entries' do
34
- let(:model) { User }
35
- let(:sourceCollectionName) { 'users' }
36
- let(:filter) { { field: 'name', operator: 'in', value: ['Merry', 'Pippin'] }.to_json }
52
+ describe 'with empty scopes' do
53
+ let(:scopes) { { } }
37
54
 
38
- it 'should have a countCurrent of 0' do
39
- subject.perform
40
- expect(subject.record.value[:countCurrent]).to eq 0
55
+ describe 'with a simple filter matching no entries' do
56
+ let(:model) { User }
57
+ let(:sourceCollectionName) { 'users' }
58
+ let(:filter) { { field: 'name', operator: 'in', value: ['Merry', 'Pippin'] }.to_json }
59
+
60
+ it 'should have a countCurrent of 0' do
61
+ subject.perform
62
+ expect(subject.record.value[:countCurrent]).to eq 0
63
+ end
41
64
  end
42
- end
43
65
 
44
- describe 'with a filter on a belongs_to string field' do
45
- let(:model) { Tree }
46
- let(:sourceCollectionName) { 'trees' }
47
- let(:filter) { { field: 'owner:name', operator: 'equal', value: 'Aragorn' }.to_json }
66
+ describe 'with a filter on a belongs_to string field' do
67
+ let(:model) { Tree }
68
+ let(:sourceCollectionName) { 'trees' }
69
+ let(:filter) { { field: 'owner:name', operator: 'equal', value: 'Aragorn' }.to_json }
48
70
 
49
- it 'should have a countCurrent of 2' do
50
- subject.perform
51
- expect(subject.record.value[:countCurrent]).to eq 2
71
+ it 'should have a countCurrent of 2' do
72
+ subject.perform
73
+ expect(subject.record.value[:countCurrent]).to eq 2
74
+ end
52
75
  end
53
- end
54
76
 
55
- describe 'with a filter on a belongs_to enum field' do
56
- let(:model) { Tree }
57
- let(:sourceCollectionName) { 'trees' }
58
- let(:filter) { { field: 'owner:title', operator: 'equal', value: 'villager' }.to_json }
77
+ describe 'with a filter on a belongs_to enum field' do
78
+ let(:model) { Tree }
79
+ let(:sourceCollectionName) { 'trees' }
80
+ let(:filter) { { field: 'owner:title', operator: 'equal', value: 'villager' }.to_json }
59
81
 
60
- it 'should have a countCurrent of 1' do
61
- subject.perform
62
- expect(subject.record.value[:countCurrent]).to eq 1
82
+ it 'should have a countCurrent of 1' do
83
+ subject.perform
84
+ expect(subject.record.value[:countCurrent]).to eq 1
85
+ end
63
86
  end
64
87
  end
65
- end
66
88
 
67
- describe 'with scopes' do
68
- let(:scopes) {
69
- {
70
- 'User' => {
71
- 'scope'=> {
72
- 'filter'=> {
73
- 'aggregator' => 'and',
74
- 'conditions' => [
75
- { 'field' => 'title', 'operator' => 'not_equal', 'value' => 'villager' }
76
- ]
77
- },
78
- 'dynamicScopesValues' => { }
89
+ describe 'with scopes' do
90
+ let(:scopes) {
91
+ {
92
+ 'User' => {
93
+ 'scope'=> {
94
+ 'filter'=> {
95
+ 'aggregator' => 'and',
96
+ 'conditions' => [
97
+ { 'field' => 'title', 'operator' => 'not_equal', 'value' => 'villager' }
98
+ ]
99
+ },
100
+ 'dynamicScopesValues' => { }
101
+ }
79
102
  }
80
103
  }
81
104
  }
82
- }
83
105
 
84
- describe 'with a filter on a belongs_to enum field' do
85
- let(:model) { User }
86
- let(:sourceCollectionName) { 'users' }
87
- let(:filter) { { field: 'title', operator: 'equal', value: 'villager' }.to_json }
106
+ describe 'with a filter on a belongs_to enum field' do
107
+ let(:model) { User }
108
+ let(:sourceCollectionName) { 'users' }
109
+ let(:filter) { { field: 'title', operator: 'equal', value: 'villager' }.to_json }
88
110
 
89
- it 'should have a countCurrent of 0' do
90
- subject.perform
91
- expect(subject.record.value[:countCurrent]).to eq 0
111
+ it 'should have a countCurrent of 0' do
112
+ subject.perform
113
+ expect(subject.record.value[:countCurrent]).to eq 0
114
+ end
92
115
  end
93
116
  end
94
117
  end
data/spec/spec_helper.rb CHANGED
@@ -19,6 +19,8 @@ SimpleCov.add_filter ['app/services/forest_liana/ability/fetch.rb', 'lib/forest_
19
19
  # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
20
20
  RSpec.configure do |config|
21
21
  ENV['RAILS_ENV'] = 'test'
22
+ ENV['ENABLE_SUPPORT_POLYMORPHISM'] = 'true'
23
+
22
24
  require File.expand_path('../dummy/config/environment', __FILE__)
23
25
 
24
26
  # rspec-expectations config goes here. You can use an alternate
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forest_liana
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.0.16
4
+ version: 8.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sandro Munda
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-16 00:00:00.000000000 Z
11
+ date: 2024-01-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -350,6 +350,7 @@ files:
350
350
  - spec/dummy/app/controllers/application_controller.rb
351
351
  - spec/dummy/app/controllers/forest/islands_controller.rb
352
352
  - spec/dummy/app/helpers/application_helper.rb
353
+ - spec/dummy/app/models/address.rb
353
354
  - spec/dummy/app/models/application_record.rb
354
355
  - spec/dummy/app/models/car.rb
355
356
  - spec/dummy/app/models/driver.rb
@@ -403,6 +404,7 @@ files:
403
404
  - spec/dummy/db/migrate/20220719094450_create_drivers.rb
404
405
  - spec/dummy/db/migrate/20220727114450_create_manufacturers.rb
405
406
  - spec/dummy/db/migrate/20220727114930_add_columns_to_products.rb
407
+ - spec/dummy/db/migrate/20231117084236_create_addresses.rb
406
408
  - spec/dummy/db/schema.rb
407
409
  - spec/dummy/lib/forest_liana/collections/island.rb
408
410
  - spec/dummy/lib/forest_liana/collections/location.rb
@@ -646,6 +648,7 @@ test_files:
646
648
  - spec/dummy/app/controllers/application_controller.rb
647
649
  - spec/dummy/app/controllers/forest/islands_controller.rb
648
650
  - spec/dummy/app/helpers/application_helper.rb
651
+ - spec/dummy/app/models/address.rb
649
652
  - spec/dummy/app/models/application_record.rb
650
653
  - spec/dummy/app/models/car.rb
651
654
  - spec/dummy/app/models/driver.rb
@@ -699,6 +702,7 @@ test_files:
699
702
  - spec/dummy/db/migrate/20220719094450_create_drivers.rb
700
703
  - spec/dummy/db/migrate/20220727114450_create_manufacturers.rb
701
704
  - spec/dummy/db/migrate/20220727114930_add_columns_to_products.rb
705
+ - spec/dummy/db/migrate/20231117084236_create_addresses.rb
702
706
  - spec/dummy/db/schema.rb
703
707
  - spec/dummy/lib/forest_liana/collections/island.rb
704
708
  - spec/dummy/lib/forest_liana/collections/location.rb