forest_admin_agent 1.12.14 → 1.12.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/forest_admin_agent/routes/abstract_authenticated_route.rb +7 -5
- data/lib/forest_admin_agent/routes/abstract_related_route.rb +9 -7
- data/lib/forest_admin_agent/routes/abstract_route.rb +7 -3
- data/lib/forest_admin_agent/routes/action/actions.rb +31 -25
- data/lib/forest_admin_agent/routes/capabilities/collections.rb +4 -4
- data/lib/forest_admin_agent/routes/charts/api_chart_collection.rb +10 -10
- data/lib/forest_admin_agent/routes/charts/api_chart_datasource.rb +8 -7
- data/lib/forest_admin_agent/routes/charts/charts.rb +64 -66
- data/lib/forest_admin_agent/routes/request_context.rb +17 -0
- data/lib/forest_admin_agent/routes/resources/count.rb +9 -9
- data/lib/forest_admin_agent/routes/resources/csv.rb +11 -11
- data/lib/forest_admin_agent/routes/resources/delete.rb +20 -19
- data/lib/forest_admin_agent/routes/resources/list.rb +14 -14
- data/lib/forest_admin_agent/routes/resources/native_query.rb +9 -9
- data/lib/forest_admin_agent/routes/resources/related/associate_related.rb +32 -29
- data/lib/forest_admin_agent/routes/resources/related/count_related.rb +9 -9
- data/lib/forest_admin_agent/routes/resources/related/csv_related.rb +10 -9
- data/lib/forest_admin_agent/routes/resources/related/dissociate_related.rb +63 -40
- data/lib/forest_admin_agent/routes/resources/related/list_related.rb +12 -11
- data/lib/forest_admin_agent/routes/resources/related/update_related.rb +60 -52
- data/lib/forest_admin_agent/routes/resources/show.rb +8 -8
- data/lib/forest_admin_agent/routes/resources/store.rb +18 -15
- data/lib/forest_admin_agent/routes/resources/update.rb +9 -9
- data/lib/forest_admin_agent/routes/resources/update_field.rb +17 -17
- data/lib/forest_admin_agent/utils/schema/schema_emitter.rb +1 -1
- data/lib/forest_admin_agent/version.rb +1 -1
- metadata +2 -1
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: d06c27d8cb7afd9c9e531c215dd12f253af4e0dc92f67496d45a789a925c8024
         | 
| 4 | 
            +
              data.tar.gz: 93fa83fec26dcce76b175cdb1cafdbc2bf55a4d94bb91835a9ed7cb830e67da8
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 48ef7caf5cec3cb36abc575bf1f49b1c12c45c21380e0a579a5637feda6ed9d894f1a088019666526cc9ec688fc0018544917327aab82060370ba97278911dcc
         | 
| 7 | 
            +
              data.tar.gz: 8b962f14fdc92733b8d4ed312d167d99169ad472c93ec885117a3569c691b271c3c0e64d37b221f42ecf62fc487a0af557d3ee0470e43d18d9768cd57ce091f9
         | 
| @@ -5,16 +5,18 @@ module ForestAdminAgent | |
| 5 5 | 
             
                    if args.dig(:headers, 'action_dispatch.remote_ip')
         | 
| 6 6 | 
             
                      Facades::Whitelist.check_ip(args[:headers]['action_dispatch.remote_ip'].to_s)
         | 
| 7 7 | 
             
                    end
         | 
| 8 | 
            -
             | 
| 9 | 
            -
                     | 
| 10 | 
            -
                     | 
| 8 | 
            +
             | 
| 9 | 
            +
                    context = super
         | 
| 10 | 
            +
                    context.caller = Utils::QueryStringParser.parse_caller(args)
         | 
| 11 | 
            +
                    context.permissions = ForestAdminAgent::Services::Permissions.new(context.caller)
         | 
| 12 | 
            +
                    context
         | 
| 11 13 | 
             
                  end
         | 
| 12 14 |  | 
| 13 | 
            -
                  def format_attributes(args)
         | 
| 15 | 
            +
                  def format_attributes(args, collection)
         | 
| 14 16 | 
             
                    record = args[:params][:data][:attributes] || {}
         | 
| 15 17 |  | 
| 16 18 | 
             
                    args[:params][:data][:relationships]&.map do |field, value|
         | 
| 17 | 
            -
                      schema =  | 
| 19 | 
            +
                      schema = collection.schema[:fields][field]
         | 
| 18 20 |  | 
| 19 21 | 
             
                      record[schema.foreign_key] = value.dig('data', 'id') if schema.type == 'ManyToOne'
         | 
| 20 22 | 
             
                    end
         | 
| @@ -2,14 +2,16 @@ module ForestAdminAgent | |
| 2 2 | 
             
              module Routes
         | 
| 3 3 | 
             
                class AbstractRelatedRoute < AbstractAuthenticatedRoute
         | 
| 4 4 | 
             
                  def build(args = {})
         | 
| 5 | 
            -
                    super
         | 
| 5 | 
            +
                    context = super
         | 
| 6 6 |  | 
| 7 | 
            -
                    relation =  | 
| 8 | 
            -
                     | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 7 | 
            +
                    relation = context.collection.schema[:fields][args[:params]['relation_name']]
         | 
| 8 | 
            +
                    context.child_collection = if relation.type == 'PolymorphicManyToOne'
         | 
| 9 | 
            +
                                                 context.datasource.get_collection(args[:params]['data']['type'])
         | 
| 10 | 
            +
                                               else
         | 
| 11 | 
            +
                                                 context.datasource.get_collection(relation.foreign_collection)
         | 
| 12 | 
            +
                                               end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                    context
         | 
| 13 15 | 
             
                  end
         | 
| 14 16 | 
             
                end
         | 
| 15 17 | 
             
              end
         | 
| @@ -7,10 +7,14 @@ module ForestAdminAgent | |
| 7 7 | 
             
                  end
         | 
| 8 8 |  | 
| 9 9 | 
             
                  def build(args)
         | 
| 10 | 
            -
                     | 
| 11 | 
            -
                     | 
| 10 | 
            +
                    context = RequestContext.new
         | 
| 11 | 
            +
                    context.datasource = ForestAdminAgent::Facades::Container.datasource
         | 
| 12 12 |  | 
| 13 | 
            -
                     | 
| 13 | 
            +
                    if args[:params]['collection_name']
         | 
| 14 | 
            +
                      context.collection = context.datasource.get_collection(args[:params]['collection_name'])
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    context
         | 
| 14 18 | 
             
                  end
         | 
| 15 19 |  | 
| 16 20 | 
             
                  def routes
         | 
| @@ -14,14 +14,14 @@ module ForestAdminAgent | |
| 14 14 |  | 
| 15 15 | 
             
                    def initialize(collection, action)
         | 
| 16 16 | 
             
                      @action_name = action
         | 
| 17 | 
            -
                      @ | 
| 17 | 
            +
                      @route_collection = collection
         | 
| 18 18 | 
             
                      super()
         | 
| 19 19 | 
             
                    end
         | 
| 20 20 |  | 
| 21 21 | 
             
                    def setup_routes
         | 
| 22 | 
            -
                      action_index = @ | 
| 22 | 
            +
                      action_index = @route_collection.schema[:actions].keys.index(@action_name)
         | 
| 23 23 | 
             
                      slug = ForestAdminAgent::Utils::Schema::GeneratorAction.get_action_slug(@action_name)
         | 
| 24 | 
            -
                      route_name = "forest_action_#{@ | 
| 24 | 
            +
                      route_name = "forest_action_#{@route_collection.name}_#{action_index}_#{slug}"
         | 
| 25 25 | 
             
                      path = "/_actions/:collection_name/#{action_index}/#{slug}"
         | 
| 26 26 |  | 
| 27 27 | 
             
                      add_route(route_name, 'post', path, proc { |args| handle_request(args) })
         | 
| @@ -47,12 +47,12 @@ module ForestAdminAgent | |
| 47 47 | 
             
                    end
         | 
| 48 48 |  | 
| 49 49 | 
             
                    def handle_request(args = {})
         | 
| 50 | 
            -
                      build(args)
         | 
| 50 | 
            +
                      context = build(args)
         | 
| 51 51 | 
             
                      args = middleware_custom_action_approval_request_data(args)
         | 
| 52 | 
            -
                      filter_for_caller = get_record_selection(args)
         | 
| 53 | 
            -
                      get_record_selection(args, include_user_scope: false)
         | 
| 52 | 
            +
                      filter_for_caller = get_record_selection(args, context)
         | 
| 53 | 
            +
                      get_record_selection(args, context, include_user_scope: false)
         | 
| 54 54 |  | 
| 55 | 
            -
                       | 
| 55 | 
            +
                      context.permissions.can_smart_action?(args, context.collection, filter_for_caller)
         | 
| 56 56 |  | 
| 57 57 | 
             
                      raw_data = args.dig(:params, :data, :attributes, :values)
         | 
| 58 58 |  | 
| @@ -60,8 +60,8 @@ module ForestAdminAgent | |
| 60 60 | 
             
                      # better send invalid data to the getForm() customer handler than to the execute() one.
         | 
| 61 61 | 
             
                      unsafe_data = Schema::ForestValueConverter.make_form_data_unsafe(raw_data)
         | 
| 62 62 |  | 
| 63 | 
            -
                      fields =  | 
| 64 | 
            -
                         | 
| 63 | 
            +
                      fields = context.collection.get_form(
         | 
| 64 | 
            +
                        context.caller,
         | 
| 65 65 | 
             
                        @action_name,
         | 
| 66 66 | 
             
                        unsafe_data,
         | 
| 67 67 | 
             
                        filter_for_caller,
         | 
| @@ -70,24 +70,27 @@ module ForestAdminAgent | |
| 70 70 |  | 
| 71 71 | 
             
                      # Now that we have the field list, we can parse the data again.
         | 
| 72 72 | 
             
                      data = Schema::ForestValueConverter.make_form_data(
         | 
| 73 | 
            -
                         | 
| 73 | 
            +
                        context.datasource,
         | 
| 74 74 | 
             
                        raw_data,
         | 
| 75 75 | 
             
                        fields.reject { |field| field.type == 'Layout' }
         | 
| 76 76 | 
             
                      )
         | 
| 77 77 |  | 
| 78 | 
            -
                      { content:  | 
| 78 | 
            +
                      { content: context.collection.execute(context.caller, @action_name, data, filter_for_caller) }
         | 
| 79 79 | 
             
                    end
         | 
| 80 80 |  | 
| 81 81 | 
             
                    def handle_hook_request(args = {})
         | 
| 82 | 
            -
                      build(args)
         | 
| 82 | 
            +
                      context = build(args)
         | 
| 83 83 | 
             
                      forest_fields = args.dig(:params, :data, :attributes, :fields)
         | 
| 84 | 
            -
                      data = ( | 
| 85 | 
            -
             | 
| 84 | 
            +
                      data = (if forest_fields
         | 
| 85 | 
            +
                                Schema::ForestValueConverter.make_form_data_from_fields(context.datasource,
         | 
| 86 | 
            +
                                                                                        forest_fields)
         | 
| 87 | 
            +
                              end)
         | 
| 88 | 
            +
                      filter = get_record_selection(args, context)
         | 
| 86 89 | 
             
                      search_values = {}
         | 
| 87 90 | 
             
                      forest_fields&.each { |field| search_values[field['field']] = field['searchValue'] }
         | 
| 88 91 |  | 
| 89 | 
            -
                      form =  | 
| 90 | 
            -
                         | 
| 92 | 
            +
                      form = context.collection.get_form(
         | 
| 93 | 
            +
                        context.caller,
         | 
| 91 94 | 
             
                        @action_name,
         | 
| 92 95 | 
             
                        data,
         | 
| 93 96 | 
             
                        filter,
         | 
| @@ -102,7 +105,9 @@ module ForestAdminAgent | |
| 102 105 |  | 
| 103 106 | 
             
                      {
         | 
| 104 107 | 
             
                        content: {
         | 
| 105 | 
            -
                          fields: form_elements[:fields].map  | 
| 108 | 
            +
                          fields: form_elements[:fields].map do |f|
         | 
| 109 | 
            +
                            Schema::GeneratorAction.build_field_schema(context.datasource, f)
         | 
| 110 | 
            +
                          end,
         | 
| 106 111 | 
             
                          layout: Schema::GeneratorAction.build_layout(form_elements[:layout])
         | 
| 107 112 | 
             
                        }
         | 
| 108 113 | 
             
                      }
         | 
| @@ -129,26 +134,26 @@ module ForestAdminAgent | |
| 129 134 | 
             
                      )[0])
         | 
| 130 135 | 
             
                    end
         | 
| 131 136 |  | 
| 132 | 
            -
                    def get_record_selection(args, include_user_scope: true)
         | 
| 137 | 
            +
                    def get_record_selection(args, context, include_user_scope: true)
         | 
| 133 138 | 
             
                      attributes = args.dig(:params, :data, :attributes)
         | 
| 134 139 |  | 
| 135 140 | 
             
                      # Match user filter + search + scope? + segment
         | 
| 136 | 
            -
                      scope = include_user_scope ?  | 
| 141 | 
            +
                      scope = include_user_scope ? context.permissions.get_scope(context.collection) : nil
         | 
| 137 142 | 
             
                      filter = Filter.new(
         | 
| 138 143 | 
             
                        condition_tree: ConditionTreeFactory.intersect(
         | 
| 139 144 | 
             
                          [
         | 
| 140 145 | 
             
                            scope,
         | 
| 141 146 | 
             
                            ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
         | 
| 142 | 
            -
                               | 
| 147 | 
            +
                              context.collection, args
         | 
| 143 148 | 
             
                            )
         | 
| 144 149 | 
             
                          ]
         | 
| 145 150 | 
             
                        )
         | 
| 146 151 | 
             
                      )
         | 
| 147 152 |  | 
| 148 153 | 
             
                      # Restrict the filter to the selected records for single or bulk actions
         | 
| 149 | 
            -
                      if  | 
| 150 | 
            -
                        selection_ids = Utils::Id.parse_selection_ids( | 
| 151 | 
            -
                        selected_ids = ConditionTreeFactory.match_ids( | 
| 154 | 
            +
                      if context.collection.schema[:actions][@action_name].scope != Types::ActionScope::GLOBAL
         | 
| 155 | 
            +
                        selection_ids = Utils::Id.parse_selection_ids(context.collection, args[:params])
         | 
| 156 | 
            +
                        selected_ids = ConditionTreeFactory.match_ids(context.collection, selection_ids[:ids])
         | 
| 152 157 | 
             
                        selected_ids = selected_ids.inverse if selection_ids[:are_excluded]
         | 
| 153 158 | 
             
                        filter = filter.override(
         | 
| 154 159 | 
             
                          condition_tree: ConditionTreeFactory.intersect([filter.condition_tree, selected_ids])
         | 
| @@ -158,10 +163,11 @@ module ForestAdminAgent | |
| 158 163 | 
             
                      # Restrict the filter further for the "related data" page
         | 
| 159 164 | 
             
                      unless attributes[:parent_association_name].nil?
         | 
| 160 165 | 
             
                        relation = attributes[:parent_association_name]
         | 
| 161 | 
            -
                        parent =  | 
| 166 | 
            +
                        parent = context.datasource.get_collection(attributes[:parent_collection_name])
         | 
| 162 167 | 
             
                        parent_primary_key_values = Utils::Id.unpack_id(parent, attributes[:parent_collection_id])
         | 
| 163 168 |  | 
| 164 | 
            -
                        filter = FilterFactory.make_foreign_filter(parent, parent_primary_key_values, relation,  | 
| 169 | 
            +
                        filter = FilterFactory.make_foreign_filter(parent, parent_primary_key_values, relation, context.caller,
         | 
| 170 | 
            +
                                                                   filter)
         | 
| 165 171 | 
             
                      end
         | 
| 166 172 |  | 
| 167 173 | 
             
                      filter
         | 
| @@ -2,7 +2,7 @@ require 'json' | |
| 2 2 | 
             
            module ForestAdminAgent
         | 
| 3 3 | 
             
              module Routes
         | 
| 4 4 | 
             
                module Capabilities
         | 
| 5 | 
            -
                  class Collections <  | 
| 5 | 
            +
                  class Collections < AbstractRoute
         | 
| 6 6 | 
             
                    include ForestAdminDatasourceToolkit::Schema
         | 
| 7 7 |  | 
| 8 8 | 
             
                    def setup_routes
         | 
| @@ -15,13 +15,13 @@ module ForestAdminAgent | |
| 15 15 | 
             
                    end
         | 
| 16 16 |  | 
| 17 17 | 
             
                    def handle_request(args = {})
         | 
| 18 | 
            -
                       | 
| 18 | 
            +
                      datasource = ForestAdminAgent::Facades::Container.datasource
         | 
| 19 19 | 
             
                      collections = args[:params]['collectionNames'] || []
         | 
| 20 20 |  | 
| 21 | 
            -
                      connections =  | 
| 21 | 
            +
                      connections = datasource.live_query_connections.keys.map { |connection_name| { name: connection_name } }
         | 
| 22 22 |  | 
| 23 23 | 
             
                      result = collections.map do |collection_name|
         | 
| 24 | 
            -
                        collection =  | 
| 24 | 
            +
                        collection = datasource.get_collection(collection_name)
         | 
| 25 25 | 
             
                        {
         | 
| 26 26 | 
             
                          name: collection.name,
         | 
| 27 27 | 
             
                          fields: collection.schema[:fields].select { |_, field| field.is_a?(ColumnSchema) }.map do |name, field|
         | 
| @@ -9,14 +9,14 @@ module ForestAdminAgent | |
| 9 9 |  | 
| 10 10 | 
             
                    def initialize(collection, chart_name)
         | 
| 11 11 | 
             
                      @chart_name = chart_name
         | 
| 12 | 
            -
                      @ | 
| 12 | 
            +
                      @route_collection = collection
         | 
| 13 13 |  | 
| 14 14 | 
             
                      super()
         | 
| 15 15 | 
             
                    end
         | 
| 16 16 |  | 
| 17 17 | 
             
                    def setup_routes
         | 
| 18 18 | 
             
                      # Mount both GET and POST, respectively for smart and api charts.
         | 
| 19 | 
            -
                      collection_name = @ | 
| 19 | 
            +
                      collection_name = @route_collection.name
         | 
| 20 20 | 
             
                      slug = @chart_name.parameterize
         | 
| 21 21 |  | 
| 22 22 | 
             
                      add_route(
         | 
| @@ -44,25 +44,25 @@ module ForestAdminAgent | |
| 44 44 | 
             
                    end
         | 
| 45 45 |  | 
| 46 46 | 
             
                    def handle_api_chart(args)
         | 
| 47 | 
            -
                      build(args)
         | 
| 47 | 
            +
                      context = build(args)
         | 
| 48 48 | 
             
                      {
         | 
| 49 49 | 
             
                        content: Serializer::ForestChartSerializer.serialize(
         | 
| 50 | 
            -
                           | 
| 51 | 
            -
                             | 
| 50 | 
            +
                          context.collection.render_chart(
         | 
| 51 | 
            +
                            context.caller,
         | 
| 52 52 | 
             
                            @chart_name,
         | 
| 53 | 
            -
                            Id.unpack_id( | 
| 53 | 
            +
                            Id.unpack_id(context.collection, args[:params]['record_id'])
         | 
| 54 54 | 
             
                          )
         | 
| 55 55 | 
             
                        )
         | 
| 56 56 | 
             
                      }
         | 
| 57 57 | 
             
                    end
         | 
| 58 58 |  | 
| 59 59 | 
             
                    def handle_smart_chart(args)
         | 
| 60 | 
            -
                      build(args)
         | 
| 60 | 
            +
                      context = build(args)
         | 
| 61 61 | 
             
                      {
         | 
| 62 | 
            -
                        content:  | 
| 63 | 
            -
                           | 
| 62 | 
            +
                        content: context.collection.render_chart(
         | 
| 63 | 
            +
                          context.caller,
         | 
| 64 64 | 
             
                          @chart_name,
         | 
| 65 | 
            -
                          Id.unpack_id( | 
| 65 | 
            +
                          Id.unpack_id(context.collection, args[:params]['record_id'])
         | 
| 66 66 | 
             
                        )
         | 
| 67 67 | 
             
                      }
         | 
| 68 68 | 
             
                    end
         | 
| @@ -7,7 +7,6 @@ module ForestAdminAgent | |
| 7 7 | 
             
                  class ApiChartDatasource < AbstractAuthenticatedRoute
         | 
| 8 8 | 
             
                    def initialize(chart_name)
         | 
| 9 9 | 
             
                      @chart_name = chart_name
         | 
| 10 | 
            -
                      @datasource = ForestAdminAgent::Facades::Container.datasource
         | 
| 11 10 |  | 
| 12 11 | 
             
                      super()
         | 
| 13 12 | 
             
                    end
         | 
| @@ -38,12 +37,13 @@ module ForestAdminAgent | |
| 38 37 | 
             
                    end
         | 
| 39 38 |  | 
| 40 39 | 
             
                    def handle_api_chart(args = {})
         | 
| 41 | 
            -
                       | 
| 40 | 
            +
                      caller = Utils::QueryStringParser.parse_caller(args)
         | 
| 41 | 
            +
                      datasource = ForestAdminAgent::Facades::Container.datasource
         | 
| 42 42 |  | 
| 43 43 | 
             
                      {
         | 
| 44 44 | 
             
                        content: Serializer::ForestChartSerializer.serialize(
         | 
| 45 | 
            -
                           | 
| 46 | 
            -
                             | 
| 45 | 
            +
                          datasource.render_chart(
         | 
| 46 | 
            +
                            caller,
         | 
| 47 47 | 
             
                            @chart_name
         | 
| 48 48 | 
             
                          )
         | 
| 49 49 | 
             
                        )
         | 
| @@ -51,11 +51,12 @@ module ForestAdminAgent | |
| 51 51 | 
             
                    end
         | 
| 52 52 |  | 
| 53 53 | 
             
                    def handle_smart_chart(args = {})
         | 
| 54 | 
            -
                       | 
| 54 | 
            +
                      caller = Utils::QueryStringParser.parse_caller(args)
         | 
| 55 | 
            +
                      datasource = ForestAdminAgent::Facades::Container.datasource
         | 
| 55 56 |  | 
| 56 57 | 
             
                      {
         | 
| 57 | 
            -
                        content:  | 
| 58 | 
            -
                           | 
| 58 | 
            +
                        content: datasource.render_chart(
         | 
| 59 | 
            +
                          caller,
         | 
| 59 60 | 
             
                          @chart_name
         | 
| 60 61 | 
             
                        )
         | 
| 61 62 | 
             
                      }
         | 
| @@ -11,8 +11,6 @@ module ForestAdminAgent | |
| 11 11 | 
             
                    include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
         | 
| 12 12 | 
             
                    include ForestAdminDatasourceToolkit::Components::Charts
         | 
| 13 13 |  | 
| 14 | 
            -
                    attr_reader :filter
         | 
| 15 | 
            -
             | 
| 16 14 | 
             
                    FORMAT = {
         | 
| 17 15 | 
             
                      Day: '%d/%m/%Y',
         | 
| 18 16 | 
             
                      Week: 'W%V-%G',
         | 
| @@ -28,97 +26,97 @@ module ForestAdminAgent | |
| 28 26 | 
             
                    end
         | 
| 29 27 |  | 
| 30 28 | 
             
                    def handle_request(args = {})
         | 
| 31 | 
            -
                      build(args)
         | 
| 32 | 
            -
                       | 
| 33 | 
            -
                       | 
| 34 | 
            -
                       | 
| 35 | 
            -
                      @filter = Filter.new(
         | 
| 29 | 
            +
                      context = build(args)
         | 
| 30 | 
            +
                      context.permissions.can_chart?(args[:params])
         | 
| 31 | 
            +
                      type = validate_and_get_type(args[:params][:type])
         | 
| 32 | 
            +
                      filter = Filter.new(
         | 
| 36 33 | 
             
                        condition_tree: ConditionTreeFactory.intersect(
         | 
| 37 34 | 
             
                          [
         | 
| 38 | 
            -
                             | 
| 35 | 
            +
                            context.permissions.get_scope(context.collection),
         | 
| 39 36 | 
             
                            ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
         | 
| 40 | 
            -
                               | 
| 37 | 
            +
                              context.collection, args
         | 
| 41 38 | 
             
                            )
         | 
| 42 39 | 
             
                          ]
         | 
| 43 40 | 
             
                        )
         | 
| 44 41 | 
             
                      )
         | 
| 45 42 |  | 
| 46 | 
            -
                      inject_context_variables
         | 
| 43 | 
            +
                      filter = inject_context_variables(filter, context, args)
         | 
| 47 44 |  | 
| 48 | 
            -
                      { content: Serializer::ForestChartSerializer.serialize(send(:"make_#{ | 
| 45 | 
            +
                      { content: Serializer::ForestChartSerializer.serialize(send(:"make_#{type}", context, filter, args)) }
         | 
| 49 46 | 
             
                    end
         | 
| 50 47 |  | 
| 51 48 | 
             
                    private
         | 
| 52 49 |  | 
| 53 | 
            -
                    def  | 
| 50 | 
            +
                    def validate_and_get_type(type)
         | 
| 54 51 | 
             
                      chart_types = %w[Value Objective Pie Line Leaderboard]
         | 
| 55 52 | 
             
                      unless chart_types.include?(type)
         | 
| 56 53 | 
             
                        raise ForestAdminDatasourceToolkit::Exceptions::ForestException, "Invalid Chart type #{type}"
         | 
| 57 54 | 
             
                      end
         | 
| 58 55 |  | 
| 59 | 
            -
                       | 
| 56 | 
            +
                      type.downcase
         | 
| 60 57 | 
             
                    end
         | 
| 61 58 |  | 
| 62 | 
            -
                    def inject_context_variables
         | 
| 63 | 
            -
                      user =  | 
| 64 | 
            -
                      team =  | 
| 59 | 
            +
                    def inject_context_variables(filter, context, args)
         | 
| 60 | 
            +
                      user = context.permissions.get_user_data(context.caller.id)
         | 
| 61 | 
            +
                      team = context.permissions.get_team(context.caller.rendering_id)
         | 
| 65 62 |  | 
| 66 63 | 
             
                      context_variables = ForestAdminAgent::Utils::ContextVariables.new(team, user,
         | 
| 67 | 
            -
                                                                                         | 
| 68 | 
            -
                      return unless  | 
| 64 | 
            +
                                                                                        args[:params][:contextVariables])
         | 
| 65 | 
            +
                      return filter unless args[:params][:filter]
         | 
| 69 66 |  | 
| 70 | 
            -
                       | 
| 71 | 
            -
                         | 
| 67 | 
            +
                      filter.override(condition_tree: ContextVariablesInjector.inject_context_in_filter(
         | 
| 68 | 
            +
                        filter.condition_tree, context_variables
         | 
| 72 69 | 
             
                      ))
         | 
| 73 70 | 
             
                    end
         | 
| 74 71 |  | 
| 75 | 
            -
                    def make_value
         | 
| 76 | 
            -
                      value = compute_value( | 
| 72 | 
            +
                    def make_value(context, filter, args)
         | 
| 73 | 
            +
                      value = compute_value(context, filter, args)
         | 
| 77 74 | 
             
                      previous_value = nil
         | 
| 78 | 
            -
                      is_and_aggregator =  | 
| 79 | 
            -
                      with_count_previous =  | 
| 75 | 
            +
                      is_and_aggregator = filter.condition_tree&.try(:aggregator) == 'And'
         | 
| 76 | 
            +
                      with_count_previous = filter.condition_tree&.some_leaf(&:use_interval_operator)
         | 
| 80 77 |  | 
| 81 78 | 
             
                      if with_count_previous && !is_and_aggregator
         | 
| 82 | 
            -
                         | 
| 79 | 
            +
                        previous_filter = FilterFactory.get_previous_period_filter(filter, context.caller.timezone)
         | 
| 80 | 
            +
                        previous_value = compute_value(context, previous_filter, args)
         | 
| 83 81 | 
             
                      end
         | 
| 84 82 |  | 
| 85 83 | 
             
                      ValueChart.new(value, previous_value).serialize
         | 
| 86 84 | 
             
                    end
         | 
| 87 85 |  | 
| 88 | 
            -
                    def make_objective
         | 
| 89 | 
            -
                      ObjectiveChart.new(compute_value( | 
| 86 | 
            +
                    def make_objective(context, filter, args)
         | 
| 87 | 
            +
                      ObjectiveChart.new(compute_value(context, filter, args)).serialize
         | 
| 90 88 | 
             
                    end
         | 
| 91 89 |  | 
| 92 | 
            -
                    def make_pie
         | 
| 93 | 
            -
                      group_field =  | 
| 90 | 
            +
                    def make_pie(context, filter, args)
         | 
| 91 | 
            +
                      group_field = args[:params][:groupByFieldName]
         | 
| 94 92 | 
             
                      aggregation = Aggregation.new(
         | 
| 95 | 
            -
                        operation:  | 
| 96 | 
            -
                        field:  | 
| 93 | 
            +
                        operation: args[:params][:aggregator],
         | 
| 94 | 
            +
                        field: args[:params][:aggregateFieldName],
         | 
| 97 95 | 
             
                        groups: group_field ? [{ field: group_field }] : []
         | 
| 98 96 | 
             
                      )
         | 
| 99 97 |  | 
| 100 | 
            -
                      result =  | 
| 98 | 
            +
                      result = context.collection.aggregate(context.caller, filter, aggregation)
         | 
| 101 99 |  | 
| 102 100 | 
             
                      PieChart.new(result.map { |row| { key: row['group'][group_field], value: row['value'] } }).serialize
         | 
| 103 101 | 
             
                    end
         | 
| 104 102 |  | 
| 105 | 
            -
                    def make_line
         | 
| 106 | 
            -
                      group_by_field_name =  | 
| 107 | 
            -
                      time_range =  | 
| 108 | 
            -
                      filter_only_with_values =  | 
| 103 | 
            +
                    def make_line(context, filter, args)
         | 
| 104 | 
            +
                      group_by_field_name = args[:params][:groupByFieldName]
         | 
| 105 | 
            +
                      time_range = args[:params][:timeRange]
         | 
| 106 | 
            +
                      filter_only_with_values = filter.override(
         | 
| 109 107 | 
             
                        condition_tree: ConditionTree::ConditionTreeFactory.intersect(
         | 
| 110 108 | 
             
                          [
         | 
| 111 | 
            -
                             | 
| 109 | 
            +
                            filter.condition_tree,
         | 
| 112 110 | 
             
                            ConditionTree::Nodes::ConditionTreeLeaf.new(group_by_field_name, ConditionTree::Operators::PRESENT)
         | 
| 113 111 | 
             
                          ]
         | 
| 114 112 | 
             
                        )
         | 
| 115 113 | 
             
                      )
         | 
| 116 | 
            -
                      rows =  | 
| 117 | 
            -
                         | 
| 114 | 
            +
                      rows = context.collection.aggregate(
         | 
| 115 | 
            +
                        context.caller,
         | 
| 118 116 | 
             
                        filter_only_with_values,
         | 
| 119 117 | 
             
                        Aggregation.new(
         | 
| 120 | 
            -
                          operation:  | 
| 121 | 
            -
                          field:  | 
| 118 | 
            +
                          operation: args[:params][:aggregator],
         | 
| 119 | 
            +
                          field: args[:params][:aggregateFieldName],
         | 
| 122 120 | 
             
                          groups: [{ field: group_by_field_name, operation: time_range }]
         | 
| 123 121 | 
             
                        )
         | 
| 124 122 | 
             
                      )
         | 
| @@ -140,51 +138,51 @@ module ForestAdminAgent | |
| 140 138 | 
             
                      LineChart.new(result).serialize
         | 
| 141 139 | 
             
                    end
         | 
| 142 140 |  | 
| 143 | 
            -
                    def make_leaderboard
         | 
| 144 | 
            -
                      field =  | 
| 141 | 
            +
                    def make_leaderboard(context, filter, args)
         | 
| 142 | 
            +
                      field = context.collection.schema[:fields][args[:params][:relationshipFieldName]]
         | 
| 145 143 |  | 
| 146 144 | 
             
                      if field && field.type == 'OneToMany'
         | 
| 147 145 | 
             
                        inverse = ForestAdminDatasourceToolkit::Utils::Collection.get_inverse_relation(
         | 
| 148 | 
            -
                           | 
| 149 | 
            -
                           | 
| 146 | 
            +
                          context.collection,
         | 
| 147 | 
            +
                          args[:params][:relationshipFieldName]
         | 
| 150 148 | 
             
                        )
         | 
| 151 149 | 
             
                        if inverse
         | 
| 152 150 | 
             
                          collection = field.foreign_collection
         | 
| 153 | 
            -
                           | 
| 151 | 
            +
                          leaderboard_filter = filter.nest(inverse)
         | 
| 154 152 | 
             
                          aggregation = Aggregation.new(
         | 
| 155 | 
            -
                            operation:  | 
| 156 | 
            -
                            field:  | 
| 157 | 
            -
                            groups: [{ field: "#{inverse}:#{ | 
| 153 | 
            +
                            operation: args[:params][:aggregator],
         | 
| 154 | 
            +
                            field: args[:params][:aggregateFieldName],
         | 
| 155 | 
            +
                            groups: [{ field: "#{inverse}:#{args[:params][:labelFieldName]}" }]
         | 
| 158 156 | 
             
                          )
         | 
| 159 157 | 
             
                        end
         | 
| 160 158 | 
             
                      end
         | 
| 161 159 |  | 
| 162 160 | 
             
                      if field && field.type == 'ManyToMany'
         | 
| 163 161 | 
             
                        origin = ForestAdminDatasourceToolkit::Utils::Collection.get_through_origin(
         | 
| 164 | 
            -
                           | 
| 165 | 
            -
                           | 
| 162 | 
            +
                          context.collection,
         | 
| 163 | 
            +
                          args[:params][:relationshipFieldName]
         | 
| 166 164 | 
             
                        )
         | 
| 167 165 | 
             
                        target = ForestAdminDatasourceToolkit::Utils::Collection.get_through_target(
         | 
| 168 | 
            -
                           | 
| 169 | 
            -
                           | 
| 166 | 
            +
                          context.collection,
         | 
| 167 | 
            +
                          args[:params][:relationshipFieldName]
         | 
| 170 168 | 
             
                        )
         | 
| 171 169 | 
             
                        if origin && target
         | 
| 172 170 | 
             
                          collection = field.through_collection
         | 
| 173 | 
            -
                           | 
| 171 | 
            +
                          leaderboard_filter = filter.nest(origin)
         | 
| 174 172 | 
             
                          aggregation = Aggregation.new(
         | 
| 175 | 
            -
                            operation:  | 
| 176 | 
            -
                            field:  | 
| 177 | 
            -
                            groups: [{ field: "#{origin}:#{ | 
| 173 | 
            +
                            operation: args[:params][:aggregator],
         | 
| 174 | 
            +
                            field: args[:params][:aggregateFieldName] ? "#{target}:#{args[:params][:aggregateFieldName]}" : nil,
         | 
| 175 | 
            +
                            groups: [{ field: "#{origin}:#{args[:params][:labelFieldName]}" }]
         | 
| 178 176 | 
             
                          )
         | 
| 179 177 | 
             
                        end
         | 
| 180 178 | 
             
                      end
         | 
| 181 179 |  | 
| 182 | 
            -
                      if collection &&  | 
| 183 | 
            -
                        rows =  | 
| 184 | 
            -
                           | 
| 185 | 
            -
                           | 
| 180 | 
            +
                      if collection && leaderboard_filter && aggregation
         | 
| 181 | 
            +
                        rows = context.datasource.get_collection(collection).aggregate(
         | 
| 182 | 
            +
                          context.caller,
         | 
| 183 | 
            +
                          leaderboard_filter,
         | 
| 186 184 | 
             
                          aggregation,
         | 
| 187 | 
            -
                           | 
| 185 | 
            +
                          args[:params][:limit]
         | 
| 188 186 | 
             
                        )
         | 
| 189 187 |  | 
| 190 188 | 
             
                        result = rows.map do |row|
         | 
| @@ -201,10 +199,10 @@ module ForestAdminAgent | |
| 201 199 | 
             
                            'Failed to generate leaderboard chart: parameters do not match pre-requisites'
         | 
| 202 200 | 
             
                    end
         | 
| 203 201 |  | 
| 204 | 
            -
                    def compute_value(filter)
         | 
| 205 | 
            -
                      aggregation = Aggregation.new(operation:  | 
| 206 | 
            -
                                                    field:  | 
| 207 | 
            -
                      result =  | 
| 202 | 
            +
                    def compute_value(context, filter, args)
         | 
| 203 | 
            +
                      aggregation = Aggregation.new(operation: args[:params][:aggregator],
         | 
| 204 | 
            +
                                                    field: args[:params][:aggregateFieldName])
         | 
| 205 | 
            +
                      result = context.collection.aggregate(context.caller, filter, aggregation)
         | 
| 208 206 |  | 
| 209 207 | 
             
                      result[0]['value'] || 0
         | 
| 210 208 | 
             
                    end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            module ForestAdminAgent
         | 
| 2 | 
            +
              module Routes
         | 
| 3 | 
            +
                # RequestContext holds request-specific data that should not be shared between concurrent requests.
         | 
| 4 | 
            +
                # This prevents race conditions when route instances are reused across multiple requests.
         | 
| 5 | 
            +
                class RequestContext
         | 
| 6 | 
            +
                  attr_accessor :datasource, :collection, :child_collection, :caller, :permissions
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def initialize(datasource: nil, collection: nil, child_collection: nil, caller: nil, permissions: nil)
         | 
| 9 | 
            +
                    @datasource = datasource
         | 
| 10 | 
            +
                    @collection = collection
         | 
| 11 | 
            +
                    @child_collection = child_collection
         | 
| 12 | 
            +
                    @caller = caller
         | 
| 13 | 
            +
                    @permissions = permissions
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
| @@ -15,24 +15,24 @@ module ForestAdminAgent | |
| 15 15 | 
             
                    end
         | 
| 16 16 |  | 
| 17 17 | 
             
                    def handle_request(args = {})
         | 
| 18 | 
            -
                      build(args)
         | 
| 19 | 
            -
                       | 
| 18 | 
            +
                      context = build(args)
         | 
| 19 | 
            +
                      context.permissions.can?(:browse, context.collection)
         | 
| 20 20 |  | 
| 21 | 
            -
                      if  | 
| 21 | 
            +
                      if context.collection.is_countable?
         | 
| 22 22 | 
             
                        filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
         | 
| 23 23 | 
             
                          condition_tree: ConditionTreeFactory.intersect(
         | 
| 24 24 | 
             
                            [
         | 
| 25 | 
            -
                               | 
| 26 | 
            -
                              parse_query_segment( | 
| 27 | 
            -
                              ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree( | 
| 25 | 
            +
                              context.permissions.get_scope(context.collection),
         | 
| 26 | 
            +
                              parse_query_segment(context.collection, args, context.permissions, context.caller),
         | 
| 27 | 
            +
                              ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(context.collection, args)
         | 
| 28 28 | 
             
                            ]
         | 
| 29 29 | 
             
                          ),
         | 
| 30 | 
            -
                          search: QueryStringParser.parse_search( | 
| 30 | 
            +
                          search: QueryStringParser.parse_search(context.collection, args),
         | 
| 31 31 | 
             
                          search_extended: QueryStringParser.parse_search_extended(args),
         | 
| 32 | 
            -
                          segment: QueryStringParser.parse_segment( | 
| 32 | 
            +
                          segment: QueryStringParser.parse_segment(context.collection, args)
         | 
| 33 33 | 
             
                        )
         | 
| 34 34 | 
             
                        aggregation = ForestAdminDatasourceToolkit::Components::Query::Aggregation.new(operation: 'Count')
         | 
| 35 | 
            -
                        result =  | 
| 35 | 
            +
                        result = context.collection.aggregate(context.caller, filter, aggregation)
         | 
| 36 36 |  | 
| 37 37 | 
             
                        return {
         | 
| 38 38 | 
             
                          name: args[:params]['collection_name'],
         |