Merge branch '8784-dir-listings'
[arvados.git] / services / api / lib / load_param.rb
index 718aaeaf690be5bc5e61592849ad4009e3b69f52..247708be47812d18907c0c6c6a028e85e3338bb3 100644 (file)
@@ -1,3 +1,7 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
 # Mixin module for reading out query parameters from request params.
 #
 # Expects:
@@ -17,7 +21,7 @@ module LoadParam
       @where = params[:where]
     elsif params[:where].is_a? String
       begin
-        @where = Oj.load(params[:where])
+        @where = SafeJSON.load(params[:where])
         raise unless @where.is_a? Hash
       rescue
         raise ArgumentError.new("Could not parse \"where\" param as an object")
@@ -33,7 +37,7 @@ module LoadParam
       @filters += params[:filters]
     elsif params[:filters].is_a? String and !params[:filters].empty?
       begin
-        f = Oj.load params[:filters]
+        f = SafeJSON.load(params[:filters])
         if not f.nil?
           raise unless f.is_a? Array
           @filters += f
@@ -72,7 +76,7 @@ module LoadParam
       (case params[:order]
        when String
          if params[:order].starts_with? '['
-           od = Oj.load(params[:order])
+           od = SafeJSON.load(params[:order])
            raise unless od.is_a? Array
            od
          else
@@ -92,11 +96,11 @@ module LoadParam
         # has used set_table_name to use an alternate table name from the Rails standard.
         # I could not find a perfect way to handle this well, but ActiveRecord::Base.send(:descendants)
         # would be a place to start if this ever becomes necessary.
-        if attr.match /^[a-z][_a-z0-9]+$/ and
+        if attr.match(/^[a-z][_a-z0-9]+$/) and
             model_class.columns.collect(&:name).index(attr) and
             ['asc','desc'].index direction.downcase
           @orders << "#{table_name}.#{attr} #{direction.downcase}"
-        elsif attr.match /^([a-z][_a-z0-9]+)\.([a-z][_a-z0-9]+)$/ and
+        elsif attr.match(/^([a-z][_a-z0-9]+)\.([a-z][_a-z0-9]+)$/) and
             ['asc','desc'].index(direction.downcase) and
             ActiveRecord::Base.connection.tables.include?($1) and
             $1.classify.constantize.columns.collect(&:name).index($2)
@@ -111,14 +115,38 @@ module LoadParam
     # (e.g., [] or ['owner_uuid desc']), fall back on the default
     # orders to ensure repeating the same request (possibly with
     # different limit/offset) will return records in the same order.
-    @orders += model_class.default_orders
+    #
+    # Clean up the resulting list of orders such that no column
+    # uselessly appears twice (Postgres might not optimize this out
+    # for us) and no columns uselessly appear after a unique column
+    # (Postgres does not optimize this out for us; as of 9.2, "order
+    # by id, modified_at desc, uuid" is slow but "order by id" is
+    # fast).
+    orders_given_and_default = @orders + model_class.default_orders
+    order_cols_used = {}
+    @orders = []
+    orders_given_and_default.each do |order|
+      otablecol = order.split(' ')[0]
+
+      next if order_cols_used[otablecol]
+      order_cols_used[otablecol] = true
+
+      @orders << order
+
+      otable, ocol = otablecol.split('.')
+      if otable == table_name and model_class.unique_columns.include?(ocol)
+        # we already have a full ordering; subsequent entries would be
+        # superfluous
+        break
+      end
+    end
 
     case params[:select]
     when Array
       @select = params[:select]
     when String
       begin
-        @select = Oj.load params[:select]
+        @select = SafeJSON.load(params[:select])
         raise unless @select.is_a? Array or @select.nil?
       rescue
         raise ArgumentError.new("Could not parse \"select\" param as an array")
@@ -129,8 +157,9 @@ module LoadParam
       # Any ordering columns must be selected when doing select,
       # otherwise it is an SQL error, so filter out invaliding orderings.
       @orders.select! { |o|
+        col, _ = o.split
         # match select column against order array entry
-        @select.select { |s| /^#{table_name}.#{s}( (asc|desc))?$/.match o }.any?
+        @select.select { |s| col == "#{table_name}.#{s}" }.any?
       }
     end