Draft API to get all collections by an account (#37139)

This commit is contained in:
David Roetzel
2025-12-08 09:56:13 +01:00
committed by GitHub
parent 7fe3e80758
commit 8625721805
7 changed files with 175 additions and 5 deletions

View File

@@ -3,20 +3,34 @@
class Api::V1Alpha::CollectionsController < Api::BaseController class Api::V1Alpha::CollectionsController < Api::BaseController
include Authorization include Authorization
DEFAULT_COLLECTIONS_LIMIT = 40
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e| rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
render json: { error: ValidationErrorFormatter.new(e).as_json }, status: 422 render json: { error: ValidationErrorFormatter.new(e).as_json }, status: 422
end end
before_action :check_feature_enabled before_action :check_feature_enabled
before_action -> { authorize_if_got_token! :read, :'read:collections' }, only: [:index, :show]
before_action -> { doorkeeper_authorize! :write, :'write:collections' }, only: [:create, :update, :destroy] before_action -> { doorkeeper_authorize! :write, :'write:collections' }, only: [:create, :update, :destroy]
before_action :require_user!, only: [:create, :update, :destroy] before_action :require_user!, only: [:create, :update, :destroy]
before_action :set_account, only: [:index]
before_action :set_collections, only: [:index]
before_action :set_collection, only: [:show, :update, :destroy] before_action :set_collection, only: [:show, :update, :destroy]
after_action :insert_pagination_headers, only: [:index]
after_action :verify_authorized after_action :verify_authorized
def index
cache_if_unauthenticated!
authorize Collection, :index?
render json: @collections, each_serializer: REST::BaseCollectionSerializer
end
def show def show
cache_if_unauthenticated! cache_if_unauthenticated!
authorize @collection, :show? authorize @collection, :show?
@@ -50,6 +64,18 @@ class Api::V1Alpha::CollectionsController < Api::BaseController
private private
def set_account
@account = Account.find(params[:account_id])
end
def set_collections
@collections = @account.collections
.with_item_count
.order(created_at: :desc)
.offset(offset_param)
.limit(limit_param(DEFAULT_COLLECTIONS_LIMIT))
end
def set_collection def set_collection
@collection = Collection.find(params[:id]) @collection = Collection.find(params[:id])
end end
@@ -65,4 +91,24 @@ class Api::V1Alpha::CollectionsController < Api::BaseController
def check_feature_enabled def check_feature_enabled
raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled? raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled?
end end
def next_path
return unless records_continue?
api_v1_alpha_account_collections_url(@account, pagination_params(offset: offset_param + limit_param(DEFAULT_COLLECTIONS_LIMIT)))
end
def prev_path
return if offset_param.zero?
api_v1_alpha_account_collections_url(@account, pagination_params(offset: offset_param - limit_param(DEFAULT_COLLECTIONS_LIMIT)))
end
def records_continue?
((offset_param * limit_param(DEFAULT_COLLECTIONS_LIMIT)) + @collections.size) < @account.collections.size
end
def offset_param
params[:offset].to_i
end
end end

View File

@@ -39,6 +39,11 @@ class Collection < ApplicationRecord
validate :items_do_not_exceed_limit validate :items_do_not_exceed_limit
scope :with_items, -> { includes(:collection_items).merge(CollectionItem.with_accounts) } scope :with_items, -> { includes(:collection_items).merge(CollectionItem.with_accounts) }
scope :with_item_count, lambda {
select('collections.*, COUNT(collection_items.id)')
.left_joins(:collection_items)
.group(collections: :id)
}
def remote? def remote?
!local? !local?

View File

@@ -0,0 +1,12 @@
# frozen_string_literal: true
class REST::BaseCollectionSerializer < ActiveModel::Serializer
attributes :uri, :name, :description, :local, :sensitive, :discoverable,
:item_count, :created_at, :updated_at
belongs_to :tag, serializer: REST::StatusSerializer::TagSerializer
def item_count
object.respond_to?(:item_count) ? object.item_count : object.collection_items.count
end
end

View File

@@ -1,11 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class REST::CollectionSerializer < ActiveModel::Serializer class REST::CollectionSerializer < REST::BaseCollectionSerializer
attributes :uri, :name, :description, :local, :sensitive, :discoverable,
:created_at, :updated_at
belongs_to :account, serializer: REST::AccountSerializer belongs_to :account, serializer: REST::AccountSerializer
belongs_to :tag, serializer: REST::StatusSerializer::TagSerializer
has_many :items, serializer: REST::CollectionItemSerializer has_many :items, serializer: REST::CollectionItemSerializer

View File

@@ -6,6 +6,10 @@ namespace :api, format: false do
# Experimental JSON / REST API # Experimental JSON / REST API
namespace :v1_alpha do namespace :v1_alpha do
resources :accounts, only: [] do
resources :collections, only: [:index]
end
resources :async_refreshes, only: :show resources :async_refreshes, only: :show
resources :collections, only: [:show, :create, :update, :destroy] resources :collections, only: [:show, :create, :update, :destroy]

View File

@@ -5,6 +5,58 @@ require 'rails_helper'
RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do
include_context 'with API authentication', oauth_scopes: 'read:collections write:collections' include_context 'with API authentication', oauth_scopes: 'read:collections write:collections'
describe 'GET /api/v1_alpha/accounts/:account_id/collections' do
subject do
get "/api/v1_alpha/accounts/#{account.id}/collections", headers: headers, params: params
end
let(:params) { {} }
let(:account) { Fabricate(:account) }
before { Fabricate.times(3, :collection, account:) }
it 'returns all collections for the given account and http success' do
subject
expect(response).to have_http_status(200)
expect(response.parsed_body.size).to eq 3
end
context 'with limit param' do
let(:params) { { limit: '1' } }
it 'returns only a single result' do
subject
expect(response).to have_http_status(200)
expect(response.parsed_body.size).to eq 1
expect(response)
.to include_pagination_headers(
next: api_v1_alpha_account_collections_url(account, limit: 1, offset: 1)
)
end
end
context 'with limit and offset params' do
let(:params) { { limit: '1', offset: '1' } }
it 'returns the correct result and headers' do
subject
expect(response).to have_http_status(200)
expect(response.parsed_body.size).to eq 1
expect(response)
.to include_pagination_headers(
prev: api_v1_alpha_account_collections_url(account, limit: 1, offset: 0),
next: api_v1_alpha_account_collections_url(account, limit: 1, offset: 2)
)
end
end
end
describe 'GET /api/v1_alpha/collections/:id' do describe 'GET /api/v1_alpha/collections/:id' do
subject do subject do
get "/api/v1_alpha/collections/#{collection.id}", headers: headers get "/api/v1_alpha/collections/#{collection.id}", headers: headers

View File

@@ -0,0 +1,55 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe REST::BaseCollectionSerializer do
subject do
serialized_record_json(collection, described_class, options: {
scope: current_user,
scope_name: :current_user,
})
end
let(:current_user) { nil }
let(:tag) { Fabricate(:tag, name: 'discovery') }
let(:collection) do
Fabricate(:collection,
name: 'Exquisite follows',
description: 'Always worth a follow',
local: true,
sensitive: true,
discoverable: false,
tag:)
end
it 'includes the relevant attributes' do
expect(subject)
.to include(
'name' => 'Exquisite follows',
'description' => 'Always worth a follow',
'local' => true,
'sensitive' => true,
'discoverable' => false,
'tag' => a_hash_including('name' => 'discovery'),
'created_at' => match_api_datetime_format,
'updated_at' => match_api_datetime_format
)
end
describe 'Counting items' do
before do
Fabricate.times(2, :collection_item, collection:)
end
it 'can count items on demand' do
expect(subject['item_count']).to eq 2
end
it 'can use precalculated counts' do
collection.define_singleton_method :item_count, -> { 8 }
expect(subject['item_count']).to eq 8
end
end
end