Merge branch '18122-select-param'
authorTom Clegg <tom@curii.com>
Mon, 20 Sep 2021 14:22:39 +0000 (10:22 -0400)
committerTom Clegg <tom@curii.com>
Mon, 20 Sep 2021 14:22:39 +0000 (10:22 -0400)
closes #18122

Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@curii.com>

12 files changed:
doc/api/methods.html.textile.liquid
doc/api/methods/collections.html.textile.liquid
lib/controller/federation/conn.go
lib/controller/localdb/collection_test.go
lib/controller/router/router_test.go
services/api/app/controllers/application_controller.rb
services/api/app/controllers/arvados/v1/collections_controller.rb
services/api/app/controllers/arvados/v1/healthcheck_controller.rb
services/api/app/controllers/arvados/v1/schema_controller.rb
services/api/lib/load_param.rb
services/api/test/functional/arvados/v1/collections_controller_test.rb
services/api/test/integration/select_test.rb

index fd529179283f84fbdfe59ad07c8cb95bd9185209..7f05142dbf2e4dc4a17dd2b52a6236ed963d4832 100644 (file)
@@ -37,6 +37,8 @@ table(table table-bordered table-condensed).
 |_. Argument |_. Type |_. Description |_. Location |
 |{resource_type}|object|Name is the singular form of the resource type, e.g., for the "collections" resource, this argument is "collection"|body|
 |{cluster_id}|string|Optional, the cluster on which to create the object if not the current cluster.|query|
+|select  |array  |Attributes of the new object to return in the response (by default, all available attributes are returned).
+Example: @["uuid","name","modified_at"]@|query|
 
 h2. delete
 
@@ -49,6 +51,8 @@ Arguments:
 table(table table-bordered table-condensed).
 |_. Argument |_. Type |_. Description |_. Location |
 {background:#ccffcc}.|uuid|string|The UUID of the object in question.|path|
+|select  |array  |Attributes of the deleted object to return in the response (by default, all available attributes are returned).
+Example: @["uuid","name","modified_at"]@|query|
 
 h2. get
 
@@ -61,10 +65,12 @@ Arguments:
 table(table table-bordered table-condensed).
 |_. Argument |_. Type |_. Description |_. Location |
 {background:#ccffcc}.|uuid|string|The UUID of the object in question.|path|
+|select  |array  |Attributes of the object to return in the response (by default, all available attributes are returned).
+Example: @["uuid","name","modified_at"]@|query|
 
 h2(#index). list
 
-The @list@ method requests an list of resources of that type.  It corresponds to the HTTP request @GET /arvados/v1/resource_type@.  All resources support "list" method unless otherwise noted.
+The @list@ method requests an list of resources of that type.  It corresponds to the HTTP request @GET /arvados/v1/resource_type@.  All resources support the @list@ method unless otherwise noted.
 
 Arguments:
 
@@ -76,11 +82,10 @@ table(table table-bordered table-condensed).
 |order   |array  |Attributes to use as sort keys to determine the order resources are returned, each optionally followed by @asc@ or @desc@ to indicate ascending or descending order.  (If not specified, it will be ascending).
 Example: @["head_uuid asc","modified_at desc"]@
 Default: @["modified_at desc", "uuid asc"]@|query|
-|select  |array  |Set of attributes to include in the response.
-Example: @["head_uuid","tail_uuid"]@
-Default: all available attributes.  As a special case, collections do not return "manifest_text" unless explicitly selected.|query|
-|distinct|boolean|@true@: (default) do not return duplicate objects
-@false@: permitted to return duplicates|query|
+|select  |array  |Attributes of each object to return in the response (by default, all available attributes are returned, except collections, which do not return @manifest_text@ unless explicitly selected).
+Example: @["uuid","name","modified_at"]@|query|
+|distinct|boolean|When returning multiple records whose selected attributes (see @select@) are equal, return them as a single response entry.
+Default is @false@.|query|
 |count|string|@"exact"@ (default): Include an @items_available@ response field giving the number of distinct matching items that can be retrieved (irrespective of @limit@ and @offset@ arguments).
 @"none"@: Omit the @items_available@ response field. This option will produce a faster response.|query|
 
@@ -187,3 +192,5 @@ table(table table-bordered table-condensed).
 |_. Argument |_. Type |_. Description |_. Location |
 {background:#ccffcc}.|uuid|string|The UUID of the resource in question.|path||
 |{resource_type}|object||query||
+|select  |array  |Attributes of the updated object to return in the response (by default, all available attributes are returned).
+Example: @["uuid","name","modified_at"]@|query|
index 6c1cc691c333419e8504c77da50a2fbcc582bda5..01efda2b0c663edcd97d2d3785289be59fb28eeb 100644 (file)
@@ -85,13 +85,13 @@ table(table table-bordered table-condensed).
 
 h3. get
 
-Gets a Collection's metadata by UUID or portable data hash.  When making a request by portable data hash, the returned record will only have the @portable_data_hash@ and @manifest_text@.
+Gets a Collection's metadata by UUID or portable data hash.  When making a request by portable data hash, attributes other than @portable_data_hash@ and @manifest_text@ are not returned, even when requested explicitly using the @select@ parameter.
 
 Arguments:
 
 table(table table-bordered table-condensed).
 |_. Argument |_. Type |_. Description |_. Location |_. Example |
-{background:#ccffcc}.|uuid|string|The UUID of the Collection in question.|path||
+{background:#ccffcc}.|uuid|string|The UUID or portable data hash of the Collection in question.|path||
 
 h3. list
 
index 586ac23013a531dbcacdab9c705f307886abc158..aa05cb1e6d58bb954e4573e5c54b4416d6f671d3 100644 (file)
@@ -262,13 +262,26 @@ func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions)
                if err != nil {
                        return err
                }
-               // options.UUID is either hash+size or
-               // hash+size+hints; only hash+size need to
-               // match the computed PDH.
-               if pdh := arvados.PortableDataHash(c.ManifestText); pdh != options.UUID && !strings.HasPrefix(options.UUID, pdh+"+") {
-                       err = httpErrorf(http.StatusBadGateway, "bad portable data hash %q received from remote %q (expected %q)", pdh, remoteID, options.UUID)
-                       ctxlog.FromContext(ctx).Warn(err)
-                       return err
+               haveManifest := true
+               if options.Select != nil {
+                       haveManifest = false
+                       for _, s := range options.Select {
+                               if s == "manifest_text" {
+                                       haveManifest = true
+                                       break
+                               }
+                       }
+               }
+               if haveManifest {
+                       pdh := arvados.PortableDataHash(c.ManifestText)
+                       // options.UUID is either hash+size or
+                       // hash+size+hints; only hash+size need to
+                       // match the computed PDH.
+                       if pdh != options.UUID && !strings.HasPrefix(options.UUID, pdh+"+") {
+                               err = httpErrorf(http.StatusBadGateway, "bad portable data hash %q received from remote %q (expected %q)", pdh, remoteID, options.UUID)
+                               ctxlog.FromContext(ctx).Warn(err)
+                               return err
+                       }
                }
                if remoteID != "" {
                        c.ManifestText = rewriteManifest(c.ManifestText, remoteID)
index e0de7256a18cd9e3cd862d676a76efd292881b5a..4a44949641da71b70985f2a5ce472e94327b81ec 100644 (file)
@@ -115,8 +115,9 @@ func (s *CollectionSuite) TestSignatures(c *check.C) {
                Filters: []arvados.Filter{{"uuid", "=", arvadostest.FooCollection}},
                Select:  []string{"uuid", "manifest_text"},
        })
-       c.Check(err, check.IsNil)
-       if c.Check(gresp.Items, check.HasLen, 1) {
+       if err != nil {
+               c.Check(err, check.ErrorMatches, `.*Invalid attribute.*manifest_text.*`)
+       } else if c.Check(gresp.Items, check.HasLen, 1) {
                c.Check(gresp.Items[0].(map[string]interface{})["uuid"], check.Equals, arvadostest.FooCollection)
                c.Check(gresp.Items[0].(map[string]interface{})["manifest_text"], check.Equals, nil)
        }
index 639d2a28b4df5f647da44e9cd946863ff49d6abc..7228956453d1d0c0f6dd460ef7638f19db76a459 100644 (file)
@@ -125,7 +125,6 @@ func (s *RouterSuite) TestOptions(c *check.C) {
                        comment:     "form-encoded expression filter in query string",
                        method:      "GET",
                        path:        "/arvados/v1/collections?filters=[%22(foo<bar)%22]",
-                       header:      http.Header{"Content-Type": {"application/x-www-form-urlencoded"}},
                        shouldCall:  "CollectionList",
                        withOptions: arvados.ListOptions{Limit: -1, Filters: []arvados.Filter{{"(foo<bar)", "=", true}}},
                },
@@ -147,6 +146,13 @@ func (s *RouterSuite) TestOptions(c *check.C) {
                        shouldCall:  "CollectionList",
                        withOptions: arvados.ListOptions{Limit: 2, Filters: []arvados.Filter{{"(foo<bar)", "=", true}, {"bar", "=", "baz"}}},
                },
+               {
+                       comment:     "json-encoded select param in query string",
+                       method:      "GET",
+                       path:        "/arvados/v1/collections/" + arvadostest.FooCollection + "?select=[%22portable_data_hash%22]",
+                       shouldCall:  "CollectionGet",
+                       withOptions: arvados.GetOptions{UUID: arvadostest.FooCollection, Select: []string{"portable_data_hash"}},
+               },
                {
                        method:       "PATCH",
                        path:         "/arvados/v1/collections",
@@ -376,7 +382,6 @@ func (s *RouterIntegrationSuite) TestSelectParam(c *check.C) {
        for _, sel := range [][]string{
                {"uuid", "command"},
                {"uuid", "command", "uuid"},
-               {"", "command", "uuid"},
        } {
                j, err := json.Marshal(sel)
                c.Assert(err, check.IsNil)
@@ -384,8 +389,6 @@ func (s *RouterIntegrationSuite) TestSelectParam(c *check.C) {
                c.Check(rr.Code, check.Equals, http.StatusOK)
 
                c.Check(resp["kind"], check.Equals, "arvados#container")
-               c.Check(resp["etag"], check.FitsTypeOf, "")
-               c.Check(resp["etag"], check.Not(check.Equals), "")
                c.Check(resp["uuid"], check.HasLen, 27)
                c.Check(resp["command"], check.HasLen, 2)
                c.Check(resp["mounts"], check.IsNil)
index c39bdde4b878a26446404979b08e8c3bd08e2b75..ff4cb88aec5a5ded76376f3d1a26b6b926bb5f7c 100644 (file)
@@ -43,13 +43,14 @@ class ApplicationController < ActionController::Base
 
   before_action :catch_redirect_hint
   before_action :load_required_parameters
+  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)
-  before_action(:set_nullable_attrs_to_null, only: [:update, :create])
-  before_action :load_limit_offset_order_params, only: [:index, :contents]
   before_action :load_where_param, only: [:index, :contents]
   before_action :load_filters_param, only: [:index, :contents]
   before_action :find_objects_for_index, :only => :index
+  before_action(:set_nullable_attrs_to_null, only: [:update, :create])
   before_action :reload_object_before_update, :only => :update
   before_action(:render_404_if_no_object,
                 except: [:index, :create] + ERROR_ACTIONS)
@@ -304,7 +305,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
@@ -618,6 +619,11 @@ class ApplicationController < ActionController::Base
 
   def self._create_requires_parameters
     {
+      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.",
@@ -635,7 +641,23 @@ class ApplicationController < ActionController::Base
   end
 
   def self._update_requires_parameters
-    {}
+    {
+      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',
+        description: "Attributes of the object to return in the response.",
+        required: false,
+      },
+    }
   end
 
   def self._index_requires_parameters
@@ -643,8 +665,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' },
index 13b0261d94f2c309b9b6f279f74f5694e0fdcaef..c9b36e19ed95dedca95d9fd473af748088641d33 100644 (file)
@@ -80,12 +80,22 @@ class Arvados::V1::CollectionsController < ApplicationController
       # it will select the Collection object with the longest
       # available lifetime.
 
-      if c = Collection.readable_by(*@read_users, opts).where({ portable_data_hash: loc.to_s }).order("trash_at desc").limit(1).first
+      select_attrs = (@select || ["manifest_text"]) | ["portable_data_hash", "trash_at"]
+      if c = Collection.
+               readable_by(*@read_users, opts).
+               where({ portable_data_hash: loc.to_s }).
+               order("trash_at desc").
+               select(select_attrs.join(", ")).
+               limit(1).
+               first
         @object = {
           uuid: c.portable_data_hash,
           portable_data_hash: c.portable_data_hash,
-          manifest_text: c.manifest_text,
+          trash_at: c.trash_at,
         }
+        if select_attrs.index("manifest_text")
+          @object[:manifest_text] = c.manifest_text
+        end
       end
     else
       super
@@ -321,7 +331,7 @@ class Arvados::V1::CollectionsController < ApplicationController
 
   protected
 
-  def load_limit_offset_order_params *args
+  def load_select_param *args
     super
     if action_name == 'index'
       # Omit manifest_text and unsigned_manifest_text from index results unless expressly selected.
index 6c3822437607ae0fd6dce94a7e20ea8731c04232..c56208207787526b8089239c50a31f34b1cafa23 100644 (file)
@@ -8,6 +8,7 @@ class Arvados::V1::HealthcheckController < ApplicationController
   skip_before_action :find_object_by_uuid
   skip_before_action :load_filters_param
   skip_before_action :load_limit_offset_order_params
+  skip_before_action :load_select_param
   skip_before_action :load_read_auths
   skip_before_action :load_where_param
   skip_before_action :render_404_if_no_object
index 8b71f72371f94bf96b9e7607852348b445624cab..43d9d3b03a1b28ca96072081eef19d358d53d38a 100644 (file)
@@ -8,6 +8,7 @@ class Arvados::V1::SchemaController < ApplicationController
   skip_before_action :find_object_by_uuid
   skip_before_action :load_filters_param
   skip_before_action :load_limit_offset_order_params
+  skip_before_action :load_select_param
   skip_before_action :load_read_auths
   skip_before_action :load_where_param
   skip_before_action :render_404_if_no_object
index 7119eb234800bf12e1460983ed7950ff9e0e4819..9a360c538b6e432e36ad72cfe00d98493af925b6 100644 (file)
@@ -145,6 +145,10 @@ module LoadParam
       end
     end
 
+    @distinct = params[:distinct] && true
+  end
+
+  def load_select_param
     case params[:select]
     when Array
       @select = params[:select]
@@ -157,7 +161,7 @@ module LoadParam
       end
     end
 
-    if @select
+    if @select && @orders
       # Any ordering columns must be selected when doing select,
       # otherwise it is an SQL error, so filter out invaliding orderings.
       @orders.select! { |o|
@@ -166,9 +170,6 @@ module LoadParam
         @select.select { |s| col == "#{table_name}.#{s}" }.any?
       }
     end
-
-    @distinct = true if (params[:distinct] == true || params[:distinct] == "true")
-    @distinct = false if (params[:distinct] == false || params[:distinct] == "false")
   end
 
 end
index eac393104cf9b5a63355d131d9b09dbd8ca1a5a1..af11715982a1adf26226a986c54bc9b6f69676c9 100644 (file)
@@ -1461,4 +1461,56 @@ EOS
       end
     end
   end
+
+  test "select param is respected in 'show' response" do
+    authorize_with :active
+    get :show, params: {
+          id: collections(:collection_owned_by_active).uuid,
+          select: ["name"],
+        }
+    assert_response :success
+    assert_raises ActiveModel::MissingAttributeError do
+      assigns(:object).manifest_text
+    end
+    assert_nil json_response["manifest_text"]
+    assert_nil json_response["properties"]
+    assert_equal collections(:collection_owned_by_active).name, json_response["name"]
+  end
+
+  test "select param is respected in 'update' response" do
+    authorize_with :active
+    post :update, params: {
+          id: collections(:collection_owned_by_active).uuid,
+          collection: {
+            manifest_text: ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:foobar.txt\n",
+          },
+          select: ["name"],
+        }
+    assert_response :success
+    assert_nil json_response["manifest_text"]
+    assert_nil json_response["properties"]
+    assert_equal collections(:collection_owned_by_active).name, json_response["name"]
+  end
+
+  [nil,
+   [],
+   ["is_trashed", "trash_at"],
+   ["is_trashed", "trash_at", "portable_data_hash"],
+   ["portable_data_hash"],
+   ["portable_data_hash", "manifest_text"],
+  ].each do |select|
+    test "select=#{select.inspect} param is respected in 'get by pdh' response" do
+      authorize_with :active
+      get :show, params: {
+            id: collections(:collection_owned_by_active).portable_data_hash,
+            select: select,
+          }
+      assert_response :success
+      if !select || select.index("manifest_text")
+        assert_not_nil json_response["manifest_text"]
+      else
+        assert_nil json_response["manifest_text"]
+      end
+    end
+  end
 end
index 7fbab3b3b00e484a5653435452f7d620178af251..2ee3b3cf94cab5c73f3b4e809db8b6afac7aec81 100644 (file)
@@ -16,11 +16,17 @@ class SelectTest < ActionDispatch::IntegrationTest
   end
 
   test "fewer distinct than total count" do
+    get "/arvados/v1/links",
+      params: {:format => :json, :select => ['link_class']},
+      headers: auth(:active)
+    assert_response :success
+    distinct_unspecified = json_response['items']
+
     get "/arvados/v1/links",
       params: {:format => :json, :select => ['link_class'], :distinct => false},
       headers: auth(:active)
     assert_response :success
-    links = json_response['items']
+    distinct_false = json_response['items']
 
     get "/arvados/v1/links",
       params: {:format => :json, :select => ['link_class'], :distinct => true},
@@ -28,9 +34,11 @@ class SelectTest < ActionDispatch::IntegrationTest
     assert_response :success
     distinct = json_response['items']
 
-    assert_operator(distinct.count, :<, links.count,
-                    "distinct count should be less than link count")
-    assert_equal links.uniq.count, distinct.count
+    assert_operator(distinct.count, :<, distinct_false.count,
+                    "distinct=true count should be less than distinct=false count")
+    assert_equal(distinct_unspecified.count, distinct_false.count,
+                    "distinct=false should be the default")
+    assert_equal distinct_false.uniq.count, distinct.count
   end
 
   test "select with order" do