Merge branch '20840-api-deprecate-notice' refs #20840
[arvados.git] / services / api / app / controllers / application_controller.rb
index 9db8b447118187ebd5b465dfa8d8e8ad16cb121b..b1915502404811177dfef15c605ff42db52df2d6 100644 (file)
@@ -46,7 +46,8 @@ class ApplicationController < ActionController::Base
   before_action :load_limit_offset_order_params, only: [:index, :contents]
   before_action :load_select_param
   before_action(:find_object_by_uuid,
-                except: [:index, :create] + ERROR_ACTIONS)
+                except: [:index, :create, :update] + ERROR_ACTIONS)
+  before_action :find_object_for_update, only: [:update]
   before_action :load_where_param, only: [:index, :contents]
   before_action :load_filters_param, only: [:index, :contents]
   before_action :find_objects_for_index, :only => :index
@@ -100,7 +101,7 @@ class ApplicationController < ActionController::Base
   end
 
   def show
-    send_json @object.as_api_response(nil, select: @select)
+    send_json @object.as_api_response(nil, select: select_for_klass(@select, model_class))
   end
 
   def create
@@ -227,6 +228,24 @@ class ApplicationController < ActionController::Base
     @objects = model_class.apply_filters(@objects, @filters)
   end
 
+  def select_for_klass sel, model_class, raise_unknown=true
+    return nil if sel.nil?
+    # Filter the select fields to only the ones that apply to the
+    # given class.
+    sel.map do |column|
+      sp = column.split(".")
+      if sp.length == 2 && sp[0] == model_class.table_name && model_class.selectable_attributes.include?(sp[1])
+        sp[1]
+      elsif model_class.selectable_attributes.include? column
+        column
+      elsif raise_unknown
+        raise ArgumentError.new("Invalid attribute '#{column}' of #{model_class.name} in select parameter")
+      else
+        nil
+      end
+    end.compact
+  end
+
   def apply_where_limit_order_params model_class=nil
     model_class ||= self.model_class
     apply_filters model_class
@@ -290,7 +309,7 @@ class ApplicationController < ActionController::Base
         # Map attribute names in @select to real column names, resolve
         # those to fully-qualified SQL column names, and pass the
         # resulting string to the select method.
-        columns_list = model_class.columns_for_attributes(@select).
+        columns_list = model_class.columns_for_attributes(select_for_klass @select, model_class).
           map { |s| "#{ar_table_name}.#{ActiveRecord::Base.connection.quote_column_name s}" }
         @objects = @objects.select(columns_list.join(", "))
       end
@@ -305,7 +324,7 @@ class ApplicationController < ActionController::Base
     @objects = @objects.order(@orders.join ", ") if @orders.any?
     @objects = @objects.limit(@limit)
     @objects = @objects.offset(@offset)
-    @objects = @objects.distinct(@distinct) if not @distinct.nil?
+    @objects = @objects.distinct() if @distinct
   end
 
   # limit_database_read ensures @objects (which must be an
@@ -316,7 +335,7 @@ class ApplicationController < ActionController::Base
     return if @limit == 0 || @limit == 1
     model_class ||= self.model_class
     limit_columns = model_class.limit_index_columns_read
-    limit_columns &= model_class.columns_for_attributes(@select) if @select
+    limit_columns &= model_class.columns_for_attributes(select_for_klass @select, model_class) if @select
     return if limit_columns.empty?
     model_class.transaction do
       limit_query = @objects.
@@ -427,7 +446,7 @@ class ApplicationController < ActionController::Base
 
   def append_info_to_payload(payload)
     super
-    payload[:request_id] = response.headers['X-Request-Id']
+    payload[:request_id] = request.request_id
     payload[:client_ipaddr] = @remote_ip
     payload[:client_auth] = current_api_client_authorization.andand.uuid || nil
   end
@@ -464,7 +483,11 @@ class ApplicationController < ActionController::Base
     controller_name
   end
 
-  def find_object_by_uuid
+  def find_object_for_update
+    find_object_by_uuid(with_lock: true)
+  end
+
+  def find_object_by_uuid(with_lock: false)
     if params[:id] and params[:id].match(/\D/)
       params[:uuid] = params.delete :id
     end
@@ -474,8 +497,23 @@ class ApplicationController < ActionController::Base
     @orders = []
     @filters = []
     @objects = nil
+
+    # This is a little hacky but sometimes the fields the user wants
+    # to selecting on are unrelated to the object being loaded here,
+    # for example groups#contents, so filter the fields that will be
+    # used in find_objects_for_index and then reset afterwards.  In
+    # some cases, code that modifies the @select list needs to set
+    # @preserve_select.
+    @preserve_select = @select
+    @select = select_for_klass(@select, self.model_class, false)
+
     find_objects_for_index
-    @object = @objects.first
+    if with_lock && Rails.configuration.API.LockBeforeUpdate
+      @object = @objects.lock.first
+    else
+      @object = @objects.first
+    end
+    @select = @preserve_select
   end
 
   def nullable_attributes
@@ -619,7 +657,11 @@ class ApplicationController < ActionController::Base
 
   def self._create_requires_parameters
     {
-      select: { type: 'array', required: false },
+      select: {
+        type: 'array',
+        description: "Attributes of the new object to return in the response.",
+        required: false,
+      },
       ensure_unique_name: {
         type: "boolean",
         description: "Adjust name to ensure uniqueness instead of returning an error on (owner_uuid, name) collision.",
@@ -638,13 +680,21 @@ class ApplicationController < ActionController::Base
 
   def self._update_requires_parameters
     {
-      select: { type: 'array', required: false },
+      select: {
+        type: 'array',
+        description: "Attributes of the updated object to return in the response.",
+        required: false,
+      },
     }
   end
 
   def self._show_requires_parameters
     {
-      select: { type: 'array', required: false },
+      select: {
+        type: 'array',
+        description: "Attributes of the object to return in the response.",
+        required: false,
+      },
     }
   end
 
@@ -653,8 +703,12 @@ class ApplicationController < ActionController::Base
       filters: { type: 'array', required: false },
       where: { type: 'object', required: false },
       order: { type: 'array', required: false },
-      select: { type: 'array', required: false },
-      distinct: { type: 'boolean', required: false },
+      select: {
+        type: 'array',
+        description: "Attributes of each object to return in the response.",
+        required: false,
+      },
+      distinct: { type: 'boolean', required: false, default: false },
       limit: { type: 'integer', required: false, default: DEFAULT_LIMIT },
       offset: { type: 'integer', required: false, default: 0 },
       count: { type: 'string', required: false, default: 'exact' },
@@ -672,11 +726,6 @@ class ApplicationController < ActionController::Base
     }
   end
 
-  def client_accepts_plain_text_stream
-    (request.headers['Accept'].split(' ') &
-     ['text/plain', '*/*']).count > 0
-  end
-
   def render *opts
     if opts.first
       response = opts.first[:json]