├── .bundler-version ├── .dependabot └── config.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── story.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── auto-approve.yml │ ├── auto-merge.yml │ └── ci.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .ruby-gemset ├── .ruby-version ├── .rubygems-version ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── bin ├── console ├── release └── setup ├── graphql-pagination.gemspec ├── lib ├── graphql-pagination.rb ├── graphql_pagination.rb └── graphql_pagination │ ├── collection_base_error.rb │ ├── collection_metadata_type.rb │ ├── collection_type.rb │ └── version.rb └── spec ├── graphql_pagination ├── collection_metadata_type_spec.rb ├── collection_type_spec.rb └── query_spec.rb ├── graphql_pagination_spec.rb └── spec_helper.rb /.bundler-version: -------------------------------------------------------------------------------- 1 | 2.6.9 2 | 3 | -------------------------------------------------------------------------------- /.dependabot/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "bundler" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | day: "monday" 9 | time: "08:00" 10 | timezone: "UTC" 11 | commit-message: 12 | prefix: "[dependabot]" 13 | labels: 14 | - "automerge" 15 | - "dependencies" 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to track an issue that has been identified 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Mutation/Query** 24 | 25 | **URL and HTTP method (for non-GQL):** 26 | 27 | **Sentry or Logs URL:** 28 | 29 | **User/authentication details** 30 | Impacted user name or service account 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/story.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New story 3 | about: Add a new story for implementation 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the solution** 11 | A clear and concise description of what you want to happen. 12 | When will this feature be done? 13 | 14 | **Describe the users** 15 | Who are we building this feature for? 16 | 17 | **Additional context** 18 | Add any other context or screenshots about the feature request here. 19 | Link to any applicable documents describing the feature. 20 | 21 | **Designs** 22 | Link to any applicable designs on Invision. 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description, motivation and context 2 | 3 | 4 | 5 | ## Related tickets(s), PR(s) or slack message: 6 | 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "bundler" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | day: "monday" 9 | time: "08:00" 10 | timezone: "UTC" 11 | commit-message: 12 | prefix: "[dependabot]" 13 | labels: 14 | - "automerge" 15 | - "dependencies" 16 | -------------------------------------------------------------------------------- /.github/workflows/auto-approve.yml: -------------------------------------------------------------------------------- 1 | name: Auto approve dependency upgrades and hot-fix PRs 2 | on: 3 | pull_request_target: 4 | types: 5 | - labeled 6 | - ready_for_review 7 | jobs: 8 | auto-approve: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: hmarr/auto-approve-action@v3 12 | if: github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]' || github.actor == 'renofidev' || contains(github.event.pull_request.labels.*.name, 'HOTFIX-AUTO-APPROVE') || contains(github.event.pull_request.labels.*.name, 'self-approve') || contains(github.event.pull_request.labels.*.name, 'dependencies') 13 | with: 14 | github-token: "${{ secrets.GITHUB_TOKEN }}" 15 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: automerge 2 | on: 3 | pull_request_target: 4 | types: 5 | - labeled 6 | pull_request_review: 7 | types: 8 | - submitted 9 | check_suite: 10 | types: 11 | - completed 12 | status: {} 13 | jobs: 14 | automerge: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: automerge 18 | uses: pascalgn/automerge-action@v0.16.4 19 | env: 20 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 21 | MERGE_METHOD: squash 22 | MERGE_DELETE_BRANCH: true 23 | MERGE_LABELS: "automerge,!automerge blocked" 24 | - name: automerge-dependencies 25 | uses: pascalgn/automerge-action@v0.16.4 26 | env: 27 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 28 | MERGE_METHOD: squash 29 | MERGE_DELETE_BRANCH: true 30 | MERGE_LABELS: "dependencies,!automerge blocked" 31 | MERGE_REMOVE_LABELS: "" 32 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Ruby CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | ci_tests_ruby: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | ruby-version: [3.3, 3.4] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up Ruby 19 | uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: ${{ matrix.ruby-version }} 22 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 23 | 24 | - name: Run tests 25 | run: bundle exec rake ci 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | 13 | # Cached rubocop config files 14 | .rubocop-http* 15 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - rubocop-rake 3 | - rubocop-rspec 4 | 5 | inherit_from: 6 | - https://raw.githubusercontent.com/RenoFi/rubocop/main/ruby.yml 7 | - https://raw.githubusercontent.com/RenoFi/rubocop/main/rspec.yml 8 | 9 | AllCops: 10 | TargetRubyVersion: 3.3 11 | 12 | Gemspec/RequiredRubyVersion: 13 | Include: 14 | - 3.3 15 | - 3.4 16 | 17 | Naming/FileName: 18 | Exclude: 19 | - 'lib/*.rb' 20 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | renofi-gems 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-3.4.4 2 | -------------------------------------------------------------------------------- /.rubygems-version: -------------------------------------------------------------------------------- 1 | 3.6.9 2 | 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'kaminari-activerecord' 6 | gem 'kaminari-core' 7 | gem 'pry' 8 | gem 'rake' 9 | gem 'rspec' 10 | gem 'rubocop' 11 | gem 'rubocop-rake' 12 | gem 'rubocop-rspec' 13 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | graphql-pagination (2.4.0) 5 | graphql (>= 2.4.7) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | activemodel (8.0.2) 11 | activesupport (= 8.0.2) 12 | activerecord (8.0.2) 13 | activemodel (= 8.0.2) 14 | activesupport (= 8.0.2) 15 | timeout (>= 0.4.0) 16 | activesupport (8.0.2) 17 | base64 18 | benchmark (>= 0.3) 19 | bigdecimal 20 | concurrent-ruby (~> 1.0, >= 1.3.1) 21 | connection_pool (>= 2.2.5) 22 | drb 23 | i18n (>= 1.6, < 2) 24 | logger (>= 1.4.2) 25 | minitest (>= 5.1) 26 | securerandom (>= 0.3) 27 | tzinfo (~> 2.0, >= 2.0.5) 28 | uri (>= 0.13.1) 29 | ast (2.4.3) 30 | base64 (0.2.0) 31 | benchmark (0.4.0) 32 | bigdecimal (3.1.9) 33 | coderay (1.1.3) 34 | concurrent-ruby (1.3.5) 35 | connection_pool (2.5.3) 36 | diff-lcs (1.6.2) 37 | drb (2.2.3) 38 | fiber-storage (1.0.1) 39 | graphql (2.5.7) 40 | base64 41 | fiber-storage 42 | logger 43 | i18n (1.14.7) 44 | concurrent-ruby (~> 1.0) 45 | json (2.12.0) 46 | kaminari-activerecord (1.2.2) 47 | activerecord 48 | kaminari-core (= 1.2.2) 49 | kaminari-core (1.2.2) 50 | language_server-protocol (3.17.0.5) 51 | lint_roller (1.1.0) 52 | logger (1.7.0) 53 | method_source (1.1.0) 54 | minitest (5.25.5) 55 | parallel (1.27.0) 56 | parser (3.3.8.0) 57 | ast (~> 2.4.1) 58 | racc 59 | prism (1.4.0) 60 | pry (0.15.2) 61 | coderay (~> 1.1) 62 | method_source (~> 1.0) 63 | racc (1.8.1) 64 | rainbow (3.1.1) 65 | rake (13.2.1) 66 | regexp_parser (2.10.0) 67 | rspec (3.13.0) 68 | rspec-core (~> 3.13.0) 69 | rspec-expectations (~> 3.13.0) 70 | rspec-mocks (~> 3.13.0) 71 | rspec-core (3.13.3) 72 | rspec-support (~> 3.13.0) 73 | rspec-expectations (3.13.4) 74 | diff-lcs (>= 1.2.0, < 2.0) 75 | rspec-support (~> 3.13.0) 76 | rspec-mocks (3.13.4) 77 | diff-lcs (>= 1.2.0, < 2.0) 78 | rspec-support (~> 3.13.0) 79 | rspec-support (3.13.3) 80 | rubocop (1.75.6) 81 | json (~> 2.3) 82 | language_server-protocol (~> 3.17.0.2) 83 | lint_roller (~> 1.1.0) 84 | parallel (~> 1.10) 85 | parser (>= 3.3.0.2) 86 | rainbow (>= 2.2.2, < 4.0) 87 | regexp_parser (>= 2.9.3, < 3.0) 88 | rubocop-ast (>= 1.44.0, < 2.0) 89 | ruby-progressbar (~> 1.7) 90 | unicode-display_width (>= 2.4.0, < 4.0) 91 | rubocop-ast (1.44.1) 92 | parser (>= 3.3.7.2) 93 | prism (~> 1.4) 94 | rubocop-rake (0.7.1) 95 | lint_roller (~> 1.1) 96 | rubocop (>= 1.72.1) 97 | rubocop-rspec (3.6.0) 98 | lint_roller (~> 1.1) 99 | rubocop (~> 1.72, >= 1.72.1) 100 | ruby-progressbar (1.13.0) 101 | securerandom (0.4.1) 102 | timeout (0.4.3) 103 | tzinfo (2.0.6) 104 | concurrent-ruby (~> 1.0) 105 | unicode-display_width (3.1.4) 106 | unicode-emoji (~> 4.0, >= 4.0.4) 107 | unicode-emoji (4.0.4) 108 | uri (1.0.3) 109 | 110 | PLATFORMS 111 | aarch64-linux-musl 112 | ruby 113 | x86_64-darwin-18 114 | x86_64-darwin-19 115 | x86_64-darwin-20 116 | x86_64-darwin-21 117 | x86_64-linux 118 | 119 | DEPENDENCIES 120 | graphql-pagination! 121 | kaminari-activerecord 122 | kaminari-core 123 | pry 124 | rake 125 | rspec 126 | rubocop 127 | rubocop-rake 128 | rubocop-rspec 129 | 130 | BUNDLED WITH 131 | 2.6.3 132 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 RenoFi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the 'Software'), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/graphql-pagination.svg)](https://rubygems.org/gems/graphql-pagination) 2 | [![Build Status](https://github.com/RenoFi/graphql-pagination/actions/workflows/ci.yml/badge.svg)](https://github.com/RenoFi/graphql-pagination/actions/workflows/ci.yml?query=branch%3Amain) 3 | 4 | # graphql-pagination 5 | 6 | Implements page-based pagination returning collection and pagination metadata. It works with `kaminari` or other pagination tools implementing similar methods. 7 | 8 | ## Installation 9 | 10 | Add `graphql-pagination` to your Gemfile, you can use `kaminari-activerecord` or `kaminari-monogid` to not implement page scope methods. Kaminari is not loaded by the gem, so you need to decide and load it on your own. 11 | 12 | ```ruby 13 | gem 'graphql-pagination' 14 | gem 'kaminari-activerecord' 15 | ``` 16 | 17 | ## Usage example 18 | 19 | Use `collection_type` instead of `connection_type` to define your type: 20 | 21 | ```ruby 22 | field :fruits, Types::FruitType.collection_type, null: true do 23 | argument :page, Integer, required: false 24 | argument :limit, Integer, required: false 25 | end 26 | 27 | def fruits(page: nil, limit: nil) 28 | ::Fruit.page(page).per(limit) 29 | end 30 | ``` 31 | 32 | Value returned by query resolver must be a kaminari object or implements its page scope methods (`current_page`, `limit_value`, `total_count`, `total_pages`). 33 | 34 | ## GraphQL query 35 | 36 | ```graphql 37 | { 38 | fruits(page: 2, limit: 2) { 39 | collection { 40 | id 41 | name 42 | } 43 | metadata { 44 | totalPages 45 | totalCount 46 | currentPage 47 | limitValue 48 | } 49 | } 50 | } 51 | ``` 52 | 53 | ```json 54 | { 55 | "data": { 56 | "checklists": { 57 | "collection": [ 58 | { 59 | "id": "93938bb3-7a6c-4d35-9961-cbb2d4c9e9ac", 60 | "name": "Apple" 61 | }, 62 | { 63 | "id": "b1ee93b2-579a-4107-8454-119bba5afb63", 64 | "name": "Mango" 65 | } 66 | ], 67 | "metadata": { 68 | "totalPages": 25, 69 | "totalCount": 50, 70 | "currentPage": 2, 71 | "limitValue": 2 72 | } 73 | } 74 | } 75 | } 76 | ``` 77 | 78 | ## Custom Base 79 | 80 | By default the resulting collection_type class is a direct descendant of 81 | graphql-ruby's GraphQL::Schema::Object, however you may require your own 82 | behaviours and properties on the collection class itself such as defining 83 | [custom visibility](https://graphql-ruby.org/authorization/visibility.html#object-visibility). 84 | 85 | This can be done by passing in your own custom class, to be inherited from: 86 | 87 | ```ruby 88 | class MyBaseType < GraphQL::Schema::Object 89 | def self.visible?(context) 90 | # ... 91 | end 92 | end 93 | 94 | field :fruits, Types::FruitType.collection_type(collection_base: MyBaseType) 95 | ``` 96 | 97 | ## Custom Metadata 98 | 99 | By default, the following fields are present in the metadata block: 100 | 101 | ```graphql 102 | metadata { 103 | totalPages 104 | totalCount 105 | currentPage 106 | limitValue 107 | } 108 | ``` 109 | 110 | These fields correspond to the `GraphqlPagination::CollectionMetadataType` used to provide the pagination information for the collection of data delivered. If you want to add more metadata fields to this block, you can do so by extending this `CollectionMetadataType`: 111 | 112 | ```ruby 113 | class MyMetadataType < GraphqlPagination::CollectionMetadataType 114 | field :custom_field, String, null: false 115 | end 116 | 117 | field :fruits, Types::FruitType.collection_type(metadata_type: MyMetadataType) 118 | ``` 119 | 120 | ## Contributing 121 | 122 | Bug reports and pull requests are welcome on GitHub at https://github.com/renofi/graphql-pagination. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 123 | 124 | ## License 125 | 126 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 127 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | require 'rubocop/rake_task' 4 | 5 | RSpec::Core::RakeTask.new(:spec) 6 | RuboCop::RakeTask.new 7 | 8 | task ci: %i[spec rubocop] 9 | task default: %i[spec rubocop:autocorrect_all] 10 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'graphql_pagination' 5 | require 'pry' 6 | require 'irb' 7 | 8 | IRB.start(__FILE__) 9 | -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | lib = File.expand_path('../lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'graphql_pagination/version' 6 | 7 | system("bundle") || exit(1) 8 | system("bundle exec rake") || exit(1) 9 | system("git commit -am 'Release #{GraphqlPagination::VERSION}'") 10 | system("bundle exec rake build release") 11 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | -------------------------------------------------------------------------------- /graphql-pagination.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('lib', __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'graphql_pagination/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'graphql-pagination' 7 | spec.version = GraphqlPagination::VERSION 8 | spec.authors = ['Krzysztof Knapik', 'RenoFi Engineering Team'] 9 | spec.email = ['knapo@knapo.net', 'engineering@renofi.com'] 10 | 11 | spec.summary = 'Page-based kaminari pagination for graphql.' 12 | spec.description = 'Page-based kaminari pagination for graphql returning collection and pagination metadata.' 13 | spec.homepage = 'https://github.com/RenoFi/graphql-pagination' 14 | spec.license = 'MIT' 15 | 16 | spec.metadata['homepage_uri'] = 'https://github.com/RenoFi/graphql-pagination' 17 | spec.metadata['source_code_uri'] = 'https://github.com/RenoFi/graphql-pagination' 18 | spec.metadata['rubygems_mfa_required'] = 'true' 19 | 20 | spec.required_ruby_version = Gem::Requirement.new('>= 3.3.0') 21 | 22 | spec.files = Dir.chdir(__dir__) do 23 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(bin/|spec/|\.rub)}) } 24 | end 25 | spec.bindir = 'exe' 26 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 27 | spec.require_paths = ['lib'] 28 | 29 | spec.add_dependency 'graphql', '>= 2.4.7' 30 | end 31 | -------------------------------------------------------------------------------- /lib/graphql-pagination.rb: -------------------------------------------------------------------------------- 1 | require 'graphql_pagination' 2 | -------------------------------------------------------------------------------- /lib/graphql_pagination.rb: -------------------------------------------------------------------------------- 1 | require 'graphql_pagination/version' 2 | require 'graphql' 3 | require 'graphql/schema' 4 | 5 | module GraphqlPagination 6 | end 7 | 8 | require 'graphql_pagination/collection_base_error' 9 | require 'graphql_pagination/collection_type' 10 | require 'graphql_pagination/collection_metadata_type' 11 | 12 | GraphQL::Schema::Object.extend GraphqlPagination::CollectionType 13 | -------------------------------------------------------------------------------- /lib/graphql_pagination/collection_base_error.rb: -------------------------------------------------------------------------------- 1 | module GraphqlPagination 2 | class CollectionBaseError < StandardError 3 | def message 4 | "CollectionBaseError: The collection_type attribute must inherit from 5 | or be a GraphQL::Schema::Object" 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/graphql_pagination/collection_metadata_type.rb: -------------------------------------------------------------------------------- 1 | module GraphqlPagination 2 | class CollectionMetadataType < GraphQL::Schema::Object 3 | description "Type for CollectionMetadataType" 4 | field :current_page, Integer, null: false, description: "Current Page of loaded data" 5 | field :limit_value, Integer, null: false, description: "The number of items per page" 6 | field :total_count, Integer, null: false, description: "The total number of items to be paginated" 7 | field :total_pages, Integer, null: false, description: "The total number of pages in the pagination" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/graphql_pagination/collection_type.rb: -------------------------------------------------------------------------------- 1 | module GraphqlPagination 2 | module CollectionType 3 | def collection_type( 4 | collection_base: GraphQL::Schema::Object, 5 | metadata_type: GraphqlPagination::CollectionMetadataType 6 | ) 7 | fail CollectionBaseError unless collection_base <= GraphQL::Schema::Object 8 | 9 | @collection_types ||= {} 10 | @collection_types[collection_base] ||= {} 11 | @collection_types[collection_base][metadata_type] ||= begin 12 | type_name = "#{graphql_name}Collection" 13 | source_type = self 14 | 15 | Class.new(collection_base) do 16 | graphql_name type_name 17 | description "#{graphql_name} type" 18 | field :collection, [source_type], null: false, description: "A collection of paginated #{graphql_name}" 19 | field :metadata, metadata_type, null: false, description: "Pagination Metadata for navigating the Pagination" 20 | 21 | def collection 22 | object 23 | end 24 | 25 | def metadata 26 | object 27 | end 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/graphql_pagination/version.rb: -------------------------------------------------------------------------------- 1 | module GraphqlPagination 2 | VERSION = '2.4.0'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /spec/graphql_pagination/collection_metadata_type_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe GraphqlPagination::CollectionMetadataType do 2 | it 'has description' do 3 | expect(described_class.description).to be_present 4 | end 5 | 6 | describe '.fields' do 7 | it 'has expected fields' do 8 | expect(described_class.fields.keys).to match_array(%w[currentPage limitValue totalCount totalPages]) 9 | end 10 | 11 | it 'has descriptions on fields' do 12 | described_class.fields.each do |key, value| 13 | expect(described_class.fields[key].description).to be_present 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/graphql_pagination/collection_type_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe GraphqlPagination::CollectionType do 2 | describe '.collection_type' do 3 | subject(:collection_type) { type.collection_type } 4 | 5 | let(:type) do 6 | Class.new(GraphQL::Schema::Object) do 7 | graphql_name 'Fruit' 8 | end 9 | end 10 | 11 | it "has expected fields" do 12 | expect(collection_type.fields.keys).to match_array(%w[collection metadata]) 13 | end 14 | 15 | it "has description" do 16 | expect(collection_type.description).to be_present 17 | end 18 | 19 | context "with custom metadata type" do 20 | let(:collection_type) { type.collection_type } 21 | let(:custom_collection_type) { type.collection_type(metadata_type:) } 22 | 23 | let(:metadata_type) do 24 | Class.new(GraphqlPagination::CollectionMetadataType) do 25 | graphql_name 'CustomCollectionMetadataType' 26 | field :foo, String, null: true 27 | end 28 | end 29 | 30 | it "returns an appropriate collection type based on metadata_type argument" do 31 | expect(collection_type.fields['metadata'].type.of_type.fields.keys).not_to include('foo') 32 | expect(custom_collection_type.fields['metadata'].type.of_type.fields.keys).to include('foo') 33 | end 34 | 35 | it "caches the type for future use" do 36 | expect(custom_collection_type).to be(type.collection_type(metadata_type:)) 37 | end 38 | 39 | it "has description" do 40 | expect(custom_collection_type.fields['metadata'].description).to be_present 41 | end 42 | end 43 | 44 | context "with custom collection base" do 45 | let(:collection_base) do 46 | Class.new(GraphQL::Schema::Object) do 47 | graphql_name 'CustomCollectionBase' 48 | field :foo, String, null: true 49 | def self.visible?(_) = false 50 | end 51 | end 52 | 53 | let(:collection_type) { type.collection_type } 54 | let(:custom_collection_type) { type.collection_type(collection_base:) } 55 | 56 | it "returns an appropriate collection type based on collection_base argument" do 57 | expect(collection_type.visible?(nil)).to be true 58 | expect(custom_collection_type.visible?(nil)).to be false 59 | 60 | expect(collection_type.fields.keys).not_to include('foo') 61 | expect(custom_collection_type.fields.keys).to include('foo') 62 | end 63 | 64 | it "has description" do 65 | expect(custom_collection_type.fields['collection'].description).to be_present 66 | end 67 | 68 | it "caches the type for future use" do 69 | expect(custom_collection_type).to be(type.collection_type(collection_base:)) 70 | end 71 | 72 | context "when collection_base is not a GraphQL::Schema::Object" do 73 | let(:collection_base) { Class.new } 74 | 75 | it "raises an error" do 76 | expect { custom_collection_type }.to raise_error(GraphqlPagination::CollectionBaseError) 77 | end 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/graphql_pagination/query_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe 'query spec' do 2 | subject(:result) { TestSchema.execute(query).to_h['data']['result'] } 3 | 4 | context 'when page and limit given' do 5 | let(:query) do 6 | %|{ 7 | result: fruits(page: 2, limit: 2) { 8 | collection { 9 | id 10 | name 11 | } 12 | metadata { 13 | totalCount 14 | totalPages 15 | limitValue 16 | currentPage 17 | } 18 | } 19 | }| 20 | end 21 | 22 | it do 23 | expect(result['collection'].size).to eq(2) 24 | expect(result['collection'][0]['id']).not_to be_empty 25 | expect(result['collection'][0]['name']).not_to be_empty 26 | expect(result['metadata']['totalCount']).to eq(11) 27 | expect(result['metadata']['totalPages']).to eq(6) 28 | expect(result['metadata']['limitValue']).to eq(2) 29 | expect(result['metadata']['currentPage']).to eq(2) 30 | end 31 | end 32 | 33 | context 'when page and limit not given' do 34 | let(:query) do 35 | %|{ 36 | result: fruits { 37 | collection { 38 | id 39 | name 40 | } 41 | metadata { 42 | totalCount 43 | totalPages 44 | limitValue 45 | currentPage 46 | } 47 | } 48 | }| 49 | end 50 | 51 | it do 52 | expect(result['collection'].size).to eq(11) 53 | expect(result['collection'][0]['id']).not_to be_empty 54 | expect(result['collection'][0]['name']).not_to be_empty 55 | expect(result['metadata']['totalCount']).to eq(11) 56 | expect(result['metadata']['totalPages']).to eq(1) 57 | expect(result['metadata']['limitValue']).to eq(25) 58 | expect(result['metadata']['currentPage']).to eq(1) 59 | end 60 | end 61 | 62 | context 'with custom metadata type' do 63 | let(:query) do 64 | %|{ 65 | result: vegetables { 66 | collection { 67 | id 68 | name 69 | } 70 | metadata { 71 | totalCount 72 | totalPages 73 | limitValue 74 | currentPage 75 | customField 76 | } 77 | } 78 | }| 79 | end 80 | 81 | it do 82 | expect(result["collection"].size).to eq(11) 83 | expect(result["collection"][0]["id"]).not_to be_empty 84 | expect(result["collection"][0]["name"]).not_to be_empty 85 | expect(result["metadata"]["totalCount"]).to eq(11) 86 | expect(result["metadata"]["totalPages"]).to eq(1) 87 | expect(result["metadata"]["limitValue"]).to eq(25) 88 | expect(result["metadata"]["currentPage"]).to eq(1) 89 | expect(result["metadata"]["customField"]).to eq("custom_value") 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /spec/graphql_pagination_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe GraphqlPagination do 2 | it 'has a version number' do 3 | expect(GraphqlPagination::VERSION).not_to be_nil 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'pry' 3 | 4 | require 'ostruct' 5 | require 'active_support' 6 | require 'active_support/core_ext' 7 | 8 | require 'graphql-pagination' 9 | require 'kaminari/core' 10 | 11 | RSpec.configure do |config| 12 | config.disable_monkey_patching! 13 | 14 | config.expect_with :rspec do |c| 15 | c.syntax = :expect 16 | end 17 | 18 | config.define_derived_metadata do |meta| 19 | meta[:aggregate_failures] = true 20 | end 21 | end 22 | 23 | class FruitModel 24 | def self.all 25 | items = (0..10).map { |i| OpenStruct.new(id: SecureRandom.uuid, name: "Mango #{i}") } 26 | ::Kaminari.paginate_array(items) 27 | end 28 | end 29 | 30 | class VegetableModel 31 | def self.all 32 | items = (0..10).map { |i| OpenStruct.new(id: SecureRandom.uuid, name: "Carrot #{i}") } 33 | ::Kaminari.paginate_array(items) 34 | end 35 | end 36 | 37 | class FruitType < GraphQL::Schema::Object 38 | field :id, ID, null: false 39 | field :name, String, null: false 40 | end 41 | 42 | class VegetableType < GraphQL::Schema::Object 43 | field :id, ID, null: false 44 | field :name, String, null: false 45 | end 46 | 47 | class VegetableMetadataType < GraphqlPagination::CollectionMetadataType 48 | field :custom_field, String, null: false 49 | end 50 | 51 | module CustomField 52 | def custom_field 53 | "custom_value" 54 | end 55 | end 56 | 57 | class TestQueryType < GraphQL::Schema::Object 58 | field :fruits, FruitType.collection_type, null: true do 59 | argument :page, Integer, required: false 60 | argument :limit, Integer, required: false 61 | end 62 | 63 | field :vegetables, VegetableType.collection_type(metadata_type: VegetableMetadataType), null: true do 64 | argument :page, Integer, required: false 65 | argument :limit, Integer, required: false 66 | end 67 | 68 | def fruits(page: nil, limit: nil) 69 | FruitModel.all.page(page).per(limit) 70 | end 71 | 72 | def vegetables(page: nil, limit: nil) 73 | results = VegetableModel.all.page(page).per(limit) 74 | results.extend(CustomField) 75 | results 76 | end 77 | end 78 | 79 | class TestSchema < GraphQL::Schema 80 | query TestQueryType 81 | end 82 | --------------------------------------------------------------------------------