diff --git a/Gemfile b/Gemfile index e51ba026e..ffde65df4 100644 --- a/Gemfile +++ b/Gemfile @@ -11,6 +11,7 @@ gem 'cancancan', '~> 3.3' gem 'faraday' gem 'importmap-rails' gem 'jbuilder' +gem 'kaminari' gem 'pg', '~> 1.1' gem 'puma', '~> 5.6' gem 'rack-cors' diff --git a/Gemfile.lock b/Gemfile.lock index 740404e5c..76d552a31 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -126,6 +126,18 @@ GEM activesupport (>= 5.0.0) jmespath (1.6.2) json (2.6.2) + kaminari (1.2.2) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) + actionview + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) + activerecord + kaminari-core (= 1.2.2) + kaminari-core (1.2.2) loofah (2.19.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) @@ -282,6 +294,7 @@ DEPENDENCIES faraday importmap-rails jbuilder + kaminari pg (~> 1.1) pry-byebug puma (~> 5.6) diff --git a/app/controllers/api/projects_controller.rb b/app/controllers/api/projects_controller.rb index 4ab555d32..08f1758da 100644 --- a/app/controllers/api/projects_controller.rb +++ b/app/controllers/api/projects_controller.rb @@ -5,11 +5,13 @@ class ProjectsController < ApiController before_action :require_oauth_user, only: %i[create update index destroy] before_action :load_project, only: %i[show update destroy] before_action :load_projects, only: %i[index] + after_action :pagination_link_header, only: [:index] load_and_authorize_resource skip_load_resource only: :create def index - render :index, formats: [:json] + @paginated_projects = @projects.page(params[:page]) + render index: @paginated_projects, formats: [:json] end def show @@ -51,7 +53,7 @@ def load_project end def load_projects - @projects = Project.where(user_id: current_user) + @projects = Project.where(user_id: current_user).order(updated_at: :desc) end def project_params @@ -65,5 +67,47 @@ def project_params } ) end + + def pagination_link_header + pagination_links = [] + pagination_links << page_links(first_page, 'first') + pagination_links << page_links(last_page, 'last') + pagination_links << page_links(next_page, 'next') + pagination_links << page_links(prev_page, 'prev') + + pagination_links.compact_blank! + headers['Link'] = pagination_links.join(', ') + end + + def page_links(to_page, rel_type) + return if to_page.nil? + + page_info = "page=#{to_page}" + "<#{request.base_url}/api/projects?#{page_info}>; rel=\"#{rel_type}\"" + end + + def page + params.key?(:page) ? params[:page].to_i : 1 + end + + def total_pages + @projects.page(1).total_pages + end + + def first_page + @projects.page(page).first_page? ? nil : 1 + end + + def last_page + @projects.page(page).last_page? ? nil : total_pages + end + + def next_page + @projects.page(page).next_page + end + + def prev_page + @projects.page(page).prev_page + end end end diff --git a/app/views/api/projects/index.json.jbuilder b/app/views/api/projects/index.json.jbuilder index c3c4d9808..f29fe2640 100644 --- a/app/views/api/projects/index.json.jbuilder +++ b/app/views/api/projects/index.json.jbuilder @@ -1,3 +1,3 @@ # frozen_string_literal: true -json.array! @projects, :identifier, :project_type, :name, :user_id, :updated_at +json.array! @paginated_projects, :identifier, :project_type, :name, :user_id, :updated_at diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb index 10b8174ff..d16944308 100644 --- a/config/initializers/cors.rb +++ b/config/initializers/cors.rb @@ -5,6 +5,6 @@ Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins origins_array - resource '*', headers: :any, methods: %i[get post patch put delete] + resource '*', headers: :any, methods: %i[get post patch put delete], expose: ['Link'] end end diff --git a/config/initializers/kaminari_config.rb b/config/initializers/kaminari_config.rb new file mode 100644 index 000000000..9942177d5 --- /dev/null +++ b/config/initializers/kaminari_config.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Kaminari.configure do |config| + config.default_per_page = 8 +end diff --git a/spec/request/projects/index_spec.rb b/spec/request/projects/index_spec.rb index edbee5a35..2b9c3789a 100644 --- a/spec/request/projects/index_spec.rb +++ b/spec/request/projects/index_spec.rb @@ -3,6 +3,8 @@ require 'rails_helper' RSpec.describe 'Project index requests', type: :request do + include PaginationLinksMock + let(:user_id) { 'e0675b6c-dc48-4cd6-8c04-0f7ac05af51a' } let(:project_keys) { %w[identifier project_type name user_id updated_at] } @@ -41,6 +43,43 @@ end end + context 'when the projects index has pagination' do + before do + create_list(:project, 10, user_id:) + mock_oauth_user(user_id) + end + + it 'returns the default number of projects on the first page' do + get '/api/projects' + returned = JSON.parse(response.body) + expect(returned.length).to eq(8) + end + + it 'returns the next set of projects on the next page' do + get '/api/projects?page=2' + returned = JSON.parse(response.body) + expect(returned.length).to eq(4) + end + + it 'has the correct response headers for the first page' do + last_link = page_links(2, 'last') + next_link = page_links(2, 'next') + expected_link_header = [last_link, next_link].join(', ') + + get '/api/projects' + expect(response.headers['Link']).to eq expected_link_header + end + + it 'has the correct response headers for the next page' do + first_link = page_links(1, 'first') + prev_link = page_links(1, 'prev') + expected_link_header = [first_link, prev_link].join(', ') + + get '/api/projects?page=2' + expect(response.headers['Link']).to eq expected_link_header + end + end + context 'when no user' do it 'returns unauthorized' do get '/api/projects' diff --git a/spec/support/pagination_links_mock.rb b/spec/support/pagination_links_mock.rb new file mode 100644 index 000000000..bb51ff856 --- /dev/null +++ b/spec/support/pagination_links_mock.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module PaginationLinksMock + def page_links(to_page, rel_type) + page_info = "page=#{to_page}" + "; rel=\"#{rel_type}\"" + end +end