Support min_id-based pagination in REST API (#8736)
* Allow min_id pagination in Feed#get * Add min_id pagination to home and list timeline APIs * Add min_id pagination to account statuses, public and tag APIs * Remove unused stub in reports API * Use min_id pagination in notifications, favourites, and fix order * Fix HomeFeed#from_database not using paginate_by_id
This commit is contained in:
		| @@ -53,6 +53,10 @@ class Api::BaseController < ApplicationController | ||||
|     [params[:limit].to_i.abs, default_limit * 2].min | ||||
|   end | ||||
|  | ||||
|   def params_slice(*keys) | ||||
|     params.slice(*keys).permit(*keys) | ||||
|   end | ||||
|  | ||||
|   def current_resource_owner | ||||
|     @current_user ||= User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token | ||||
|   end | ||||
|   | ||||
| @@ -28,10 +28,9 @@ class Api::V1::Accounts::StatusesController < Api::BaseController | ||||
|  | ||||
|   def account_statuses | ||||
|     statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses | ||||
|     statuses = statuses.paginate_by_max_id( | ||||
|     statuses = statuses.paginate_by_id( | ||||
|       limit_param(DEFAULT_STATUSES_LIMIT), | ||||
|       params[:max_id], | ||||
|       params[:since_id] | ||||
|       params_slice(:max_id, :since_id, :min_id) | ||||
|     ) | ||||
|  | ||||
|     statuses.merge!(only_media_scope) if truthy_param?(:only_media) | ||||
| @@ -82,7 +81,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController | ||||
|  | ||||
|   def prev_path | ||||
|     unless @statuses.empty? | ||||
|       api_v1_account_statuses_url pagination_params(since_id: pagination_since_id) | ||||
|       api_v1_account_statuses_url pagination_params(min_id: pagination_since_id) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   | ||||
| @@ -26,10 +26,9 @@ class Api::V1::FavouritesController < Api::BaseController | ||||
|   end | ||||
|  | ||||
|   def results | ||||
|     @_results ||= account_favourites.paginate_by_max_id( | ||||
|     @_results ||= account_favourites.paginate_by_id( | ||||
|       limit_param(DEFAULT_STATUSES_LIMIT), | ||||
|       params[:max_id], | ||||
|       params[:since_id] | ||||
|       params_slice(:max_id, :since_id, :min_id) | ||||
|     ) | ||||
|   end | ||||
|  | ||||
| @@ -49,7 +48,7 @@ class Api::V1::FavouritesController < Api::BaseController | ||||
|  | ||||
|   def prev_path | ||||
|     unless results.empty? | ||||
|       api_v1_favourites_url pagination_params(since_id: pagination_since_id) | ||||
|       api_v1_favourites_url pagination_params(min_id: pagination_since_id) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   | ||||
| @@ -37,10 +37,9 @@ class Api::V1::NotificationsController < Api::BaseController | ||||
|   end | ||||
|  | ||||
|   def paginated_notifications | ||||
|     browserable_account_notifications.paginate_by_max_id( | ||||
|     browserable_account_notifications.paginate_by_id( | ||||
|       limit_param(DEFAULT_NOTIFICATIONS_LIMIT), | ||||
|       params[:max_id], | ||||
|       params[:since_id] | ||||
|       params_slice(:max_id, :since_id, :min_id) | ||||
|     ) | ||||
|   end | ||||
|  | ||||
| @@ -64,7 +63,7 @@ class Api::V1::NotificationsController < Api::BaseController | ||||
|  | ||||
|   def prev_path | ||||
|     unless @notifications.empty? | ||||
|       api_v1_notifications_url pagination_params(since_id: pagination_since_id) | ||||
|       api_v1_notifications_url pagination_params(min_id: pagination_since_id) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   | ||||
| @@ -7,11 +7,6 @@ class Api::V1::ReportsController < Api::BaseController | ||||
|  | ||||
|   respond_to :json | ||||
|  | ||||
|   def index | ||||
|     @reports = current_account.reports | ||||
|     render json: @reports, each_serializer: REST::ReportSerializer | ||||
|   end | ||||
|  | ||||
|   def create | ||||
|     @report = ReportService.new.call( | ||||
|       current_account, | ||||
|   | ||||
| @@ -30,7 +30,8 @@ class Api::V1::Timelines::HomeController < Api::BaseController | ||||
|     account_home_feed.get( | ||||
|       limit_param(DEFAULT_STATUSES_LIMIT), | ||||
|       params[:max_id], | ||||
|       params[:since_id] | ||||
|       params[:since_id], | ||||
|       params[:min_id] | ||||
|     ) | ||||
|   end | ||||
|  | ||||
| @@ -51,7 +52,7 @@ class Api::V1::Timelines::HomeController < Api::BaseController | ||||
|   end | ||||
|  | ||||
|   def prev_path | ||||
|     api_v1_timelines_home_url pagination_params(since_id: pagination_since_id) | ||||
|     api_v1_timelines_home_url pagination_params(min_id: pagination_since_id) | ||||
|   end | ||||
|  | ||||
|   def pagination_max_id | ||||
|   | ||||
| @@ -32,7 +32,8 @@ class Api::V1::Timelines::ListController < Api::BaseController | ||||
|     list_feed.get( | ||||
|       limit_param(DEFAULT_STATUSES_LIMIT), | ||||
|       params[:max_id], | ||||
|       params[:since_id] | ||||
|       params[:since_id], | ||||
|       params[:min_id] | ||||
|     ) | ||||
|   end | ||||
|  | ||||
| @@ -53,7 +54,7 @@ class Api::V1::Timelines::ListController < Api::BaseController | ||||
|   end | ||||
|  | ||||
|   def prev_path | ||||
|     api_v1_timelines_list_url params[:id], pagination_params(since_id: pagination_since_id) | ||||
|     api_v1_timelines_list_url params[:id], pagination_params(min_id: pagination_since_id) | ||||
|   end | ||||
|  | ||||
|   def pagination_max_id | ||||
|   | ||||
| @@ -21,10 +21,9 @@ class Api::V1::Timelines::PublicController < Api::BaseController | ||||
|   end | ||||
|  | ||||
|   def public_statuses | ||||
|     statuses = public_timeline_statuses.paginate_by_max_id( | ||||
|     statuses = public_timeline_statuses.paginate_by_id( | ||||
|       limit_param(DEFAULT_STATUSES_LIMIT), | ||||
|       params[:max_id], | ||||
|       params[:since_id] | ||||
|       params_slice(:max_id, :since_id, :min_id) | ||||
|     ) | ||||
|  | ||||
|     if truthy_param?(:only_media) | ||||
| @@ -53,7 +52,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController | ||||
|   end | ||||
|  | ||||
|   def prev_path | ||||
|     api_v1_timelines_public_url pagination_params(since_id: pagination_since_id) | ||||
|     api_v1_timelines_public_url pagination_params(min_id: pagination_since_id) | ||||
|   end | ||||
|  | ||||
|   def pagination_max_id | ||||
|   | ||||
| @@ -29,10 +29,9 @@ class Api::V1::Timelines::TagController < Api::BaseController | ||||
|     if @tag.nil? | ||||
|       [] | ||||
|     else | ||||
|       statuses = tag_timeline_statuses.paginate_by_max_id( | ||||
|       statuses = tag_timeline_statuses.paginate_by_id( | ||||
|         limit_param(DEFAULT_STATUSES_LIMIT), | ||||
|         params[:max_id], | ||||
|         params[:since_id] | ||||
|         params_slice(:max_id, :since_id, :min_id) | ||||
|       ) | ||||
|  | ||||
|       if truthy_param?(:only_media) | ||||
| @@ -62,7 +61,7 @@ class Api::V1::Timelines::TagController < Api::BaseController | ||||
|   end | ||||
|  | ||||
|   def prev_path | ||||
|     api_v1_timelines_tag_url params[:id], pagination_params(since_id: pagination_since_id) | ||||
|     api_v1_timelines_tag_url params[:id], pagination_params(min_id: pagination_since_id) | ||||
|   end | ||||
|  | ||||
|   def pagination_max_id | ||||
|   | ||||
| @@ -19,5 +19,13 @@ module Paginable | ||||
|       query = query.where(arel_table[:id].gt(min_id)) if min_id.present? | ||||
|       query | ||||
|     } | ||||
|  | ||||
|     scope :paginate_by_id, ->(limit, **options) { | ||||
|       if options[:min_id].present? | ||||
|         paginate_by_min_id(limit, options[:min_id]).reverse | ||||
|       else | ||||
|         paginate_by_max_id(limit, options[:max_id], options[:since_id]) | ||||
|       end | ||||
|     } | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -6,16 +6,20 @@ class Feed | ||||
|     @id   = id | ||||
|   end | ||||
|  | ||||
|   def get(limit, max_id = nil, since_id = nil) | ||||
|     from_redis(limit, max_id, since_id) | ||||
|   def get(limit, max_id = nil, since_id = nil, min_id = nil) | ||||
|     from_redis(limit, max_id, since_id, min_id) | ||||
|   end | ||||
|  | ||||
|   protected | ||||
|  | ||||
|   def from_redis(limit, max_id, since_id) | ||||
|     max_id     = '+inf' if max_id.blank? | ||||
|     since_id   = '-inf' if since_id.blank? | ||||
|     unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:first).map(&:to_i) | ||||
|   def from_redis(limit, max_id, since_id, min_id) | ||||
|     if min_id.blank? | ||||
|       max_id     = '+inf' if max_id.blank? | ||||
|       since_id   = '-inf' if since_id.blank? | ||||
|       unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:first).map(&:to_i) | ||||
|     else | ||||
|       unhydrated = redis.zrangebyscore(key, "(#{min_id}", '+inf', limit: [0, limit], with_scores: true).map(&:first).map(&:to_i) | ||||
|     end | ||||
|  | ||||
|     Status.where(id: unhydrated).cache_ids | ||||
|   end | ||||
|   | ||||
| @@ -7,9 +7,9 @@ class HomeFeed < Feed | ||||
|     @account = account | ||||
|   end | ||||
|  | ||||
|   def get(limit, max_id = nil, since_id = nil) | ||||
|   def get(limit, max_id = nil, since_id = nil, min_id = nil) | ||||
|     if redis.exists("account:#{@account.id}:regeneration") | ||||
|       from_database(limit, max_id, since_id) | ||||
|       from_database(limit, max_id, since_id, min_id) | ||||
|     else | ||||
|       super | ||||
|     end | ||||
| @@ -17,9 +17,9 @@ class HomeFeed < Feed | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def from_database(limit, max_id, since_id) | ||||
|   def from_database(limit, max_id, since_id, min_id) | ||||
|     Status.as_home_timeline(@account) | ||||
|           .paginate_by_max_id(limit, max_id, since_id) | ||||
|           .paginate_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id) | ||||
|           .reject { |status| FeedManager.instance.filter?(:home, status, @account.id) } | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -268,7 +268,7 @@ Rails.application.routes.draw do | ||||
|       resources :blocks,       only: [:index] | ||||
|       resources :mutes,        only: [:index] | ||||
|       resources :favourites,   only: [:index] | ||||
|       resources :reports,      only: [:index, :create] | ||||
|       resources :reports,      only: [:create] | ||||
|       resources :filters,      only: [:index, :create, :show, :update, :destroy] | ||||
|       resources :endorsements, only: [:index] | ||||
|  | ||||
|   | ||||
| @@ -64,7 +64,7 @@ RSpec.describe Api::V1::FavouritesController, type: :controller do | ||||
|           get :index, params: { limit: 1 } | ||||
|  | ||||
|           expect(response.headers['Link'].find_link(['rel', 'next']).href).to eq "http://test.host/api/v1/favourites?limit=1&max_id=#{favourite.id}" | ||||
|           expect(response.headers['Link'].find_link(['rel', 'prev']).href).to eq "http://test.host/api/v1/favourites?limit=1&since_id=#{favourite.id}" | ||||
|           expect(response.headers['Link'].find_link(['rel', 'prev']).href).to eq "http://test.host/api/v1/favourites?limit=1&min_id=#{favourite.id}" | ||||
|         end | ||||
|  | ||||
|         it 'does not add pagination headers if not necessary' do | ||||
|   | ||||
| @@ -12,16 +12,6 @@ RSpec.describe Api::V1::ReportsController, type: :controller do | ||||
|     allow(controller).to receive(:doorkeeper_token) { token } | ||||
|   end | ||||
|  | ||||
|   describe 'GET #index' do | ||||
|     let(:scopes) { 'read:reports' } | ||||
|  | ||||
|     it 'returns http success' do | ||||
|       get :index | ||||
|  | ||||
|       expect(response).to have_http_status(200) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'POST #create' do | ||||
|     let(:scopes)  { 'write:reports' } | ||||
|     let!(:status) { Fabricate(:status) } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user