diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 1ba490f5ec..8a58f51f78 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -582,6 +582,7 @@ Metrics/MethodLength: - 'app/controllers/spree/orders_controller.rb' - 'app/helpers/checkout_helper.rb' - 'app/helpers/spree/admin/navigation_helper.rb' + - "app/json_schemas/json_api_schema.rb" - 'app/models/spree/ability.rb' - 'app/models/spree/gateway/pay_pal_express.rb' - 'app/models/spree/order/checkout.rb' diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index d317d97b50..c3884f668e 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -5,6 +5,8 @@ module Api class BaseController < ActionController::API include CanCan::ControllerAdditions include RequestTimeouts + include Pagy::Backend + include JsonApiPagination check_authorization diff --git a/app/controllers/api/v1/customers_controller.rb b/app/controllers/api/v1/customers_controller.rb index 0d9da27542..65783afc70 100644 --- a/app/controllers/api/v1/customers_controller.rb +++ b/app/controllers/api/v1/customers_controller.rb @@ -11,9 +11,9 @@ module Api before_action :authorize_action, only: [:show, :update, :destroy] def index - customers = search_customers + @pagy, customers = pagy(search_customers, pagy_options) - render json: Api::V1::CustomerSerializer.new(customers, is_collection: true) + render json: Api::V1::CustomerSerializer.new(customers, pagination_options) end def show diff --git a/app/controllers/concerns/json_api_pagination.rb b/app/controllers/concerns/json_api_pagination.rb new file mode 100644 index 0000000000..1bf53351d4 --- /dev/null +++ b/app/controllers/concerns/json_api_pagination.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module JsonApiPagination + extend ActiveSupport::Concern + + DEFAULT_PER_PAGE = 50 + MAX_PER_PAGE = 200 + + def pagination_options + { + is_collection: true, + meta: meta_options, + links: links_options, + } + end + + def pagy_options + { items: final_per_page_value } + end + + private + + def meta_options + { + pagination: { + results: @pagy.count, + pages: total_pages, + page: current_page, + per_page: final_per_page_value + } + } + end + + def links_options + { + self: pagination_url(current_page), + first: pagination_url(1), + prev: pagination_url(previous_page), + next: pagination_url(next_page), + last: pagination_url(total_pages) + } + end + + def pagination_url(page_number) + return if page_number.nil? + + url_for(only_path: false, params: request.query_parameters.merge(page: page_number)) + end + + # User-specified value, or DEFAULT_PER_PAGE, capped at MAX_PER_PAGE + def final_per_page_value + (params[:per_page] || DEFAULT_PER_PAGE).to_i.clamp(1, MAX_PER_PAGE) + end + + def current_page + params[:page] || 1 + end + + def total_pages + @pagy.pages + end + + def previous_page + return nil if current_page < 2 + + current_page - 1 + end + + def next_page + return nil if current_page >= total_pages + + current_page + 1 + end +end diff --git a/app/json_schemas/json_api_schema.rb b/app/json_schemas/json_api_schema.rb index 1bc13098ef..db12a1c425 100644 --- a/app/json_schemas/json_api_schema.rb +++ b/app/json_schemas/json_api_schema.rb @@ -22,7 +22,8 @@ class JsonApiSchema type: :object, properties: data_properties(**options) }, - meta: { type: :object } + meta: { type: :object }, + links: { type: :object } }, required: [:data] } @@ -39,9 +40,33 @@ class JsonApiSchema properties: data_properties(**options) } }, - meta: { type: :object } + meta: { + type: :object, + properties: { + pagination: { + type: :object, + properties: { + results: { type: :integer, example: 250 }, + pages: { type: :integer, example: 5 }, + page: { type: :integer, example: 2 }, + per_page: { type: :integer, example: 50 }, + } + } + }, + required: [:pagination] + }, + links: { + type: :object, + properties: { + self: { type: :string }, + first: { type: :string }, + prev: { type: :string, nullable: true }, + next: { type: :string, nullable: true }, + last: { type: :string } + } + } }, - required: [:data] + required: [:data, :meta, :links] } end diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index 2019f25b2e..652fef8fb3 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -66,6 +66,8 @@ components: type: object meta: type: object + links: + type: object required: - data securitySchemas: @@ -149,8 +151,43 @@ paths: type: object meta: type: object + properties: + pagination: + type: object + properties: + results: + type: integer + example: 250 + pages: + type: integer + example: 5 + page: + type: integer + example: 2 + per_page: + type: integer + example: 50 + required: + - pagination + links: + type: object + properties: + self: + type: string + first: + type: string + prev: + type: string + nullable: true + next: + type: string + nullable: true + last: + type: string required: - data + - meta + - links post: summary: Create customer tags: @@ -208,6 +245,8 @@ paths: type: object meta: type: object + links: + type: object required: - data '422': @@ -322,6 +361,8 @@ paths: type: object meta: type: object + links: + type: object required: - data '404': @@ -430,6 +471,8 @@ paths: type: object meta: type: object + links: + type: object required: - data '422': @@ -546,6 +589,8 @@ paths: type: object meta: type: object + links: + type: object required: - data "/api/v1/enterprises/{enterprise_id}/customers": @@ -614,7 +659,42 @@ paths: type: object meta: type: object + properties: + pagination: + type: object + properties: + results: + type: integer + example: 250 + pages: + type: integer + example: 5 + page: + type: integer + example: 2 + per_page: + type: integer + example: 50 + required: + - pagination + links: + type: object + properties: + self: + type: string + first: + type: string + prev: + type: string + nullable: true + next: + type: string + nullable: true + last: + type: string required: - data + - meta + - links servers: - url: "/"