pax_global_header00006660000000000000000000000064151411506260014513gustar00rootroot0000000000000052 comment=6770b636883fe766d9043e9eec47c7cbc89f115b graphql-ruby-2.5.19/000077500000000000000000000000001514115062600142265ustar00rootroot00000000000000graphql-ruby-2.5.19/.codeclimate.yml000066400000000000000000000004151514115062600173000ustar00rootroot00000000000000version: "2" exclude_patterns: - "spec/" - "lib/graphql/language/lexer.rb" - "lib/graphql/language/parser.rb" # plugin - duplication - is failing on these - "spec/graphql/language/printer_spec.rb" - "lib/graphql/compatibility/query_parser_specification.rb" graphql-ruby-2.5.19/.gitattributes000066400000000000000000000000641514115062600171210ustar00rootroot00000000000000*.snap linguist-generated *.lock linguist-generated graphql-ruby-2.5.19/.github/000077500000000000000000000000001514115062600155665ustar00rootroot00000000000000graphql-ruby-2.5.19/.github/ISSUE_TEMPLATE/000077500000000000000000000000001514115062600177515ustar00rootroot00000000000000graphql-ruby-2.5.19/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000026511514115062600224470ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve graphql-ruby title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **Versions** `graphql` version: `rails` (or other framework): other applicable versions (`graphql-batch`, etc) **GraphQL schema** Include relevant types and fields (in Ruby is best, in GraphQL IDL is ok). Any custom extensions, etc? ```ruby class Product < GraphQL::Schema::Object field :id, ID, hash_key: :id # … end class ApplicationSchema < GraphQL::Schema query QueryType # … end ``` **GraphQL query** Example GraphQL query and response (if query execution is involved) ```graphql query { products { id title } } ``` ```json { "data": { "products": […] } } ``` **Steps to reproduce** Steps to reproduce the behavior **Expected behavior** A clear and concise description of what you expected to happen. **Actual behavior** What specifically went wrong? Place full backtrace here (if a Ruby exception is involved):
Click to view exception backtrace ``` Something went wrong 2.6.0/gems/graphql-1.9.17/lib/graphql/subscriptions/instrumentation.rb:34:in `after_query' … don't hesitate to include all the rows here: they will be collapsed ```
**Additional context** Add any other context about the problem here. With these details, we can efficiently hunt down the bug! graphql-ruby-2.5.19/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011271514115062600234770ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. graphql-ruby-2.5.19/.github/contributing.md000066400000000000000000000026511514115062600206230ustar00rootroot00000000000000# Contributing Thanks for getting involved! I hope the information below will help you contribute to graphql-ruby. ## Issues When reporting a bug, please include these details when applicable: - `graphql` version and other applicable versions (Rails, `graphql-batch`, etc) - Definitions of schema or relevant types and fields (in Ruby is best, in GraphQL IDL is ok) - Example GraphQL query and response (if query execution is involved) - Full backtrace (if a Ruby exception is involved) With these details, we can efficiently hunt down the bug! ## Code It's important for code to fit in with design and maintenance goals of the project. For this reason, consider an issue to discuss new features or large refactors. That way we can work together to find suitable solution! ## Legal By submitting a Pull Request, you disavow any rights or claims to any changes submitted to `graphql-ruby` and assign the copyright of those changes to Robert Mosolgo, the author and maintainer of `graphql-ruby`. If you cannot or don't want to reassign those rights (your employment contract for your employer may not allow this), don't submit a PR. Instead, open an issue so that someone else can give it a try. In short, contributing code means that the code belongs to the maintainer. That's generally what you want, since the burden of upkeep, support and distribution falls on the maintainer anyways. I hope this doesn't prohibit you from contributing! graphql-ruby-2.5.19/.github/dependabot.yml000066400000000000000000000001661514115062600204210ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" graphql-ruby-2.5.19/.github/workflows/000077500000000000000000000000001514115062600176235ustar00rootroot00000000000000graphql-ruby-2.5.19/.github/workflows/ci.yaml000066400000000000000000000111721514115062600211040ustar00rootroot00000000000000name: CI Suite on: - pull_request jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: ruby-version: 3.4 bundler-cache: true - run: bundle exec rake rubocop system_tests: runs-on: ubuntu-latest steps: - uses: shogo82148/actions-setup-redis@v1 with: redis-version: "7.x" - run: redis-cli ping - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: ruby-version: 3.4 bundler-cache: true env: BUNDLE_GEMS__GRAPHQL__PRO: ${{ secrets.BUNDLE_GEMS__GRAPHQL__PRO }} BUNDLE_GEMFILE: gemfiles/rails_master.gemfile - run: bin/rails test:all working-directory: ./spec/dummy env: BUNDLE_GEMS__GRAPHQL__PRO: ${{ secrets.BUNDLE_GEMS__GRAPHQL__PRO }} BUNDLE_GEMFILE: ../../gemfiles/rails_master.gemfile # Some coverage goals of these tests: # - Test once without Rails at all # - Test postgres, to make sure that the ActiveRecord # stuff works on that (as well as the default sqlite) # - Test mongoid -- and several versions, since they're quite different # - Run the JS unit tests once # - Test each major version of Rails we support # - Test the min/max minor Ruby version we support (and others?) test: strategy: fail-fast: false matrix: include: - gemfile: Gemfile ruby: head - gemfile: Gemfile ruby: 2.7 # lowest supported version - gemfile: gemfiles/rails_8.0.gemfile ruby: 3.3 graphql_reject_numbers_followed_by_names: 1 - gemfile: gemfiles/rails_8.1.gemfile ruby: 4.0 graphql_reject_numbers_followed_by_names: 1 redis: 1 - gemfile: gemfiles/rails_master.gemfile ruby: 3.4 graphql_reject_numbers_followed_by_names: 1 isolation_level_fiber: 1 redis: 1 runs-on: ubuntu-latest steps: - run: echo BUNDLE_GEMFILE=${{ matrix.gemfile }} > $GITHUB_ENV - run: echo GRAPHQL_REJECT_NUMBERS_FOLLOWED_BY_NAMES=1 > $GITHUB_ENV if: ${{ !!matrix.graphql_reject_numbers_followed_by_names }} - run: echo ISOLATION_LEVEL_FIBER=1 > $GITHUB_ENV if: ${{ !!matrix.isolation_level_fiber }} - uses: shogo82148/actions-setup-redis@v1 with: redis-version: "7.x" if: ${{ !!matrix.redis }} - run: redis-cli ping if: ${{ !!matrix.redis }} - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - run: bundle exec rake compile - run: bundle exec rake test javascript_test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: '21' - run: npm ci working-directory: ./javascript_client - run: npm test working-directory: ./javascript_client postgres_test: runs-on: ubuntu-latest strategy: matrix: include: - gemfile: gemfiles/rails_master.gemfile ruby: 3.3 isolation_level_fiber: 1 - gemfile: gemfiles/rails_7.2_postgresql.gemfile ruby: 3.3 services: postgres: image: postgres:latest env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: postgres ports: - 5432:5432 # needed because the postgres container does not provide a healthcheck options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - run: echo BUNDLE_GEMFILE='' > $GITHUB_ENV - run: echo DATABASE='POSTGRESQL' > $GITHUB_ENV - run: echo PGPASSWORD='postgres' > $GITHUB_ENV - run: echo GRAPHQL_CPARSER=1 > $GITHUB_ENV - run: echo ISOLATION_LEVEL_FIBER=1 > $GITHUB_ENV if: ${{ !!matrix.isolation_level_fiber }} - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: ruby-version: "3.3" bundler-cache: true - run: bundle exec rake compile test mongodb_test: strategy: fail-fast: false matrix: gemfile: - gemfiles/mongoid_9.gemfile - gemfiles/mongoid_8.gemfile runs-on: ubuntu-latest services: mongodb: image: mongo ports: - 27017:27017 steps: - run: echo BUNDLE_GEMFILE=${{ matrix.gemfile }} > $GITHUB_ENV - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: ruby-version: 3.4 bundler-cache: true - run: bundle exec rake compile test graphql-ruby-2.5.19/.github/workflows/pronto.yaml000066400000000000000000000013051514115062600220270ustar00rootroot00000000000000name: Pronto on: - pull_request_target jobs: pronto: runs-on: ubuntu-latest steps: - run: echo BUNDLE_GEMFILE=gemfiles/pronto.gemfile > $GITHUB_ENV - name: Checkout code uses: actions/checkout@v6 - run: git fetch --no-tags --prune --unshallow origin +refs/heads/*:refs/remotes/origin/* - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: 3.4 bundler-cache: true - name: Run Pronto run: bundle exec pronto run -f github_pr -c origin/${{ github.base_ref }} env: PRONTO_PULL_REQUEST_ID: ${{ github.event.pull_request.number }} PRONTO_GITHUB_ACCESS_TOKEN: "${{ github.token }}" graphql-ruby-2.5.19/.github/workflows/website.yaml000066400000000000000000000055641514115062600221630ustar00rootroot00000000000000name: Publish Website on: # For some reason, `on: release: ...` didn't work with `nektos/act` push: # Sequence of patterns matched against refs/tags tags: - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 workflow_dispatch: inputs: publish_website: description: "Publish guides to website?" type: boolean required: true default: true publish_version: description: "If present, pull this GraphQL-Ruby version to rebuild API docs" required: false type: string permissions: {} jobs: website: if: ${{ inputs.publish_website || github.ref_name }} permissions: contents: write name: Publish Website runs-on: ubuntu-latest steps: - name: Checkout master uses: actions/checkout@v6 - name: Checkout GitHub pages branch uses: actions/checkout@v6 with: path: gh-pages ref: gh-pages - uses: ruby/setup-ruby@v1 with: ruby-version: '3.1' bundler-cache: true - name: Build HTML, reindex env: ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }} run: | bundle exec rake site:fetch_latest site:build_doc site:update_search_index site:clean_html site:build_html - name: Commit changes as last committer run: | git config --global user.name "$(git log --format="%aN" -n 1)" git config --global user.email "$(git log --format="%aE" -n 1)" bundle exec rake site:commit_changes - name: Deploy to GitHub pages via gh-pages branch uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./gh-pages api_docs: needs: website if: ${{ inputs.publish_version || github.ref_name }} permissions: contents: write name: Publish API Docs runs-on: ubuntu-latest steps: - name: Checkout release tag uses: actions/checkout@v6 with: ref: ${{ env.GITHUB_REF }} - name: Checkout GitHub pages branch uses: actions/checkout@v6 with: path: gh-pages ref: gh-pages - uses: ruby/setup-ruby@v1 with: ruby-version: '3.2' bundler-cache: true - name: Build API docs run: | bundle exec rake site:fetch_latest apidocs:gen_version["${{ inputs.publish_version || env.GITHUB_REF }}"] - name: Commit changes as rmosolgo run: | git config --global user.name rmosolgo git config --global user.email rdmosolgo@gmail.com git status bundle exec rake site:commit_changes git status - name: Deploy to GitHub pages via gh-pages branch uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./gh-pages graphql-ruby-2.5.19/.gitignore000066400000000000000000000014061514115062600162170ustar00rootroot00000000000000.ruby-version doc/ .yardoc/ guides/yardoc/ pkg/ Gemfile.lock gemfiles/*.lock # Test database *.db *.db-shm *.db-wal *.sqlite3 .byebug_history .bundle/ vendor/ .idea/ coverage/ _site .sass-cache .jekyll-metadata .jekyll-cache gh-pages/ tmp/* node_modules/ yarn.lock OperationStoreClient.js DumpPayloadExample.json spec/integration/tmp .vscode/ *.swp *.swo .DS_Store # These are generated for distribution, but shouldn't be # versioned with the typescript source (which is in javascript_client/src): javascript_client/__tests__ javascript_client/subscriptions javascript_client/sync javascript_client/cli* javascript_client/index* # Don't commit compiled extension files: *.bundle *.so # Ragel generates Ruby type hints which is great, but I'm not ready to support them *.ri graphql-ruby-2.5.19/.rubocop.yml000066400000000000000000000045641514115062600165110ustar00rootroot00000000000000require: - ./cop/development/none_without_block_cop - ./cop/development/no_eval_cop - ./cop/development/no_focus_cop - ./lib/graphql/rubocop/graphql/default_null_true - ./lib/graphql/rubocop/graphql/default_required_true - ./cop/development/context_is_passed_cop - ./cop/development/trace_methods_cop.rb AllCops: DisabledByDefault: true SuggestExtensions: false TargetRubyVersion: 2.7 Exclude: - 'lib/graphql/language/lexer.rb' - 'lib/graphql/language/parser.rb' - 'gemfiles/**/*' - 'tmp/**/*' - 'vendor/**/*' - 'spec/integration/tmp/**/*' - 'spec/fixtures/cop/*.rb' Development/ContextIsPassedCop: Exclude: - 'spec/**/*' - 'cop/**/*' - 'lib/graphql/schema/validation.rb' - 'lib/graphql/static_validation/literal_validator.rb' - 'lib/graphql/static_validation/rules/**/*.rb' # AST-related: - 'lib/graphql/schema/build_from_definition.rb' - 'lib/graphql/language/printer.rb' - 'lib/graphql/language/nodes.rb' # Build-time, not runtime: - 'lib/graphql/schema/addition.rb' - 'lib/graphql/schema/introspection_system.rb' # Methods from generators - 'lib/generators/graphql/type_generator.rb' Development/NoneWithoutBlockCop: Include: - "lib/**/*" - "spec/**/*" Development/NoEvalCop: Include: - "lib/**/*" Development/NoFocusCop: Include: - "spec/**/*" Development/TraceMethodsCop: Include: - "lib/graphql/tracing/perfetto_trace.rb" - "lib/graphql/tracing/notifications_trace.rb" # def ... # end Layout/DefEndAlignment: EnforcedStyleAlignWith: def # value = if # # ... # end Layout/EndAlignment: EnforcedStyleAlignWith: variable Lint/UselessAssignment: Enabled: true Lint/DuplicateMethods: Enabled: true Metrics/ParameterLists: Max: 7 CountKeywordArgs: false Style/ClassAndModuleChildren: EnforcedStyle: nested Layout/EmptyLineBetweenDefs: AllowAdjacentOneLineDefs: true NumberOfEmptyLines: [0, 1, 2] Style/FrozenStringLiteralComment: Enabled: true Layout/IndentationWidth: Width: 2 Style/LambdaCall: EnforcedStyle: call Layout/LeadingCommentSpace: Enabled: true Naming/MethodName: EnforcedStyle: snake_case Style/WordArray: EnforcedStyle: brackets # ->(...) { ... } Layout/SpaceInLambdaLiteral: Enabled: true # Default is "require_no_space" GraphQL/DefaultNullTrue: Enabled: true GraphQL/DefaultRequiredTrue: Enabled: true graphql-ruby-2.5.19/.yardopts000066400000000000000000000001621514115062600160730ustar00rootroot00000000000000--no-private --markup=markdown --readme=readme.md --title='GraphQL Ruby API Documentation' 'lib/**/*.rb' - '*.md' graphql-ruby-2.5.19/CHANGELOG-enterprise.md000066400000000000000000000113451514115062600202210ustar00rootroot00000000000000# graphql-enterprise ### Breaking Changes ### Deprecations ### New Features ### Bug Fix # 1.6.0 (25 Nov 2025) - `RuntimeLimiter` and `ActiveOperationLimiter` now support `Redis::Cluster` via `redis_cluster: ...` options #5465 # 1.5.9 (21 Nov 2025) - RuntimeLimiter: improve compatibility with ObjectCache # 1.5.8 (19 Sep 2025) - ObjectCache: Fix deprecation regarding `Resolve.resolve_all` by using a forward-compatible approach to lazy value resolution #5437 # 1.5.7 (2 May 2025) - ObjectCache: Use Rails's `.cache_key_with_version` in `CacheableRelation` for proper cache busting # 1.5.6 (13 Dec 2024) - ObjectCache: Add `CacheableRelation` helper for top-level ActiveRecord relations # 1.5.5 (10 Dec 2024) - Changesets: Add missing `ensure_loaded` call for class-based changesets # 1.5.4 (31 Oct 2024) - ObjectCache: Add `reauthorize_cached_objects: false` # 1.5.3 (1 Oct 2024) - Limiters: Add expiration to rate limit data (to reduce Redis footprint) # 1.5.2 (6 Sept 2024) - Limiters: Add `connection_pool:` support # 1.5.1 (30 Aug 2024) - ObjectCache: Add `connection_pool:` support # 1.5.0 (26 Jul 2024) - ObjectCache: Add Dalli backend for Memcached # 1.4.2 (11 Jun 2024) - ObjectCache: Add `Schema.fingerprint` hook and `context[:refresh_object_cache]` # 1.4.1 (30 May 2024) - ObjectCache: properly handle when object fingerprints are evicted but the cached result wasn't # 1.4.0 (11 Apr 2024) - ObjectCache: add support for `redis_cluster: ...` backend # 1.3.4 (18 Mar 2024) - ObjectCache: use new `trace_with` API for instrumentation # 1.3.3 (30 Jan 2024) - ObjectCache: fix compatibility with `run_graphql_field` test helper #4816 # 1.3.2 (15 Jan 2024) ### Bug Fix - Limiters: Migrate to new `trace_with` instrumentation API, requires GraphQL-Ruby 2.0.18+ # 1.3.1 (12 June 2023) ### Bug Fix - Add missing `require "graphql"` #4511 # 1.3.0 (29 May 2023) ### New Features - Changesets: Add `added_in: ...` and `removed_in: ...` for inline definition changes # 1.2.0 (10 February 2023) ### New Features - Support the `redis-client` gem as `redis:` (requires graphql-pro 1.24.0+) # 1.1.14 (3 November 2022) ### New Features - Limiters: Support `dashboard_charts: false` to disable built-in instrumentation - Limiters: Support `assign_as:` to use a different accessor method for storing limiter instances on schema classes (add a corresponding `class << self; attr_accessor ...; end` to the schema class to use it) - Limiters: Support `context_key:` to put runtime info in a different key in query context - Runtime Limiter: Add `window_ms:` to runtime info # 1.1.13 (21 October 2022) ### Bug Fix - Limiter: handle missing fields in MutationLimiter # 1.1.12 (18 October 2022) ### New Features - Limiters: add MutationLimiter ### Bug Fix - ObjectCache: Update Redis calls to support redis-rb 5.0 # 1.1.11 (25 August 2022) ### Bug Fix - ObjectCache: also update `delete` to handle more than 1000 objects in Lua # 1.1.10 (19 August 2022) ### Bug Fix - ObjectCache: read and write objects 1000-at-a-time to avoid overloading Lua scripts in Redis # 1.1.9 (3 August 2022) ### New Features - ObjectCache: Add a message to context when a type or field causes a query to be treated as "private" ### Bug Fix - ObjectCache: skip the query analyzer when `context[:skip_object_cache]` is present # 1.1.8 (1 August 2022) ### New Features - ObjectCache: Add `ObjectType.cache_dependencies_for(object, context)` to customize dependencies for an object ### Bug Fix - ObjectCache: Fix to make `context[:object_cache][:objects]` a Set # 1.1.7 (28 July 2022) ### Bug Fix - ObjectCache: remove needless `resolve_type` calls # 1.1.6 (28 July 2022) ### Bug Fix - ObjectCache: persist the type names of cached objects, pass them to `Schema.resolve_type` when validating cached responses. # 1.1.5 (22 July 2022) ### New Features - ObjectCache: add `cache_introspection: { ttl: ... }` for setting an expiration (in seconds) on introspection fields. # 1.1.4 (19 March 2022) ### Bug Fix - ObjectCache: don't create a cache fingerprint if the query is found to be uncacheable during analysis. # 1.1.3 (3 March 2022) ### Bug Fix - Changesets: Return an empty set when a schema doesn't use changesets #3972 # 1.1.2 (1 March 2022) ### New Features - Changesets: Add introspection methods `Schema.changesets` and `Changeset.changes` # 1.1.1 (14 February 2021) ### Bug Fix - Changesets: don't require `context.schema` for plain-Ruby calls to introspection methods #3929 # 1.1.0 (24 November 2021) ### New Features - Changesets: Add `GraphQL::Enterprise::Changeset` # 1.0.1 (9 November 2021) ### Bug Fix - Object Cache: properly handle invalid queries #3703 # 1.0.0 (13 October 2021) ### New Features - Rate limiters: first release - Object cache: first release graphql-ruby-2.5.19/CHANGELOG-pro.md000066400000000000000000001021621514115062600166370ustar00rootroot00000000000000# graphql-pro ### Breaking Changes ### Deprecations ### New Features # 1.29.14 (21 Nov 2025) - Add configuration for `ostruct` dependency # 1.29.13 (22 Sept 2025) - Stable connections: fix condition grouping with IS NULL #5435 - `@defer`: Correctly handle fields where `@defer` is present twice #5434 - `@defer`: Update implementation to address warnings in GraphQL-Ruby 2.5.12+ # 1.29.12 (12 Sept 2025) - `OperationStore`: also support `visibility_profile:` on lazy routes. # 1.29.11 (12 Sept 2025) - `OperationStore`: add `visibility_profile: ...` argument to `operation_store_sync` so that incoming operations are bound to the given profile. # 1.29.10 (3 Jun 2025) - `@defer`, `@stream`: Include `"data"` in the payload, if there is any, even if there are `"errors"` #5365 # 1.29.9 (20 May 2025) - Stable relation connection: fix missing records with `nil` values in Postgres #5346 # 1.29.8 (15 May 2025) - `FutureStream`: Support `GraphQL::ExecutionError` raised from lazy enumerators # 1.29.7 (12 May 2025) - `FutureStream`: Add `#to_incremental_h` # 1.29.6 (5 May 2025) - `@stream`: Add `FutureStream` for lazy enumerators # 1.29.5 (31 Mar 2025) - OperationStore: Improve Redis cleanup when deleting a single client - Stable connections: Fix NULL handling on Rails 7.2 + Postgresql # 1.29.4 (18 Nov 2024) - OperationStore: Add forward compatibility for removing old validation code #5164 # 1.29.3 (15 Nov 2024) - OperationStore: Improve `sync` performance with `GraphQL::Schema::Visibility` # 1.29.2 (4 Sept 2024) - Subscriptions: show broadcast subscriber count in dashboard (Pusher requires "subscription count" to be turned on and `use ... show_broadcast_subscribers_count: true`) # 1.29.1 (29 Aug 2024) - OperationStore: Accept a `context:` in `#add` # 1.29.0 (28 Aug 2024) - Subscriptions: use a single Pusher or Ably channel to deliver broadcast payloads to subscribers - Dashboard: fix crash when a topic had no active subscriptions # 1.28.1 (22 Aug 2024) - Subscriptions: Track `last_triggered_at`; add more metadata to the dashboard. # 1.28.0 (20 Aug 2024) - OperationStore: require the `ActiveRecord` backend inside an `ActiveSupport.on_load(:active_record) { ... }` block to improve Rails compatibility # 1.27.7 (13 Aug 2024) - Subscriptions: Fix _another_ Lua error in big cleanup operations # 1.27.6 (13 Aug 2024) - Subscriptions: Fix Lua error when cleaning up huge numbers of inactive subscriptions # 1.27.5 (9 May 2024) - OperationStore: remove needless call to `.metadata` #4947 # 1.27.4 (2 May 2024) - Pundit, CanCan, OperationStore: add Rails generators for getting started # 1.27.3 (1 May 2024) - OperationStore: Fix `.reindex` for many stored operations #4940 # 1.27.2 (30 Apr 2024) - Dashboard: handle missing index references gracefully #4940 # 1.27.1 (18 Apr 2024) - OperationStore: Don't call `query.query_string` if there's already a parsed document #4922 # 1.27.0 (11 Apr 2024) - RelationConnection: support Arel's `NullsFirst` and `NullsLast` nodes #4910 # 1.26.5 (1 Mar 2024) - OperationStore::AddOperationBatch: remove rescue for StatementInvalid inside transaction # 1.26.4 (27 Feb 2024) - RelationConnection: Don't quote table names that weren't quoted in original SQL, fixes #4508 (comment) # 1.26.3 (19 Feb 2024) - OperationStore: fix `sync` endpoint for Rack 3+ #4829 - Improve error message handling on Rails 7.1 # 1.26.2 (30 Jan 2024) - `@defer` / `@stream`: Write delimiters at the end of each patch so that clients respond to payloads more quickly. (Previously, delimiters were added at the start of each patch, so clients had to wait for the _next_ patch before they knew the current one was complete.) # 1.26.1 (23 Jan 2024) - Pundit integration: improve error message when a `Scope` class is missing # 1.26.0 (19 Jan 2024) ### Breaking Changes - Pundit integration: when the integration encounters an Array, it tries to find a configured policy class. If it can't, it raises an error. Previously, the integration silently permitted all items in the array; this default has been changed. See #4726 for more discussion of this change. If you encounter this error: - add `scope: false` to any fields that return arrays to get the previous behavior (no authorization applied to the array; each item authorized on its own) - Or, apply [scoping](https://graphql-ruby.org/authorization/scoping.html) by manually configuring a `pundit_policy_class` in the field's return type, then adding a `class Scope ...` inside that policy class. See the Pundit docs for the scope class API: https://github.com/varvet/pundit#scopes. If you want to continue passing _all_ arrays through without scoping (for example, if you know they've already been authorized another way, or if you're OK with them being authorized one-at-a-time later), you can implement this in your base `Scope` class, for example: ```ruby class BasePolicy class Scope def initialize(user, items) @user = user @items = items end def resolve if items.is_a?(Array) items else raise "Implement #{self.class}#resolve to filter these items: #{items.inspect}" end end end # Pass this scope class along to subclasses: def self.inherited(child_class) child_class.const_set(:Scope, Class.new(BasePolicy::Scope)) super end end ``` Alternatively, you could implement `def self.scope_items(items, context)` to skip arrays, for example: ```ruby module SkipScopingOnArrays def scope_items(items, context) if items.is_a?(Array) items # return these as-is else super end end end # Then, in type definitions which should skip scoping on arrays: extend SkipScopingOnArrays ``` # 1.25.2 (29 Dec 2023) ### New Features - Subscriptions: send `more: false` when the server calls `unsubscribe` # 1.25.1 (21 Dec 2023) ### Bug Fix - Ably subscriptions: update webhook handler for `presence.message` events # 1.25.0 (7 Dec 2023) ### Bug Fix - OperationStore: `.dup` the given `context` to avoid leaking state between queries when indexing - Subscriptions: use the schema or query logger to output debug messages # 1.24.15 (17 Nov 2023) ### Bug Fix - OperationStore: don't sort directives when normalizing, properly retain directives on Operation and Fragment definitions #4703 # 1.24.14 (16 Nov 2023) ### Bug Fix - OperationStore: also pass `context:` for ActiveRecord backend batches # 1.24.13 (13 Nov 2023) ### New Features - OperationStore: accept `context:` for `AddOperationBatch.call` #4697 # 1.24.12 (13 Nov 2023) ### New Features - OperationStore: accept `context:` to `Validate.validate` #4697 ### Bug Fix - OperationStore: don't rescue application-raised `KeyError`s #4699 # 1.24.11 (8 Nov 2023) ### Bug Fix - OperationStore: fix compatibility with 1.12.x #4696 # 1.24.10 (2 Nov 2023) ### Bug Fix - Improve compatibility with GraphQL-Ruby 1.12.x # 1.24.9 (4 Oct 2023) ### Bug Fix - OperationStore: Preserve variable default values of `false` when normalizing queries # 1.24.8 (29 Aug 2023) ### Bug Fix - OperationStore: search for operation during `Query#initialize` to avoid races with other instrumentation. Add `use ... trace: true` to get the old behavior. # 1.24.7 (16 June 2023) ### Bug Fix - Stable relation connections: quote table names and column names in `WHERE` clauses #4508 # 1.24.6 (24 May 2023) ### New Features - Defer: Add `incremental: true` for new proposed wire format, add example for working with GraphQL-Batch #4477 # 1.24.5 (24 May 2023) ### Bug Fix - Stable relation connection: Quote table names and column names in selects and orders #4485 # 1.24.4 (18 April 2023) ### Bug Fix - `@defer`: update `context[:current_path]` usage to fix `path:` on deferred errors # 1.24.3 (14 April 2023) ### Bug Fix - `OperationStore`: fix when used with Changesets (or other ways of defining arguments with the same name) #4440 # 1.24.2 (20 Mar 2023) ### Bug Fix - Remove debug output, oops # 1.24.1 (20 Mar 2023) ### Bug Fix - Fix `OperationStore` with new module-based execution traces (#4389) # 1.24.0 (10 Feb 2023) ### New Features - Support the `redis-client` gem as `redis:` # 1.23.9 (2 Feb 2023) ### Bug Fix - Dashboard: Support Ruby 3.2.0 # 1.23.8 (27 Jan 2023) ### New Features - OperationStore: Support `Changeset-Version` header for syncing with changesets #4304 # 1.23.7 (25 Jan 2023) ### Bug Fix - Stable Relation Connections: Fix handling of Postgres JSON accesses # 1.23.6 ### New Features - Subscriptions: accept `connection_pool:` instead of `redis:` for use with the `connection_pool` gem ### Bug Fix - Stable connections: rescue `ActiveRecord::StatementInvalid` when loading nodes and return a client-facing error instead # 1.23.5 (29 December 2022) ### New Features - Ably subscriptions: Also listen for `presence.leave` webhooks to clean up subscriptions more quickly # 1.23.4 (20 December 2022) ### Bug Fix - Dashboard: nicely render subscriptions that are not found or cleaned up by `read_subscription_failed_error` # 1.23.3 (19 December 2022) ### New Features - Add `GraphQL::Pro::Subscriptions#read_subscription_failed_error` for handling errors that are raised when reloading queries from storage # 1.23.2 (18 October 2022) ### New Features - Add dashboard component for Enterprise mutation limiter # 1.23.1 (25 August 2022) ### Bug Fix - Redis: update redis usage to be forward-compatible with redis 5.x #4167 # 1.23.0 (2 August 2022) ### New Features - Stable connections: support SQL queries that sort by `IS NOT NULL` #4153 # 1.22.3 (26 July 2022) ### Bug Fix - Stable connections: handle `edges {...}` when an invalid cursor is given #4148 # 1.22.2 (20 April 2022) ### Bug Fix - Use `deprecated_accepts_definitions` to stop warnings when loading this gem on 1.13.x # 1.22.1 (22 March 2022) ### Bug Fix - Pusher subscriptions: don't try to send empty trigger batches to Pusher # 1.22.0 (19 March 2022) ### New Features - Pusher subscriptions: it now sends updates in groups of 10 by default, pass `use ..., batch_size: 1` to revert to the previous behavior. - OperationStore: when using ActiveRecord for storage, it now batches updates to `last_used_at` every 5 seconds. Pass `use ..., update_last_used_at_every: 0` to update that column synchronously, instead, as before. # 1.21.6 (16 March 2022) ### Bug Fix - OperationStore: Fix no method error in Redis pipeline usage # 1.21.5 (7 March 2022) ### Bug Fix - Postgres stable connection: support more complex aliased selects #3976 # 1.21.4 (15 February 2022) ### Bug Fix - Encoders: don't extend `DeprecatedDefine` if it's not present (graphql-ruby < 1.12) # 1.21.3 (9 February 2022) ### New Features - Future-proof for GraphQL-Ruby 2.0 # 1.21.2 (27 January 2022) ### New Features - Dashboard, Routes: support lazy-loading the schema with `Routes::Lazy` #3868 - OperationStore: Update deprecated usage of `@redis.pipelined` to address warning # 1.21.1 (20 January 2022) ### Bug Fix - Stream, Defer: Include `hasNext: true|false` in patches # 1.21.0 (20 January 2022) ### New Features - Stream: Add `@stream` directive for evaluating list items one-at-a-time # 1.20.4 (4 December 2021) ### Bug Fix - Stable connections: Fix using startCursor / endCursor without nodes #3752 # 1.20.3 (27 November 2021) ### Bug Fix - Stable Connections: Properly handle cursors containing invalid JSON #3735 # 1.20.2 (15 November 2021) ### New Features - Operation Store sync: ActiveRecord backend performance improvements: when syncing operations, only validate newly-added operations, reduce allocations when normalizing incoming query strings # 1.20.1 (8 November 2021) ### Bug Fix - Operation Store sync: fix when operations are re-synced with new aliases # 1.20.0 (5 November 2021) ### New Features - Operation Store: Use Rails `insert_all` for better performance when adding new operations # 1.19.2 (26 October 2021) ### New Features - Pundit and CanCan integrations: Add `ResolverIntegration` modules for plain resolvers #3392 ### Bug Fix - OperationStore Redis backend: pipeline updates to last_used_at values #3672 # 1.19.1 (15 October 2021) ### Bug Fix - OperationStore: fix a stack overflow error on GraphQL 1.9 #3653 # 1.19.0 (13 October 2021) ### New Features - Dashboard: add a component for GraphQL-Enterprise rate limiters # 1.18.3 (1 Sept 2021) ### Breaking Changes - Stable cursors: raise an error on unrecognized orderings instead of ignoring them #3605 ### Bug Fix - Stable cursors: Handle `Arel::Attributes::Attribute` and `Arel::SqlLiteral` #3605 # 1.18.2 (16 August 2021) ### Bug Fix - Stable connections: nicely handle incoming cursors with too many sort values #3581 # 1.18.1 (20 July 2021) ### Bug Fix - Stable connections: improve handling of `SELECT` with `CASE` #3558 - Defer: fix to support runtime wrapper objects in graphql-ruby 1.12.13 # 1.18.0 (31 May 2021) ### New Features - Ably subscriptions: send `quickAck: true` for a faster response from Ably ### Bug Fix - Defer: support dataloader inside deferred blocks # 1.17.15 (29 Apr 2021) ### New Features - Defer: support `label:` argument which is returned in the patch for that deferral #3454 # 1.17.14 (14 Apr 2021) - Dashboard: fix stack error when OperationStore isn't configured on a class-based schema # 1.17.13 (14 Apr 2021) - Stable Connections: When using aliases and GROUP BY, replace the alias when building a HAVING condition. - Pundit integration: Add a `use_owner_role(true)` configuration option # 1.17.12 (3 Apr 2021) ### Bug Fix - Stable Connections: Re-select aliased fields that are referenced by ORDER BY. #3421 # 1.17.11 (12 Mar 2021) ### Bug Fix - Pundit integration: properly halt when `unauthorized_by_pundit` returns errors-as-data after a mutation argument fails authorization #3384 # 1.17.10 (11 Mar 2021) ### Bug Fix - Pundit, CanCan integrations: properly call configured auth hooks for arguments that are lists and input objects # 1.17.9 (3 Mar 2021) ### Bug Fix - Fix OperationStore assignment on GraphQL-Ruby 1.9 # 1.17.8 (23 Feb 2021) ### New Features - Subscriptions: change the default `cleanup_delay_s:` to 5 seconds (use `cleanup_delay_s: 0` to get the old behavior) ### Bug Fix - Subscriptions: Handle unsubscribe race condition #3357 # 1.17.7 (19 Feb 2021) ### New Features - CanCan integration: support `can_can_subject:` config for overriding the use of `object` as the CanCan subject #3350 ### Bug Fixes - Subscriptions: Support `Redis::Namespace` without deprecation warnings for `script load` #3347 # 1.17.6 (18 Feb 2021) ### New Features - Stable connections: implement `range_add_edge` to leverage GraphQL-Ruby 1.12.5's improved RangeAdd #2184 ### Bug Fix - Defer: Update to work with Dataloader # 1.17.5 (13 Feb 2021) ### Bug Fix - Subscriptions: Use `MULTI` instead of Lua for some operations - Subscriptions: Use `EVAL_SHA` for duplicate scripts to reduce network overhead #3285 - Subscriptions: Don't use `redis.call`, which is unsupported in the `redis-namespace` gem #3322 # 1.17.4 (4 Feb 2021) ## Bug Fix - Stable Relation Connection: Don't emit `OR ... IS NULL` for columns that are known to be `null: false` (this improves index utilization) ## 1.17.3 (2 Feb 2021) ### New Features - Pusher subscriptions: `context[:compress_pusher_payload] = true` will cause the payload to be gzipped before being sent to Pusher ## 1.17.2 (30 Jan 2021) ### Bug Fix - Subscriptions: don't generate keys inside Lua scripts (for redis-namespace compatibility, and probably better support for Redis cluster) #3307 ## 1.17.1 (25 Jan 2021) ### New Features - OperationStore: add `OperationStore::AddOperationBatch.call` for adding data directly - Subscriptions: use Lua scripts for more efficient Redis access ## 1.17.0 (20 Jan 2021) ### New Features - Updates for 1.12.0 compatibility ### Bug Fix - OperationStore: improve performance by batching reads and writes during updates ## 1.16.2 (21 Dec 2020) ### New Features - Subscriptions: Add `stale_ttl_s:` and `cleanup_delay_s:` to customize persistence in Redis #3252 ## 1.16.1 (3 Dec 2020) ### Bug Fix - Fix duplicate calls to `Argument#authorized?` in CanCan and Pundit integrations #3242 ## 1.16.0 (10 Nov 2020) ### New Features - Ably Subscriptions: `cipher_base:` sets up end-to-end encryption ## 1.15.7 (29 Sept 2020) ### Bug Fix - Encoder: fix Ruby 2.7 warning #3161 - Stable connections: Handle `ARRAY[...]` selections and cursors on Postgres #3166 - Pundit: properly lookup policies for list inputs #3146 ## 1.15.6 (17 Sept 2020) ### Bug Fix - Stable Connections: Use method access to get `.cursor_#{idx}` values instead of `.attributes[:cursor_#{idx}]`, fixes #3149 ## 1.15.5 ### New Features - Stable Connections: use `.to_sql` to handle orderings that use complex Arel expressions (#3109) ## 1.15.4 (28 July 2020) ### New Features - Pundit: add `pundit_policy_class_for(object, context)` and `pundit_role_for(object, context)` for custom runtime lookups ## 1.15.3 (17 July 2020) ### Bug Fix - Subscriptions: don't send empty updates when subscriptions return `:no_update` ## 1.15.2 (16 July 2020) ### New Features - OperationStore: improve handling of archived operations in index views ## 1.15.1 (16 July 2020) (Oops, bad release!) ## 1.15.0 (15 July 2020) - OperationStore: Store & display `last_used_at` for operation store clients and operations. To upgrade, add the column to your ActiveRecord table: ```ruby add_column :graphql_client_operations, :last_used_at, :datetime ``` (It works out-of-the-box with the Redis backend.) You can opt out of this feature by adding `use GraphQL::Pro::OperationStore, ... default_touch_last_used_at: false` to your schema setup. - OperationStore: Add archive/unarchive workflow for operations. To upgrade, add the column to your table: ```ruby add_column :graphql_client_operations, :is_archived, :boolean, index: true ``` (It works out-of-the-box with the Redis backend.) - OperationStore: Fix indexing of enum values ## 1.14.1 (29 June 2020) - CanCan: Accept `can_can_attribute:` configuration, which is passed as the third input to `.can?(...)` ## 1.14.0 (13 June 2020) ### New Features - Add PubnubSubscriptions - Update subscription implementations to support `broadcast: true` when available ### Bug Fix - More Ruby 2.7 warning fixes ## 1.13.6 (8 June 2020) ### Bug Fix - Return the proper `pageInfo` values when it's requested before `edges` or `nodes` (#2972) ## 1.13.5 (11 May 2020) ### Bug Fix - Fix some warnings on Ruby 2.7 ## 1.13.4 (17 Apr 2020) ### Bug Fix - StableRelationConnection: properly return `hasNextPage: true` when `before` and `max_page_size` are used. ## 1.13.3 (2 Apr 2020) ### New Features - `GraphQL::Pro::OperationStore::Migration` can be used to copy persisted operations from one backend to another (eg, ActiveRecord to Redis). See the source file, `lib/graphql/pro/operation_store/migration.rb` for docs. ## 1.13.2 (28 Mar 2020) ### Deprecations - `GraphQL::Pro::Subscriptions` is deprecated; use `GraphQL::Pro::PusherSubscriptions` instead which works the same, but better (see below). This new name avoids confusion with the later-added `AblySubscriptions`. ### New Features - `GraphQL::Pro::PusherSubscriptions` replaces `GraphQL::Pro::Subscriptions` and adds orphaned record cleanup. (No more dangling records in Redis.) ## 1.13.1 (12 Mar 2020) - Use `nonce: true` when working with cursors in new stable connections ## 1.13.0 (10 Feb 2020) ### New Features - OperationStore supports a `redis:` backend - OperationStore supports an arbitrary `backend_class:` for persistence operations ### Bug Fix - Use a loop when clearing Redis subscription state to avoid large stack traces #2701 - Handle empty subscription keys when publishing updates #2061 ## 1.12.2 (22 Jan 2020) ### Bug Fix - Improve backwards compat with OperationStore (Improve adding `.tracer`, use `.graphql_name` when indexing) ## 1.12.1 (20 Jan 2020) ### Bug Fix - Fix OperationStore on class-based schemas with query instrumenters that use the query string ## 1.12.0 (20 Jan 2020) ### Deprecations - `GraphQL::Pro::Monitoring` is deprecated; see Tracing for a replacement: https://graphql-ruby.org/queries/tracing.html - `GraphQL::Pro::Repository` is deprecated; see OperationStore for a replacement: https://graphql-ruby.org/operation_store/overview.html ### New Features - New stable connection support based on GraphQL-Ruby 1.10's new pagination implementation. New classes provide better handling of `NULL` values in order-by columns and they can be applied on a field-by-field basis(`GraphQL::Pro::SqliteStableRelationConnection`, `GraphQL::Pro::MySQLStableRelationConnection`, `GraphQL::Pro::PostgresStableRelationConnection`). ### Bug Fix - Add the Access query analyzer to class-based schemas ## 1.11.0 (10 Oct 2019) ### New Features - Forwards-compatibility for graphql 1.10.0 - Support 1.10.0.pre1's input object argument `loads:` authorization ## 1.10.8 (8 Oct 2019) ### Bug Fix - Continue authorizing input object arguments - Use millisecond-aware string format for datetimes in cursors ## 1.10.7 (22 Jul 2019) ### Bug Fix - Support multiple subscriptions in one document ## 1.10.6 (27 Jun 2019) ### New Features - Support custom `#can_can_ability` methods on query context for CanCanIntegration - Support custom `#pundit_user` method on query context for PunditIntegration ### Bug Fix - Fix off-by-one error when paginating backwards from the last item in a stable relation connection ## 1.10.5 (11 May 2019) ### New Features - Include expected HMAC digest in OperationStore debug output ## 1.10.4 (26 Mar 2019) ### Bug Fix - Include content-length and content-type headers in OperationStore JSON responses ## 1.10.3 (13 Mar 2019) ### Bug Fix - Support stable connections ordered by Arel SQL literals ## 1.10.2 (11 Mar 2019) ### Bug Fix - Support stable connections on realized views (which don't have primary keys) ## 1.10.1 (8 Mar 2019) ### Bug Fix - Pundit integration: support `pundit_policy_class` String names when scoping connections ## 1.10.0 (5 Mar 2019) ### New Features - Add `GraphQL::Pro::Defer`, implementing `@defer` for streaming responses ## 1.9.13 (4 Mar 2019) ### Bug Fix - Pundit integration: correctly authorize fields when Query root is nil ## 1.9.12 (22 Feb 2019) ### Bug Fix - Pundit integration: use overridden `pundit_policy_class` for scoping and mutation authorization ## 1.9.11 (20 Feb 2019) ### Bug Fix - Pundit integration: Fields use the owner's configured `pundit_policy_class` if there is one - Pundit integration: avoid conflicts with `#initialize` for schema classes that don't need it ## 1.9.10 (19 Feb 2019) ### Bug Fix - Support inheritance with `pundit_policy_class(...)` ## 1.9.9 (13 Feb 2019) ### New Features - Support `pundit_policy_class(...)` and `pundit_policy_class:` to manually specify a class or class name. ## 1.9.8 (30 Jan 2019) ### New Features - Inject `context` into policy lookup hooks instead of just the user ## 1.9.7 (30 Jan 2019) ### New Features - Extract `pundit_policy` and `scope_by_pundit_policy` hooks for user override ## 1.9.6 (18 Jan 2019) ### Bug Fix - Properly render subscription context in dashboard ## 1.9.5 (14 Jan 2019) ## Bug Fix - Don't pass arrays to Pundit scopes (fixes https://github.com/rmosolgo/graphql-ruby/issues/2008) ## 1.9.4 (11 Jan 2019) ## Bug Fix - Prepare for future compat with graphql-ruby 1.9 ## 1.9.3 (3 Dec 2018) ### Bug Fix - Include table name when adding a default order-by-id to ActiveRecord Relations - Raise if a required cursor attribute is missing - Improve `rake routes` output for operation store endpoint - Support already-parsed queries in subscription RedisStorage ## 1.9.2 (2 Nov 2018) ### Bug Fix - Derp, remove the dummy app's `.log` files from the gem bundle - Fix ordering bug when a SQL function call doesn't have an explicit order ## 1.9.1 (1 Nov 2018) ### Bug Fix - Fix Pusher reference in AblySubscriptions ## 1.9.0 (27 Oct 2018) ### New Features - Add `GraphQL::Pro::AblySubscriptions` for GraphQL subscriptions over Ably.io transport ## 1.8.2 (22 Oct 2018) ### Bug Fix - Support `NULLS LAST` in stable cursors ## 1.8.1 (16 Oct 2018) ### Bug Fix - Improve operation store models to work when `config.active_record.primary_key_prefix_type` is set ## 1.8.0 (11 Oct 2018) ### New Features - Support Rails 3.2 with OperationStore - Use `.select` to filter items in CanCanIntegration ### Bug Fix - Properly send an _ability_ and the configured `can_can_action` to `.accessible_by` - Use a string (not integer) for `Content-Length` header in the dashboard ## 1.7.13 (2 Oct 2018) ### Breaking Change - `PunditIntegration`: instead of raising `MutationAuthorizationFailed` when an argument fails authorization, it will send a `GraphQL::UnauthorizedError` to your `Schema.unauthorized_object` hook. (This is what all other authorization failures do.) To retain the previous behavior, in your base mutation, add: ```ruby def unauthorized_by_pundit(owner, value) # Raise a runtime error to halt query execution raise "#{value} failed #{owner}'s auth check" end ``` Otherwise, customize the handling of this behavior with `Schema.unauthorized_object`. ### Bug Fix - Auth: mutation arguments which have authorization constraints but _don't_ load an object from the database will have _mutation instance_ passed to the auth check, not the input value. ## 1.7.12 (29 Aug 2018) ### New Features - Add `GraphQL::Pro::CanCanIntegration` which leverages GraphQL-Ruby's built-in auth ## 1.7.11 (21 Aug 2018) ### Bug Fix - `PunditIntegration`: Don't try to authorize loaded objects when they're `nil` ## 1.7.10 (10 Aug 2018) ### New Features - Update `PunditIntegration` for arguments, unions, interfaces and mutations ## 1.7.9 (9 Aug 2018) ### New Features - Add a new `PunditIntegration` which leverages the built-in authorization methods ## 1.7.8 (10 July 2018) ### Bug Fix - Authorization: fix scoping lists of abstract type when there's no `#scope` method on the strategy ## 1.7.7 (10 May 2018) ### Bug Fix - Fix ordering of authorization field instrumenter (put it at the end, not the beginning of the list) ## 1.7.6 (2 May 2018) ### New Features - Authorization: Add `view`/`access`/`authorize` methods to `GraphQL::Schema::Mutation` ## 1.7.5 (19 Apr 2018) ### New Features - Authorization: when a `fallback:` configuration is given, apply it to each field which doesn't have a configuration of its own or from its return type. _Don't_ apply that configuration at schema level (it's applied to each otherwise uncovered field instead). ## 1.7.4 (16 Apr 2018) ### New Features - Support Mongoid::Criteria in authorization scoping ## 1.7.3 (12 Apr 2018) ### Bug Fix - Fix authorization code for when `ActiveRecord` is not defined ## 1.7.2 (10 Apr 2018) ### Bug Fix - Use a more permissive regexp (`/^\s*((?:[a-z._]+)\(.*\))\s*(asc|desc)?\s*$/im`) to parse SQL functions ## 1.7.1 (4 Apr 2018) ### Bug Fix - Fix route helpers to support class-based schemas ## 1.7.0 (25 Mar 2018) ### New Features - Support `1.8-pre` versions of GraphQL-Ruby ### Bug Fix - Fix OperationStore when other query instrumenters need `.query_string` ## 1.6.5 (7 Feb 2018) ### Bug Fix - Support `LEAST(...)` in stable cursors ## 1.6.4 (7 Feb 2018) ### Bug Fix - Support `CASE ... END` in stable cursors ## 1.6.3 (26 Jan 2018) ### Bug Fix - Support `FIELD(...)` in stable cursors ## 1.6.2 (13 Jan 2018) ### Bug Fix - Improve detection of `OperationStore` for the dashboard - Serve `Content-Type` and `Content-Length` headers with dashboard pages - Better `Dashboard#inspect` for Rails routes output - Use a string to apply order-by-primary-key for better Rails 3 support ## 1.6.1 (22 Nov 2017) ### New Features - Support `composite_primary_keys` gem ## 1.6.0 (13 Nov 2017) ### Breaking Changes - `GraphQL::Pro::UI` renamed to `GraphQL::Pro::Dashboard` ### Deprecations - Routing method `.ui` was renamed to `.dashboard` ### New Features - Added `GraphQL::Pro::Subscriptions` - Added subscriptions component to Dashboard ## 1.5.9 (10 Oct 2017) ### Bug Fix - Don't crash when scoping lists of abstract types with Pundit ## 1.5.8 (2 Oct 2017) ### New Features - Use `authorize(:pundit, namespace: )` to lookup policies in a namespace instead of the global namespace. ### Bug Fix - Introspection data is allowed through `fallback:` `authorize:` and `access:` filters. (It can be hidden with a `view:` filter.) ## 1.5.7 (20 Sept 2017) ### Bug Fix - Properly return `nil` when a list of authorized objects returns `nil` ## 1.5.6 (19 Sept 2017) ### New Features - Add `authorization(..., operation_store:)` option for authorizing operation store requests ## 1.5.5 (18 Sept 2017) ### New Features - Support `ConnectionType.bidrectional_pagination?` in stable RelationConnection ## 1.5.4 (18 Sept 2017) ### Bug Fix - Fix load issue when Rails is not present ## 1.5.3 (4 Sept 2017) ### Bug Fix - Fix OperationStore views on PostgresQL - Fix stable cursors when joined tables have the same column names __Note:__ This is implemented by adding extra fields to the `SELECT` clause with aliases like `cursor_#{idx}`, so you'll notice this in your SQL logs. ## 1.5.2 (4 Aug 2017) ### Bug Fix - Bump `graphql` dependency to `1.6` ## 1.5.1 (2 Aug 2017) ### New Features - Routing extensions moved to `using GraphQL::Pro::Routes` ### Deprecations - Deprecate `using GraphQL::Pro`, move extensions to `GraphQL::Pro::Routes` ## 1.5.0 (31 Jul 2017) ### New Features - Add `GraphQL::Pro::OperationStore` for persisted queries with Rails ## 1.4.8 (14 Jul 2017) ### Bug Fix - Update `authorization` to use type-level `resolve_type` hooks ## 1.4.7 (13 Jul 2017) ### Bug Fix - Update authorization instrumentation for `graphql >= 1.6.5` ## 1.4.6 (6 Jul 2017) ### Bug Fix - Fix typo in RelationConnection source ## 1.4.5 (6 Jul 2017) ### Bug Fix - Correctly fall back to offset-based cursors with `before:` argument ## 1.4.4 (15 Jun 2017) ### New Features - Add `Schema#unauthorized_object(obj, ctx)` hook for failed runtime checks ### Bug Fix - Prevent usage of `parent_role:` with `view:` or `access:` (since parent role requires a runtime check) - Fix versioned, encrypted cursors with 16-byte legacy cursors ## 1.4.3 (13 Jun 2017) ### New Features - `OrderedRelationConnection` supports ordering by joined fields ### Bug Fix - Update auth plugin for new Relay instrumenters - `Pro::Encoder` supports `encoder(...)` as documented ## 1.4.2 (2 May 2017) ### Bug Fix - Fix compatibility of `RelationConnection` and `RangeAdd` helper ## 1.4.1 (19 Apr 2017) ### New Features - Add `:datadog` monitoring ## 1.4.0 (19 Apr 2017) ### New Features - `ActiveRecord::Relation`s can be scoped by Pundit `Scope`s, CanCan `accessible_by`, or custom strategy's `#scope(gate, relation)` methods - Default authorization configuration can be provided with `authorization(..., fallback: { ... })` - Authorization's `:current_user` key can be customized with `authorization(..., current_user: ...)` ## 1.3.0 (7 Mar 2017) ### New Features - Serve static, persisted queries with `GraphQL::Pro::Repository` ## 1.2.3 (2 May 2017) ### Bug Fix - Fix compatibility of `RelationConnection` and `RangeAdd` helper ## 1.2.2 (6 Mar 2017) ### Bug Fix - Raise `GraphQL::Pro::RelationConnection::InvalidRelationError` when a grouped, unordered relation is returned from a field. (This relation can't be stably paginated.) ## 1.2.1 (3 Mar 2017) ### New Features - Formally support ActiveRecord `>= 4.1.0` ### Bug Fix - Support grouped relations in `GraphQL::Pro::RelationConnection` ## 1.2.0 (1 Mar 2017) ### New Features - Authorize fields based on their parent object, for example: ```ruby AccountType = GraphQL::ObjectType.define do name "Account" # This field is visible to all users: field :name, types.String # This is only visible when the current user is an `:owner` # of this account field :account_balance, types.Int, authorize: { parent_role: :owner } end ``` ## 1.1.1 (22 Feb 2017) ### Bug Fixes - Fix monitoring when `Query#selected_operation` is nil ## 1.1.0 (9 Feb 2017) ### New Features - Add AppSignal monitoring platform - Add type- and field-level opting in and opting out of monitoring - Add `monitor_scalars: false` to skip monitoring on scalars ### Bug Fixes - Fix `OrderedRelationConnection` when neither `first` nor `last` are provided (use `max_page_size` or don't limit) ## 1.0.4 (23 Jan 2017) ### Bug Fixes - `OrderedRelationConnection` exposes more metadata methods: `parent`, `field`, `arguments`, `max_page_size`, `first`, `after`, `last`, `before` ## 1.0.3 (23 Jan 2017) ### Bug Fixes - When an authorization check fails on a non-null field, propagate the null and add a response to the errors key (as if the field had returned null). It previously leaked the internal symbol `__graphql_pro_access_not_allowed__`. - Apply a custom Pundit policy even when the value isn't `nil`. (It previously fell back to `Pundit.policy`, skipping a `pundit_policy_name` configuration.) ## 1.0.2 ### Bug Fixes - `OrderedRelationConnection` exposes the underlying relation as `#nodes` (like `RelationConnection` does), supporting custom connection fields. ## 1.0.1 ### New Features - CanCan integration now supports a custom `Ability` class with the `ability_class:` option: ```ruby authorize :cancan, ability_class: CustomAbility ``` ## 1.0.0 - `GraphQL::Pro` released graphql-ruby-2.5.19/CHANGELOG-relay.md000066400000000000000000000066771514115062600171710ustar00rootroot00000000000000# graphql-relay ### Breaking Changes ### Deprecations ### New Features ### Bug Fix ## 0.12.0 (21 Jul 2016) ### Breaking Changes - Don't cache a global node identification config #51 To migrate, assign your node identification helper to the schema: ```ruby NodeIdentification = GraphQL::Relay::GlobalNodeIdentification.define { ... } MySchema.node_identification = NodeIdentification ``` ### New Features - Support lazy definition blocks from graphql-ruby 0.17 - Add `startCursor` and `endCursor` to `PageInfo` #60 ### Bug Fix - Support `field:` keyword for connection helper #58 ## 0.11.2 (6 Jul 2016) ### New Features - Include description for built-in objects #55 ## 0.11.1 (24 Jun 2016) ### Bug Fix - Correctly pass parent object to Connections #53 ## 0.11.0 (19 Jun 2016) ### Breaking Changes - `BaseType.define_connection` no longer caches the result to use as the default `BaseType.connection_type`. Now, store the result of `.define_connection` in a variable and pass that variable into the schema: ```ruby # Capture the returned type: SomethingCustomConnectionType = SomethingType.define_connection { ... } DifferentThingType = GraphQL::ObjectType.define do # And pass it to the connection helper: connection :somethings, SomethingCustomConnectionType end ``` ### New Features - Support for custom edge types / classes #50 - Support for multiple connection classes #50 ## 0.10.0 (31 May 2016) ### New Feature - Support `graphql` 0.14.0 #47 ### Bug Fix - Use strings as argument names, not symbols #47 ## 0.9.5 ### New Feature - Root `id` field may have a description #43 ## 0.9.4 (29 Apr 2016) ### Bug Fix - Fix Node interface to support GraphQL 0.13.0+ ## 0.9.2 (29 Apr 2016) ### Bug Fix - Fix Node interface when type_from_object returns nil ## 0.9.1 (6 Apr 2016) ### Bug Fix - Respond to connection fields without any pagination arguments - Limit by `max_page_size` even when no arguments are present ## 0.9.0 (30 Mar 2016) ### Breaking change - Remove the `order` argument from connection fields. This isn't part of the spec and shouldn't have been there in the first place! You can implement this behavior with a custom argument, for example: ```ruby field :cities, CityType.connection_type do argument :order, types.String, default_value: "name" resolve ->(obj, args, ctx) { obj.order(args[:order]) } end ``` ### Bug Fix - Include the MIT license in the project's source ## 0.8.1 (22 Mar 2016) ### Bug Fix - Accept description for Mutations ## 0.8.0 (20 Mar 2016) ### New Feature - Accept configs for `to_global_id` and `from_global_id` - Support `graphql` 0.12+ ## 0.7.1 (29 Feb 2016) ### Bug Fix - Limit the `count(*)` when testing next page with ActiveRecord #28 ## 0.7.0 (20 Feb 2016) ### New Feature - `max_page_size` option for connections - Support ActiveSupport 5.0.0.beta2 ## 0.6.2 (11 Feb 2016) ### Bug Fix - Correctly cast values from connection cursors #21 - Use class _name_ instead of class _object_ when finding a connection implementation (to support Rails autoloading) #16 ## 0.6.1 (14 Dec 2015) ### Bug Fix - Stringify `id` when passed into `to_global_id` ## 0.6.0 (11 Dec 2015) ### Breaking Change - `GlobalNodeIdentification#object_from_id(id, ctx)` now accepts context as the second argument #9 ## 0.5.1 (11 Dec 2015) ### Feature - Allow custom UUID join string #15 ### Bug Fix - Remove implicit ActiveSupport dependency #14 graphql-ruby-2.5.19/CHANGELOG.md000066400000000000000000005241741514115062600160540ustar00rootroot00000000000000# Changelog [Versioning guidelines](https://graphql-ruby.org/development.html#versioning) ### Breaking changes ### Deprecations ### New features ### Bug fixes # 2.5.19 (5 Feb 2026) - `DetailedTrace`: add ActiveRecord backend, generator #5525 - `DetailedTrace`: improve ActiveRecord backend, filter hashes before logging #5527 # 2.5.18 (22 Jan 2026) - `GraphQL::Dashboard`: properly require `action_controller` before using it #5510 - `GraphQL::Dashboard`: don't use `config.asset_host` for Dashboard assets since they're handled by the dashboard itself #5511 # 2.5.17 (21 Jan 2026) ### Bug fixes - `GraphQL::Dashboard`: fix routes compatibility with Devise #5505 - `GraphQL::Dashboard`: fix HTML/erb lint issues #5497 - Parser: improve check for invalid number followed by name #5492 #5492 - `GraphQL::Field`: optimize boot memory by removing `Field.from_options` #5495 - `field`, `argument`: improve API documentation for DSL methods #5491 # 2.5.16 (10 Dec 2025) ### Bug fixes - `fiber-storage`: properly include dependency on Ruby < 3.2 #5484 - Fix typo in `legacy_invalid_empty_selections_on_union_with_type` warning #5481 # 2.5.15 (9 Dec 2025) ### New features - `DetailedTrace`: add separate spans for debug-only `.inspect` calls, support `debug: false` config #5477 - `required:` validator: Raise a developer error when all `one_of:` options are hidden, support `allow_all_hidden: true` to allow this case #5474 - `GraphQL::Testing::MockActionCable`: added to support testing ActionCableSubscriptions #5482 - `legacy_invalid_empty_selections_on_union_with_type`: new method added for better metadata about legacy behavior #5480 ### Bug fixes - Fix typo in date encoding error #5447 - Fix schema printer bug #5468 - Ensure `data` exists for execution errors #5452 - Improve SDL directive argument coercion #5469 - Don't require `fiber-storage` on Ruby 3.2+ #5456 - Visibility: default to `preload: true` when Rails.env.staging? #5409 # 2.5.14 (8 Oct 2025) ### Bug fixes - Fix error when GraphQL-Batch is used (???) #5444 # 2.5.13 (22 Sep 2025) ### New features - Testing helpers: support `visibility_profile: ...` #5439 ### Bug fixes - Directives: correctly handle schema directive arguments which are lists of input objects #5440 # 2.5.12 (15 Sep 2025) ### New features - Runtime: add hooks for experimental custom runtimes #5425, #5429 - Lazy handling and Dataloader have been merged under the hood #5422 - Doc: merk `load_application_object_failed` as public #5426 # 2.5.11 (9 Jul 2025) ### Bug fixes - Dataloader: improve compatibility when objects are loaded by GraphQL-Batch but `.authorized?` uses Dataloader #5400 # 2.5.10 (3 Jul 2025) ### New features - Schema: Add `.freeze_schema` for minimal Ractor support #5370 ### Bug fixes - Schema: inherit validation configurations #5382 - Visibility: fix inheriting visibility with `preload: true` #5386 - Improve error messages with interfaces from SDL #5372 - Remove needless counter in execution code #5392 - Reduce execution overhead in schemas built from SDL #5393 - RequiredValidator: remove hidden definitions from error message #5396 - `.possible_types`: don't return interfaces in this list #5395 - `dataload_association`: fix loading associations with different scopes on the same object #5398 # 2.5.9 (6 Jun 2025) ### New features - Improve metadata on Scalar coercion errors #5375 ### Bug fixes - Directives: fix validation of Ruby values on definition directive arguments #5377 - `loads:`: fix typechecking of Interface `loads:` values #5379 # 2.5.8 (28 May 2025) ### New features - Timeout: support disabling during a query #5361 - Query::Partial: support running a fragment in isolation #5362 ### Bug fixes - Visibility: improve performance for `loadable?` #5355 - RequiredValidator: Fix typo #5359 - Scalar validation: remove redundant infinity handling #5358 - Directives: fix validation of schema definition directives #5368 # 2.5.7 (15 May 2025) ### Bug fixes - `PerfettoTrace`: Handle non-ascii strings #5351 - `Partial`: Add `#selected_operation_name` to support `GraphQL::Current` #5353 # 2.5.6 (5 May 2025) ### New features - Execution: Add `Query#run_partials` for running sub-trees of valid queries #5183 # 2.5.5 (29 Apr 2025) ### Bug fixes - Visibility: fix when `::Rails` doesn't have `.env` #5339 - Compatibility: restore default (legacy) behavior when no setting is configured #5343 - `ActiveSupport::Notifications`: fix fiber resume without previous event #5335 - Simplify non-null input object argument handling #5333 - Fix compatibility with `ruby-head` #5342 # 2.5.4 (18 Apr 2025) ### Bug fixes - `ActiveRecordSource`: Support composite primary keys #5330 - `ActiveRecordAssociationSource`: Support has_many associations #5331 - Remove broken `Context#path` method (use `#current_path` instead) #5332 # 2.5.3 (14 Apr 2025) ### Deprecations - Validation: two non-spec behaviors are deprecated: - When a query includes two scalar fields of different types which may occur in the same place in the response, the query was previously allowed. The spec says it should be rejected. This version emits a warning in this case. See `Schema.allow_legacy_invalid_return_type_conflicts` for migration support. #4351 - When a query selects a field which returns a Union, but doesn't make any subselections on the Union, the spec says the query should be rejected as invalid but previous GraphQL-Ruby allowed it. It now emits a warning. See `Schema.allow_legacy_invalid_empty_selections_on_union` for migration support #5322 - Complexity: several bugs about merging complexity cost across branches of a query have been fixed but require opting in. They may produce higher complexity scores. See `Schema.complexity_cost_calculation_mode` for migration support. #4843 ### New features - `AlwaysVisible`: improve speed (using `Schema::Visibility`) #5326 - Return more descriptive errors when non-nullable list elements are `null` #5301 - Visibility: improve performance on large schemas #5325 # 2.5.2 (8 Apr 2025) ### New features - Resolver: accept `deprecation_reason` #5320 ### Bug fixes - Visibility: hide argument types whose uses are all hidden (to match Warden) #5291 - InputObject: fix validation for nested input object with `prepare:` method configs #5321 # 2.5.1 (3 Apr 2025) ### Bug fixes - Datadog trace: fix Dataloader source tracing method #5318 - Sentry trace: handle `nil` current span #5313 # 2.5.0 (1 Apr 2025) ### Breaking changes - Subscriptions: GraphQL-Ruby now implements the spec's requirement that a subscription has only one root selection #5250 - Datadog trace: the custom `prepare_span` hook now receives an execution-related object instead of a hash of keywords. #5298 ### New features - Tracers: APM tracers have been updated to reflect Dataloader's fiber stops and starts #5296 #5298 # 2.4.16 (1 Apr 2025) ### New features - Move some more modules into GraphQL::Dashboard #5308 #5310 ### Bug fixes - Parser: raise when variable definitions don't include a type name #5305 - PerfettoTrace: Don't create zombie ActiveSupport::Notifications subscribers #5307 # 2.4.15 (19 Mar 2025) ### New features - `Schema.from_definition`: support custom base type classes #5282 - `Schema.from_definition`: support type extensions #5281 ### Bug fixes - Handle `GraphQL::ExecutionError` from `resolve_type` #5274 - Backtrace: handle inline fragments # 5274 - `run_graphql_field`: fix when `.authorized?` calls Dataloader #5289 - InputObject: run validators even when custom `def prepare` is present #5285 - Multiplex: don't attempt to execute zero queries #5278 # 2.4.14 (13 Mar 2025) ### Bug fixes - New Relic tracing: fix dataloaded, skipped scalars #5277 # 2.4.13 (12 Mar 2025) - Security: Fix CVE-2025-27407 # 2.4.12 (11 Mar 2025) ### Breaking changes - Remove `InvalidNullError#value` which is always `nil` #5256 ### New features - `validate_timeout` is 3 seconds by default #5258 ### Bug fixes - New Relic: reimplement skipping scalars by default #5271 - Resolver: revert inheriting overridden `graphql_name` #5260 - Analysis: manually implement timeout to handle I/O better #5263 - Parser: properly handle extra token at the end of the query string #5267 - Validation: fix conflicting aliases inside fragment #5268 # 2.4.11 (28 Feb 2025) ### Breaking changes - Enums: enum value accessor methods have been switched to opt-in. Add `value_methods(true)` to your base enum class to opt back in. #5255 ### New features - `InvalidNullError`: Improve default handling to add path and locations #5257 - `DetailedTrace`: Add a sampling profiler for creating detailed traces #5244 ### Bug fixes - `InvalidNullError`: use `GraphQL::Error` as a base class #5248 - CI: test on Mongoid 8 and 9 #5251 # 2.4.10 (18 Feb 2025) ### New features - Dataloader: improve built-in Rails integration #5213 ### Bug fixes - `NewRelicTrace`: don't double-count time waiting on Dataloader fibers - Fix possible type memberships inherited from superclass #5236 - `Visibility`: properly use configured contexts for visibility profiles #5235 - `Enum`: reduce needless `value_method` warnings #5230 #5220 - `Backtrace`: fix error handling with `rescue_from` #5227 - Parser: return a proper error when variable type is missing #5225 # 2.4.9 (29 Jan 2025) ### New features - Enum: Enum types now have methods to access GraphQL-ready values directly #5206 #5218 ### Bug fixes - Validation: fix order dependency and mutual exclusion bug in `required: { one_of: [ ... ] }` - Backtrace: simplify trace setup and rendering code - Fix dependencies for Ruby 3.4 #5199 - Resolver: inherit description from superclass #5195 - Visibility: fix for when multiple implementations are all hidden #5191 # 2.4.8 (10 Dec 2024) ### New features - Subscriptions: support calling `write_subscription` within `resolve` #5142 ### Bug fixes - Autoloading: improve autoloading of `Tracing` classes #5190 # 2.4.7 (7 Dec 2024) ### Bug fixes - Remove warning when code isn't eager-loaded #5187 - Add missing `require "ostruct"` in ActionCableSubscriptions #5184 # 2.4.6 (5 Dec 2024) ### Bug fixes - Autoloading: fix referencing built-in types #5181 - Autoloading: use Rails `config.before_eager_load` hook for better integration #5182 - `loads:`: Check possible types for `loads:`-only unions #5180 # 2.4.5 (2 Dec 2024) ### Breaking changes - In non-Rails production environments, GraphQL-Ruby will emit a warning about calling `.eager_load!` for better boot performance. #5178 ### New features - Loading: GraphQL-Ruby now uses Ruby's `autoload ...` for many constants. #5178 - Input objects may be pattern matched (they implement `#deconstruct_keys`) #5170 ### Bug fixes - Visibility: hide definition directives in SDL #5175 - Internals: use `Fiber[...]` for internal state instead of `Thread.current` #5176 - Dataloader: properly handle arrays of all falsey values #5167 #5169 - Visibility: hide directives when their uses are all hidden #5163 - Require object types to have fields and require input objects to have arguments (to comply with the GraphQL spec) #5137 - Improve error message when a misplaced `-` is encountered #5115 # 2.4.4 (18 Nov 2024) - Visibility: improve performance with `sync` #5161 # 2.4.3 (11 Nov 2024) ### Bug fixes - Lookahead: return an empty hash for `.arguments` when they raised a `GraphQL::ExecutionError` #5155 - Visibility: fix error when Mutation is lazy-loaded #5158 - Visibility: improve performance of `Schema.types` #5157 # 2.4.2 (7 Nov 2024) ### Bug fixes - Validation: fix error message when selections are made on an enum #5144 #5145 - Visibility: fix preloading when no profiles are named #5148 # 2.4.1 (4 Nov 2024) ### Bug fixes - Visibility: support dynamically-generated `#enum_values` #5141 # 2.4.0 (31 Oct 2024) ### Deprecations - Visibility: Implementing `visible?` now requires `use GraphQL::Schema::Visibility` or `use GraphQL::Schema::Warden` in your schema definition #5123 ### New features - Validation: Add "did you mean" to error messages when `DidYouMean` is available #4966 - Schema: types can be lazy-loaded when using `GraphQL::Schema::Visibility` #4919 # 2.3.20 (31 Oct 2024) ### Bug fixes - Arguments: suppress warning for `objectId` arguments #5124 - Arguments: don't require input object arguments when a default value is configured # 2.3.19 (24 Oct 2024) ### New features - Dataloader: accept a `fiber_limit:` option #5132 ### Bug fixes - Argument Validation: improve the `one_of:` error message #5130 - Lookahead: return a null lookahead from `Query#lookahead` when no operation is selected #5129 - Static Validation: speed up FieldsWillMerge when some fields are not defined #5125 # 2.3.18 (7 Oct 2024) ### Bug fixes - Properly use trace options when `trace_with` is used after `trace_class` #5118 # 2.3.17 (4 Oct 2024) ### Bug fixes - Fix `InvalidNullError#inspect` #5103 - Add server-side tests for ActionCableSubscriptions #5108 - RuboCop: Fix FieldTypeInBlock for list types and interface types #5107 #5112 - Subscriptions: Fix triggering with nested input objects #5117 - Extensions: fix extensions which add other extensions #5116 # 2.3.16 (12 Sept 2024) ### Bug fixes - RuboCop: fix `FieldTypeInBlock` for single-line classes #5098 - Testing: Add `context[:current_field]` to testing helpers #5096 # 2.3.15 (10 Sept 2024) ### New features - Type definitions accept `comment("...")` for annotating SDL #5067 - Parser: add `tokens_count` method #5066 - Schema: allow `validate_timeout` to be reset #5062 ### Bug fixes - Optimize `Language.escape_single_quoted_newlines` #5095 - Generators: Add `# frozen_string_literal: true` to base resolver #5092 - Parser: Properly handle minus followed by name #5090 - Migrate some attr_reader methods #5080 - Handle variable definition directives #5072 - Handle `GraphQL::ExecutionError` when loading arguments during analysis #5071 - NotificationsTrace: properly call `super` - Use symbols for namespaced_types generator option #5068 - Reduce memory usage in lazy resolution #5061 - Fix default trace inheritance #5045 # 2.3.14 (13 Aug 2024) ### Bug fixes - Subscriptions: fix subscriptions when subscription type is added after subscription plug-in #5063 # 2.3.13 (12 Aug 2024) ### New features - Authorization: Call `EnumValue#authorized?` during execution #5058 - `Subset`: support lazy-loading root types and field return types (not documented yet) #5055, #5054 ### Bug fixes - Validation: don't validate `nil` if null value is permitted for incoming lists #5048 - Multiplex: fix `Mutation#ready?` dataloader cache in multiplexes #5059 # 2.3.12 (5 Aug 2024) ### Bug fixes - Add `fiber-storage` dependency for Ruby < 3.2 support # 2.3.11 (2 Aug 2024) ### New features - `GraphQL::Current` offers globally-available methods for runtime metadata #5034 - Continue improving `Schema::Subset` (not production-ready yet, though) #5018 #5039 ### Bug fixes - Fix `Node#line` and `Node#col` when nodes are created by manually #5047 - Remove unused `interpreter?`, `using_ast_analysis?` and `new_connections?` flag methods #5039 - Clean up `.compare_by_identity` usages #5037 # 2.3.10 (19 Jul 2024) ### Bug fixes - Parser: fix parsing operation names that match keywords #5033 - Parser: support leading pipes in Union type definitions #5027 - Validation: remove rule that prohibits non-null variables from having default values #5030 - Dataloader: raise fresh error instances when sources return errors #5021 - Enum and Union: don't create nested error classes in anonymous classes (eg, when parsing SDL -- to improve bug tracker integration) #5022 # 2.3.9 (13 Jul 2024) ### Bug fixes - Subscriptions: fix `subscriptionType` in introspection #5019 # 2.3.8 (12 Jul 2024) ### New features - Input validation: Add `all: { ... }` validator #5013 - Visibility: Add `Query#types` for future type filtering improvements #4998 - Broadcast: Add `default_broadcast(true)` option for Connection and Edge types #5012 ### Bug fixes - Remove unused `InvalidTypeError` #5003 - Parser: remove unused `previous_token` and `Token` #5015 # 2.3.7 (27 Jun 2024) ### Bug fixes - Properly merge field directives and resolver directives #5001 # 2.3.6 (25 Jun 2024) ### New features - Analysis classes are now in `GraphQL::Analysis` (`GraphQL::Analysis::AST` still works, too) #4996 - Resolvers and Mutations accept `directive ...` configurations #4995 ### Bug fixes - `AsyncDataloader`: Copy Fiber-local variables into Async tasks #4994 - `Dataloader`: properly batch `fetch` calls with `loads:` arguments that call Dataloader sources during `.authorized?` #4997 # 2.3.5 (13 Jun 2024) ### Breaking changes - Remove default `load_*` implementations in arguments -- this could break calls to `super` if you have redefined this method in subclasses #4978 - `Schema.possible_types` and `Schema.references_to` now use type classes as keys instead of type names (Strings). You can create a new Hash with the old structure using `.transform_keys(&:graphql_name)`. #4986 #4971 ### Bug fixes - Enums: fix parsing enum values that match GraphQL keywords (eg `type`, `extend`) #4987 - Consolidate runtime state #4969 - Simplify schema type indexes #4971 #4986 - Remove duplicate when clause #4976 - Address many Ruby warnings #4978 - Remove needless `ruby2_keywords` usage #4989 - Fix some YARD docs #4984 # 2.3.4 (21 May 2024) ### New features - Async Dataloader: document integration with Rails database connections #4944 #4964 ### Bug fixes - `Query#fingerprint`: handle `nil` query strings like `""` #4963 - `Language::Nodes`: support marshalling parsed ASTs #4959 - Directives: fix directives in nested fragment spreads #4958 - Tracing: fix conflicts between Sentry and Prometheus traces #4957 # 2.3.3 (9 May 2024) ### New features - Max Complexity: add `count_introspection:` option #4939 ### Bug fixes - Language: Fix regression in `Nodes#line` and `Nodes#col` #4949 - Runtime: Simplify runtime state management #4935 # 2.3.2 (26 Apr 2024) ### Bug fixes - Properly `.prepare` lists of input objects #4933 - Fix deleting directives using the AST visitor #4931 # 2.3.1 (22 Apr 2024) ### New features - `Schema.max_query_string_tokens`: support a limit on the number of tokens the lexer should identify #4929 - Parser: add an option to reject numbers followed immediately by argument names #4924 - Parser and CParser: reduce allocated and retained strings when parsing schemas #4899 - `run_graphql_field`: support `:lookahead` and `:ast_node` field extras #4930 ### Bug fixes - Rescue when trying to print integers that are too big for Ruby #4923 - Mutation: clear the Dataloader cache before resolving #4903 - Fix `FieldUsage` analyzer when InputObjects return a prepared value #4902 - Add a minimal query string for `run_graphql_field` #4891 - Fix PrometheusTrace with multiple tracers #4888 # 2.3.0 (20 Mar 2024) ### Breaking Changes - `orphan_types`: Only object types are accepted here; other types may be added to the schema through `extra_types` instead. #4869 - Parser: line terminators are no longer allowed in single-quoted strings (as per the GraphQL spec). Escape newline characters instead; see `GraphQL::Language.escape_single_quoted_newline(query_str)` if you need to transform incoming query strings #4834 ### Deprecations - `.tracer(...)` is deprecated, use `.trace_with(...)` instead, using trace modules (https://graphql-ruby.org/queries/tracing.html) #4878 ### Bug fixes - Parser: handle some escaped character edge cases according to the GraphQL spec #4824 - Analyzers: fix fragment skip/include tracking #4865 - Remove unused Context modules #4876 # 2.2.14 (18 Mar 2024) ### Bug fixes - Parser: properly handle stray hyphens in query strings #4879 # 2.2.13 (11 Mar 2024) ### Bug fixes - Tracing: when a new base `:default` trace class is added, merge already-configured trace modules into it #4875 # 2.2.12 (6 Mar 2024) ### Deprecations - `Schema.{query|mutation|subscription}_execution_strategy` methods are deprecated without replacement #4867 ### Breaking Changes - Connections: Revert changes to `hasNextPage` returning `false` when no `first` is given (previously changed in 2.2.6) #4866 ### Bug fixes - Complexity: handle unauthorized argument errors better #4868 - Pass `context` when fetching argument for `loads: ...` #4870 # 2.2.11 (27 Feb 2024) ### New features - Sentry: support transaction names in tracing #4853 ### Bug fixes - Tracing: handle unknown trace modes at runtime #4856 # 2.2.10 (20 Feb 2024) ### New features - Parser: support directives on variable definitions #4847 ### Bug fixes - Fix compatibility with Ruby 3.4 #4846 - Tracing: Fix applying default options to non-default modes #4849, #4850 # 2.2.9 (15 Feb 2024) ### New features - Complexity: Treat custom Connection fields as metadata (like `totalCount`), not as if they were evaluated for each item in the list #4842 - Subscriptions: Serialize `ActiveRecord::Relation`s given to `.trigger` #4840 ### Bug fixes - Complexity: apply configured `complexity ...` to connection fields #4841 - Authorization: properly handle Resolver arguments that return `false` for `#authorized?` #4839 # 2.2.8 (7 Feb 2024) ### New features - Responses have `"errors"` before `"data"`, as recommended by the GraphQL spec #4823 ### Bug fixes - Sentry: fix integration with other trace modules #4830 - Sentry: fix when child span is `nil` (test environments) #4828 - Remove needless Base64 backport #4820 - Fix module arrangement to support RDoc #4819 # 2.2.7 (29 Jan 2024) ### Deprecations - Deprecate returning `.resolve` dataloader requests (use `.load` instead) #4807 - Deprecate `error_bubbling(true)`, no replacement. Please open an issue if you need this option. #4813 ### Bug fixes - Remove unused `racc` dependency #4814 - Fix `backtrace: true` when used with `@defer` and batch-loaded lists #4815 - Accept input objects when required arguments aren't provided but have default values #4811 # 2.2.6 (25 Jan 2024) ### Deprecations - `instrument(:query | :multiplex, ...)` was deprecated, use a `trace_with` module instead. #4771 - Legacy `PlatformTracing` classes are deprecated, use a `PlatformTrace` module instead #4779 ### New features - `FieldUsage` analyzer: returns a `used_deprecated_enum_values: ...` array in its result Hash #4805 - `validate_timeout` applies to query analysis as well as static validation #4800 - `SentryTrace` is added for instrumenting with Sentry #4775 ### Bug fixes - `FieldUsage` analyzer: properly find deprecated arguments in non-null input objects #4805 - DataDog: replace usage of `span_type` setter with `span` setter #4776 - Fix coercion error handing with given `null` values #4799 - Raise a better error when variables are defined with non-input types #4791 - Fix `hasNextPage` when `max_page_size` is set #4780 # 2.2.5 (10 Jan 2024) ### Bug fixes - Parser: fix enum values named `type` #4772 - GraphQL::Deprecation: remove this unused helper module #4769 # 2.2.4 (3 Jan 2024) ### Bug fixes - AsyncDataloader: don't resolve fields with event loop #4757 - Parser: properly parse some fields and args named after keywords #4759 - Performance: use `all?` to check classes directly #4760 # 2.2.3 (28 Dec 2023) ### Bug fixes - AsyncDataloader: avoid leftover `suspended` Fibers #4754 - Generators: fix path and constant name of BaseResolver #4755 # 2.2.2 (27 Dec 2023) ### Bug fixes - Dataloader: remove `Fiber#transfer` support because Ruby's control flow is unpredictable (#4748, #4752, #4743) - Parser: fix handling of single-token document - QueryComplexity: improve performance # 2.2.1 (20 Dec 2023) ### Bug fixes - `AsyncDataloader`: re-raise errors from fields and sources #4736 - Parser: fix parsing directives on interfaces in SDL #4738 # 2.2.0 (18 Dec 2023) ### Breaking changes - `loads:` now requires a schema's `self.resolve_type` method to be implemented so that loaded objects can be verified to be of the expected type #4678 - Tracing: the new Ruby-based parser doesn't emit a "lex" event. (`graphql/c_parser` still does.) ### New features - `GraphQL::Dataloader::AsyncDataloader`: a Dataloader class that uses the `async` gem to run I/O from fields and Dataloader sources in parallel #4727 - Parser: use a heavily-optimized lexer and a hand-written parser for better performance #4718 - `run_graphql_field`: a helper method for running fields in tests #4732 # 2.1.10 (27 Dec 2023) - Dataloader: remove Fiber#transfer support because of unpredictable Ruby control flow #4753 # 2.1.9 (21 Dec 2023) ### Bug fixes - Dataloader: fix some fiber scheduling bugs #4744 # 2.1.8 (18 Dec 2023) ### New features - Rails generators: generate a base resolver class by default #4513 - Dataloader: add some support for transfer-based Fiber schedulers, simplify algorithm #4625 #4729 - `prepare`: check for the named method on the argument owner, too #4717 # 2.1.7 (4 Dec 2023) ### New features - Make `NullContext` inherit from `Context`, to make typechecking easier #4709 - Accept a custom `Schema.query_class` to use for executing queries #4679 ### Bug fixes - Default `reauthorize_scoped_objects` to false #4720 - Fix `subscriptions.trigger` with custom enum values #4713 - Fix `backtrace: true` with GraphQL-Pro `@defer` #4708 - Omit `to_h` from Input Object validation error message #4701 - When trimming whitespace from block strings, remove first and last lines that contain only whitespace #4704 # 2.1.6 (2 Nov 2023) ### Breaking Changes - The parser cache is now opt-in. Add `config.graphql.parser_cache = true` to your Rails environment setup to enable it. #4648 ### New features - New `ISO8601Duration` scalar #4688 ### Bug fixes - Trace: fix custom trace mode inheritance #4693 # 2.1.5 (25 Oct 2023) ### Bug fixes - Logger: Fix `Schema.default_logger` when Rails is present but doesn't have a logger #4686 # 2.1.4 (24 Oct 2023) ### New features - Add `Query#logger` #4674 - Lookahead: Add `with_alias:` option #2912 - Improve support for `load_application_object_failed` #4667 ### Bug fixes - Execution: Fix runtime loop in some cases with fragments #4684 - Fix `Connection#initialize` outside of execution #4675 - Fix ParseError in `Subscriptions#trigger` #4673 - Mongo: don't load all records in hasNextPage #4671 - Interfaces: fix `definition_methods` when interfaces implement other interfaces #4670 - Migrate `NullContext` to use the built-in Singleton module #4669 - Speed up type lookup #4664 - Fix `ScopeExtension#after_resolve` outside of execution #4685 - Speed up `one_of?` checks #4680 # 2.1.3 (12 Oct 2023) ### Bug fixes - Tracing: fix legacy tracers added to `GraphQL::Schema` #4663 - Add `racc` as a dependency because it's not included by default in Ruby 3.3 #4661 - Connections: don't add automatic connection behaviors for types named "Connection" #4668 # 2.1.2 (11 Oct 2023) ### New features - Depth: accept `count_introspection_fields: false` #4658 - Dataloader: add `get_fiber_variables` and `set_fiber_variables` #4593 - Trace: Add `Schema.default_trace_mode` #4642 ### Bug fixes - Fix merging results after calling directives #4639 #4660 - Visibility: don't reveal implementers of hidden abstract types #4589 - Bump required Ruby version to 2.7 since numbered block arguments are used #4659 - `hash_key:`: use the configured hash key when the underlying Hash has a default value Proc #4656 # 2.1.1 (2 Oct 2023) ### New features - Mutations: `HasSingleInput` provides Relay Classic-like `input: ...` argument behavior #4581 - Add `@specifiedBy` default directive #4633 - Analysis: support `visit?` hook to skip visit but still return a value - Add `context.scoped` for a long-lived reference to the current scoped context #4605 ### Bug fixes - Sanitized printer: Correctly print enum variable defaults #4652 - Schema printer: use `extend schema` when the schema has directives #4647 - Performance: pass runtime state through interpreter code #4621 - Performance: add `StaticVisitor` for faster AST visits #4645 - Performance: faster field lookup #4626 - Improve generator templates #4627 - Dataloader: clear cache between root mutation fields #4617 - Performance: Improve argument checks #4622 - Remove unused legacy connection code #4606 # 2.1.0 (30 Aug 2023) ### Breaking changes - Visitor: legacy-style proc-based visitors are no longer supported #4577 #4583 - Deprecated `GraphQL::Filter` is removed #4325 - Language::Printer has been re-written to append to a buffer; custom printers will need to be updated #4394 ### New features - Authorization: Items in a list can skip object-level `.authorized?` checks if the type is configured with `reauthorize_scoped_objects(false)` #3994 - Subscriptions: `unsubscribe(...)` accepts a value to be used to return a result along with unsubscribing #4283 - Language::Printer is much faster #4394 # 2.0.27 (30 Aug 2023) ### New features - Validators: Support `%{value}` in custom messages #4601 ### Bug fixes - Resolvers: Support `return false, nil` from `ready?` and `authorized?` #4585 - Enums: properly load directives from Schema IDL #4596 - Language: faster scanner #4576 - Language: support fields and arguments named `"null"` #4586 - Language: fix block string quote unescaping #4580 - Generator: use generated node type in Relay-related fields #4598 # 2.0.26 (8 Aug 2023) ### Bug fixes - Datadog Tracing: fix LocalJumpError #4579 # 2.0.25 (7 Aug 2023) ### New features - Tracing: add trace modes #4571 - Dataloader: add `Source#result_key_for` for customizing cache keys in sources #4569 ### Bug fixes - Tracing: Support multiple tracing platforms at once #4543 # 2.0.24 (27 Jun 2023) ### New features - `Schema::Object.wrap` can be used to customize how objects are (or aren't) wrapped by `GraphQL::Schema::Object` instances at runtime #4524 - `Query`: accept a `static_validator:` option in `#initialize` to use instead of the default validation configuration. ### Bug fixes - Performance: Reduce memory usage when adding types to a schema #4533 - Performance, `Dataloader`: when loading specific keys, only run dataloader until those specific keys are resolved #4519 # 2.0.23 (19 Jun 2023) ### New features - Printer: print extensions in SDL #4516 - Trace: accept trace instances during query execution #4497 - AlwaysVisible: Make a way to bypass type visibility #4442, #4491 ### Bug fixes - Tests: fix assertion for Ruby 3.3.0-dev #4515 - Performance: improve fragment possible type lookup #4506 - Docs: document Timeout can handle floats #4505 - Performance: use a dedicated object for field extension state #4401 - Backtrace: fix `backtrace: true` with other trace modules #4505 - Handle `context.warden` being nil #4503 - Dev: disable Minitest::Reporters for RubyMin #4494 - Trace: fix compatibility with inheritance #4487 - Context: fix NullContext compatibility with fetch, dig and key? #4483 # 2.0.22 (17 May 2023) ### New features - Warden: manually instantiating doesn't require a `filter` instance #4462 ### Bug fixes - Enum: fix procs for enum values #4474 - Lexer: force UTF-8 encoding #4467 - Trace: inherit superclass `trace_options` #4470 - Dataloader: properly run mutations in sequence #4461 - NotificationsTrace: Add `execute_multiplex.graphql` event #4460 - Fix `Context#dig` when called with one key #4458 - Performance: Use a plain hash for selection sets at runtime #4453 - Performance: Memoize current trace #4450, #4452 - Performance: Pass is_non_null to runtime check #4449 - Performance: Use `compare_by_identity` on some runtime caches - Properly support nested queries (fix `Thread.current` clash) #4445 # 2.0.21 (11 April 2023) ### Deprecations - Deprecate `GraphQL::Filter` (use `visible?` methods instead) #4424 ### New features - PrometheusTracing: support histograms #4418 ### Bug fixes - Backtrace: improve compatibility with `trace_with` #4437 - Consolidate internally-used empty value constants #4434 - Fix some warnings #4422 - Performance: improve runtime speed #4436 #4433 #4428 #4430 #4427 #4399 - Validation: fix inline fragment selection on scalar #4429 - `@oneOf`: print definition in the SDL when it's used - SDL: load schema directives when they're used - Appsignal tracing: Fix `resolve_type` definition # 2.0.20 (30 March 2023) ### Bug fixes - `.resolve_type`: fix returning `[Type, false]` from resolve_type #4412 - Parsing: improve usage of `GraphQL.default_parser` #4411 - AppsignalTrace: implement missing methods #4390 - Runtime: Fix `current_depth` method in some lazy lists #4386 - Performance: improve `Object` object shape #4365 - Tracing: return execution errors raised from field resolution to `execute_field` hooks #4398 # 2.0.19 (14 March 2023) ### Bug fixes - Scoped context: fix `context.scoped_context.current_path` #4376 - Tracing: fix `tracer` inheritance in Schema classes #4379 - Timeout: fix `Timeout` plugin when other tracers are used #4383 - Performance: use Arrays instead of `GraphQL::Language::Token`s when scanning #4366 # 2.0.18 (9 March 2023) ### Breaking Changes - Tracing: `"execute_field"` events on fields defined on interface types will now receive the _interface_ type as `data[:owner]` instead of the current object type. To get the old behavior, use `data[:object].class` instead. #4292 ### New features - Add `TypeKind#leaf?` #4352 ### Bug fixes - Tracing: use the interface type as `data[:owner]` instead of the object type #4292 - Performance: improve Shape compatibility of `GraphQL::Schema::Field` #4360 - Performance: improve Shape compatibility of `GraphQL::Schema::Warden` #4361 - Performance: rewrite the token scanner in plain Ruby #4369 - Performance: make `deprecation_reason` faster #4356 - Performance: improve lazy value resolution in execution #4333 - Performance: create `current_path` only when the application needs it #4342 - Performance: add `GraphQL::Tracing::Trace` as a lower-overhead tracing API #4344 - Connections: fix `hasNextPage` for already-loaded ActiveRecord Relations #4349 # 2.0.17.2 (29 March 2023) ### Bug fixes - Unions and Interfaces: support returning `[type_module, false]` from `resolve_type` #4413 # 2.0.17.1 (27 March 2023) ### Bug fixes - Tracing: restore behavior returning execution errors raised during field resolution #4402 # 2.0.17 (14 February 2023) ### Breaking changes - Enums: require at least one value in a definition #4278 ### New features - Enums: support `nil` as a Ruby value #4311 ### Bug fixes - Don't re-encode ASCII strings as UTF-8 #4319, #4343 - Fix `handle_or_reraise` with arguments validation #4341 - Performance: Remove error handling from `Lazy#value` (unused) #4335 - Performance: Use codegen instead of dynamic dispatch in `Language::Visitor` and `Analysis::AST::Visitor` #4338 - Performance: reduce indirection in `#introspection?` and `#graphql_name` #4327 - Clean up thread-based state after running queries #4329 - JSON types: don't pass raw NullValue AST nodes to `coerce_input` #4324, #4320 - Performance: reduce `.is_a?` calls at runtime #4318 - Performance: cache interface type memberships #4311 - Performance: eagerly define some type instance variables for Shape friendliness #4300 #4295 #4297 - Performance: reduce argument overhead, don't scope introspection by default, reduce duplicate call to Field#type #4317 - Fix anonymous `eval` usage #4288 - Authorization: fix field auth fail call after lazy #4289 - Subscriptions: fix `loads:`/`as:` # 2.0.16 (19 December 2022) ### Breaking changes - `Union`: Only accept Object types in `possible_types` (previously, other types were also accepted, but this was against the spec) #4269 ### New features - Rake: support introspection query options in the `RakeTask` #4247 - Subscriptions: Merge `.trigger(... context: { ... })` into the query context when running updates #4242 ### Bug fixes - Make BaseEdge and subclasses return true for `.default_relay?` #4272 - Validation: return a proper error for duplicate-named fragments when used indirectly #4268 - Don't re-apply `scope_items` to `nodes { ... }` or `edges { ... }` arrays #4263 - Fix `Concurrent::Map` initialization to prevent race conditions - Speed up scoped context lookup #4245 - Support overriding built-in context keys #4239 - Context: properly `dig` into `:current_arguments` #4249 # 2.0.15 (22 October 2022) ### New features - SDL: support extensions on the schema itself #4203 - SDL: recognize `.graphqls` files in `.from_definition` #4204 - Schema: add a reader method of `TypeMembership#options` #4209 ### Bug fixes - Node Behaviors: call the id-from-object hook with the type definition, not the type instance #4233 - RelayClassicMutation: add a period to the generated description of the payload type #4229 - Dataloader: make scoped context work with Dataloader #4220 - SDL: fix parsing repeatable directives #4218 - Lookahead: reduce more allocations in `.selects?` #4212 - Introspection Query: strip blank lines from generated query strings #4208 - Enums: Add error handling to result coercion #4206 - Lookahead: add `selected_type:` to `.selects?` #4194 - Lookahead: fix `.selects?` on unions #4193 - Fields: use field-local `connection:` config over resolver config #4191 # 2.0.14 (8 September 2022) ### New features - Input Objects: support `one_of` for input objects that allow exactly one argument #4184 - Dataloader: add `source.merge({ ... })` for adding objects to dataloader source caches #4186 - Validation: generate new schemas with a suggested `validate_max_errors` of 100 #4179 ### Bug fixes - Lookahead: improve performance when field names are given as symbols #4189 - Runtime: simplify some internal code #4183 - Datadog tracing: remove deprecated options #4159 # 2.0.13 (12 August 2022) ### New features - Fields: add configuration methods for `default_value` and `prepare` #4156 - Static validation: merge directive errors when they're on the same location or directive ### Bug fixes - Subscriptions: properly use the given `.trigger(... context: )` for determining subscription root field visibility #4160 - Fix fields that use `hash_key:` and have a falsy value for that key #4132 - Variable validation: respect `validate_max_error` limit - Performance: use `Array#+` to add objects during execution #4142 # 2.0.12 (19 July 2022) ### New features - Support returning `[Type, nil]` from `resolve_type` #4130 ### Bug fixes - SDL: Don't print empty braces for input objects with no arguments #4138 - Arguments: always call `prepare` before loading objects based on ID (`loads:`) #4128 - Don't support re-assigning `Query#validate=` after validation has run #4127 # 2.0.11 (20 June 2022) ### New features - Support full unicode range #4090 ### Bug fixes - Subscriptions: support overriding subscriptions in subclasses #4108 - Schema: support types with duplicate names and cyclical references #4107 - Connections: don't exceed application-applied `LIMIT` with `max_page_size` #4104 - Field: add `Field#relay_nodes_field` config reader #4103 - Remove partial `opentelementry` implementation, oops #4086 - Remove unused method `Lazy.resolve` # 2.0.10 (20 June 2022) Oops, this version was accidentally released to RubyGems as "2.10.0". I yanked it. See 2.0.11 instead. # 2.0.9 (31 May 2022) ### New features - Connections: use `Schema.default_page_size`, `Field#default_page_size`, or `Resolver.default_page_size` when one of them is available and no `first` or `last` is given #4081 - Tracing: Add `OpenTelementryTracing` #4077 ### Bug fixes - Field usage analyzer: don't crash on null input objects #4078 - Complexity: properly handle `ExecutionError`s raised in `prepare:` hooks #4079 # 2.0.8 (24 May 2022) ### New Features - Fields: return `fallback_value:` when method or hash key field resolution fails #4069 - Support `hash_key:` lookups on Hash-like objects #4072 - Datadog tracing: support `prepare_span` hook for adding custom tags #4067 ### Bug fixes - Fields: When `hash_key:` is given, populate `#method_str` based on it #4072 - Errors: rescue errors raised when calling `.each` on list values #4052 - Date type: continue accepting dates without hyphens #4061 - Parser: properly parse empty type definitions #4046 # 2.0.7 (25 April 2022) ### New Features - Subscriptions: support `validate_update: false` to disable validation when running subscription updates #4039 - Expose duplicated name on `DuplicateNamesError` #4022 ### Bug Fixes - Datadog: improve tracer #4038 - `hash_key:` try stringified hash key when resolving fields (this restores previous behavior) #4043 - Printer: Don't print empty field set when types have no fields (`{\n}`) #4042 - Dataloader: improve handoff between lazy resolution and dataloader resolution #4036 - Remove unused `Lazy::Resolve` module from legacy execution code #4035 # 2.0.6 (14 April 2022) ### Bug fixes - Dataloader: make multiplexes use custom dataloaders #4026 - ISO8601Date: properly accept `nil` as input #4025 - Mutation: fix error message when `ready?` returns an invalid result #4029 - ISO8601 scalars: add `specified_by_url` configs #4014 - Array connection: don't return all items when `before` is the first cursor #4012 - Introspection: fix typo `specifiedByUrl` -> `specifiedByURL` - Fields: fix `hash_key` to take priority over method lookup #4015 # 2.0.5 (28 March 2022) ### Bug Fixes - Resolvers: fix inheriting arguments when parent classes aren't hooked up directly to the schema #4006 # 2.0.4 (21 March 2022) ### Bug fixes - Fields: make sure `null:` config overrides a default from a resolver #4000 # 2.0.3 (21 March 2022) ### Bug fixes - Fields: make sure field configs override resolver defaults #3975 - Fix `Field#scoped?` when the field uses a resolver #3990 - Allow schema members to have multiple of `repeatable` directives #3986 - Remove some legacy code #3979 #9995 - SDL: fix indirect interface implementation when loading a schema #3982 - Datadog tracing: Support ddtrace 1.0 #3978 - Fix `Node` implementation when connection types include built-in behavior modules #3967 - Small stack trace size reduction #3957 # 2.0.2 (1 March 2022) ### New features - Reduce schema memory footprint #3959 ### Bug fixes - Mutation: Correctly use a configured `type(...)` #3965 - Interfaces: De-duplicate indirectly implemented interfaces #3932 - Remove an unnecessary require #3961 # 2.0.1 (21 February 2022) ### Breaking changes - Resolvers: refactored so that, instead of _copying_ configurations to `field ...` instances, `GraphQL::Schema::Field`s reference their provided `resolver: ...`, `mutation: ...`, or `subscription: ...` classes for many properties. This _shouldn't_ break anything -- all of graphql-ruby's own tests passed just fine -- but it's mentioned here in case you notice anything out-of-sorts in your own application #3916 - Remove deprecated field options `field:`, `function:`, and `resolve:` (these were already no-ops, but they were overlooked in 2.0.0) #3917 ### Bug fixes - Scoped context: fix usage with dataloader #3950 - Subscriptions: support multiple definitions for subscription root fields with `.trigger` #3897 #3935 - Improve some error messages #3920 #3923 - Clean up scalar validation code #3982 # 2.0.0 (9 February 2022) ### Breaking Changes - __None, ideally.__ If you have an application that ran without warnings on v1.13, you should be able to update to 2.0.0 without a hitch. If this isn't the case, please [open an issue](https://github.com/rmosolgo/graphql-ruby/issues/new?template=bug_report.md&title=[2.0%20update]%20describe%20your%20problem) and let me know what happened! I plan to maintain 1.13 for a while in order to ensure a smooth transition. - But, many legacy code components were removed, so if there are any more references to those, there will be name errors! See #3729 for a list of removed components. # 1.13.19 (2 February 2023) ### Bug fixes - Performance: don't re-encode schema member names #4323 - Performance: fix a duplicate field.type call #4316 - Performance: use `scope: false` for introspection types #4315 - Performance: improve argument coercion and validation #4312 - Performance: improve interface type membership lookup #4309 # 1.13.18 (10 January 2023) ### New Features - `hash_key:`: perform `[...]` lookups even when the underlying object isn't a Hash #4286 # 1.13.17 (17 November 2022) ### Bug fixes - Handle ExecutionErrors from prepare hooks when calculating complexity #4248 # 1.13.16 (31 August 2022) ### New Features - Make variable validation respect `validate_max_errors` #4178 # 1.13.15 (30 June 2022) ### Bug fixes - Remove partial OpenTelementry tracing #4086 - Properly use `Query#validate` to skip static validation #3881 # 1.13.14 (20 June 2022) ### New Features - Add `Field#relay_nodes_field` reader #4103 - Datadog: detect tracing module #4100 # 1.13.13 (31 May 2022) ### New features - Datadog: update tracer for ddtrace 1.0 #4038 - Datadog: Add `#prepare_span` hook for custom tags #4067 - Tracing: Add `OpenTelementry` tracing #4077 # 1.13.12 (14 April 2022) - Pass `context[:dataloader]` to multiplex context #4026 - Add a deprecation warning to `.accepts_definitions` #4002 # 1.13.11 (21 March 2022) ### Deprecations - `RangeAdd` warns when `context:` isn't provided (it's required in GraphQL-Ruby 2.0) #3996 # 1.13.10 ### Breaking changes - `id` fields: #3914 Previously, when a field was created with `global_id_field`, it would pass a _legacy-style_ type definition (an instance of `GraphQL::ObjectType`) to `Schema.id_from_object(...)`. Now, it passes a class-based definition instead. If your `id_from_object(...)` method was using any methods from those legacy definitions, they should be migrated. (Most notably, uses of `type.name` should be migrated to `type.graphql_name`.) ### Deprecations - Connections: deprecation warnings were added to configuration methods `.bidirectional_pagination = ...` and `.default_nodes_field = ...`. These two configurations don't apply to the new pagination implementation, so they can be removed. #3918 # 1.13.9 (9 February 2022) ### Breaking changes - Authorization: #3903 In graphql-ruby v1.12.17-1.13.8, when input objects used `prepare: -> { ... }` , the returned values were not authorized at all. However, this release goes back to the behavior from 1.12.16 and before, where a returned `Hash` is validated just like an input object that didn't have a `prepare:` hook. To get the previous behavior, you can implement `def self.authorized?` in the input object you want to skip authorization in: ```ruby class Types::BaseInputObject < GraphQL::Schema::InputObject def self.authorized?(obj, value, ctx) if value.is_a?(self) super else true # graphql-ruby skipped auth in this case for v1.12.17-v1.13.8 end end end ``` ### Bug fixes - Support re-setting `query.validate = ...` after a query is initialized #3881 - Handle validation errors in connection complexity calculations #3906 - Input Objects: try to authorize values when `prepare:` returns a Hash (this was default < v1.12.16) #3903 - SDL: fix when a type has two directives # 1.13.8 (1 February 2022) ### Bug fixes - Introspection query: hide newly-supported fields behind arguments, maintain backwards-compatible INTROSPECTION_QUERY #3877 # 1.13.7 (28 January 2022) ### New Features - Arguments: `replace_null_with_default: true` replaces incoming `null`s with the configured `default_value:` #3871 - Arguments: support `dig: [key1, key2, ...]` for nested hash key access #3856 - Generators: support more Postgresql field types #3577 - Generators: support downcased generator argument types #3577 - Generators: add an input type generator #3577 - Generators: support namespaces in generators #3577 ### Bug Fixes - Field: better error for nil `owner` #3870 - ISO8601DateTime: don't accept inputs with partial time parts #3862 - SDL: fix for base connection classes that implement interfaces #3859 - Cops: find `required: true` on `f.argument` calls (with explicit receiver) #3858 - Analysis: handle undefined or hidden fields with `nil` in `visitor.field_definition` #3857 # 1.13.6 (20 January 2022) ### New features - Introspection: support `__Schema.description`, `__Directive.isRepeatable`, `__Type.specifiedByUrl`, and `__DirectiveLocation.VARIABLE_DEFINITION` #3854 - Directives: Call `Directive.resolve_each` for list items #3853 - Dataloader: Run each list item in its own fiber (to support batching across list items) #3841 ### Bug fixes - RelationConnection: Preserve `OFFSET` when it's already set on the relation #3846 - `Types::ISO8601Date`: Accept default values as Ruby date objects #3563 # 1.13.5 (13 January 2022) ### New features - Directives: support `repeatable` directives #3837 - Tracing: use `context[:fallback_transaction_name]` when operations aren't named #3778 ### Bug fixes - Performance: improve performance of queries with directives #3835 - Fix crash on undefined constant `NodeField` #3832 - Fix crash on partially-required `ActiveSupport` #3829 # 1.13.4 (7 January 2022) ### Bug fixes - Connections: Fix regression in 1.13.3 on unbounded Relation connections #3822 # 1.13.3 (6 January 2022) ### Deprecations - `GraphQL::Relay::NodeField` and `GraphQL::Relay::NodesField` are deprecated; use `GraphQL::Types::Relay::HasNodesField` or `GraphQL::Types::Relay::HasNodeField` instead. (The underlying field instances require a reference to their owner type, but `NodeField` and `NodesField` can't do that, since they're shared instances) #3791 ### New features - Arguments: support `required: :nullable` to make an argument required to be _present_, even if it's `null` #3784 - Connections: When paginating an AR::Relation, use already-loaded results if possible #3790 - Tracing: Support DRY::Notifications #3776 - Improve the error when a Ruby method doesn't support the defined GraphQL arguments #3785 - Input Objects: call `.authorized?` on them at runtime #3786 - Field extensions: add `extras(...)` for extension-related extras with automatic cleanup #3787 ### Bug fixes - Validation: accept nullable variable types for arguments with default values #3819 - Validation: raise a better error when a schema receives a `query { ... }` but has no query root #3815 - Improve the error message when `Schema.get_field` can't make sense of the arguments #3815 - Subscriptions: losslessly serialize Rails 7 TimeWithZone #3774 - Field Usage analyzer: handle errors from `prepare:` hooks #3794 - Schema from definition: fix default values with camelized arguments #3780 # 1.13.2 (15 December 2021) ### Bug fixes - Authorization: only authorize arguments _once_, after they've been loaded with `loads:` #3782 - Execution: always provide an `Interpreter::Arguments` instance as `context[:current_arguments]` #3783 # 1.13.1 (13 December 2021) ### Deprecations - `.to_graphql` and `.graphql_definition` are deprecated and will be removed in GraphQL-Ruby 2.0. All features using those legacy definitions are already removed and all behaviors should have been ported to class-based definitions. So, you should be able to remove those calls entirely. Please open an issue if you have trouble with it! #3750 #3765 ### New features - `context.response_extensions[...] = ...` adds key-value pairs to the `"extensions" => {...}` hash in the final response #3770 - Connections: `node_type` and `edge_type` accept `field_options:` to pass custom options to generated fields #3756 - Field extensions: Support `default_argument ...` configuration for adding arguments if the field doesn't already have them #3751 ### Bug fixes - fix `rails destroy graphql:install` #3739 - ActionCable subscriptions: close channel when unsubscribing from server #3737 - Mutations: call `.authorized?` on arguments from `input_object_class`, `input_type`, too #3738 - Prevent blank strings with `validates: { length: ... }, allow_blank: false` #3747 - Lexer: return mutable strings when strings are empty #3741 - Errors: don't send execution errors to schema-defined handlers from inside lazies #3749 - Complexity: don't multiple `edges` and `nodes` fields by page size #3758 - Performance: fix validation performance degradation from 1.12.20 #3762 # 1.13.0 (24 November 2021) Since this version, GraphQL-Ruby is tested on Ruby 2.4+ and Rails 4+ only. ### Breaking changes - ActionCable Subscriptions: No update is delivered if all subscriptions return `NO_UPDATE` #3713 - Subscription classes: If a subscription has a `scope ...` configuration, then a `scope:` option is required in `.trigger(...)`. Use `scope ..., optional: true` to get the old behavior. #3692 - Arguments whose default values are used aren't checked for authorization #3665 - Complexity: Connection fields have a default complexity implementation based on `first`/`last`/`max_page_size` #3609 - Arguments: if arguments are configured to return `false` for `.visible?(context)`, their default values won't be applied ### New features - Visibility: A schema may contain multiple members with the same name. For each name, GraphQL-Ruby will use the one that returns true for `.visible?(context)` for each query (and raise an error if multiple objects with the same name are visible). #3651 #3716 #3725 - Dataloader: `nonblocking: true` will make GraphQL::Dataloader use `Fiber.scheduler` to run fields and load data with sources, supporting non-blocking IO. #3482 - `null: true` and `required: true` are now default. GraphQL-Ruby includes some RuboCop cops, `GraphQL/DefaultNullTrue` and `GraphQL/DefaultRequiredTrue`, which identify and remove those needless configurations. #3612 - Interfaces may `implement ...` other interfaces #3613 ### Bug fixes - Enum `value(...)` and Input Object `argument(...)` methods return the defined object #3727 - When a field returns an array of mixed errors and values, the result will contain `nil` where there were errors in the list #3656 # 1.12.24 (4 February 2022) ### Bug fixes - SDL: fix parsing schemas where types have multiple directives #3886 # 1.12.23 (20 December 2021) ### Bug fixes - FieldUsage analyzer: handle arguments that raise an error during `prepare:` #3795 # 1.12.22 (8 December 2021) ### Bug fixes - Static validation: fix regression and improve performance of fields_will_merge validation #3761 # 1.12.21 (23 November 2021) ### Bug fixes - Validators: Fix `format:`/`allow_blank: true` to correctly accept a blank string #3726 - Generators: generate a correct `Schema.type_error` hook #3722 # 1.12.20 (17 November 2021) ### New Features - Static validation: improve error messages when fields won't merge #3698 - Generators: improve id_from_object and type_error suggested implementations #3710 - Connections: make the new connections module fall back to old connections #3704 ### Bug fixes - Dataloader: re-enqueue sources when one call to `yield` didn't satisfy their pending requests #3707 - Subscriptions: Fix when JSON-typed arguments are used #3705 # 1.12.19 (5 November 2021) ### New Features - Argument validation: Make `allow_null` and `allow_blank` work standalone #3671 - Add field and path info to Encoding errors #3697 - Add `Resolver#unauthorized_object` for handling loaded but unauthorized objects #3689 ### Bug fixes - Properly hook up `Schema.validate_max_errors` at runtime #3691 # 1.12.18 (2 November 2021) ### New features - Subscriptions: Add `NO_UPDATE` constant for skipping subscription updates #3664 - Validation: Add `Schema.validate_max_errors(integer)` for halting validation when it reaches a certain number #3683 - Call `self.load_...` methods on Input objects for loading arguments #3682 - Use `import_methods` in Refinements when available #3674 - `AppsignalTracing`: Add `set_action_name` #3659 ### Bug fixes - Authorize objects returned from custom `def load_...` methods #3682 - Fix `context[:current_field]` when argument `prepare:` hooks raise an error #3666 - Raise a helpful error when a Resolver doesn't have a configured `type(...)` #3679 - Better error message when subscription clients are using ActionCable #3668 - Dataloader: Fix dataloading of input object arguments #3666 - Subscriptions: Fix parsing time zones #3667 - Subscriptions: Fix parsing with non-null arguments #3620 - Authorization: Call `schema.unauthorized_field` for unauthorized resolvers - Fix when literal `null` is used as a value for a list argument #3660 # 1.12.17 (15 October 2021) ### New features - Support `extras: [:parent]` #3645 - Support ranges in `NumericalityValidator` #3635 - Add some Dataloader methods for testing #3335 ### Bug fixes - Support input object arguments called `context` #3654 - Support single-item default values for list arguments #3652 - Ensure query strings are strings before running a query #3628 - Fix empty hash kwargs for Ruby 3 #3610 - Fix wrongly detecting Ipnut objects in authorization #3606 # 1.12.16 (31 August 2021) ### New features - Connections: automatically support Mongoid 7.3 #3599 - Support `def self.topic_for` in Subscription classes for server-filtered streams #3597 - When a list item or object field has an invalid null, stop executing that list or ### Bug fixes - Perf: don't refine String when unnecessary #3593 - BigInt: always parse as base 10 #3586 - Errors: only return one error when a node in a non-null connection has an invalid null #3601 # 1.12.15 (23 August 2021) ### New Features - Subscriptions: add support for multi-tenant setups when deserializing context #3574 - Analyzers: also track deprecated arguments #3549 # 1.12.14 (22 July 2021) ### Bug fixes - SDL: support directive arguments referencing overridden built-in scalars #3564 - Use `"_"` as the name for `field :_, ...` fields #3560 - Support `sanitized_printer(...)` in the schema definition for `Query#sanitized_query_string` - `GraphQL::Backtrace`: fix multiplex support # 1.12.13 (20 June 2021) ### Breaking changes - Add a trailing newline to the `Schema.to_definition` output string #3541 ### Bug fixes - Properly handled list results in GraphQL::Backtrace #3540 - Return plain `Hash`es and `Array`s from queries instead of customized subclasses #3533 - Fix errors raised from non-null fields #3537 - Resolver: don't pass frozen array of extensions when none were configured #3515 - Configure the right `owner` for `node` and `nodes` fields #3509 - Improve error message for invalid enum value #3507 - Properly halt on lazily-returned `context.skip`s #3514 - Fix: call overridden `to_h` methods on InputObject classes #3539 - Halt execution when a runtime directive argument raises a `GraphQL::ExecutionError` #3542 # 1.12.12 (31 May 2021) ### Bug fixes - Directives on inline fragments and fragment spreads receive `.resolve(...)` calls #3499 # 1.12.11 (28 May 2021) ### Bug fixes - Validate argument default values when adding them to the schema #3496 - Resolvers inherit extensions from superclasses #3500 - Greatly reduce runtime overhead #3494, #3505 - Remove hidden directives from introspection #3488 # 1.12.10 (18 May 2021) ### New features - Use `GlobalID::Locator.locate_many` for arrays of global Ids #3481 - Support runtime directives (call `.resolve`) on `QUERY` #3474 ### Bug fixes - Don't override Resolver `#load_*` methods when they're inherited #3486 - Fix validation of runtime directive arguments that have input objects #3485 - Add a final newline to rake task output - Don't add connection arguments to fields loaded from introspection responses #3470 - Fix `rescue_from` on loading arguments #3471 # 1.12.9 (7 May 2021) ### New features - Overriding `.authorized_new(...)` to call `.new(...)` effectively skips object authorization #3446 - Dataloader copies Fiber-local values from `Thread.current[...]` when initializing new Fibers #3461 ### Bug fixes - Fix introspection of default value input objects #3456 - Add `StandardError => ...` condition to the generated GraphqlController #3460 - Fix `Dataloader::Source` on Ruby 3 with keyword arguments - Respect directive visibility at runtime #3450 - ActionCable subscriptions: only deserialize the broadcast payload once #3443 - Don't re-add `graphiql-rails` when `generate graphql:install` is run twice #3441 - Allow differing selections on mutually exclusive interfaces #3063 - Respect `max_page_size: nil` override in fields #3438 # 1.12.8 (12 Apr 2021) ### Bug fixes - Fix loading single-key hashes in Subscriptions #3428 - Fix looking up `rescue_from` handlers inherited from parent schema classes #3431 # 1.12.7 (7 Apr 2021) ### Breaking changes - `Execution::Errors` (which implements `rescue_from`) was refactored so that, when an error matches more than one registered handler, it picks the _most specific_ handler instead of the _first match_ in the underlying Hash. This might "break" your code if your application registered a handler for a parent class and a child class, but expects instances of the child class to be handled by the handler for the parent class. (This seems very unlikely -- I consider the change to be a "breaking fix.") #3404 ### New features - Errors: pick the most specific error handlers (instead of an order-dependent selection) #3404 - Add `node_nullable(...)` connection configuration options #3389 - Add `has_nodes_field(true|false)` connection configuration option #3388 - Store more metadata in argument-related static validation errors #3406 ### Bug fixes - Fix connection nullability settings to properly handle `false` #3386 - Fix returning `RawValue`s as part of a list #3403 - Fix introspection for deprecated directive arguments #3416 - Optimize `has_next_page` for ActiveRecord::Relation connections #3414 - Tracing: consistent event sequencing when queries are executed with `Query#result` #3408 # 1.12.6 (11 March 2021) ### Breaking changes - Static validation: previously, variables passed as arguments to input objects were not properly type-checked. #3370 fixed type checking in this case, but may case existing (invalid) queries to break. ### New features - Connection types: support `edges_nullable(false)` and `edge_nullable(false)` for non-null fields #3376 - Connections: add `.arguments` reader to new `Pagination::Connection` classes #3360 ### Bug fixes - Relation connection: Remove extra `COUNT` query from some scenarios #3373 - Add a Bootsnap-style parsing cache when Bootsnap is detected #3156 - Fix input validation for input object variables #3370 # 1.12.5 (18 February 2021) ### New features - Resolvers: support `max_page_size` config #3338 - RangeAdd: call `range_add_edge` (if supported) to improve stable connection support #3341 ### Bug fixes - Backtrace: fix new tracer when analyzing multiplex without executing it #3342 - Dataloader: pass along `throw`s #3333 - Skip possible_types filtering for non-interface types #3336 - Improve debugging message for ListResultFailedError #3339 # 1.12.4 (8 February 2021) ### Bug fixes - Allow prepended modules to add fields #3325 - Fix ConnectionExtension when another extension short-circuits `resolve` #3326 - Backtrace: Fix GraphQL::Backtrace with static validation (used by graphql-client) #3324 - Dataloader: Fix yield from root fiber when accessing arguments from analyzers. Fix arguments sometimes containing unresolved `Execution::Lazy`s #3320 - Dataloader: properly pass raised errors to `handle_error` handlers #3319 - Fix NameError in validation error #3303 - Dataloader: properly batch when parent fields were not batched #3312 # 1.12.3 (27 January 2021) ### Bug fixes - Fix constant names for legacy scalar types # 1.12.2 (26 January 2021) ### New features - `GraphQL::Deprecation.warn` is used for GraphQL-Ruby 2.0 deprecation warnings (and calls through to `ActiveSupport::Deprecation.warn` if it's available) #3292 # 1.12.1 (25 January 2021) ### Bug fixes - `GraphQL::Dataloader`: properly support selections with multiple fields #3297 # 1.12.0 (20 January 2021) ### Breaking changes - `GraphQL::Schema` defaults to `GraphQL::Execution::Interpreter`, `GraphQL::Analysis::AST`, `GraphQL::Pagination::Connections`, and `GraphQL::Execution::Errors`. (#3145) To get the previous (deprecated) behaviors: ```ruby # Revert to deprecated execution behaviors: use GraphQL::Execution::Execute use GraphQL::Analysis # Disable the new connection implementation: self.connections = nil ``` - `GraphQL::Execution::Interpreter::Arguments` instances are frozen (#3138). (Usually, GraphQL code doesn't interact with these objects, but they're used some places under the hood.) ### Deprecations - Many, many legacy classes and methods were deprecated. #3275 Deprecation errors include links to migration documentation. For a full list, see: https://github.com/rmosolgo/graphql-ruby/issues/3056 ### New features - Rails-like argument validations (#3207) - Fiber-based `GraphQL::Dataloader` for batch-loading data #3264 - Connection and edge behaviors are available as mixins #3071 - Schema definition supports schema directives #3224 ### Bug fixes # 1.11.10 (5 Nov 2021) ### Bug fixes - Properly hook up `Schema.max_validation_errors` at query runtime #3690 # 1.11.9 (1 Nov 2021) ### New Features - `Schema.max_validation_errors(val)` limits the number of errors that can be added during static validation #3675 # 1.11.8 (12 Feb 2021) ### Bug fixes - Improve performance of `Schema.possible_types(t)` for object types #3172 # 1.11.7 (18 January 2021) ### Breaking changes - Incoming integer values are properly bound (as per the spec) #3206 To continue receiving out-of-bound integer values, add this to your schema's `def self.type_error(err, ctx)` hook: ```ruby def self.type_error(err, ctx) if err.is_a?(GraphQL::IntegerDecodingError) return err.integer_value # return it anyways, since this is how graphql-ruby used to work end # ... end ``` ### New features - Support Ruby 3.0 #3278 - Add validation timeout option #3234 - Support Prometheus custom_labels in GraphQLCollector #3215 ### Bug fixes - Handle `GraphQL::UnauthorizedError` in interpreter in from arguments #3276 - Set description for auto-generated `input:` argument #3141 - Improve performance of fields will merge validation #3228 - Use `Float` graphql type for ActiveRecord decimal columns #3246 - Add some custom methods to ArrayConnection #3238 - Fix generated fields for types ending Connection #3223 - Improve runtime performance #3217 - Improve argument handling when extensions shortcut the defined resolve #3212 - Bind scalar ints as per the spec #3206 - Validate that input object names are unique #3205 ## 1.11.6 (29 October 2020) ### Breaking changes FieldExtension: pass extended values instead of originals to `after_resolve` #3168 ### Deprecations ### New features - Accept additional options in `global_id_field` macro #3196 ### Bug fixes - Use `graphql_name` in `UnauthorizedError` default message (fixes #3174) #3176 - Improve error handling for base 64 decoding (in `UniqueWithinType`) #3179 - Fix `.valid_isolated_input?` on parsed schemas (fixes #3181) #3182 - Fix fields nullability in subscriptions documentation #3194 - Update `RangeAdd` to use new connections when available #3195 ## 1.11.5 (30 September 2020) ### New features - SanitizedPrinter: accept `inline_variables: false` option and add `#redact_argument_value?` and `#redacted_argument_value` hooks #3167 - GraphQL::Schema::Timeoout#max_seconds(query) can provide a per-query timeout duration #3167 - Implement Interpreter::Arguments#fetch - Assign `current_{path,field,arguments,object}` in `query.context` #3139. The values at these keys change while the query is running. - ActionCableSubscriptions: accept `use(..., namespace: "...")` for running multiple schemas in the same application #3076 - Add `deprecation_reason:` to arguments #3015 ### Bug fixes - SanitizedPrinter: Fix lists and JSON scalars #3171 - Improve retained memory in Schema.from_definition #3153 - Make it easier to cache schema parsing #3153 - Make sure deprecated arguments aren't required #3137 - Use `.empty?` instead of `.length.zero?` in lexer #3134 - Return a proper error when a stack error happens #3129 - Assert valid input types on arguments #3120 - Improve Validator#validate performance #3125 - Don't wrap `RawValue` in ConnectionExtension #3122 - Fix interface possible types visibility #3124 ## 1.11.4 (24 August 2020) ### Breaking changes ### New features - Add `node_nullable` option for `edge_type` #3083 - Use module namespacing for template generators #3098 ### Bug fixes - Rescue `SystemStackError`s during validation #3107 - Add `require 'digest/sha2'` for fingerprint #3103 - Optimize `GraphQL::Query::Context#dig` #3090 - Check if new connections before calling method on it (fixes #3059) #3100 - Thread field owner type through interpreter runtime (fixes #3086) #3099 - Check for visible interfaces on the type in warden #3096 - Update `AppOpticsTracing` with latest changes in `PlatformTracing` #3097 - Use throw instead of raise to halt subscriptions early #3084 - Optimize `GraphQL::Query::Context#fetch` #3081 ## 1.11.3 (13 August 2020) ### Breaking changes - Reverted the `required` and `default_value` argument behaviour change in 1.11.2 since it was not spec compliant #3066 ### New features - Improve resolver method conflict warning #3069, #3062 - Store arguments on `Mutation` instances after they're loaded #3073 ### Bug fixes - Fix connection wrappers on lazy lists #3070 ## 1.11.2 (1 August 2020) ### Breaking changes - Previously, GraphQL-Ruby allowed _both_ `default_value: ...` and `required: true` in argument definitions. However, this definition doesn't make sense -- a default value is never used for a `required: true` argument. This configuration now raises an error. Remove the `default_value:` to get rid of the error. #3011 ### New features - Support Date, Time and OpenStruct in Subscription::Serialize #3057 ### Bug fixes - Speed up `DELETE_NODE` check #3053 - Reject invalid enum values during definition #3055 - Fix `.trigger` from unsubscribed ActionCable channel #3051 - Fix error message from VariablesAreUsedAndDefined for anonymous queries #3050 - Fix renaming variable identifiers in AST visitor #3045 - Reject `default_value: ...` used with `required: true` during definition #3011 - Use the configured `edge_class:` with new connections #3036 - Don't call visible for unused arguments #3030, #3031 - Properly load directives from introspection results #3021 - Reject interfaces as members of unions #3024 - Load deprecation reason from introspection results #3014 - Fix arguments caching when extension modify arguments #3009 ## 1.11.1 (17 June 2020) ### New Features - Add `StatsdTracing` #2996 ### Bug Fixes - Raise the proper `InvalidNullError` when a mutation field returns an invalid `nil` #2997 ## 1.11.0 (13 June 2020) ### Breaking changes - Global tracers are removed (deprecated since 1.7.4) #2936 - Fields defined in camel case (`field :doStuff`) will not line up to methods that are underscore case (`def do_stuff`). Instead, the given symbol is used _verbatim_. #2938 To work around this: - Change the name of the method to match the field (eg, `def doStuff`) - Change the name of the field to match the method (eg, `field :do_stuff`, let graphql-ruby camelize it for you) - Or, add `resolver_method: :do_stuff` to explicitly map the field to a method on the object type definition You can probably find instances of this in your application with a regexp like `/field :[a-z]+[A-Z]/`, and review them. ### New features - `extend SubscriptionRoot` is no longer necessary #2770 - Add `broadcast: true` option to subscriptions #2959 - Add `Edge#parent` to new connection classes #2961 ### Bug fixes - Use the field name as configured for hash key or method name #2906 ## 1.10.12 (13 June 2020) ### Bug fixes - Fix compatibility of `YYYY-mm-dd` with `Types::ISO8601DateTime` #2989 - Remove unused ivar in InputObject #2987 ## 1.9.21 (12 June 2020) ### Bug fixes - Fix `extras:` on subscription fields #2983 ## 1.10.11 (11 June 2020) ### New features - Scout tracer adds transaction name to traces #2969 - `resolve_type` can optionally return a resolved object #2976 - DateTime scalar returns a `Time` for better timezone handling #2973 - Interpreter memory improvements #2980, #2978 - Support lazy values from field-level authorization hooks #2977 - Object generator infers fields from model classes #2954 - Add type-specific runtime errors #2957 ### Bug fixes - Fix for error when using `extras:` with subscription fields #2984 - Improve Schema.error_handler inheritance #2975 - Add raw_value to conflict warning list #2958 - Arguments#each_value yields ArgumentValues #2956 ## 1.10.10 (20 May 2020) ### Bug Fixes - Fix lazy `loads:` with list arguments #2949 - Show object fields even when inherited ones are hidden #2950 - Use `reverse_each` in instrumenters #2945 - Fix underscored names in introspection loader #2941 - Fix array input to Date/DateTime types #2927 - Fix method conflict warnings on schema loader #2934 - Fix some Ruby 2.7 warnings #2925 ## 1.9.20 (20 May 2020) ### Bug fixes - Fix `default_value: {}` on Ruby 2.7 ## 1.10.9 (4 May 2020) ### New features - Add `Interpreter::Arguments#dig` #2912 ## 1.10.8 (27 April 2020) ### Breaking changes - With the interpreter, `Query#arguments_for` returns `Interpreter::Arguments` instances instead of plain hashes. (They should work mostly the same, though.) #2881 ### New features - `Schema::Field#introspection?` returns true for built-in introspection-related fields ### Bug fixes - Fix Ruby 2.7 warning on `Schema.to_json` #2905 - Pass `&block` to nested method calls to reduce stack depths #2900 - Fix lazy `loads:` with list arguments #2894 - Fix `loads:` on nested input object #2895 - Rescue base64 encoding errors in the encoder #2896 ## 1.10.7 (16 April 2020) ### Breaking changes - `Schema.from_introspection(...)` builds class-based schemas #2876 ### New features - `Date` and `DateTime` types also accept well-formatted strings #2848 - `Schema.from_introspection(...)` builds class-based schemas #2876 - `Schema#to_definition` now dumps all directives that were part of the original IDL, if the schema was parsed with `.from_definition` #2879 ### Bug fixes - Fix memory leak in legacy runtime #2884 - Fix interface inheritance in legacy runtime #2882 - Fix description on `List` and `NonNull` types (for introspection) #2875 - Fix over-rescue of NoMethodError when building list responses #2887 ## 1.10.6 (6 April 2020) ### New features - Add options to `implements(...)` and interface type visibility #2791 - Add `Query#fingerprint` for logging #2859 - Add `--playground` option to install generator #2839 - Support lazy-loaded objects from input object `loads:` #2834 ### Bug fixes - Fix `Language::Nodes` equality: move `eql?` to `==` #2861 - Make rake task properly detect rails `environment` task #2862 - Fix `nil` override for `max_page_size` #2843 - Fix `pageInfo` methods when they're called before `nodes` #2845 - Make the default development error match a normal GraphQL error #2825 - Fix `loads:` with `require: false` #2833 - Fix typeerror for `BigInt` given `nil` #2827 ## 1.10.5 (12 March 2020) ### New features - Add `#field_complexity` hook to `AST::QueryComplexity` analyzer #2807 ### Bug fixes - Pass `nonce: true` when encoding cursors #2821 - Ignore empty-string cursors #2821 - Properly pass along `Analysis::AST` to schema instances #2820 - Support filtering unreachable types in schemas from IDL #2816 - Use `Query#arguments_for` for lookahead arguments #2811 - Fix pagination bug on old connections #2799 - Support new connection system on old runtime #2798 - Add details to raise CoercionErrors #2796 ## 1.10.4 (3 March 2020) ### Breaking changes - When an argument is defined with a symbol (`argument :my_arg, ...`), that symbol is used _verbatim_ to build Ruby keyword arguments. Previously it was converted to underscore-case, but this autotransform was confusing and wrong in some cases. You may have to change the symbol in your `argument(...)` configuration if you were depending on that underscorization. #2792 - Schemas from `.from_definition` previously had half-way connection support. It's now completely removed, so you have to add connection wrappers manually. See #2782 for migration notes. ### New features - Add `Appoptics` tracing #2789 - Add `Query#sanitized_query_string` #2785 - Improved duplicate type error message #2777 ### Bug fixes - Fix arguments ending in numbers, so they're injected with the same name that they're configured with #2792 - Improve `Query#arguments_for` with interpreter #2781 - Fix visitor replacement of variable definitions #2752 - Remove half-broken connection handling from `Schema.from_definition` #2782 ## 1.10.3 (17 Feb 2020) ### New features - Support `loads:` with plain field arguments #2720 - Support `raw_value(...)` to halt execution with a certain value #2699 - `.read_subscription` can return `nil` to bypass executing a subscription #2741 ### Bug fixes - Connection wrappers are properly inherited #2750 - `prepare(...)` is properly applied to default values in subscription fields #2748 - Code tidying for RSpec warnings #2741 - Include new analysis module when generating a schema #2734 - Include directive argument types in printed schemas #2733 - Use `module_parent_name` in Rails #2713 - Fix overriding default scalars in build_from_definition #2722 - Fix some non-null errors in lists #2651 ## 1.10.2 (31 Jan 2020) ### Bug fixes - Properly wrap nested input objects in instances #2710 ## 1.10.1 (28 Jan 2020) ### Bug fixes - Include Interface-level `orphan_types` when building a schema #2705 - Properly re-enter selections in complexity analyzer #2595 - Fix input objects with null values #2690 - Fix default values of `{}` in `.define`-based schemas #2703 - Fix field extension presence check #2689 - Make new relation connections more efficient #2697 - Don't include fields `@skip(if: true)` or `@include(if: false)` in lookahead #2700 ## 1.9.19 (28 Jan 2020) ### Bug Fixes - Fix argument default value of `{}` with Ruby 2.7 argument handling #2704 ## 1.10.0 (20 Jan 2020) ### Breaking Changes - Class-based schemas using the new interpreter will now use _definition classes_ at runtime. #2363 (Previously, `.to_graphql` methods were used to generate singletons which were used at runtime.) This means: - Methods that used to receive types at runtime will now receive classes instead of those singletons. - `.name` will now call `Class#name`, which will give the class name. Use `.graphql_name` to get the name of a GraphQL type. (Fields, arguments and directives have `.graphql_name` too, so you can use it everywhere.) - Some methods that return hashes are slow because they merge hashes according to class inheritance, for example `MySchema.types` and `MyObjectType.fields`. Instead: - If you only need one item out of the Hash, use `.get_type(type_name)` or `.get_field(field_name)` instead. Those methods find a match without performing Hash merges. - If you need the whole Hash, get a cached value from `context.warden` (an instance of `GraphQL::Schema::Warden`) at runtime. Those values reflect the types and fields which are permitted for the current query, and they're cached for life of the query. Check the API docs to see methods on the `warden`. - Class-based schemas using the interpreter _must_ add `use GraphQL::Analysis::AST` to their schema (and update their custom analyzers, see https://graphql-ruby.org/queries/ast_analysis.html) #2363 - ActiveSupport::Notifications events are correctly named in event.library format #2562 - Field and Argument `#authorized?` methods now accept _three_ arguments (instead of 2). They now accept `(obj, args, ctx)`, where `args` is the arguments (for a field) or the argument value (for an argument). #2520 - Double-null `!!` is disallowed by the parser #2397 - (Non-interpreter only) The return value of subscription fields is passed along to execute the subscription. Return `nil` to get the previous behavior. #2536 - `Schema.from_definition` builds a _class-based schema_ from the definition string #2178 - Only integers are accepted for `Int` type #2404 - Custom scalars now call `.coerce_input` on all input values - previously this call was skipped for `null` values. ### Deprecations - `.define` is deprecated; class-based schema definitions should be used instead. If you're having trouble or you can't find information about an upgrade path, please open an issue on GitHub! ### New Features - Add tracing events for `.authorized?` and `.resolve_type` calls #2660 - `Schema.from_definition` accepts `using:` for installing plugins (equivalent to `use ...` in class-based schemas) #2307 - Add `$` to variable names in error messages #2531 - Add invalid value to argument error message #2531 - Input object arguments with `loads:` get the loaded object in their `authorized?` hook, as `arg` in `authorized?(obj, args, ctx)`. #2536 - `GraphQL::Pagination` auto-pagination system #2143 - `Schema.from_definition` builds a _class-based schema_ from the definition string #2178 ### Bug Fixes - Fix warnings on Ruby 2.7 #2668 - Fix Ruby keyword list to support Ruby 2.7 #2640 - Reduce memory of class-based schema #2636 - Improve runtime performance of interpreter #2630 - Big numbers (ie, greater than Ruby's `Infinity`) no longer :boom: when being reserialized #2320 - Fix `hasNextPage`/`hasPrevious` page when max_page_size limits the items returned #2608 - Return parse errors for empty documents and empty argument lists #2344 - Properly serialize `defaultValue` of input objects containing enum values #2439 - Don't crash when a query contains `!!`. #2397 - Resolver `loads:` assign the value to argument `@loads` #2364 - Only integers are accepted for `Int` type #2404 ## 1.9.18 (15 Jan 2020) ### New features - Support disabling `__type` or `__schema` individually #2657 - Support Ruby 2.7, and turn on CI for it :tada: #2665 ### Bug fixes - Fix Ruby 2.7 warnings #2653 #2669 - Properly build camelized names for directive classes #2666 - Use schema-defined context class for SDL generation #2656 - Apply visibility checks when generating SDL #2637 ## 1.9.17 (17 Dec 2019) ### New features - Scoped context for propagating values to child fields #2634 - Add `type_membership_class` with possible_type visibility #2391 ### Bug fixes - Don't return unreachable types in introspection response #2596 - Wrap more of execution with error handling #2632 - Fix InputObject `.prepare` for the interpreter #2624 - Fix Ruby keyword list to support Ruby 2.7 #2640 - Fix performance of urlsafe_encode64 backport #2643 ## 1.9.16 (2 Dec 2019) ### Breaking changes - `GraphQL::Schema::Resolver#initialize` accepts a new keyword argument, `field:`. If you have overridden this method, you'll have to add that keyword to your argument list (and pass it along to `super`.) #2605 ### Deprecations - `SkylightTracing` is disabled; the Skylight agent contains its own GraphQL support. See Skylight's docs for migration. #2601 ### New features ### Bug fixes - Fix multiplex max_depth calculation #2613 - Use monotonic time in TimeoutMiddleware #2622 - Use underscored names in Mutation generator #2617 - Fix lookahead when added to mutations in their `field(...)` definitions #2605 - Handle returned lists of errors from Mutations #2567 - Fix lexer error on block strings containing only newlines #2598 - Fix mutation generator to reference the new base class #2580 - Use the right camelization configuration when generating subscription topics #2552 ## 1.9.15 (30 Oct 2019) ### New features - Improve parser performance #2572 - Add `def prepare` API for input objects #1869 - Support `extensions` config in Resolver classes #2570 - Support custom `.connection_extension` in field classes #2561 - Warn when a field name is a Ruby keyword #2559 - Improve performance for ActiveRecord connection #2547 ### Bug fixes - Fix errantly generated `def resolve_field` method in `BaseField` #2578 - Comment out the `null_session` handling in the generated controller, for better compat with Rails API mode #2557 - Fix validation error with duplicate, self-referencing fragment #2577 - Revert the `.authorized?` behavior of InputObjects to handle cyclical references. See 1.10.0.pre1 for a better behavior. #2576 - Replace `NotImplementedError` (which is meant for operating system APIs) with `GraphQL::RequiredImplementationMissingError` #2543 ## 1.9.14 (14 Oct 2019) ### New features - Add `null_session` CSRF handing in `install` generator #2524 - Correctly report InputObjects without arguments and Objects without fields as invalid #2539 #2462 ### Bug fixes - Fix argument incompatibility #2541 - Add a `require` for `Types::ISO8691Date` #2528 - Fix errors re-raised after lazy fields #2525 ## 1.9.13 (8 Oct 2019) ### Breaking changes - Enum values were (erroneously) accepted as ID or String values, but they aren't anymore. #2505 ### New features - Add `Query#executed?` #2486 - Add `Types::ISO8601Date` #2471 ### Bug fixes - Don't accept Enums as IDs or Strings #2505 - Call `.authorized?` hooks on arguments that belong to input objects #2519 - Fix backslash parsing edge case #2510 - Improve performance #2504 #2498 - Properly stringify keys in error extensions #2508 - Fix `extras:` handling in RelayClassicMutation #2484 - Use `Types::BaseField` in scaffold #2470 ## 1.9.12 (9 Sept 2019) ### Breaking Changes - AST Analyzers follow fragments spreads as if they were inline fragments. #2463 ### New Features - `use GraphQL::Execution::Errors` provides error handling for the new interpreter. #2458 ### Bug Fixes - Fix false positive on enum value validation #2454 ## 1.9.11 (29 Aug 2019) ### Breaking Changes - Introspection fields are now considered for query depth validations, so you'll need at least `max_depth: 13` to run the introspection query #2437 ### New features - Add `extras` setter to `GraphQL::Schema::Field` #2450 - Add extensions in `CoercionError` #2431 ### Bug fixes - Make `extensions` kwarg on field on more flexible for extensions with options #2443 - Fix list validation error handling #2441 - Include introspective fields in query depth calculations #2437 - Correct the example for using 'a class method to generate fields' #2435 - Enable multiple execution errors for Fields defined to return a list #2433 ## 1.9.10 (20 Aug 2019) ### New features - Support required arguments with default values #2416 ### Bug fixes - Properly disable `max_complexity` and `max_depth` when `nil` is passed #2409 - Fix printing class-based schemas #2406 - Improve field method naming conflict check #2420 ## 1.9.9 (30 July 2019) ### New features - Memoize generated strings in `.to_query_string` #2400 - Memoize generated strings in platform tracing #2401 ### Bug fixes - Support class-based subscription type in `.define`-based schema #2403 ## 1.9.8 (24 July 2019) ### New features - Schema classes pass their configuration to subclasses #2384 - Improve memory consumption of lexer and complexity validator #2389 - The `install` generator creates a BaseArgument #2379 - When a field name conflicts with a built-in method name, give a warning #2376 ### Bug fixes - When a resolver argument uses `loads:`, the argument definition will preserve the type in `.loads` #2365 - When an required argument is hidden, it won't add a validation error #2393 - Fix handling of invalid UTF-8 #2372, #2377 - Empty block strings are parsed correctly #2381 - For resolvers, only authorize arguments once #2378 ## 1.9.7 (25 June 2019) ### Breaking changes - `Analysis::AST::Visitor#argument_definition` no longer returns the _previous_ argument definition. Instead, it returns the _current_ argument definition and `#previous_argument_definition` returns the previous one. You might have to replace calls to `.argument_definition` with `.previous_argument_definition` for compatibility. #2226 ### New features - Accept a `subscription_scope` configuration in Subscription classes #2297 - Add a `disable_introspection_entry_points` configuration in Schema classes #2327 - Add `Analysis::AST::Visitor#argument_definition` which returns the _current_ argument definition, `#previous_argument_definition` returns the _previous_ one #2226 - Run CI on Ruby 2.6 #2328 - Autogenerate base field class #2216 - Add timeout support with interpreter #2220 ### Bug fixes - Fix Stack overflow when calling `.to_json` on input objects #2343 - Fix off-by-one error with hasNextPage and ArrayConnections #2349 - Fix GraphQL-Pro operation store compatibility #2350 - Fix class-based transformer when multiple mutations are in one file #2309 - Use `default_graphql_name` for Edge classes #2224 - Support nested `loads:` with input objects #2323 - Support `max_complexity` with multiplex & AST analysis #2306 ## 1.9.6 (23 May 2019) ### Bug fixes - Backport `String#-@` for Ruby 2.2 support #2305 ## 1.9.5 (22 May 2019) ### New features - Support `rescue_from` returning `GraphQL::ExecutionError` #2140 - Accept `context:` in `Schema.validate` #2256 - Include `query:` in interpreter tracing for `execute_field` and `execute_field_lazy` #2236 - Add `Types::JSON` #2227 - Add `null:` option to `BaseEdge.node_type` #2249 ### Bug fixes - Fix Ruby 2.2 compatibility #2302 - Distinguish aliased selections in lookahead #2266 - Properly show list enum default values in introspection #2263 - Performance improvements: #2289, #2244, #2258, #2257, #2240 - Don't recursively unwrap inputs for RelayClassicMutation #2236 - Fix `Schema::Field#scoped?` when no return type #2255 - Properly forward more authorization errors #2165 - Raise `ParseError` for `.parse(nil)` #2238 ## 1.9.4 (5 Apr 2019) ### Breaking Changes - `GraphQL::Schema::Resolver::LoadApplicationObjectFailedError` was renamed to `GraphQL::LoadApplicationObjectFailedError`. (This will only break if you're referencing the class by name and running Ruby 2.5+) #2080 ### New features - Add `Types::BigInt` #2150 - Add auto-loading arguments support in Input Object types #2080 - Add analytics tag to Datadog tracing #2154 ### Bug fixes - Fix `Query#execute` when no explicit query string is passed in #2142 - Fix when a root type returns nil because unauthorized #2144 - Fix tracing `node` by threading `owner:` through field tracing #2156 - Fix interpreter handling of exceptions raised during argument preparation #2198 - Fix ActionCableLink when there are errors but no data #2176 - Provide empty hash as default option for field resolvers #2189 - Prevent argument names from overwriting Arguments methods #2171 - Include array indices in error paths #2162 - Handle non-node arrays in AST visitor #2161 ## 1.9.3 (20 Feb 2019) ### Bug fixes - Fix `Schema::Subscription` when it has no arguments #2135 - Don't try to scope `nil`, just skip scoping altogether #2134 - Fix when a root `.authorized?` returns `false` and there's no `root_value` #2136 - Fix platform tracing with interpreter & introspection #2137 - Support root Subscription types with name other than `Subscription` #2102 - Fix nested list-type input object nullability validation #2123 ## 1.9.2 (15 Feb 2019) ### Bug fixes - Properly support connection fields with resolve procs #2115 ## 1.9.1 (14 Feb 2019) ### Bug fixes - Properly pass errors to Resolver `load_application_object_failed` methods #2110 ## 1.9.0 (13 Feb 2019) ### Breaking Changes - AST nodes are immutable. To modify a parsed GraphQL query, see `GraphQL::Language::Visitor` for its mutation API, which builds a new AST with the specified mutations applied. #1338, #1740 - Cursors use urlsafe Base64. This won't break your clients (it's backwards-compatible), but it might break your tests, so it's listed here. #1698 - Add `field(..., resolver_method:)` for when GraphQL-Ruby should call a method _other than_ the one whose name matches the field name (#1961). This means that if you're using `method:` to call a different method _on the Schema::Object subclass_, you should update that configuration to `resolver_method:`. (`method:` is still used to call a different method on the _underlying application object_.) - `Int` type now applies boundaries as [described in the spec](https://facebook.github.io/graphql/June2018/#sec-Int) #2101. To preserve the previous, unbounded behavior, handle the error in your schema's `.type_error(err, ctx)` hook, for example: ```ruby class MySchema < GraphQL::Schema def self.type_error(err, ctx) if err.is_a?(GraphQL::IntegerEncodingError) # Preserve the previous unbounded behavior # by returning the out-of-bounds value err.integer_value else super end end end ``` - `field(...)` configurations don't create implicit method definitions (#1961). If one resolver method depended on the implicitly-created method from another field, you'll have to refactor that call or manually add a `def ...` for that field. - Calling `super` in a field method doesn't work anymore (#1961) - Error `"problems"` are now in `"extensions" : { "problems": ... }` #2077 - Change schema default to `error_bubbling false` #2069 ### New Features - Add class-based subscriptions with `GraphQL::Schema::Subscription` #1930 - Add `GraphQL::Execution::Interpreter` (#1394) and `GraphQL::Analysis::AST` (#1824) which together cut GraphQL overhead by half (time and memory) - Add `Schema.unauthorized_field(err)` for when `Field#authorized?` checks fail (#1994) - Add class-based custom directives for the interpreter (#2055) - Add `Schema::FieldExtension` for customizing field execution with class-based fields #1795 - Add `Query#lookahead` for root-level selection info #1931 - Validation errors have `"extensions": { ... }` which includes metadata about that error #1970 ### Bug fixes - Fix list-type arguments passed with a single value #2085 - Support `false` as an Enum value #2050 - Support `hash_key:` fields when the key isn't a valid Ruby method name #2016 ## 1.8.15 (13 Feb 2019) ### Bug fixes - Fix unwrapping inputobject types when turning arguments to hashes #2094 - Support lazy objects from `.resolve_type` hooks #2108 ## 1.8.14 (9 Feb 2019) ### Bug Fixes - Fix single-item list inputs that aren't passed as lists #2095 ## 1.8.13 (4 Jan 2019) ### Bug fixes - Fix regression in block string parsing #2032 ## 1.8.12 (3 Jan 2019) ### Breaking changes - When an input object's argument has a validation error, that error is reported on the _argument_ instead of its parent input object. #2013 ### New features - Add `error_bubbling false` Schema configuration for nicer validation of compound inputs #2013 - Print descriptions as block strings in SDL #2011 - Improve string-to-constant resolution #1810 - Add `Query::Context#to_hash` for splatting #1955 - Add `#dig` to `Schema::InputObject` and `Query::Arguments` #1968 - Add `.*_execution_strategy` methods to class-based schemas #1914 - Accept multiple errors when adding `.rescue_from` handlers #1991 ### Bug fixes - Fix scalar tracing in NewRelic and Skylight #1954 - Fix lexer for multiple block strings #1937 - Add `unscope(:order)` when counting relations #1911 - Improve build-from-definition error message #1998 - Fix regression in legacy compat #2000 ## 1.8.11 (16 Oct 2018) ### New features - `extras: [:lookahead]` injects a `GraphQL::Execution::Lookahead` ### Bug fixes - Fix type printing in Printer #1902 - Rescue `GraphQL::ExecutionError` in `.before_query` hooks #1898 - Properly load default values that are lists of input objects from the IDL #1874 ## 1.8.10 (21 Sep 2018) ### Bug fixes - When using `loads:` with a nullable mutation input field, allow `null` values to be provided. #1851 - When an invalid Base64 encoded cursor is provided, raise a `GraphQL::ExecutionError` instead of `ArgumentError`. #1855 - Fix an issue with `extras: [:path]` would use the field's `path` instead of the `context`. #1859 ### New features - Add scalar type generator `rails g graphql:scalar` #1847 - Add `#dig` method to `Query::Context` #1861 ## 1.8.9 (13 Sep 2018) ### Breaking changes - When `field ... ` is called with a block and the block has one argument, the field is yielded, but `self` inside the block is _not_ changed to the field. #1843 ### New features - `extras: [...]` can inject values from the field instance #1808 - Add `ISO8601DateTime.time_precision` for customization #1845 - Fix input objects with default values of enum #1827 - `Schema.sync_lazy(value)` hook for intercepting lazy-resolved objects #1784 ### Bug fixes - When a field block is provided with an arity of `1`, yield the field #1843 ## 1.8.8 (27 Aug 2018) ### Bug fixes - When using `RelayClassicMutation`, `client_mutation_id` will no longer be passed to `authorized?` method #1771 - Fix issue in schema upgrader script which would cause `.to_non_null_type` calls in type definition to be ignored #1783 - Ensure enum values respond to `graphql_name` #1792 - Fix infinite resolution bug that could occur when an exception not inheriting from `StandardError` is thrown #1804 ### New features - Add `#path` method to schema members #1766 - Add `as:` argument to allow overriding the name of the argument when using `loads:` #1773 - Add support for list of IDs when using `loads:` in an argument definition #1797 ## 1.8.7 (9 Aug 2018) ### Breaking changes - Some mutation authorization hooks added in 1.8.5 were changed, see #1736 and #1737. Roughly: - `before_prepare` was changed to `#ready?` - `validate_*` hooks were replaced with a single `#authorized?` method ### Bug fixes - Argument default values include nested default values #1728 - Clean up duplicate method defs #1739 ### New features - Built-in support for Mongoid 5, 6, 7 #1754 - Mutation `#ready?` and `#authorized?` may halt flow and/or return data #1736, #1737 - Add `.scope_items(items, ctx)` hook for filtering lists - Add `#default_graphql_name` for overriding default logic #1729 - Add `#add_argument` for building schemas #1732 - Cursors are decoded using `urlsafe_decode64` to future-proof for urlsafe cursors #1748 ## 1.8.6 (31 July 2018) ### Breaking changes - Only allow Objects to implement actual Interfaces #1715. Use `include` instead for plain Ruby modules. - Revert extending interface methods onto Objects #1716. If you were taking advantage of this feature, you can create a plain Ruby module with the functionality and include it in both the interface and object. ### Deprecations ### New features - Support string descriptions (from June 2018 GraphQL spec) #1725 - Add some accessors to Schema members #1722 - Yield argument for definition block with arity of one #1714 - Yield field for definition blocks with arity of one #1712 - Support grouping by "endpoint" with skylight instrumentation #1663 - Validation: Don't traverse irep if no handlers are registered #1696 - Add `nodes_field` option to `edge_type` to hide nodes field #1693 - Add `GraphQL::Types::ISO8601DateTime` to documentation #1694 - Conditional Analyzers #1690 - Improve error messages in `ActionCableSubscriptions` #1675 - Add Prometheus tracing #1672 - Add `map` to `InputObject` #1669 ### Bug fixes - Improve the mutation generator #1718 - Fix method inheritance for interfaces #1709 - Fix Interface inheritance chain #1686 - Fix require in `tracing.rb` #1685 - Remove delegate for `FieldResolutionContext#schema` #1682 - Remove duplicated `object_class` method #1667 ## 1.8.5 (10 July 2018) ### Breaking changes - GraphQL validation errors now include `"filename"` if the parsed document had a `filename` #1618 ### Deprecations - `TypeKind#resolves?` is deprecated in favor of `TypeKind#abstract?` #1619 ### New features - Add Mutation loading/authorization system #1609 - Interface `definition_methods` are inherited by object type classes #1635 - include `"filename"` in GraphQL errors if the parsed document has a filename #1618 - Add `Schema::InputObject#empty?` #1651 - require `ISO8601DateTime` by default #1660 - Support `extend` in the parser #1620 - Improve generator to have nicer error handling in development ### Bug fixes - Fix `@skip`/`@include` with default value of `false` #1617 - Fix lists of abstract types with promises #1613 - Don't check the type of `nil` when it's in a list #1610 - Fix NoMethodError when `variables: nil` is passed to `execute(...)` #1661 - Objects returned from `Schema.unauthorized_objects` are properly wrapped by their type proxies #1662 ## 1.8.4 (21 June 2018) ### New features - Add class-based definitions for Relay types #1568 - Add a built-in auth system #1494 ### Bug fixes - Properly rescue coercion errors in variable values #1602 ## 1.8.3 (14 June 2018) ### New features - Add an ISO 8601 DateTime scalar: `Types::ISO8601DateTime`. #1566 - Use classes under the hood for built-in scalars. These are now accessible via `Types::` namespace. #1565 - Add `possible_types` helpers to abstract types #1580 ### Bug fixes - Fix `Language::Visitor` when visiting `InputObjectTypeDefinition` nodes to include child `Directive` nodes. #1584 - Fix an issue preventing proper subclassing of `TimeoutMiddleware`. #1579 - Fix `graphql:interface` generator such that it generates working code. #1577 - Update the description of auto-generated `before` and `after` arguments to better describe their input type. #1572 - Add `Language::Nodes::DirectiveLocation` AST node to represent directive locations in directive definitions. #1564 ## 1.8.2 (6 June 2018) ### Breaking changes - `Schema::InputObject#to_h` recursively transforms hashes to underscorized, symbolized keys. #1555 ### New features - Generators create class-based types #1562 - `Schema::InputObject#to_h` returns a underscorized, symbolized hash #1555 ### Bug fixes - Support `default_mask` in class-based schemas #1563 - Fix null propagation for list types #1558 - Validate unique arguments in queries #1557 - Fix `RelayClassicMutation`s with no arguments #1543 ## 1.8.1 (1 June 2018) ### Breaking changes - When filtering items out of a schema, Unions will now be hidden if their possible types are all hidden or if all fields returning it are hidden. #1515 ### New features - `GraphQL::ExecutionError.new` accepts an `extensions:` option which will be merged into the `"extensions"` key in that error's JSON #1552 ### Bug fixes - When filtering items out of a schema, Unions will now be hidden if their possible types are all hidden or if all fields returning it are hidden. #1515 - Require that fields returning interfaces have selections made on them #1551 - Correctly mark introspection types and fields as `introspection?` #1535 - Remove unused introspection objects #1534 - use `object`/`context` in the upgrader instead of `@object`/`@context` #1529 - (Development) Don't require mongodb for non-mongo tests #1548 - Track position of union member nodes in the parser #1541 ## 1.8.0 (17 May 2018) `1.8.0` has been in prerelease for 6 months. See the prerelease changelog for change-by-change details. Here's a high-level changelog, followed by a detailed list of changes since the last prerelease. ### High-level changes #### Breaking Changes - GraphQL-Ruby is not tested on Ruby 2.1. #1070 Because Ruby 2.1 doesn't garbage collect Symbols, it's possible that GraphQL-Ruby will introduce a OOM vulnerability where unique symbols are dynamically created, for example, turning user input into Symbols. No instances of this are known in GraphQL-Ruby ... yet! - `GraphQL::Delegate`, a duplicate of Ruby's `Forwardable`, was removed. Use `Forwardable` instead, and update your Ruby if you're on `2.4.0`, due to a performance regression in `Forwardable` in that version. - `MySchema.subscriptions.trigger` asserts that its inputs are valid arguments #1400. So if you were previously passing invalid options there, you'll get an error. Remove those options. #### New Features - A new class-based API for schema definition. The old API is completely supported, but the new one is much nicer to use. If you migrate, some schema extensions may require a bit of extra work. - Built-in support for Mongoid-backed Relay connections - `.execute(variables: ...)` and `subscriptions.trigger` both accept Symbol-keyed hashes - Lots of other small things around SDL parsing, tracing, runtime ... everything. Read the details below for a full list. #### Bug Fixes - Many, many bug fixes. See the detailed list if you're curious about specific bugs. ### Changes since `1.8.0.pre11`: #### Breaking Changes - `GraphQL::Schema::Field#initialize`'s signature changed to accept keywords and a block only. `type:`, `description:` and `name:` were moved to keywords. See `Field.from_options` for how the `field(...)` helper's arguments are merged to go to `Field.new`. #1508 #### New Features - `Schema::Resolver` is a replacement for `GraphQL::Function` #1472 - Fix subscriptions with class-based schema #1478 - `Tracing::NewRelicTracing` accepts `set_transaction_name:` to use the GraphQL operation name as the NewRelic transaction name #1430 #### Bug fixes - Backported `accepts_definition`s are inherited #1514 - Fix Schema generator's `resolve_type` method #1481 - Fix constant assignment warnings with interfaces including multiple other interfaces #1465 - InputObject types loaded from SDL have the proper AST node assigned to them #1512 ## 1.8.0.pre11 (3 May 2018) ### Breaking changes - `Schema::Mutation.resolve_mutation` was moved to an instance method; see changes to `Schema::RelayClassicMutation` in #1469 for an example refactor - `GraphQL::Delegate` was removed, use Ruby's `Forwardable` instead (warning: bad performance on Ruby 2.4.0) - `GraphQL::Schema::Interface` is a module, not a class #1372. To refactor, use a base module instead of a base class: ```ruby module BaseInterface include GraphQL::Schema::Interface end ``` And include that in your interface types: ```ruby module Reservable include BaseInterface field :reservations, ... end ``` In object types, no change is required; use `implements` as before: ```ruby class EventVenue < BaseObject implements Reservable end ``` ### New features - `GraphQL::Schema::Interface` is a module - Support `prepare:` and `as:` argument options #1469 - First-class support for Mongoid connections #1452 - More type inspection helpers for class-based types #1446 - Field methods may call `super` to get the default behavior #1437 - `variables:` accepts symbol keys #1401 - Reprint any directives which were parsed from SDL #1417 - Support custom JSON scalars #1398 - Subscription `trigger` accepts symbol, underscored arguments and validates their presence #1400 - Mutations accept a `null(true | false)` setting to affect field nullability #1406 - `RescueMiddleware` uses inheritance to match errors #1393 - Resolvers may return a list of errors #1231 ### Bug fixes - Better error for anonymous class names #1459 - Input Objects correctly inherit arguments #1432 - Fix `.subscriptions` for class-based Schemas #1391 ## 1.8.0.pre10 (4 Apr 2018) ### New features - Add `Schema::Mutation` and `Schema::RelayClassicMutation` base classes #1360 ### Bug fixes - Fix using anonymous classes for field types #1358 ## 1.8.0.pre9 (19 Mar 2018) - New version number. (I needed this because I messed up build tooling for 1.8.0.pre8). ## 1.8.0.pre8 (19 Mar 2018) ### New Features - Backport `accepts_definition` for configurations #1357 - Add `#owner` method to Schema objects - Add `Interface.orphan_types` config for orphan types #1346 - Add `extras: :execution_errors` for `add_error` #1313 - Accept a block to `Schema::Argument#initialize` #1356 ### Bug Fixes - Support `cursor_encoder` #1357 - Don't double-count lazy/eager field time in Tracing #1321 - Fix camelization to support single leading underscore #1315 - Fix `.resolve_type` for Union and Interface classes #1342 - Apply kwargs before block in `Argument.from_dsl` #1350 ## 1.8.0.pre7 (27 Feb 2018) ### New features - Upgrader improvements #1305 - Support `global_id_field` for interfaces #1299 - Add `camelize: false` #1300 - Add readers for `context`, `object` and `arguments` #1283 - Replace `Schema.method_missing` with explicit whitelist #1265 ## 1.8.0.pre6 (1 Feb 2018) ### New features - Custom enum value classes #1264 ### Bug fixes - Properly print SDL type directives #1255 ## 1.8.0.pre5 (1 Feb 2018) ### New features - Upgrade argument access with the upgrader #1251 - Add `Schema#find(str)` for finding schema members by name #1232 ### Bug fixes - Fix `Schema.max_complexity` #1246 - Support cyclical connections/edges #1253 ## 1.8.0.pre4 (18 Jan 2018) ### Breaking changes - `Type.fields`, `Field.arguments`, `Enum.values` and `InputObject.arguments` return a Hash instead of an Array #1222 ### New features - By default, fields try hash keys which match their name, as either a symbol or a string #1225 - `field do ... end` instance_evals on the Field instance, not a FieldProxy #1227 - `[T, null: true]` creates lists with nullable items #1229 - Upgrader improvements #1223 ### Bug fixes - Don't require `parser` unless the upgrader is run #1218 ## 1.8.0.pre3 (12 Jan 2018) ### New Features - Custom `Context` classes for class-based schemas #1161 - Custom introspection for class-based schemas #1170 - Improvements to upgrader tasks and internals #1151, #1178, #1212 - Allow description inside field blocks #1175 ## 1.8.0.pre2 (29 Nov 2017) ### New Features - Add `rake graphql:upgrade[app/graphql]` for automatic upgrade #1110 - Automatically camelize field names and argument names #1143, #1126 - Improved error message when defining `name` instead of `graphql_name` #1104 ### Bug fixes - Fix list wrapping when value is `nil` #1117 - Fix ArgumentError typo #1098 ## 1.8.0.pre1 (14 Nov 2017) ### Breaking changes - Stop official support for Ruby 2.1 #1070 ### New features - Add class-based schema definition API #1037 ## 1.7.14 (4 Apr 2018) ### New features - Support new IDL spec for `&` for interfaces #1304 - Schema members built from IDL have an `#ast_node` #1367 ### Bug fixes - Fix paging backwards with `hasNextPage` #1319 - Add hint for `orphan_types` in error message #1380 - Use an empty hash for `result` when a query has unhandled errors #1382 ## 1.7.13 (28 Feb 2018) ### Bug fixes - `Schema#as_json` returns a hash, not a `GraphQL::Query::Result` #1288 ## 1.7.12 (13 Feb 2018) ### Bug fixes - `typed_children` should always return a Hash #1278 ## 1.7.11 (13 Feb 2018) ### Bug fixes - Fix compatibility of `irep_node.typed_children` on leaf nodes #1277 ## 1.7.10 (13 Feb 2018) ### Breaking Changes - Empty selections (`{ }`) are invalid in the GraphQL spec, but were previously allowed by graphql-ruby. They now return a parse error. #1268 ### Bug fixes - Fix error when inline fragments are spread on scalars #1268 - Fix printing SDL when types have interfaces and directives #1255 ## 1.7.9 (1 Feb 2018) ## New Features - Support block string inputs #1219 ## Bug fixes - Fix deprecation regression in schema printer #1250 - Fix resource names in DataDog tracing #1208 - Fix passing `context` to multiplex in `Query#result` #1200 ## 1.7.8 (11 Jan 2018) ### New features - Refactor `Schema::Printer` to use `Language::Printer` #1159 - Add `ArgumentValue#default_used?` and `Arguments#default_used?` #1152 ### Bug fixes - Fix Scout Tracing #1187 - Call `#inspect` for `EnumType::UnresolvedValueError` #1179 - Parse empty field sets in IDL parser #1145 ## 1.7.7 (29 Nov 2017) ### New features - `Schema#to_document` returns a `Language::Nodes::Document` #1134 - Add `trace_scalars` and `trace: true|false` to monitoring #1103 - Add `Tracing::DataDogPlatform` monitoring #1129 - Support namespaces in `rails g graphql:function` and `:loader` #1127 - Support `serializer:` option for `ActionCableSubscriptions` #1085 ### Bug fixes - Properly count the column after a closing quote #1136 - Fix default value input objects in `Schema.from_definition` #1135 - Fix `rails destroy graphql:mutation` #1119 - Avoid unneeded query in RelationConnection with Sequel #1101 - Improve & document instrumentation stack behavior #1101 ## 1.7.6 (13 Nov 2017) ### Bug fixes - Serialize symbols in with `GraphQL::Subscriptions::Serialize` #1084 ## 1.7.5 (7 Nov 2017) ### Breaking changes - Rename `Backtrace::InspectResult#inspect` to `#inspect_result` #1022 ### New features - Improved website search with Algolia #934 - Support customized generator directory #1047 - Recursively serialize `GlobalID`-compliant objects in Arrays and hashes #1030 - Add `Subscriptions#build_id` helper #1046 - Add `#non_null?` and `#list?` helper methods to type objects #1054 ### Bug fixes - Fix infinite loop in query instrumentation when error is raised #1074 - Don't try to trace error when it's not raised during execution - Improve validation of query variable definitions #1073 - Fix Scout tracing module load order #1064 ## 1.7.4 (9 Oct 2017) ### Deprecations - `GraphQL::Tracing.install` is deprecated, use schema-local or query-local tracers instead #996 ### New features - Add monitoring plugins for AppSignal, New Relic, Scout and Skylight #994, #1013 - Custom coercion errors for custom scalars #988 - Extra `options` for `GraphQL::ExecutionError` #1002 - Use `GlobalID` for subscription serialization when available #1004 - Schema- and query-local, threadsafe tracers #996 ### Bug fixes - Accept symbol-keyed arguments to `.trigger` #1009 ## 1.7.3 (20 Sept 2017) ### Bug fixes - Fix arguments on `Query.__type` field #978 - Fix `Relay::Edge` objects in `Backtrace` tables #975 ## 1.7.2 (20 Sept 2017) ### Bug fixes - Correctly skip connections that return `ctx.skip` #972 ## 1.7.1 (18 Sept 2017) ### Bug fixes - Properly release changes from 1.7.0 ## 1.7.0 (18 Sept 2017) ### Breaking changes - `GraphQL::Result` is the returned from GraphQL execution. #898 `Schema#execute` and `Query#result` both return a `GraphQL::Result`. It implements Hash-like methods to preserve compatibility. ### New features - `puts ctx.backtrace` prints out a GraphQL backtrace table #946 - `GraphQL::Backtrace.enable` wraps unhandled errors with GraphQL backtraces #946 - `GraphQL::Relay::ConnectionType.bidrectional_pagination = true` turns on _true_ bi-directional pagination checks for `hasNextPage`/`hasPreviousPage` fields. This will become the default behavior in a future version. #960 - Field arguments may be accessed as methods on the `args` object. This is an alternative to `#[]` syntax which provides did-you-mean behavior instead of returning `nil` on a typo. #924 For example: ```ruby # using hash syntax: args[:limit] # => 10 args[:limittt] # => nil # using method syntax: args.limit # => 10 args.limittt # => NoMethodError ``` The old syntax is _not_ deprecated. - Improvements to schema filters #919 - If a type is not referenced by anything, it's hidden - If a type is an abstract type, but has no visible members, it's hidden - `GraphQL::Argument.define` builds re-usable arguments #948 - `GraphQL::Subscriptions` provides hooks for subscription platforms #672 - `GraphQL::Subscriptions::ActionCableSubscriptions` implements subscriptions over ActionCable #672 - More runtime values are accessible from a `ctx` object #923 : - `ctx.parent` returns the `ctx` from the parent field - `ctx.object` returns the current `obj` for that field - `ctx.value` returns the resolved GraphQL value for that field These can be used together, for example, `ctx.parent.object` to get the parent object. - `GraphQL::Tracing` provides more hooks into gem internals for performance monitoring #917 - `GraphQL::Result` provides access to the original `query` and `context` after executing a query #898 ### Bug fixes - Prevent passing _both_ query string and parsed document to `Schema#execute` #957 - Prevent invalid names for types #947 ## 1.6.8 (8 Sept 2017) ### Breaking changes - Validate against EnumType value names to match `/^[_a-zA-Z][_a-zA-Z0-9]*$/` #915 ### New features - Use stdlib `forwardable` when it's not Ruby 2.4.0 #926 - Improve `UnresolvedTypeError` message #928 - Add a default field to the Rails generated mutation type #922 ### Bug fixes - Find types via directive arguments when traversing the schema #944 - Assign `#connection?` when building a schema from IDL #941 - Initialize `@edge_class` to `nil` #942 - Disallow invalid enum values #915 - Disallow doubly-nested non-null types #916 - Fix `Query#selected_operation_name` when no selections are present #899 - Fix needless `COUNT` query for `hasNextPage` #906 - Fix negative offset with `last` argument #907 - Fix line/col for `ArgumentsAreDefined` validation #890 - Fix Sequel error when limit is `0` #892 ## 1.6.7 (11 Aug 2017) ### New features - Add `GraphQL.parse_file` and `AbstractNode#filename` #873 - Support `.graphql` filepaths with `Schema.from_definition` #872 ### Bug fixes - Fix variable usage inside non-null list #888 - Fix unqualified usage of ActiveRecord::Relation #885 - Fix `FieldsWillMerge` handling of equivalent input objects - Fix to call `prepare:` on nested input types ## 1.6.6 (14 Jul 2017) ### New features - Validate `graphql-pro` downloads with `rake graphql:pro:validate[$VERSION]` #846 ### Bug fixes - Remove usage of Rails-only `Array.wrap` #840 - Fix `RelationConnection` to count properly when relation contains an alias #838 - Print name of Enum type when a duplicate value is added #843 ## 1.6.5 (13 Jul 2017) ### Breaking changes - `Schema#types[](type_name)` returns `nil` when there's no type named `type_name` (it used to raise `RuntimeError`). To get an error for missing types, use `.fetch` instead, for example: ```ruby # Old way: MySchema.types[type_name] # => may raise RuntimeError # New way: MySchema.types.fetch(type_name) # => may raise KeyError ``` - Schema build steps happen in one pass instead of two passes #819 . This means that `instrument(:field)` hooks may not access `Schema#types`, `Schema#possible_types` or `Schema#get_field`, since the underlying data hasn't been prepared yet. There's not really a clear upgrade path here. It's a bit of a mess. If you're affected by this, feel free to open an issue and we'll try to find something that works! ### Deprecations - `Schema#resolve_type` is now called with `(abstract_type, obj, ctx)` instead of `(obj, ctx)` #834 . To update, add an unused parameter to the beginning of your `resolve_type` hook: ```ruby MySchema = GraphQL::Schema.define do # Old way: resolve_type ->(obj, ctx) { ... } # New way: resolve_type ->(type, obj, ctx) { ... } end ``` ### New features - `rails g graphql:mutation` will add Mutation boilerplate if it wasn't added already #812 - `InterfaceType` and `UnionType` both accept `resolve_type ->(obj, ctx) { ... }` functions for type-specific resolution. This function takes precedence over `Schema#resolve_type` #829 #834 - `Schema#resolve_type` is called with three arguments, `(abstract_type, obj, ctx)`, so you can distinguish object type based on interface or union. - `Query#operation_name=` may be assigned during query instrumentation #833 - `query.context.add_error(err)` may be used to add query-level errors #833 ### Bug fixes - `argument(...)` DSL accepts custom keywords #809 - Use single-query `max_complexity` overrides #812 - Return a client error when `InputObjectType` receives an array as input #803 - Properly handle raised errors in `prepare` functions #805 - Fix using `as` and `prepare` in `argument do ... end` blocks #817 - When types are added to the schema with `instrument(:field, ...)`, make sure they're in `Schema#types` #819 - Raise an error when duplicate `EnumValue` is created #831 - Properly resolve all query levels breadth-first when using `lazy_resolve` #835 - Fix tests to run on PostgresQL; Run CI on PostgresQL #814 - When no query string is present, return a client error instead of raising `ArgumentError` #833 - Properly validate lists containing variables #824 ## 1.6.4 (20 Jun 2017) ### New features - `Schema.to_definition` sorts fields and arguments alphabetically #775 - `validate: false` skips static validations in query execution #790 ### Bug fixes - `graphql:install` adds `operation_name: params[:operationName]` #786 - `graphql:install` skips `graphiql-rails` for API-only apps #772 - `SerialExecution` calls `.is_a?(Skip)` to avoid user-defined `#==` methods #794 - `prepare:` functions which return `ExecutionError` are properly handled when default values are present #801 ## 1.6.3 (7 Jun 2017) ### Bug fixes - Run multiplex instrumentation when running a single query with a legacy execution strategy #766 - Check _each_ strategy when looking for overridden execution strategy #765 - Correctly wrap `Method`s with BackwardsCompatibility #763 - Various performance improvements #764 - Don't call `#==(other)` on user-provided objects (use `.is_a?` instead) #761 - Support lazy object from custom connection `#edge_nodes` #762 - If a lazy field returns an invalid null, stop evaluating its siblings #767 ## 1.6.2 (2 Jun 2017) ### New features - `Schema.define { default_max_page_size(...) }` provides a Connection `max_page_size` when no other is provided #752 - `Schema#get_field(type, field)` accepts a string type name #756 - `Schema.define { rescue_from(...) }` accepts multiple error classes for the handler #758 ### Bug fixes - Use `*_execution_strategy` when executing a single query (doesn't support `Schema#multiplex`) #755 - Fix NameError when `ActiveRecord` isn't loaded #747 - Fix `Query#mutation?` etc to support lazily-loaded AST #754 ## 1.6.1 (28 May 2017) ### New Features - `Query#selected_operation_name` returns the operation to execute, even if it was inferred (not provided as `operation_name:`) #746 ### Bug fixes - Return `nil` from `Query#operation_name` if no `operation_name:` was provided #746 ## 1.6.0 (27 May 2017) ### Breaking changes - `InternalRepresentation::Node#return_type` will now return the wrapping type. Use `return_type.unwrap` to access the old value #704 - `instrument(:query, ...)` instrumenters are applied as a stack instead of a queue #735. If you depend on queue-based behavior, move your `before_query` and `after_query` hooks to separate instrumenters. - In a `Relay::Mutation`, Raising or returning a `GraphQL::Execution` will nullify the mutation field, not the field's children. #731 - `args.to_h` returns a slightly different hash #714 - keys are always `String`s - if an argument is aliased with `as:`, the alias is used as the key - `InternalRepresentation::Node#return_type` includes the original "wrapper" types (non-null or list types), call `.unwrap` to get the inner type #20 ```ruby # before irep_node.return_type # after irep_node.return_type.unwrap ``` ### Deprecations - Argument `prepare` functions which take one argument are deprecated #730 ```ruby # before argument :id, !types.ID, prepare: ->(val) { ... } # after argument :id, !types.ID, prepare: ->(val, ctx) { ... } ``` ### New features - `Schema#multiplex(queries)` runs multiple queries concurrently #691 - `GraphQL::RakeTask` supports dumping the schema to IDL or JSON #687 - Improved support for `Schema.from_definition` #699 : - Custom scalars are supported with `coerce_input` and `coerce_result` functions - `resolve_type` function will be used for abstract types - Default resolve behavior is to check `obj` for a method and call it with 0, 1, or 2 arguments. - `ctx.skip` may be returned from field resolve functions to exclude the field from the response entirely #688 - `instrument(:field, ..., after_built_ins: true)` to apply field instrumentation after Relay wrappers #740 - Argument `prepare` functions are invoked with `(val, ctx)` (previously, it was only `(val)`) #730 - `args.to_h` returns stringified, aliased arguments #714 - `ctx.namespace(:my_namespace)` provides namespaced key-value storage #689 - `GraphQL::Query` can be initialized without a query_string; it can be added after initialization #710 - Improved filter support #713 - `Schema.execute(only:, except:)` accept a callable _or_ an array of callables (multiple filters) - Filters can be added to a query via `Query#merge_filters(only:, except:)`. You can add a filter to every query by merging it in during query instrumentation. ### Bug fixes - Correctly apply cursors and `max_page_size` in `Relay::RelationConnection` and `Relay::ArrayConnection` #728 - Nullify a mutation field when it raises or returns an error #731 ## 1.5.14 (27 May 2017) ### New features - `UniqueWithinType` Relay ID generator supports `-` in the ID #742 - `assign_metadata_key` assigns `true` when the definition method is called without arguments #724 - Improved lexer performance #737 ### Bug fixes - Assign proper `parent` when a `connection` resolve returns a promise #736 ## 1.5.13 (11 May 2017) - Fix raising `ExecutionError` inside mutation resolve functions (it nullifies the field) #722 ## 1.5.12 (9 May 2017) - Fix returning `nil` from connection resolve functions (now they become `null`) #719 - Fix duplicate AST nodes when merging fragments #721 ## 1.5.11 (8 May 2017) ### New features - `Schema.from_definition` accepts a `parser:` option (to work around lack of schema parser in `graphql-libgraphqlparser`) #712 - `Query#internal_representation` exposes an `InternalRepresentation::Document` #701 - Update generator usage of `graphql-batch` #697 ### Bug fixes - Handle fragments with the same name as operations #706 - Fix type generator: ensure type name is camelized #718 - Fix `Query#operation_name` to return the operation name #707 - Fix pretty-print of non-null & list types #705 - Fix single input objects passed to list-type arguments #716 ## 1.5.10 (25 Apr 2017) ### New features - Support Rails 5.1 #693 - Fall back to `String#encode` for non-UTF-8/non-ASCII strings #676 ### Bug Fixes - Correctly apply `Relay::Mutation`'s `return_field ... property:` argument #692 - Handle Rails 5.1's `ActionController::Parameters` #693 ## 1.5.9 (19 Apr 2017) ### Bug Fixes - Include instrumentation-related changes in introspection result #681 ## 1.5.8 (18 Apr 2017) ### New features - Use Relay PageInfo descriptions from graphql-js #673 ### Bug Fixes - Allow fields with different arguments when fragments are included within inline fragments of non-overlapping types #680 - Run `lazy_resolve` instrumentation for `connection` fields #679 ## 1.5.7 (14 Apr 2017) ### Bug fixes - `InternalRepresentation::Node#definition` returns `nil` instead of raising NoMethodError for operation fields #675 - `Field#function` is properly populated for fields derived from `GraphQL::Function`s #674 ## 1.5.6 (9 Apr 2017) ## Breaking Changes - Returned strings which aren't encoded as UTF-8 or ASCII will raise `GraphQL::StringEncodingError` instead of becoming `nil` #661 To preserve the previous behavior, Implement `Schema#type_error` to return `nil` for this error, eg: ```ruby GraphQL::Schema.define do type_error ->(err, ctx) { case err # ... when GraphQL::StringEncodingError nil end } ``` - `coerce_non_null_input` and `validate_non_null_input` are private #667 ## Deprecations - One-argument `coerce_input` and `coerce_result` functions for custom scalars are deprecated. #667 Those functions now accept a second argument, `ctx`. ```ruby # From ->(val) { val.to_i } # To: ->(val, ctx) { val.to_i } ``` - Calling `coerce_result`, `coerce_input`, `valid_input?` or `validate_input` without a `ctx` is deprecated. #667 Use `coerce_isolated_result` `coerce_isolated_input`, `valid_isolated_input?`, `validate_input` to explicitly bypass `ctx`. ## New Features - Include `#types` in `GraphQL::Function` #654 - Accept `prepare:` function for arguments #646 - Scalar coerce functions receive `ctx` #667 ## Bug Fixes - Properly apply default values of `false` #658 - Fix application of argument options in `GraphQL::Relay::Mutation` #660 - Support concurrent-ruby `>1.0.0` #663 - Only raise schema validation errors on `#execute` to avoid messing with Rails constant loading #665 ## 1.5.5 (31 Mar 2017) ### Bug Fixes - Improve threadsafety of `lazy_resolve` cache, use `Concurrent::Map` if it's available #631 - Properly handle unexpeced input objects #638 - Handle errors during definition by preseriving the definition #632 - Fix `nil` input for nullable list types #637, #639 - Handle invalid schema IDL with a validation error #647 - Properly serialize input object default values #635 - Fix `as:` on mutation `input_field` #650 - Fix null propagation for `nil` members of non-null list types #649 ## 1.5.4 (22 Mar 2017) ### Breaking Changes - Stop supporting deprecated one-argument schema masks #616 ### Bug Fixes - Return a client error for unknown variable types when default value is provided or when directives are present #627 - Fix validation performance regression on nested abstract fragment conditions #622, #624 - Put back `InternalRepresentation::Node#parent` and fix it for fragment fields #621 - Ensure enum names are strings #619 ## 1.5.3 (20 Mar 2017) ### Bug Fixes - Fix infinite loop triggered by user input. #620 This query would cause an infinite loop: ```graphql query { ...frag } fragment frag on Query { __typename } fragment frag on Query { ...frag } ``` - Validate fragment name uniqueness #618 ## 1.5.2 (16 Mar 2017) ### Breaking Changes - Parse errors are no longer raised to the application. #607 Instead, they're returned to the client in the `"errors"` key. To preserve the previous behavior, you can implement `Schema#parse_error` to raise the error: ```ruby MySchema = GraphQL::Schema.define do # ... parse_error ->(err, ctx) { raise(err) } end ``` ### New Features - Add `graphq:enum` generator #611 - Parse errors are returned to the client instead of raised #607 ### Bug Fixes - Handle negative cursor pagination args as `0` #612 - Properly handle returned `GraphQL::ExecutionError`s from connection resolves #610 - Properly handle invalid nulls in lazy scalar fields #609 - Properly handle invalid input objects passed to enum arguments #604 - Fix introspection response of enum default values #605 - Allow `Schema.from_definition` default resolver hashes to have defaults #608 ## 1.5.1 (12 Mar 2017) ### Bug fixes - Fix rewrite performance regressions from 1.5.0 #599 - Remove unused `GraphQL::Execution::Lazy` initialization API #597 ## 1.5.0 (10 Mar 2017), yanked ### Breaking changes - _Only_ UTF-8-encoded strings will be returned by `String` fields. Strings with other encodings (or objects whose `#to_s` method returns a string with a different encoding) will return `nil` instead of that string. #517 To opt into the _previous_ behavior, you can modify `GraphQL::STRING_TYPE`: ```ruby # app/graphql/my_schema.rb # Restore previous string behavior: GraphQL::STRING_TYPE.coerce_result = ->(value) { value.to_s } MySchema = GraphQL::Schema.define { ... } ``` - Substantial changes to the internal query representation (#512, #536). Query analyzers may notice some changes: - Nodes skipped by directives are not visited - Nodes are always on object types, so `Node#owner_type` always returns an object type. (Interfaces and Unions are replaced with concrete object types which are valid in the current scope.) See [changes to `Analysis::QueryComplexity`](https://github.com/rmosolgo/graphql-ruby/compare/v1.4.5...v1.5.0#diff-8ff2cdf0fec46dfaab02363664d0d201) for an example migration. Here are some other specific changes: - Nodes are tracked on object types only, not interface or union types - Deprecated, buggy `Node#children` and `Node#path` were removed - Buggy `#included` was removed - Nodes excluded by directives are entirely absent from the rewritten tree - Internal `InternalRepresentation::Selection` was removed (no longer needed) - `Node#spreads` was replaced by `Node#ast_spreads` which returns a Set ### New features - `Schema#validate` returns a list of errors for a query string #513 - `implements ...` adds interfaces to object types _without_ inherit-by-default #548, #574 - `GraphQL::Relay::RangeAdd` for implementing `RANGE_ADD` mutations #587 - `use ...` definition method for plugins #565 - Rails generators #521, #580 - `GraphQL::Function` for reusable resolve behavior with arguments & return type #545 - Support for Ruby 2.4 #475 - Relay `node` & `nodes` field can be extended with a custom block #552 - Performance improvements: - Resolve fragments only once when validating #504 - Reuse `Arguments` objects #500 - Skip needless `FieldResult`s #482 - Remove overhead from `ensure_defined` #483 - Benchmark & Profile tasks for gem maintenance #520, #579 - Fetch `has_next_page` while fetching items in `RelationConnection` #556 - Merge selections on concrete object types ahead of time #512 - Support runnable schemas with `Schema.from_definition` #567, #584 ### Bug fixes - Support different arguments on non-overlapping typed fragments #512 - Don't include children of `@skip`ped nodes when parallel branches are not skipped #536 - Fix offset in ArrayConnection when it's larger than the array #571 - Add missing `frozen_string_literal` comments #589 ## 1.4.5 (6 Mar 2017) ### Bug Fixes - When an operation name is provided but no such operation is present, return an error (instead of executing the first operation) #563 - Require unique operation names #563 - Require selections on root type #563 - If a non-null field returns `null`, don't resolve any more sibling fields. #575 ## 1.4.4 (17 Feb 2017) ### New features - `Relay::Node.field` and `Relay::Node.plural_field` accept a custom `resolve:` argument #550 - `Relay::BaseConnection#context` provides access to the query context #537 - Allow re-assigning `Field#name` #541 - Support `return_interfaces` on `Relay::Mutation`s #533 - `BaseType#to_definition` stringifies the type to IDL #539 - `argument ... as:` can be used to alias an argument inside the resolve function #542 ### Bug fixes - Fix negative offset from cursors on PostgresQL #510 - Fix circular dependency issue on `.connection_type`s #535 - Better error when `Relay::Mutation.resolve` doesn't return a Hash ## 1.4.3 (8 Feb 2017) ### New features - `GraphQL::Relay::Node.plural_field` finds multiple nodes by UUID #525 ### Bug fixes - Properly handle errors from lazy mutation results #528 - Encode all parsed strings as UTF-8 #516 - Improve error messages #501 #519 ## 1.4.2 (23 Jan 2017) ### Bug fixes - Absent variables aren't present in `args` (_again_!) #494 - Ensure definitions were executed when accessing `Field#resolve_proc` #502 (This could have caused errors when multiple instrumenters modified the same field in the schema.) ## 1.4.1 (16 Jan 2017) ### Bug fixes - Absent variables aren't present in `args` #479 - Fix grouped ActiveRecord relation with `last` only #476 - `Schema#default_mask` & query `only:`/`except:` are combined, not overridden #485 - Root types can be hidden with dynamic filters #480 ## 1.4.0 (8 Jan 2017) ### Breaking changes ### Deprecations - One-argument schema filters are deprecated. Schema filters are now called with _two_ arguments, `(member, ctx)`. #463 To update, add a second argument to your schema filter. - The arity of middleware `#call` methods has changed. Instead of `next_middleware` being the last argument, it is passed as a block. To update, call `yield` to continue the middleware chain or use `&next_middleware` to capture `next_middleware` into a local variable. ```ruby # Previous: def call(*args, next_middleware) next_middleware.call end # Current def call(*args) yield end # Or def call(*args, &next_middleware) next_middleware.call end ``` ### New features - You can add a `nodes` field directly to a connection. #451 That way you can say `{ friends { nodes } }` instead of `{ freinds { edges { node } } }`. Either pass `nodes_field: true` when defining a custom connection type, for example: ```ruby FriendsConnectionType = FriendType.define_connection(nodes_field: true) ``` Or, set `GraphQL::Relay::ConnectionType.default_nodes_field = true` before defining your schema, for example: ```ruby GraphQL::Relay::ConnectionType.default_nodes_field = true MySchema = GraphQL::Schema.define { ... } ``` - Middleware performance was dramatically improved by reducing object allocations. #462 `next_middleware` is now passed as a block. In general, [`yield` is faster than calling a captured block](https://github.com/JuanitoFatas/fast-ruby#proccall-and-block-arguments-vs-yieldcode). - Improve error messages for wrongly-typed variable values #423 - Cache the value of `resolve_type` per object per query #462 - Pass `ctx` to schema filters #463 - Accept whitelist schema filters as `only:` #463 - Add `Schema#to_definition` which accepts `only:/except:` to filter the schema when printing #463 - Add `Schema#default_mask` as a default `except:` filter #463 - Add reflection methods to types #473 - `#introspection?` marks built-in introspection types - `#default_scalar?` marks built-in scalars - `#default_relay?` marks built-in Relay types - `#default_directive?` marks built-in directives ### Bug fixes - Fix ArrayConnection: gracefully handle out-of-bounds cursors #452 - Fix ArrayConnection & RelationConnection: properly handle `last` without `before` #362 ## 1.3.0 (8 Dec 2016) ### Deprecations - As per the spec, `__` prefix is reserved for built-in names only. This is currently deprecated and will be invalid in a future version. #427, #450 ### New features - `Schema#lazy_resolve` allows you to define handlers for a second pass of resolution #386 - `Field#lazy_resolve` can be instrumented to track lazy resolution #429 - `Schema#type_error` allows you to handle `InvalidNullError`s and `UnresolvedTypeErrors` in your own way #416 - `Schema#cursor_encoder` can be specified for transforming cursors from built-in Connection implementations #345 - Schema members `#dup` correctly: they shallowly copy their state into new instances #444 - `Query#provided_variables` is now public #430 ### Bug fixes - Schemas created from JSON or strings with custom scalars can validate queries (although they still can't check if inputs are valid for those custom scalars) #445 - Always use `quirks_mode: true` when serializing values (to support non-stdlib `JSON`s) #449 - Calling `#redefine` on a Schema member copies state outside of previous `#define` blocks (uses `#dup`) #444 ## 1.2.6 (1 Dec 2016) ### Bug fixes - Preserve connection behaviors after `redefine` #421 - Implement `respond_to_missing?` on `DefinedObjectProxy` (which is `self` inside `.define { ... }`) #414 ## 1.2.5 (22 Nov 2016) ### Breaking changes - `Visitor` received some breaking changes, though these are largely-private APIs (#401): - Global visitor hooks (`Visitor#enter` and `Visitor#leave`) have been removed - Returning `SKIP` from a visitor hook no longer skips sibling nodes ### New features - `Schema#instrument` may be called outside of `Schema.define` #399 - Validation: assert that directives on a node are unique #409 - `instrument(:query)` hooks are executed even if the query raises an error #412 ### Bug fixes - `Mutation#input_fields` should trigger lazy definition #392 - `ObjectType#connection` doesn't modify the provided `GraphQL::Field` #411 - `Mutation#resolve` may return a `GraphQL::ExecutionError` #405 - `Arguments` can handle nullable arguments passed as `nil` #410 ## 1.2.4 (14 Nov 2016) ### Bug fixes - For invalid enum values, print the enum name in the error message (not a Ruby object dump) #403 - Improve detection of invalid UTF-8 escapes #394 ## 1.2.3 (14 Nov 2016) ### Bug fixes - `Lexer` previous token should be a local variable, not a method attribute #396 - `Arguments` should wrap values according to their type, not their value #398 ## 1.2.2 (7 Nov 2016) ### New features - `Schema.execute` raises an error if `variables:` is a string ### Bug fixes - Dynamic fields `__schema`, `__type` and `__typename` are properly validated #391 ## 1.2.1 (7 Nov 2016) ### Bug fixes - Implement `Query::Context#strategy` and `FieldResolutionContext#strategy` to support GraphQL::Batch #382 ## 1.2.0 (7 Nov 2016) ### Breaking changes - A breaking change from 1.1.0 was reverted: two-character `"\\u"` _is_ longer treated as the Unicode escape character #372 - Due to the execution bug described below, the internal representation of a query has changed. Although `Node` responds to the same methods, tree is built differently and query analyzers visit it differently. #373, #379 The difference is in cases like this: ```graphql outer { ... on A { inner1 { inner2 } } ... on B { inner1 { inner3 } } } ``` Previously, visits would be: - `outer`, which has one child: - `inner1`, which has two definitions (one on `A`, another on `B`), then visit its two `children`: - `inner2` which has one definition (on the return type of `inner1`) - `inner3` which has one definition (on the return type of `inner1`) This can be wrong for some cases. For example, if `A` and `B` are mutually exclusive (both object types, or union types with no shared members), then `inner2` and `inner3` will never be executed together. Now, the visit goes like this: - `outer` which has two entries in `typed_children`, one on `A` and another on `B`. Visit each `typed_chidren` branch: - `inner1`, then its one `typed_children` branch: - `inner2` - `inner1`, then its one `typed_children` branch: - `inner3` As you can see, we visit `inner1` twice, once for each type condition. `inner2` and `inner3` are no longer visited as siblings. Instead they're visited as ... cousins? (They share a grandparent, not a parent.) Although `Node#children` is still present, it may not contain all children actually resolved at runtime, since multiple `typed_children` branches could apply to the same runtime type (eg, two branches on interface types can apply to the same object type). To track all children, you have to do some bookkeeping during visitation, see `QueryComplexity` for an example. You can see PR #373 for how built-in analyzers were changed to reflect this. ### Deprecations - `InternalRepresentation::Node#children` and `InternalRepresentation::Node#definitions` are deprecated due to the bug described below and the breaking change described above. Instead, use `InternalRepresentation::Node#typed_children` and `InternalRepresentation::Node#definition`. #373 ### New features - `null` support for the whole library: as a query literal, variable value, and argument default value. To check for the presence of a nullable, use `Arguments#key?` #369 - `GraphQL::Schema::UniqueWithinType.default_id_separator` may be assigned to a custom value #381 - `Context#add_error(err)` may be used to add a `GraphQL::ExecutionError` to the response's `"errors"` key (and the resolve function can still return a value) #367 - The third argument of `resolve` is now a `FieldResolutionContext`, which behaves just like a `Query::Context`, except that it is not modified during query execution. This means you can capture a reference to that context and access some field-level details after the fact: `#path`, `#ast_node`, `#irep_node`. (Other methods are delegated to the underlying `Query::Context`) #379 - `TimeoutMiddleware`'s second argument is a _proxied_ query object: it's `#context` method returns the `FieldResolutionContext` (see above) for the timed-out field. Other methods are delegated to the underlying `Query` #379 ### Bug fixes - Fix deep selection merging on divergently-typed fragments. #370, #373, #379 Previously, nested selections on different fragments were not distinguished. Consider a case like this: ```graphql ... on A { inner1 { inner2 } } ... on B { inner1 { inner3 } } ``` Previously, an object of type `A` would resolve `inner1`, then the result would receive _both_ `inner2` and `inner3`. The same was true for an object of type `B`. Now, those are properly distinguished. An object of type `A` resolves `inner1`, then its result receives `inner2`. An object of type `B` receives `inner1`, then `inner3`. ## 1.1.0 (1 Nov 2016) ### Breaking changes - Two-character `"\\u"` is no longer treated as the Unicode escape character, only the Unicode escape character `"\u"` is treated that way. (This behavior was a bug, the migration path is to use the Unicode escape character.) #366 - `GraphQL::Language::ParserTests` was removed, use `GraphQL::Compatibility` instead. #366 - Non-null arguments can't be defined with default values, because those values would never be used #361 ### New features - `Schema.from_definition(definition_string)` builds a `GraphQL::Schema` out of a schema definition. #346 - Schema members (types, fields, arguments, enum values) can be hidden on a per-query basis with the `except:` option #300 - `GraphQL::Compatibility` contains `.build_suite` functions for testing user-provided parsers and execution strategies with GraphQL internals. #366 - Schema members respond to `#redefine { ... }` for making shallow copies with extended definitions. #357 - `Schema#instrument` provides an avenue for observing query and field resolution with no overhead. - Some `SerialExecution` objects were converted to functions, resulting in a modest performance improvement for query resolution. ### Bug fixes - `NonNullType` and `ListType` have no name (`nil`), as per the spec #355 - Non-null arguments can't be defined with default values, because those values would never be used #361 ## 1.0.0 (25 Oct 2016) ### Breaking changes - `validate: false` option removed from `Schema.execute` (it didn't work anyways) #338 - Some deprecated methods were removed: #349 - `BaseConnection#object` was removed, use `BaseConnection#nodes` - `BaseConnection.connection_for_items` was removed, use `BaseConnection#connection_for_nodes` - Two-argument resolve functions for `Relay::Mutation`s are not supported, use three arguments instead: `(root_obj, input, ctx)` - `Schema.new` no longer accepts initialization options, use `Schema.define` instead - `GraphQL::ObjectType::UnresolvedTypeError` was removed, use `GraphQL::UnresolvedTypeError` instead - Fragment type conditions should be parsed as `TypeName` nodes, not strings. (Users of `graphql-libgraphqlparser` should update to `1.0.0` of that gem.) #342 ### New Features - Set `ast_node` and `irep_node` on query context before sending it to middleware #348 - Enum values can be extended with `.define` #341 ### Bug Fixes - Use `RelationConnection` for Rails 3 relations (which also extend `Array`) #343 - Fix schema printout when arguments have comments #335 ## 0.19.4 (18 Oct 2016) ### Breaking changes - `Relay::BaseConnection#order` was removed (it always returned `nil`) #313 - In the IDL, Interface names & Union members are parsed as `TypeName` nodes instead of Strings #322 ### New features - Print and parse descriptions in the IDL #305 - Schema roots from IDL are omitted when their names match convention #320 - Don't add `rescue_middleware` to a schema if it's not using `rescue_from` #328 - `Query::Arguments#each_value` yields `Query::Argument::ArgumentValue` instances which contain key, value and argument definition #331 ### Bug fixes - Use `JSON.generate(val, quirks_mode: true)` for compatibility with other JSON implementations #316 - Improvements for compatibility with 1.9.3 branch #315 #314 #313 - Raise a descriptive error when calculating a `cursor` for a node which isn't present in the connection's members #327 ## 0.19.3 (13 Oct 2016) ### Breaking Changes - `GraphQL::Query::Arguments.new` requires `argument_definitions:` of type `{String => GraphQL::Argument }` #304 ### Deprecations - `Relay::Mutation#resolve` has a new signature. #301 Previously, it was called with two arguments: ```ruby resolve ->(inputs, ctx) { ... } ``` Now, it's called with three inputs: ```ruby resolve ->(obj, inputs, ctx) { ... } ``` `obj` is the value of `root_value:` given to `Schema#execute`, as with other root-level fields. Two-argument resolvers are still supported, but they are deprecated and will be removed in a future version. ### New features - `Relay::Mutation` accepts a user-defined `return_type` #310 - `Relay::Mutation#resolve` receives the `root_value` passed to `Schema#execute` #301 - Derived `Relay` objects have descriptions #303 ### Bug fixes - Introspection query is 7 levels deep instead of 3 #308 - Unknown variable types cause validation errors, not runtime errors #310 - `Query::Arguments` doesn't wrap hashes from parsed scalars (fix for user-defined "JSONScalar") #304 ## 0.19.2 (6 Oct 2016) ### New features - If a list entry has a `GraphQL::ExecutionError`, replace the entry with `nil` and return the error #295 ### Bug fixes - Support graphql-batch rescuing `InvalidNullError`s #296 - Schema printer prints Enum names, not Ruby values for enums #297 ## 0.19.1 (4 Oct 2016) ### Breaking changes - Previously-deprecated `InterfaceType#resolve_type` hook has been removed, use `Schema#resolve_type` instead #290 ### New features - Eager-load schemas at definition time, validating types & schema-level hooks #289 - `InvalidNullError`s contain the type & field name that returned null #293 - If an object is resolved with `Schema#resolve_type` and the resulting type is not a member of the expected possible types, raise an error #291 ### Bug fixes - Allow `directive` as field or argument name #288 ## 0.19.0 (30 Sep 2016) ### Breaking changes - `GraphQL::Relay::GlobalNodeIdentification` was removed. Its features were moved to `GraphQL::Schema` or `GraphQL::Relay::Node`. The new hooks support more robust & flexible global IDs. #243 - Relay's `"Node"` interface and `node(id: "...")` field were both moved to `GraphQL::Relay::Node`. To use them in your schema, call `.field` and `.interface`. For example: ```ruby # Adding a Relay-compliant `node` field: field :node, GraphQL::Relay::Node.field ``` ```ruby # This object type implements Relay's `Node` interface: interfaces [GraphQL::Relay::Node.interface] ``` - UUID hooks were renamed and moved to `GraphQL::Schema`. You should define `id_from_object` and `object_from_id` in your `Schema.define { ... }` block. For example: ```ruby MySchema = GraphQL::Schema.define do # Fetch an object by UUID object_from_id ->(id, ctx) { MyApp::RelayLookup.find(id) } # Generate a UUID for this object id_from_object ->(obj, type_defn, ctx) { MyApp::RelayLookup.to_id(obj) } end ``` - The new hooks have no default implementation. To use the previous default, use `GraphQL::Schema::UniqueWithinType`, for example: ```ruby MySchema = GraphQL::Schema.define do object_from_id ->(id, ctx) { # Break the id into its parts: type_name, object_id = GraphQL::Schema::UniqueWithinType.decode(id) # Fetch the identified object # ... } id_from_object ->(obj, type_defn, ctx) { # Provide the type name & the object's `id`: GraphQL::Schema::UniqueWithinType.encode(type_defn.name, obj.id) } end ``` If you were using a custom `id_separator`, it's now accepted as an input to `UniqueWithinType`'s methods, as `separator:`. For example: ```ruby # use "---" as a ID separator GraphQL::Schema::UniqueWithinType.encode(type_name, object_id, separator: "---") GraphQL::Schema::UniqueWithinType.decode(relay_id, separator: "---") ``` - `type_from_object` was previously deprecated and has been replaced by `Schema#resolve_type`. You should define this hook in your schema to return a type definition for a given object: ```ruby MySchema = GraphQL::Schema.define do # ... resolve_type ->(obj, ctx) { # based on `obj` and `ctx`, # figure out which GraphQL type to use # and return the type } end ``` - `Schema#node_identification` has been removed. - `Argument` default values have been changed to be consistent with `InputObjectType` default values. #267 Previously, arguments expected GraphQL values as `default_value`s. Now, they expect application values. (`InputObjectType`s always worked this way.) Consider an enum like this one, where custom values are provided: ```ruby PowerStateEnum = GraphQL::EnumType.define do name "PowerState" value("ON", value: 1) value("OFF", value: 0) end ``` __Previously__, enum _names_ were provided as default values, for example: ```ruby field :setPowerState, PowerStateEnum do # Previously, the string name went here: argument :newValue, default_value: "ON" end ``` __Now__, enum _values_ are provided as default values, for example: ```ruby field :setPowerState, PowerStateEnum do # Now, use the application value as `default_value`: argument :newValue, default_value: 1 end ``` Note that if you __don't have custom values__, then there's no change, because the name and value are the same. Here are types that are affected by this change: - Custom scalars (previously, the `default_value` was a string, now it should be the application value, eg `Date` or `BigDecimal`) - Enums with custom `value:`s (previously, the `default_value` was the name, now it's the value) If you can't replace `default_value`s, you can also use a type's `#coerce_input` method to translate a GraphQL value into an application value. For example: ```ruby # Using a custom scalar, "Date" # PREVIOUSLY, provide a string: argument :starts_on, DateType, default_value: "2016-01-01" # NOW, transform the string into a Date: argument :starts_on, DateType, default_value: DateType.coerce_input("2016-01-01") ``` ### New features - Support `@deprecated` in the Schema language #275 - Support `directive` definitions in the Schema language #280 - Use the same introspection field descriptions as `graphql-js` #284 ### Bug fixes - Operation name is no longer present in execution error `"path"` values #276 - Default values are correctly dumped & reloaded in the Schema language #267 ## 0.18.15 (20 Sep 2016) ### Breaking changes - Validation errors no longer have a `"path"` key in their JSON. It was renamed to `"fields"` #264 - `@skip` and `@include` over multiple selections are handled according to the spec: if the same field is selected multiple times and _one or more_ of them would be included, the field will be present in the response. Previously, if _one or more_ of them would be skipped, it was absent from the response. #256 ### New features - Execution errors include a `"path"` key which points to the field in the response where the error occurred. #259 - Parsing directives from the Schema language is now supported #273 ### Bug fixes - `@skip` and `@include` over multiple selections are now handled according to the spec #256 ## 0.18.14 (20 Sep 2016) ### Breaking changes - Directives are no longer considered as "conflicts" in query validation. This is in conformity with the spec, but a change for graphql-ruby #263 ### Features - Query analyzers may emit errors by raising `GraphQL::AnalysisError`s during `#call` or returning a single error or an array of errors from `#final_value` #262 ### Bug fixes - Merge fields even when `@skip` / `@include` are not identical #263 - Fix possible infinite loop in `FieldsWillMerge` validation #261 ## 0.18.13 (19 Sep 2016) ### Bug fixes - Find infinite loops in nested contexts, too #258 ## 0.18.12 (19 Sep 2016) ### New features - `GraphQL::Analysis::FieldUsage` can be used to check for deprecated fields in the query analysis phase #245 ### Bug fixes - If a schema receives a query on `mutation` or `subscription` but that root doesn't exist, return a validation error #254 - `Query::Arguments#to_h` only includes keys that were provided in the query or have a default value #251 ## 0.18.11 (11 Sep 2016) ### New features - `GraphQL::Language::Nodes::Document#slice(operation_name)` finds that operation and its dependencies and puts them in a new `Document` #241 ### Bug fixes - Validation errors for non-existent fields have the location of the field usage, not the parent field #247 - Properly `require "forwardable"` #242 - Remove `ALLOWED_CONSTANTS` for boolean input, use a plain comparison #240 ## 0.18.10 (9 Sep 2016) ### New features - Assign `#mutation` on objects which are derived from a `Relay::Mutation` #239 ## 0.18.9 (6 Sep 2016) ### Bug fixes - fix backward compatibility for `type_from_object` #238 ## 0.18.8 (6 Sep 2016) ### New features - AST nodes now respond to `#eql?(other)` to test value equality #231 ### Bug fixes - The `connection` helper no longer adds a duplicate field #235 ## 0.18.7 (6 Sep 2016) ### New features - Support parsing nameless fragments (but not executing them) #232 ### Bug fixes - Allow `__type(name: "Whatever")` to return null, as per the spec #233 - Include a Relay mutation's description with a mutation field #225 ## 0.18.6 (29 Aug 2016) ### New features - ` GraphQL::Schema::Loader.load(schema_json)` turns an introspection result into a `GraphQL::Schema` #207 - `.define` accepts plural definitions for: object fields, interface fields field arguments, enum values #222 ## 0.18.5 (27 Aug 2016) ### Deprecations - `Schema.new` is deprecated; use `Schema.define` instead. Before: ```ruby schema = GraphQL::Schema.new( query: QueryType, mutation: MutationType, max_complexity: 100, types: [ExtraType, OtherType] ) schema.node_identification = MyGlobalID schema.rescue_from(ActiveRecord::RecordNotFound) { |err| "..." } ``` After: ```ruby schema = GraphQL::Schema.define do query QueryType mutation MutationType max_complexity 100 node_identification MyGlobalID rescue_from(ActiveRecord::RecordNotFound) { |err| "..." } # Types was renamed to `orphan_types` to avoid conflict with the `types` helper orphan_types [ExtraType, OtherType] end ``` This unifies the disparate methods of configuring a schema and provides new, more flexible design space. It also adds `#metadata` to schemas for user-defined storage. - `UnionType#resolve_type`, `InterfaceType#resolve_type`, and `GlobalNodeIdentification#type_from_object` are deprecated, unify them into `Schema#resolve_type` instead. Before: ```ruby GraphQL::Relay::GlobalNodeIdentification.define do type_from_object ->(obj) { ... } end GraphQL::InterfaceType.define do resolve_type ->(obj, ctx) { ... } end ``` After: ```ruby GraphQL::Schema.define do resolve_type ->(obj, ctx) { ... } end ``` This simplifies type inference and prevents unexpected behavior when different parts of the schema resolve types differently. ### New features - Include expected type in Argument errors #221 - Define schemas with `Schema.define` #208 - Define a global object-to-type function with `Schema#resolve_type` #216 ### Bug fixes ## 0.18.4 (25 Aug 2016) ### New features - `InvalidNullError`s expose a proper `#message` #217 ### Bug fixes - Return an empty result for queries with no operations #219 ## 0.18.3 (22 Aug 2016) ### Bug fixes - `Connection.new(:field)` is optional, not required #215 - 0.18.2 introduced a more restrictive approach to resolving interfaces & unions; revert that approach #212 ## 0.18.2 (17 Aug 2016) ### New features - Connection objects expose the `GraphQL::Field` that created them via `Connection#field` #206 ## 0.18.1 (7 Aug 2016) ### Deprecations - Unify `Relay` naming around `nodes` as the items of a connection: - `Relay::BaseConnection.connection_for_nodes` replaces `Relay::BaseConnection.connection_for_items` - `Relay::BaseConnection#nodes` replaces `Relay::BaseConnection#object` ### New features - Connection fields' `.resolve_proc` is an instance of `Relay::ConnectionResolve` #204 - Types, fields and arguments can store arbitrary values in their `metadata` hashes #203 ## 0.18.0 (4 Aug 2016) ### Breaking changes - `graphql-relay` has been merged with `graphql`, you should remove `graphql-relay` from your gemfile. #195 ### Deprecations ### New features - `GraphQL.parse` can turn schema definitions into a `GraphQL::Language::Nodes::Document`. The document can be stringified again with `Document#to_query_string` #191 - Validation errors include a `path` to the part of the query where the error was found #198 - `.define` also accepts keywords for each helper method, eg `GraphQL::ObjectType.define(name: "PostType", ...)` ### Bug fixes - `global_id_field`s have default complexity of 1, not `nil` - Relay `pageInfo` is correct for connections limited by `max_page_size` - Rescue invalid variable errors & missing operation name errors during query analysis ## 0.17.2 (26 Jul 2016) ### Bug fixes - Correctly spread fragments when nested inside other fragments #194 ## 0.17.1 (26 Jul 2016) ### Bug fixes - Fix `InternalRepresentation::Node#inspect` ## 0.17.0 (21 Jul 2016) ### Breaking changes - `InternalRepresentation::Node` API changes: - `#definition_name` returns the field name on field nodes (while `#name` may have an alias) - `#definitions` returns `{type => field}` pairs for possible fields on this node - `#definition` is gone, it is equivalent to `node.definitions.values.first` - `#on_types` is gone, it is equivalent to `node.definitions.keys` ### New features - Accept `hash_key:` field option - Call `.define { }` block lazily, so `-> { }` is not needed for circular references #182 ### Bug fixes - Support `on` as an Enum value - If the same field is requested on multiple types, choose the maximum complexity among them (not the first) ## 0.16.1 (20 Jul 2016) ### Bug fixes - Fix merging fragments on Union types (see #190, broken from #180) ## 0.16.0 (14 Jul 2016) ### Breaking changes & deprecations - I don't _know_ that this breaks anything, but `GraphQL::Query::SerialExecution` now iterates over a tree of `GraphQL::InternalRepresentation::Node`s instead of an AST (`GraphQL::Language::Nodes::Document`). ### New features - Query context keys can be assigned with `Context#[]=` #178 - Cancel further field resolution with `TimeoutMiddleware` #179 - Add `GraphQL::InternalRepresentation` for normalizing queries from AST #180 - Analyze the query before running it #180 - Assign complexity cost to fields, enforce max complexity before running it #180 - Log max complexity or max depth with `MaxComplexity` or `MaxDepth` analyzers #180 - Query context exposes `#irep_node`, the internal representation of the current node #180 ### Bug fixes - Non-null errors are propagated to the next nullable field, all the way up to `data` #174 ## 0.15.3 (28 Jun 2016) ### New features - `EnumValue`s can receive their properties after instantiation #171 ## 0.15.2 (16 Jun 2016) ### New features - Support lazy type arguments in Object's `interfaces` and Union's `possible_types` #169 ### Bug fixes - Support single-member Unions, as per the spec #170 ## 0.15.1 (15 Jun 2016) ### Bug fixes - Whitelist operation types in `lexer.rb` ## 0.15.0 (11 Jun 2016) ### Breaking changes & deprecations - Remove `debug:` option, propagate all errors. #161 ## 0.14.1 (11 Jun 2016) ### Breaking changes & deprecations - `debug:` is deprecated (#165). Propagating errors (`debug: true`) will become the default behavior. You can get a similar implementation of error gobbling with `CatchallMiddleware`. Add it to your schema: ```ruby MySchema.middleware << GraphQL::Schema::CatchallMiddleware ``` ### New features ### Bug fixes - Restore previous introspection fields on DirectiveType as deprecated #164 - Apply coercion to input default values #162 - Proper Enum behavior when a value isn't found ## 0.14.0 (31 May 2016) ### Breaking changes & deprecations ### New features - `GraphQL::Language::Nodes::Document#to_query_string` will re-serialize a query AST #151 - Accept `root_value:` when running a query #157 - Accept a `GraphQL::Language::Nodes::Document` to `Query.new` (this allows you to cache parsed queries on the server) #152 ### Bug fixes - Improved parse error messages #149 - Improved build-time validation #150 - Raise a meaningful error when a Union or Interface can't be resolved during query execution #155 ## 0.13.0 (29 Apr 2016) ### Breaking changes & deprecations - "Dangling" object types are not loaded into the schema. The must be passed in `GraphQL::Schema.new(types: [...])`. (This was deprecated in 0.12.1) ### New features - Update directive introspection to new spec #121 - Improved schema validation errors #113 - 20x faster parsing #119 - Support inline fragments without type condition #123 - Support multiple schemas composed of the same types #142 - Accept argument `description` and `default_value` in the block #138 - Middlewares can send _new_ arguments to subsequent middlewares #129 ### Bug fixes - Don't leak details of internal errors #120 - Default query `context` to `{}` #133 - Fixed list nullability validation #131 - Ensure field names are strings #128 - Fix `@skip` and `@include` implementation #124 - Interface membership is not shared between schemas #142 ## 0.12.1 (26 Apr 2016) ### Breaking changes & deprecations - __Connecting object types to the schema _only_ via interfaces is deprecated.__ It will be unsupported in the next version of `graphql`. Sometimes, object type is only connected to the Query (or Mutation) root by being a member of an interface. In these cases, bugs happen, especially with Rails development mode. (And sometimes, the bugs don't appear until you deploy to a production environment!) So, in a case like this: ```ruby HatInterface = GraphQL::ObjectType.define do # ... end FezType = GraphQL::ObjectType.define do # ... interfaces [HatInterface] end QueryType = GraphQL::ObjectType.define do field :randomHat, HatInterface # ... end ``` `FezType` can only be discovered by `QueryType` _through_ `HatInterface`. If `fez_type.rb` hasn't been loaded by Rails, `HatInterface.possible_types` will be empty! Now, `FezType` must be passed to the schema explicitly: ```ruby Schema.new( # ... types: [FezType] ) ``` Since the type is passed directly to the schema, it will be loaded right away! ### New features ### Bug fixes ## 0.12.0 (20 Mar 2016) ### Breaking changes & deprecations - `GraphQL::DefinitionConfig` was replaced by `GraphQL::Define` #116 - Many scalar types are more picky about which inputs they allow (#115). To get the previous behavior, add this to your program: ```ruby # Previous coerce behavior for scalars: GraphQL::BOOLEAN_TYPE.coerce = ->(value) { !!value } GraphQL::ID_TYPE.coerce = ->(value) { value.to_s } GraphQL::STRING_TYPE.coerce = ->(value) { value.to_s } # INT_TYPE and FLOAT_TYPE were unchanged ``` - `GraphQL::Field`s can't be renamed because `#resolve` may depend on that name. (This was only a problem if you pass the _same_ `GraphQL::Field` instance to `field ... field:` definitions.) - `GraphQL::Query::DEFAULT_RESOLVE` was removed. `GraphQL::Field#resolve` handles that behavior. ### New features - Can override `max_depth:` from `Schema#execute` - Base `GraphQL::Error` for all graphql-related errors ### Bug fixes - Include `""` for String default values (so it's encoded as a GraphQL string literal) ## 0.11.1 (6 Mar 2016) ### New features - Schema `max_depth:` option #110 - Improved validation errors for input objects #104 - Interfaces provide field implementations to object types #108 ## 0.11.0 (28 Feb 2016) ### Breaking changes & deprecations - `GraphQL::Query::BaseExecution` was removed, you should probably extend `SerialExecution` instead #96 - `GraphQL::Language::Nodes` members no longer raise if they don't get inputs during `initialize` #92 - `GraphQL.parse` no longer accepts `as:` for parsing partial queries. #92 ### New features - `Field#property` & `Field#property=` can be used to access & modify the method that will be sent to the underlying object when resolving a field #88 - When defining a field, you can pass a string for as `type`. It will be looked up in the global namespace. - `Query::Arguments#to_h` unwraps `Arguments` objects recursively - If you raise `GraphQL::ExecutionError` during field resolution, it will be rescued and the message will be added to the response's `errors` key. #93 - Raise an error when non-null fields are `nil` #94 ### Bug fixes - Accept Rails params as input objects - Don't get a runtime error when input contains unknown key #100 ## 0.10.9 (15 Jan 2016) ### Bug fixes - Handle re-assignment of `ObjectType#interfaces` #84 - Fix merging queries on interface-typed fields #85 ## 0.10.8 (14 Jan 2016) ### Bug fixes - Fix transform of nested lists #79 - Fix parse & transform of escaped characters #83 ## 0.10.7 (22 Dec 2015) ### New features - Support Rubinius ### Bug fixes - Coerce values into one-item lists for ListTypes ## 0.10.6 (20 Dec 2015) ### Bug fixes - Remove leftover `puts`es ## 0.10.5 (19 Dec 2015) ### Bug fixes - Accept enum value description in definition #71 - Correctly parse empty input objects #75 - Correctly parse arguments preceded by newline - Find undefined input object keys during static validation ## 0.10.4 (24 Nov 2015) ### New features - Add `Arguments#to_h` #66 ### Bug fixes - Accept argument description in definition - Correctly parse empty lists ## 0.10.3 (11 Nov 2015) ### New features - Support root-level `subscription` type ### Bug fixes - Require Set for Schema::Printer ## 0.10.2 (10 Nov 2015) ### Bug fixes - Handle blank strings in queries - Raise if a field is configured without a proper type #61 ## 0.10.1 (22 Oct 2015) ### Bug fixes - Properly merge fields on fragments within fragments - Properly delegate enumerable-ish methods on `Arguments` #56 - Fix & refactor literal coersion & validation #53 ## 0.10.0 (17 Oct 2015) ### New features - Scalars can have distinct `coerce_input` and `coerce_result` methods #48 - Operations don't require a name #54 ### Bug fixes - Big refactors and fixes to variables and arguments: - Correctly apply argument default values - Correctly apply variable default values - Raise at execution-time if non-null variables are missing - Incoming values are coerced to their proper types before execution ## 0.9.5 (1 Oct 2015) ### New features - Add `Schema#middleware` to wrap field access - Add `RescueMiddleware` to handle errors during field execution - Add `Schema::Printer` for printing the schema definition #45 ### Bug fixes ## 0.9.4 (22 Sept 2015) ### New features - Fields can return `GraphQL::ExecutionError`s to add errors to the response ### Bug fixes - Fix resolution of union types in some queries #41 ## 0.9.3 (15 Sept 2015) ### New features - Add `Schema#execute` shorthand for running queries - Merge identical fields in fragments so they're only resolved once #34 - An error during parsing raises `GraphQL::ParseError` #33 ### Bug fixes - Find nested input types in `TypeReducer` #35 - Find variable usages inside fragments during static validation ## 0.9.2, 0.9.1 (10 Sept 2015) ### Bug fixes - remove Celluloid dependency ## 0.9.0 (10 Sept 2015) ### Breaking changes & deprecations - remove `GraphQL::Query::ParallelExecution` (use [`graphql-parallel`](https://github.com/rmosolgo/graphql-parallel)) ## 0.8.1 (10 Sept 2015) ### Breaking changes & deprecations - `GraphQL::Query::ParallelExecution` has been extracted to [`graphql-parallel`](https://github.com/rmosolgo/graphql-parallel) ## 0.8.0 (4 Sept 2015) ### New features - Async field resolution with `context.async { ... }` - Access AST node during resolve with `context.ast_node` ### Bug fixes - Fix for validating arguments returning up too soon - Raise if you try to define 2 types with the same name - Raise if you try to get a type by name but it doesn't exist ## 0.7.1 (27 Aug 2015) ### Bug fixes - Merge nested results from different fragments instead of using the latest one only ## 0.7.0 (26 Aug 2015) ### Breaking changes & deprecations - Query keyword argument `params:` was removed, use `variables:` instead. ### Bug fixes - `@skip` has precedence over `@include` - Handle when `DEFAULT_RESOVE` returns nil ## 0.6.2 (20 Aug 2015) ### Bug fixes - Fix whitespace parsing in input objects ## 0.6.1 (16 Aug 2015) ### New features - Parse UTF-8 characters & escaped characters ### Bug fixes - Properly parse empty strings - Fix argument / variable compatibility validation ## 0.6.0 (14 Aug 2015) ### Breaking changes & deprecations - Deprecate `params` option to `Query#new` in favor of `variables` - Deprecated `.new { |obj, types, fields, args| }` API was removed (use `.define`) ### New features - `Query#new` accepts `operation_name` argument - `InterfaceType` and `UnionType` accept `resolve_type` configs ### Bug fixes - Gracefully handle blank-string & whitespace-only queries - Handle lists in variable definitions and arguments - Handle non-null input types ## 0.5.0 (12 Aug 2015) ### Breaking changes & deprecations - Deprecate definition API that yielded a bunch of helpers #18 ### New features - Add new definition API #18 graphql-ruby-2.5.19/CNAME000066400000000000000000000000211514115062600147650ustar00rootroot00000000000000graphql-ruby.org graphql-ruby-2.5.19/Gemfile000066400000000000000000000010551514115062600155220ustar00rootroot00000000000000# frozen_string_literal: true source "https://rubygems.org" gemspec gem 'bootsnap' # required by the Rails apps generated in tests gem 'stackprof', platform: :ruby gem 'pry' gem 'pry-stack_explorer', platform: :ruby if RUBY_VERSION >= "3.0" gem "libev_scheduler" gem "evt" end if RUBY_VERSION >= "3.2.0" gem "minitest-mock" gem "async", "~>2.0" gem "minitest-mock" end # Required for running `jekyll algolia ...` (via `rake site:update_search_index`) group :jekyll_plugins do gem 'jekyll-algolia', '~> 1.0' gem 'jekyll-redirect-from' end graphql-ruby-2.5.19/MIT-LICENSE000066400000000000000000000020361514115062600156630ustar00rootroot00000000000000Copyright 2015 Robert Mosolgo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. graphql-ruby-2.5.19/Rakefile000066400000000000000000000132671514115062600157040ustar00rootroot00000000000000# frozen_string_literal: true require "bundler/gem_helper" Bundler::GemHelper.install_tasks require "rake/testtask" require_relative "guides/_tasks/site" require_relative "lib/graphql/rake_task/validate" require 'rake/extensiontask' Rake::TestTask.new do |t| t.libs << "spec" << "lib" << "graphql-c_parser/lib" exclude_integrations = [] ['mongoid', 'rails'].each do |integration| begin require integration rescue LoadError exclude_integrations << integration end end t.test_files = FileList.new("spec/**/*_spec.rb") do |fl| fl.exclude(*exclude_integrations.map { |int| "spec/integration/#{int}/**/*" }) end # After 2.7, there were not warnings for uninitialized ivars anymore if RUBY_VERSION < "3" t.warning = false end end require 'rubocop/rake_task' RuboCop::RakeTask.new default_tasks = [:test, :rubocop] if ENV["SYSTEM_TESTS"] task(default: ["test:system"] + default_tasks) else task(default: default_tasks) end def assert_dependency_version(dep_name, required_version, check_script) version = `#{check_script}` if !version.include?(required_version) raise <<-ERR build_parser requires #{dep_name} version "#{required_version}", but found: $ #{check_script} > #{version} To fix this issue: - Update #{dep_name} to the required version - Update the assertion in `Rakefile` to match the current version ERR end end namespace :bench do def prepare_benchmark $LOAD_PATH << "./lib" << "./spec/support" require_relative("./benchmark/run.rb") end desc "Benchmark parsing" task :parse do prepare_benchmark GraphQLBenchmark.run("parse") end desc "Benchmark lexical analysis" task :scan do prepare_benchmark GraphQLBenchmark.run("scan") end desc "Benchmark the introspection query" task :query do prepare_benchmark GraphQLBenchmark.run("query") end desc "Benchmark validation of several queries" task :validate do prepare_benchmark GraphQLBenchmark.run("validate") end desc "Profile a validation" task :validate_memory do prepare_benchmark GraphQLBenchmark.validate_memory end desc "Generate a profile of the introspection query" task :profile do prepare_benchmark GraphQLBenchmark.profile end desc "Run benchmarks on a very large result" task :profile_large_result do prepare_benchmark GraphQLBenchmark.profile_large_result end desc "Run benchmarks on a small result" task :profile_small_result do prepare_benchmark GraphQLBenchmark.profile_small_result end desc "Run introspection on a small schema" task :profile_small_introspection do prepare_benchmark GraphQLBenchmark.profile_small_introspection end desc "Dump schema to SDL" task :profile_to_definition do prepare_benchmark GraphQLBenchmark.profile_to_definition end desc "Load schema from SDL" task :profile_from_definition do prepare_benchmark GraphQLBenchmark.profile_from_definition end desc "Compare GraphQL-Batch and GraphQL-Dataloader" task :profile_batch_loaders do prepare_benchmark GraphQLBenchmark.profile_batch_loaders end desc "Run benchmarks on schema creation" task :profile_boot do prepare_benchmark GraphQLBenchmark.profile_boot end desc "Check the memory footprint of a large schema" task :profile_schema_memory_footprint do prepare_benchmark GraphQLBenchmark.profile_schema_memory_footprint end desc "Check the depth of the stacktrace during execution" task :profile_stack_depth do prepare_benchmark GraphQLBenchmark.profile_stack_depth end desc "Run a very big introspection query" task :profile_large_introspection do prepare_benchmark GraphQLBenchmark.profile_large_introspection end task :profile_small_query_on_large_schema do prepare_benchmark GraphQLBenchmark.profile_small_query_on_large_schema end desc "Run analysis on a big query" task :profile_large_analysis do prepare_benchmark GraphQLBenchmark.profile_large_analysis end desc "Run analysis on parsing" task :profile_parse do prepare_benchmark GraphQLBenchmark.profile_parse end end namespace :test do desc "Run system tests for ActionCable subscriptions" task :system do success = Dir.chdir("spec/dummy") do system("bundle install") system("bundle exec bin/rails test:system") end success || abort end task js: "js:test" end namespace :js do client_dir = "./javascript_client" desc "Run the tests for javascript_client" task :test do success = Dir.chdir(client_dir) do system("yarn run test") end success || abort end desc "Install JS dependencies" task :install do Dir.chdir(client_dir) do system("yarn install") end end desc "Compile TypeScript to JavaScript" task :build do Dir.chdir(client_dir) do system("yarn tsc") end end task all: [:install, :build, :test] end task :build_c_lexer do assert_dependency_version("Ragel", "7.0.4", "ragel -v") `ragel -F1 graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl` end Rake::ExtensionTask.new("graphql_c_parser_ext") do |t| t.ext_dir = 'graphql-c_parser/ext/graphql_c_parser_ext' t.lib_dir = "graphql-c_parser/lib/graphql" end task :build_yacc_parser do assert_dependency_version("Bison", "3.8", "yacc --version") `yacc graphql-c_parser/ext/graphql_c_parser_ext/parser.y -o graphql-c_parser/ext/graphql_c_parser_ext/parser.c -Wyacc` end task :move_binary do # For some reason my local env doesn't respect the `lib_dir` configured above `mv graphql-c_parser/lib/*.bundle graphql-c_parser/lib/graphql` end desc "Build the C Extension" task build_ext: [:build_c_lexer, :build_yacc_parser, "compile:graphql_c_parser_ext", :move_binary] graphql-ruby-2.5.19/benchmark/000077500000000000000000000000001514115062600161605ustar00rootroot00000000000000graphql-ruby-2.5.19/benchmark/abstract_fragments.graphql000066400000000000000000000006741514115062600234200ustar00rootroot00000000000000query AbstractFragments { node(id: "1") { ...Frag1 } } fragment Frag1 on Commentable { id __typename ...Frag2 } fragment Frag2 on Commentable { id __typename ...Frag3 } fragment Frag3 on Commentable { id __typename ...Frag4 } fragment Frag4 on Commentable { id __typename ...Frag5 } fragment Frag5 on Commentable { id __typename ...Frag6 } fragment Frag6 on Commentable { comments { body } } graphql-ruby-2.5.19/benchmark/abstract_fragments_2.graphql000066400000000000000000000012301514115062600236260ustar00rootroot00000000000000query AbstractFragments { node(id: "1") { ...Frag1 } } fragment Frag1 on Commentable { id __typename ...Frag9 ...Frag2 } fragment Frag2 on Commentable { id __typename ...Frag9 ...Frag3 } fragment Frag3 on Commentable { id __typename ...Frag9 ...Frag4 } fragment Frag4 on Commentable { id __typename ...Frag9 ...Frag5 } fragment Frag5 on Commentable { id __typename ...Frag9 ...Frag6 } fragment Frag6 on Commentable { ...Frag7 ...Frag9 name id comments { ...Frag8 ...Frag7 id } } fragment Frag7 on Node { id } fragment Frag8 on Comment { body } fragment Frag9 on Named { name } graphql-ruby-2.5.19/benchmark/batch_loading.rb000066400000000000000000000064321514115062600212700ustar00rootroot00000000000000# frozen_string_literal: true module BatchLoading class GraphQLBatchSchema < GraphQL::Schema DATA = [ { id: "1", name: "Bulls", player_ids: ["2", "3"] }, { id: "2", name: "Michael Jordan", team_id: "1" }, { id: "3", name: "Scottie Pippin", team_id: "1" }, { id: "4", name: "Braves", player_ids: ["5", "6"] }, { id: "5", name: "Chipper Jones", team_id: "4" }, { id: "6", name: "Tom Glavine", team_id: "4" }, ] class DataLoader < GraphQL::Batch::Loader def initialize(column: :id) @column = column end def perform(keys) keys.each do |key| record = DATA.find { |d| d[@column] == key } fulfill(key, record) end end end class Team < GraphQL::Schema::Object field :name, String, null: false field :players, "[BatchLoading::GraphQLBatchSchema::Player]", null: false def players DataLoader.load_many(object[:player_ids]) end end class Player < GraphQL::Schema::Object field :name, String, null: false field :team, Team, null: false def team DataLoader.load(object[:team_id]) end end class Query < GraphQL::Schema::Object field :team, Team do argument :name, String end def team(name:) DataLoader.for(column: :name).load(name) end end query(Query) use GraphQL::Batch end class GraphQLDataloaderSchema < GraphQL::Schema class DataSource < GraphQL::Dataloader::Source def initialize(options = {column: :id}) @column = options[:column] end def fetch(keys) keys.map { |key| d = GraphQLBatchSchema::DATA.find { |d| d[@column] == key } # p [key, @column, d] d } end end class Team < GraphQL::Schema::Object field :name, String, null: false field :players, "[BatchLoading::GraphQLDataloaderSchema::Player]", null: false def players dataloader.with(DataSource).load_all(object[:player_ids]) end end class Player < GraphQL::Schema::Object field :name, String, null: false field :team, Team, null: false def team dataloader.with(DataSource).load(object[:team_id]) end end class Query < GraphQL::Schema::Object field :team, Team do argument :name, String end def team(name:) dataloader.with(DataSource, column: :name).load(name) end end query(Query) use GraphQL::Dataloader end class GraphQLNoBatchingSchema < GraphQL::Schema DATA = GraphQLBatchSchema::DATA class Team < GraphQL::Schema::Object field :name, String, null: false field :players, "[BatchLoading::GraphQLNoBatchingSchema::Player]", null: false def players object[:player_ids].map { |id| DATA.find { |d| d[:id] == id } } end end class Player < GraphQL::Schema::Object field :name, String, null: false field :team, Team, null: false def team DATA.find { |d| d[:id] == object[:team_id] } end end class Query < GraphQL::Schema::Object field :team, Team do argument :name, String end def team(name:) DATA.find { |d| d[:name] == name } end end query(Query) end end graphql-ruby-2.5.19/benchmark/big_query.graphql000066400000000000000000000155611514115062600215360ustar00rootroot00000000000000query Anc_inbox_layout($first_0:Int!,$first_5:Int!,$first_6:Int!,$first_a:Int!,$first_c:Int!,$order_by_1:ReportOrderInput!,$substate_2:ReportStateEnum!,$pre_submission_review_states_3:[ReportPreSubmissionReviewStateEnum]!,$size_4:ProfilePictureSizes!,$size_9:ProfilePictureSizes!,$not_types_7:[ActivityTypes]!,$order_by_8:ActivityOrderInput!,$order_by_b:TeamOrderInput!) { query { id, ...FE } } fragment F0 on User { username, _profile_picturePkPpF:profile_picture(size:$size_4), impact, reputation, signal, id } fragment F1 on Report { reporter { username, _profile_picturePkPpF:profile_picture(size:$size_4), impact, reputation, signal, _duplicate_users2Nhzxe:duplicate_users(first:$first_5) { pageInfo { hasNextPage, hasPreviousPage }, total_count, edges { node { id, ...F0 }, cursor } }, id }, id } fragment F2 on Report { id } fragment F3 on Node { id, __typename } fragment F4 on ReportActivityInterface { automated_response, genius_execution_id, report { team { handle, id }, id }, __typename, ...F3 } fragment F5 on Team { url, internet_bug_bounty, _profile_pictureihzmG:profile_picture(size:$size_9), name, id } fragment F6 on User { username, url, _profile_pictureihzmG:profile_picture(size:$size_9), id } fragment F7 on ActivitiesBugDuplicate { original_report_id, id } fragment F8 on ActivitiesReferenceIdAdded { reference, reference_url, id } fragment F9 on ActivitiesCveIdAdded { cve_ids, id } fragment Fa on ActivitiesAgreedOnGoingPublic { first_to_agree, id } fragment Fb on ActivitiesBugCloned { original_report_id, id } fragment Fc on ActivitiesUserAssignedToBug { assigned_user { url, username, id }, id } fragment Fd on ActivitiesGroupAssignedToBug { assigned_group { name, id }, id } fragment Fe on ActivitiesExternalUserInvited { email, id } fragment Ff on ActivitiesExternalUserInvitationCancelled { email, id } fragment Fg on ActivitiesExternalUserRemoved { removed_user { id }, id } fragment Fh on ActivitiesUserBannedFromProgram { removed_user { id }, id } fragment Fi on ActivitiesBountyAwarded { bounty_amount, bounty_currency, bonus_amount, report { reporter { username, url, id }, id }, id } fragment Fj on ActivitiesBountySuggested { bounty_amount, bounty_currency, bonus_amount, id } fragment Fk on ActivitiesBugResolved { report { reporter { username, url, id }, id }, id } fragment Fl on ActivitiesSwagAwarded { report { reporter { username, url, id }, id }, id } fragment Fm on ActivitiesChangedScope { old_scope { asset_identifier, id }, new_scope { asset_identifier, id }, id } fragment Fn on ActivityInterface { _id, internal, i_can_edit, __typename, message, markdown_message, created_at, updated_at, actor { __typename, ...F5, ...F6, ...F3 }, attachments { _id, file_name, content_type, expiring_url, id }, ...F7, ...F8, ...F9, ...Fa, ...Fb, ...Fc, ...Fd, ...Fe, ...Ff, ...Fg, ...Fh, ...Fi, ...Fj, ...Fk, ...Fl, ...Fm, ...F3 } fragment Fo on User { username, url, __typename, id } fragment Fp on TeamMemberGroup { name, __typename, id } fragment Fq on Report { _id, url, title, state, substate, created_at, assignee { __typename, ...Fo, ...Fp, ...F3 }, cloned_from { _id, id }, reporter { username, url, id }, team { _id, url, handle, name, twitter_handle, website, about, offers_bounties, id }, id } fragment Fr on Report { state, stage, disclosed_at, cve_ids, singular_disclosure_disabled, disclosed_at, bug_reporter_agreed_on_going_public_at, team_member_agreed_on_going_public_at, comments_closed, mediation_requested_at, vulnerability_information, vulnerability_information_html, reporter { disabled, username, url, _profile_picture2g6hJa:profile_picture(size:$size_4), id }, weakness { id, name }, original_report { id, url }, attachments { _id, file_name, expiring_url, content_type, id }, allow_singular_disclosure_at, allow_singular_disclosure_after, singular_disclosure_allowed, severity { rating, score, author_type, id }, structured_scope { _id, asset_type, asset_identifier, max_severity, id }, _activities4z6spP:activities(first:$first_6,not_types:$not_types_7,order_by:$order_by_8) { edges { node { __typename, ...F4, ...Fn, ...F3 }, cursor }, pageInfo { hasNextPage, hasPreviousPage } }, id, ...Fq } fragment Fs on Report { id, ...Fr } fragment Ft on Report { title, id } fragment Fu on Report { _id, pre_submission_review_state, i_can_anc_review, reporter { username, id }, team { handle, id }, id, ...F2 } fragment Fv on Report { team { policy_html, id }, structured_scope { asset_identifier, asset_type, instruction, id }, id } fragment Fw on Report { weakness { name, id }, id } fragment Fx on Report { severity { rating, score, id }, id } fragment Fy on Report { latest_activity_at, created_at, id, ...Fq } fragment Fz on Query { me { username, _teamsWbVmT:teams(order_by:$order_by_b,first:$first_c) { edges { node { name, handle, id }, cursor }, pageInfo { hasNextPage, hasPreviousPage } }, id }, id } fragment FA on Query { _reports1t04lE:reports(page:$first_0,first:$first_a,limit:$first_a,order_by:$order_by_1,substate:$substate_2,pre_submission_review_states:$pre_submission_review_states_3) { total_count, edges { node { _id, id, ...Fy }, cursor }, pageInfo { hasNextPage, hasPreviousPage } }, id, ...Fz } fragment FB on Query { id, ...Fz } fragment FC on Query { id } fragment FD on Query { me { username, _profile_pictureihzmG:profile_picture(size:$size_9), id }, id, ...FC } fragment FE on Query { _reports3QQXft:reports(first:$first_0,order_by:$order_by_1,substate:$substate_2,pre_submission_review_states:$pre_submission_review_states_3) { edges { node { id, ...F1, ...F2, ...Fs, ...Ft, ...Fu, ...Fv, ...Fw, ...Fx }, cursor }, pageInfo { hasNextPage, hasPreviousPage } }, id, ...FA, ...FB, ...FD } graphql-ruby-2.5.19/benchmark/big_schema.graphql000066400000000000000000003372231514115062600216330ustar00rootroot00000000000000# Autogenerated input type of AcceptInvitation input AcceptInvitationInput { # A unique identifier for the client performing the mutation. clientMutationId: String handle: String! } # Autogenerated return type of AcceptInvitation type AcceptInvitationPayload { # A unique identifier for the client performing the mutation. clientMutationId: String me: User } # Autogenerated input type of AcknowledgeProgramHealthAcknowledgementMutation input AcknowledgeProgramHealthAcknowledgementMutationInput { # A unique identifier for the client performing the mutation. clientMutationId: String program_health_acknowledgement_id: ID! } # Autogenerated return type of AcknowledgeProgramHealthAcknowledgementMutation type AcknowledgeProgramHealthAcknowledgementMutationPayload { acknowledged_program_health_acknowledgement_id: String! # A unique identifier for the client performing the mutation. clientMutationId: String errors: [Error] me: User was_successful: Boolean! } # A Activities::AgreedOnGoingPublic activity for a report type ActivitiesAgreedOnGoingPublic implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! first_to_agree: Boolean! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::BountyAwarded activity for a report type ActivitiesBountyAwarded implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean bonus_amount: String bounty_amount: String bounty_currency: String created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::BountySuggested activity for a report type ActivitiesBountySuggested implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean bonus_amount: String bounty_amount: String bounty_currency: String created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::BugCloned activity for a report type ActivitiesBugCloned implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String original_report: Report original_report_id: Int @deprecated(reason: "Deprecated in favor of .original_report") report: Report updated_at: String! } # A Activities::BugDuplicate activity for a report type ActivitiesBugDuplicate implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String original_report: Report original_report_id: Int @deprecated(reason: "Deprecated in favor of .original_report") report: Report updated_at: String! } # A Activities::BugFiled activity for a report type ActivitiesBugFiled implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::BugInformative activity for a report type ActivitiesBugInformative implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::BugNeedsMoreInfo activity for a report type ActivitiesBugNeedsMoreInfo implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::BugNew activity for a report type ActivitiesBugNew implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::BugNotApplicable activity for a report type ActivitiesBugNotApplicable implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::BugReopened activity for a report type ActivitiesBugReopened implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::BugResolved activity for a report type ActivitiesBugResolved implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::BugSpam activity for a report type ActivitiesBugSpam implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::BugTriaged activity for a report type ActivitiesBugTriaged implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::ChangedScope activity for a report type ActivitiesChangedScope implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String new_scope: StructuredScope old_scope: StructuredScope report: Report updated_at: String! } # A Activities::Comment activity for a report type ActivitiesComment implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::CommentsClosed activity for a report type ActivitiesCommentsClosed implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::CveIdAdded activity for a report type ActivitiesCveIdAdded implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! cve_ids: [String] genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::ExternalAdvisoryAdded activity for a report type ActivitiesExternalAdvisoryAdded implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::ExternalUserInvitationCancelled activity for a report type ActivitiesExternalUserInvitationCancelled implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! email: String genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::ExternalUserInvited activity for a report type ActivitiesExternalUserInvited implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! email: String genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::ExternalUserJoined activity for a report type ActivitiesExternalUserJoined implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! duplicate_report: Report duplicate_report_id: Int @deprecated(reason: "Deprecated in favor of .duplicate_report") genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::ExternalUserRemoved activity for a report type ActivitiesExternalUserRemoved implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String removed_user: User! report: Report updated_at: String! } # A Activities::GroupAssignedToBug activity for a report type ActivitiesGroupAssignedToBug implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! assigned_group: TeamMemberGroup attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") group: TeamMemberGroup @deprecated(reason: "deprecated in favor of assigned group") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::HackerRequestedMediation activity for a report type ActivitiesHackerRequestedMediation implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::ManuallyDisclosed activity for a report type ActivitiesManuallyDisclosed implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::MediationRequested activity for a report type ActivitiesMediationRequested implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # An Activities::NobodyAssignedToBug activity for a report type ActivitiesNobodyAssignedToBug implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::NotEligibleForBounty activity for a report type ActivitiesNotEligibleForBounty implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::ReassignedToTeam activity for a report type ActivitiesReassignedToTeam implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::ReferenceIdAdded activity for a report type ActivitiesReferenceIdAdded implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String reference: String reference_url: String report: Report updated_at: String! } # A Activities::ReportBecamePublic activity for a report type ActivitiesReportBecamePublic implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::ReportCollaboratorInvited activity for a report type ActivitiesReportCollaboratorInvited implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::ReportCollaboratorJoined activity for a report type ActivitiesReportCollaboratorJoined implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::ReportSeverityUpdated activity for a report type ActivitiesReportSeverityUpdated implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::ReportTitleUpdated activity for a report type ActivitiesReportTitleUpdated implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String new_title: String! old_title: String! report: Report updated_at: String! } # A Activities::ReportVulnerabilityTypesUpdated activity for a report type ActivitiesReportVulnerabilityTypesUpdated implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String new_weakness: Weakness old_weakness: Weakness report: Report updated_at: String! } # A Activities::SwagAwarded activity for a report type ActivitiesSwagAwarded implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report swag: Swag! updated_at: String! } # A Activities::TeamPublished activity for a team type ActivitiesTeamPublished implements ActivityInterface, Node { _id: ID! actor: ActorUnion! attachments: [Attachment] created_at: String! i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String updated_at: String! } # A Activities::UserAssignedToBug activity for a report type ActivitiesUserAssignedToBug implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! assigned_user: User! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String report: Report updated_at: String! } # A Activities::UserBannedFromProgram activity for a report type ActivitiesUserBannedFromProgram implements ActivityInterface, Node, ReportActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] automated_response: Boolean created_at: String! genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String removed_user: User! report: Report updated_at: String! } # A Activities::UserJoined activity for a user type ActivitiesUserJoined implements ActivityInterface, Node { _id: ID! actor: ActorUnion! attachments: [Attachment] created_at: String! i_can_edit: Boolean! id: ID! internal: Boolean markdown_message: String message: String updated_at: String! } # The connection type for ActivityUnion. type ActivityConnection { # A list of edges. edges: [ActivityUnionEdge] max_updated_at: String # Information to aid in pagination. pageInfo: PageInfo! total_count: Int! } # A interface for the common fields on an HackerOne Activity interface ActivityInterface { _id: ID! actor: ActorUnion! attachments: [Attachment] created_at: String! i_can_edit: Boolean! internal: Boolean markdown_message: String message: String updated_at: String! } # Fields on which a collection of activities can be ordered enum ActivityOrderField { created_at updated_at } input ActivityOrderInput { direction: OrderDirection! field: ActivityOrderField! } # Possible types for an activity enum ActivityTypes { AgreedOnGoingPublic BountyAwarded BountySuggested BugCloned BugDuplicate BugFiled BugInformative BugNeedsMoreInfo BugNew BugNotApplicable BugReopened BugResolved BugSpam BugTriaged ChangedScope Comment CommentsClosed CveIdAdded ExternalAdvisoryAdded ExternalUserInvitationCancelled ExternalUserInvited ExternalUserJoined ExternalUserRemoved GroupAssignedToBug HackerRequestedMediation ManuallyDisclosed MediationRequested NobodyAssignedToBug NotEligibleForBounty ReassignedToTeam ReferenceIdAdded ReportBecamePublic ReportCollaboratorInvited ReportCollaboratorJoined ReportSeverityUpdated ReportTitleUpdated ReportVulnerabilityTypesUpdated SwagAwarded TeamPublished UserAssignedToBug UserBannedFromProgram UserJoined } # Activities can be of multiple types union ActivityUnion = ActivitiesAgreedOnGoingPublic | ActivitiesBountyAwarded | ActivitiesBountySuggested | ActivitiesBugCloned | ActivitiesBugDuplicate | ActivitiesBugFiled | ActivitiesBugInformative | ActivitiesBugNeedsMoreInfo | ActivitiesBugNew | ActivitiesBugNotApplicable | ActivitiesBugReopened | ActivitiesBugResolved | ActivitiesBugSpam | ActivitiesBugTriaged | ActivitiesChangedScope | ActivitiesComment | ActivitiesCommentsClosed | ActivitiesCveIdAdded | ActivitiesExternalAdvisoryAdded | ActivitiesExternalUserInvitationCancelled | ActivitiesExternalUserInvited | ActivitiesExternalUserJoined | ActivitiesExternalUserRemoved | ActivitiesGroupAssignedToBug | ActivitiesHackerRequestedMediation | ActivitiesManuallyDisclosed | ActivitiesMediationRequested | ActivitiesNobodyAssignedToBug | ActivitiesNotEligibleForBounty | ActivitiesReassignedToTeam | ActivitiesReferenceIdAdded | ActivitiesReportBecamePublic | ActivitiesReportCollaboratorInvited | ActivitiesReportCollaboratorJoined | ActivitiesReportSeverityUpdated | ActivitiesReportTitleUpdated | ActivitiesReportVulnerabilityTypesUpdated | ActivitiesSwagAwarded | ActivitiesTeamPublished | ActivitiesUserAssignedToBug | ActivitiesUserBannedFromProgram | ActivitiesUserJoined # The connection type for ActivityUnion. type ActivityUnionConnection { # A list of edges. edges: [ActivityUnionEdge] # Information to aid in pagination. pageInfo: PageInfo! } # An edge in a connection. type ActivityUnionEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: ActivityUnion } # The actor of an activity can be multiple types union ActorUnion = Team | User # A HackerOne user's address used for submitting swag type Address implements Node { _id: ID! city: String! country: String! created_at: String! id: ID! name: String! phone_number: String postal_code: String! state: String! street: String! tshirt_size: TshirtSizeEnum @deprecated(reason: "Query tshirt size on User instead") } # Autogenerated input type of ArchiveStructuredScope input ArchiveStructuredScopeInput { # A unique identifier for the client performing the mutation. clientMutationId: String structured_scope_id: ID! } # Autogenerated return type of ArchiveStructuredScope type ArchiveStructuredScopePayload { # A unique identifier for the client performing the mutation. clientMutationId: String query: Query structured_scope: StructuredScope } # Report can be assigned to either a user or a team member group union AssigneeUnion = TeamMemberGroup | User # A HackerOne attachment for a report type Attachment implements Node { _id: ID! content_type: String! created_at: String! expiring_url: String! file_name: String! file_size: Int! id: ID! } # Types of authentication methods for users enum AuthenticationServiceEnum { database saml token } # A HackerOne badge type Badge implements Node { _id: ID! description: String! id: ID! image_path: String! name: String! } # Represents a badge earned by a user type BadgesUsers implements Node { _id: ID! badge: Badge! created_at: String! id: ID! user: User! } # The connection type for BadgesUsers. type BadgesUsersConnection { # A list of edges. edges: [BadgesUsersEdge] # Information to aid in pagination. pageInfo: PageInfo! } # An edge in a connection. type BadgesUsersEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: BadgesUsers } # Resources for setting up the Bank Transfer payment method type BankTransferReference implements Node { beneficiary_required_details(currency: String!, bank_account_country: String!, beneficiary_country: String!): BeneficiaryRequiredDetail countries: [Country] currencies: [Currency] id: ID! } # A specification of information needed to create a bank transfer payment preference type BeneficiaryRequiredDetail implements Node { bank_account_country: String! beneficiary_country: String! beneficiary_required_details( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): BeneficiaryRequiredDetailsConnection currency: String! id: ID! } input BeneficiaryRequiredDetailInput { field: String! value: String! } # A specification of the possibilities for creating a bank transfer payout preference type BeneficiaryRequiredDetails implements Node { beneficiary_entity_type: String! beneficiary_required_fields( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): BeneficiaryRequiredFieldConnection description: String! fee: String! id: ID! payment_type: String! } # The connection type for BeneficiaryRequiredDetails. type BeneficiaryRequiredDetailsConnection { # A list of edges. edges: [BeneficiaryRequiredDetailsEdge] # Information to aid in pagination. pageInfo: PageInfo! } # An edge in a connection. type BeneficiaryRequiredDetailsEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: BeneficiaryRequiredDetails } # A specification of the format of a field used to create a bank transfer payout preference type BeneficiaryRequiredField implements Node { description: String! field: String! id: ID! regex: String! } # The connection type for BeneficiaryRequiredField. type BeneficiaryRequiredFieldConnection { # A list of edges. edges: [BeneficiaryRequiredFieldEdge] # Information to aid in pagination. pageInfo: PageInfo! } # An edge in a connection. type BeneficiaryRequiredFieldEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: BeneficiaryRequiredField } # A HackerOne bounty for a report type Bounty implements Node { _id: ID! amount: String! awarded_amount: String! awarded_bonus_amount: String! awarded_currency: String! bonus_amount: String! created_at: String! id: ID! report: Report! status: BountyStatusEnum! } # The connection type for Bounty. type BountyConnection { # A list of edges. edges: [BountyEdge] # Information to aid in pagination. pageInfo: PageInfo! total_amount: Float! total_count: Int! } # An edge in a connection. type BountyEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: Bounty } # Status which reflect the state of a bounty enum BountyStatusEnum { cancelled confirmed failed failed_ofac_check needs_payout_method needs_tax_form no_mileage_account no_status no_tax_form ofac_reject pending pending_ofac_check rejected sent } # A subset of weaknesses that share a common characteristic type Cluster implements Node { created_at: String! description: String id: ID! name: String! updated_at: String! weaknesses( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int order_by: WeaknessOrder = {direction: ASC, field: name} search: String = null team_handle: String = null team_weakness_state: [TeamWeaknessStates] = [enabled, disabled, hidden] ): ClusterWeaknessConnection } # The connection type for Cluster. type ClusterConnection { # A list of edges. edges: [ClusterEdge] # Information to aid in pagination. pageInfo: PageInfo! total_count: Int! } # An edge in a connection. type ClusterEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: Cluster } input ClusterOrder { direction: OrderDirection! field: ClusterOrderField! } # Fields on which a collection of Cluster can be ordered enum ClusterOrderField { BROWSING_FRIENDLY } # The connection type for Weakness. type ClusterWeaknessConnection { # A list of edges. edges: [ClusterWeaknessEdge] # Information to aid in pagination. pageInfo: PageInfo! total_count: Int! } # An edge in a connection. type ClusterWeaknessEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: Weakness team_weakness(team_handle: String!): TeamWeakness } # A Coinbase Payout Preference type CoinbasePayoutPreferenceType implements Node, PayoutPreferenceInterface { _id: ID! default: Boolean email: String id: ID! } # A common response type CommonResponse implements Node { # The primary key from the database _id: ID! created_at: String! id: ID! message: String! team: Team! title: String! updated_at: String! } # The connection type for CommonResponse. type CommonResponseConnection { # A list of edges. edges: [CommonResponseEdge] # Information to aid in pagination. pageInfo: PageInfo! total_count: Int! } # An edge in a connection. type CommonResponseEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: CommonResponse } input CommonResponseOrder { direction: OrderDirection! field: CommonResponseOrderField! } # Fields on which a collection of common responses can be ordered enum CommonResponseOrderField { title } # Will only return values for valid SeverityRatingEnum and null. scalar CountBySeverity # A country as specified in ISO 3166 type Country implements Node { alpha2: String! currency_code: String id: ID! name: String! } # Autogenerated input type of CreateActivityComment input CreateActivityCommentInput { # A unique identifier for the client performing the mutation. clientMutationId: String internal: Boolean! message: String! report_id: ID! } # Autogenerated return type of CreateActivityComment type CreateActivityCommentPayload { activities: ActivityUnionConnection activity: ActivitiesComment # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash new_activity_edge: ActivityUnionEdge report: Report } # Autogenerated input type of CreateBounty input CreateBountyInput { amount: Float bonus_amount: Float # A unique identifier for the client performing the mutation. clientMutationId: String message: String report_id: ID! } # Autogenerated return type of CreateBounty type CreateBountyPayload { bounty: Bounty! # A unique identifier for the client performing the mutation. clientMutationId: String } # Autogenerated input type of CreateBountySuggestion input CreateBountySuggestionInput { amount: Float bonus_amount: Float # A unique identifier for the client performing the mutation. clientMutationId: String message: String report_id: ID! } # Autogenerated return type of CreateBountySuggestion type CreateBountySuggestionPayload { activity: ActivitiesBountySuggested! # A unique identifier for the client performing the mutation. clientMutationId: String } # Autogenerated input type of CreateCoinbasePayoutPreference input CreateCoinbasePayoutPreferenceInput { # A unique identifier for the client performing the mutation. clientMutationId: String coinbase_email: String! default_method: Boolean! } # Autogenerated return type of CreateCoinbasePayoutPreference type CreateCoinbasePayoutPreferencePayload { # A unique identifier for the client performing the mutation. clientMutationId: String me: User } # Autogenerated input type of CreateCurrencycloudBankTransferPayoutPreference input CreateCurrencycloudBankTransferPayoutPreferenceInput { bank_account_country: String! bank_account_holder_name: String! beneficiary_entity_type: CurrencycloudBankTransferEntityType! beneficiary_required_details: [BeneficiaryRequiredDetailInput]! # A unique identifier for the client performing the mutation. clientMutationId: String currency: String! default_method: Boolean! payment_type: CurrencycloudBankTransferPaymentType! } # Autogenerated return type of CreateCurrencycloudBankTransferPayoutPreference type CreateCurrencycloudBankTransferPayoutPreferencePayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash me: User } # Autogenerated input type of CreateJiraOauthUrl input CreateJiraOauthUrlInput { # A unique identifier for the client performing the mutation. clientMutationId: String site: String! team_id: ID! } # Autogenerated return type of CreateJiraOauthUrl type CreateJiraOauthUrlPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash team: Team url: String } # Autogenerated input type of CreateJiraWebhookToken input CreateJiraWebhookTokenInput { # A unique identifier for the client performing the mutation. clientMutationId: String team_id: ID! } # Autogenerated return type of CreateJiraWebhookToken type CreateJiraWebhookTokenPayload { # A unique identifier for the client performing the mutation. clientMutationId: String team: Team! webhook_url: String! } # Autogenerated input type of CreateMailingAddress input CreateMailingAddressInput { city: String! # A unique identifier for the client performing the mutation. clientMutationId: String country: String! name: String! phone_number: String! postal_code: String! state: String! street: String! } # Autogenerated return type of CreateMailingAddress type CreateMailingAddressPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: [Error] me: User was_successful: Boolean! } # Autogenerated input type of CreateOrUpdateHackeroneToJiraEventsConfiguration input CreateOrUpdateHackeroneToJiraEventsConfigurationInput { assignee_changes: Boolean # A unique identifier for the client performing the mutation. clientMutationId: String comments: Boolean public_disclosures: Boolean rewards: Boolean state_changes: Boolean team_id: ID! } # Autogenerated return type of CreateOrUpdateHackeroneToJiraEventsConfiguration type CreateOrUpdateHackeroneToJiraEventsConfigurationPayload { # A unique identifier for the client performing the mutation. clientMutationId: String team: Team! } # Autogenerated input type of CreateOrUpdateJiraIntegration input CreateOrUpdateJiraIntegrationInput { assignee: String base_url: String! # A unique identifier for the client performing the mutation. clientMutationId: String custom: String description: String! generate_webhook_in_jira_if_does_not_exist: Boolean issue_type: Int! labels: String pid: Int! summary: String! team_id: ID! } # Autogenerated return type of CreateOrUpdateJiraIntegration type CreateOrUpdateJiraIntegrationPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash team: Team } # Autogenerated input type of CreatePaypalPreference input CreatePaypalPreferenceInput { # A unique identifier for the client performing the mutation. clientMutationId: String default_method: Boolean! paypal_email: String! } # Autogenerated return type of CreatePaypalPreference type CreatePaypalPreferencePayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: [Error] me: User was_successful: Boolean! } # Autogenerated input type of CreateSlackPipeline input CreateSlackPipelineInput { channel: String! # A unique identifier for the client performing the mutation. clientMutationId: String descriptive_label: String notification_report_agreed_on_going_public: Boolean! notification_report_assignee_changed: Boolean! notification_report_became_public: Boolean! notification_report_bounty_paid: Boolean! notification_report_bounty_suggested: Boolean! notification_report_bug_closed_as_spam: Boolean! notification_report_bug_duplicate: Boolean! notification_report_bug_informative: Boolean! notification_report_bug_needs_more_info: Boolean! notification_report_bug_new: Boolean! notification_report_bug_not_applicable: Boolean! notification_report_closed_as_resolved: Boolean! notification_report_comments_closed: Boolean! notification_report_created: Boolean! notification_report_internal_comment_added: Boolean! notification_report_manually_disclosed: Boolean! notification_report_not_eligible_for_bounty: Boolean! notification_report_public_comment_added: Boolean! notification_report_reopened: Boolean! notification_report_swag_awarded: Boolean! notification_report_triaged: Boolean! team_id: ID! } # Autogenerated return type of CreateSlackPipeline type CreateSlackPipelinePayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash new_slack_pipeline_edge: SlackPipelineEdge slack_pipelines_connection: SlackPipelineConnection team: Team! } # Autogenerated input type of CreateStructuredScope input CreateStructuredScopeInput { asset_identifier: String! asset_type: StructuredScopedAssetTypeEnum availability_requirement: String # A unique identifier for the client performing the mutation. clientMutationId: String confidentiality_requirement: String eligible_for_bounty: Boolean eligible_for_submission: Boolean instruction: String integrity_requirement: String reference: String team_id: ID! } # Autogenerated return type of CreateStructuredScope type CreateStructuredScopePayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash team: Team } # Autogenerated input type of CreateSurveyAnswer input CreateSurveyAnswerInput { # A unique identifier for the client performing the mutation. clientMutationId: String feedback: String invitation_token: String structured_response_ids: [ID]! } # Autogenerated return type of CreateSurveyAnswer type CreateSurveyAnswerPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: [Error] me: User was_successful: Boolean! } # Autogenerated input type of CreateSwag input CreateSwagInput { # A unique identifier for the client performing the mutation. clientMutationId: String message: String report_id: ID! } # Autogenerated return type of CreateSwag type CreateSwagPayload { # A unique identifier for the client performing the mutation. clientMutationId: String swag: Swag! } # Autogenerated input type of CreateTaxForm input CreateTaxFormInput { # A unique identifier for the client performing the mutation. clientMutationId: String type: TaxFormTypeEnum! } # Autogenerated return type of CreateTaxForm type CreateTaxFormPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: [String] me: User } # Autogenerated input type of CreateUserBountiesReport input CreateUserBountiesReportInput { # A unique identifier for the client performing the mutation. clientMutationId: String } # Autogenerated return type of CreateUserBountiesReport type CreateUserBountiesReportPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash me: User } # Autogenerated input type of CreateUserLufthansaAccount input CreateUserLufthansaAccountInput { # A unique identifier for the client performing the mutation. clientMutationId: String first_name: String! last_name: String! number: String! } # Autogenerated return type of CreateUserLufthansaAccount type CreateUserLufthansaAccountPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash me: User } # A currency as defined by ISO 4217 type Currency implements Node { code: String! id: ID! name: String! } # Possible currencies codes for bounties enum CurrencyCode { USD XLA } # Different entity types for currencycloud payout preferences enum CurrencycloudBankTransferEntityType { company individual } # Different payment types for currencycloud payout preferences enum CurrencycloudBankTransferPaymentType { priority regular } # A CurrencyCloud Bank Transfer Payout Preference type CurrencycloudBankTransferPayoutPreferenceType implements Node, PayoutPreferenceInterface { _id: ID! default: Boolean id: ID! name: String } # Autogenerated input type of DeleteBiDirectionalJiraIntegration input DeleteBiDirectionalJiraIntegrationInput { # A unique identifier for the client performing the mutation. clientMutationId: String team_id: ID! } # Autogenerated return type of DeleteBiDirectionalJiraIntegration type DeleteBiDirectionalJiraIntegrationPayload { # A unique identifier for the client performing the mutation. clientMutationId: String team: Team } # Autogenerated input type of DeleteJiraWebhook input DeleteJiraWebhookInput { # A unique identifier for the client performing the mutation. clientMutationId: String jira_webhook_id: ID! } # Autogenerated return type of DeleteJiraWebhook type DeleteJiraWebhookPayload { # A unique identifier for the client performing the mutation. clientMutationId: String team: Team } # Autogenerated input type of DeletePhabricatorIntegration input DeletePhabricatorIntegrationInput { # A unique identifier for the client performing the mutation. clientMutationId: String team_id: ID! } # Autogenerated return type of DeletePhabricatorIntegration type DeletePhabricatorIntegrationPayload { # A unique identifier for the client performing the mutation. clientMutationId: String team: Team } # Autogenerated input type of DeleteSlackPipeline input DeleteSlackPipelineInput { # A unique identifier for the client performing the mutation. clientMutationId: String slack_pipeline_id: String! } # Autogenerated return type of DeleteSlackPipeline type DeleteSlackPipelinePayload { # A unique identifier for the client performing the mutation. clientMutationId: String deleted_slack_pipeline_id: String! team: Team } # Autogenerated input type of DeleteTeamMember input DeleteTeamMemberInput { # A unique identifier for the client performing the mutation. clientMutationId: String team_member_id: ID! } # Autogenerated return type of DeleteTeamMember type DeleteTeamMemberPayload { # A unique identifier for the client performing the mutation. clientMutationId: String deletedMembershipId: ID errors: Hash me: User } # Autogenerated input type of DeleteTeamSlackIntegration input DeleteTeamSlackIntegrationInput { # A unique identifier for the client performing the mutation. clientMutationId: String slack_integration_id: ID! } # Autogenerated return type of DeleteTeamSlackIntegration type DeleteTeamSlackIntegrationPayload { # A unique identifier for the client performing the mutation. clientMutationId: String team: Team } # Autogenerated input type of DeleteUserLufthansaAccount input DeleteUserLufthansaAccountInput { # A unique identifier for the client performing the mutation. clientMutationId: String } # Autogenerated return type of DeleteUserLufthansaAccount type DeleteUserLufthansaAccountPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash me: User } # Autogenerated input type of DisableTwoFactorAuthentication input DisableTwoFactorAuthenticationInput { # A unique identifier for the client performing the mutation. clientMutationId: String password: String! totp_code: String! } # Autogenerated return type of DisableTwoFactorAuthentication type DisableTwoFactorAuthenticationPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: [String] me: User } # Autogenerated input type of DismissProgramHealthAcknowledgementMutation input DismissProgramHealthAcknowledgementMutationInput { # A unique identifier for the client performing the mutation. clientMutationId: String program_health_acknowledgement_id: ID! } # Autogenerated return type of DismissProgramHealthAcknowledgementMutation type DismissProgramHealthAcknowledgementMutationPayload { # A unique identifier for the client performing the mutation. clientMutationId: String dismissed_program_health_acknowledgement_id: String! errors: [Error] me: User was_successful: Boolean! } # Autogenerated input type of EnableTwoFactorAuthentication input EnableTwoFactorAuthenticationInput { backup_codes: [String]! # A unique identifier for the client performing the mutation. clientMutationId: String password: String! signature: String! totp_code: String! totp_secret: String! } # Autogenerated return type of EnableTwoFactorAuthentication type EnableTwoFactorAuthenticationPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: [String] me: User } # Autogenerated input type of EnableUser input EnableUserInput { # A unique identifier for the client performing the mutation. clientMutationId: String } # Autogenerated return type of EnableUser type EnableUserPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash me: User } # Autogenerated input type of EnrollForProgram input EnrollForProgramInput { # A unique identifier for the client performing the mutation. clientMutationId: String team_id: ID! } # Autogenerated return type of EnrollForProgram type EnrollForProgramPayload { # A unique identifier for the client performing the mutation. clientMutationId: String deleted_recommended_program_edge: TeamEdge query: Query! teams_connection: TeamConnection } # An error type Error implements Node { field: String id: ID! message: String! type: ErrorTypeEnum! } # Types of errors that can occur enum ErrorTypeEnum { ARGUMENT AUTHORIZATION } # An External Program type ExternalProgram implements Node { # The primary key from the database _id: ID! about: String handle: String! # ID of the object. id: ID! name: String! profile_picture(size: ProfilePictureSizes!): String! } # Autogenerated input type of GenerateMfaOtp input GenerateMfaOtpInput { # A unique identifier for the client performing the mutation. clientMutationId: String } # Autogenerated return type of GenerateMfaOtp type GenerateMfaOtpPayload { backup_codes: [String] # A unique identifier for the client performing the mutation. clientMutationId: String me: User qrcode: Hash signature: String totp_secret: String } # Autogenerated input type of GenerateTaxFormUrl input GenerateTaxFormUrlInput { # A unique identifier for the client performing the mutation. clientMutationId: String } # Autogenerated return type of GenerateTaxFormUrl type GenerateTaxFormUrlPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash me: User } # A HackeronePayroll Payout Preference type HackeronePayrollPayoutPreferenceType implements Node, PayoutPreferenceInterface { _id: ID! default: Boolean email: String id: ID! } # Configuration for the events sent from HackerOne to JIRA type HackeroneToJiraEventsConfiguration implements Node { assignee_changes: Boolean! comments: Boolean! id: ID! public_disclosures: Boolean! rewards: Boolean! state_changes: Boolean! team: Team! } scalar Hash # An interface for the common fields on a HackerOne Invitation interface InvitationInterface { _id: ID! } # User invitation preference type enum InvitationPreferenceTypeEnum { always bounty_only never } # Invitations can be of multiple types union InvitationUnion = InvitationsSoftLaunch # The connection type for InvitationUnion. type InvitationUnionConnection { # A list of edges. edges: [InvitationUnionEdge] # Information to aid in pagination. pageInfo: PageInfo! total_count: Int! } # An edge in a connection. type InvitationUnionEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: InvitationUnion } # An invitation to join a private team as a hacker type InvitationsSoftLaunch implements InvitationInterface, Node { _id: ID! expires_at: String id: ID! markdown_message: String message: String team: Team! token: String } # A JIRA integration for a team type JiraIntegration implements Node { assignee: String base_url: String! created_at: String! custom: String description: String! id: ID! issue_type: Int! labels: String pid: Int! summary: String! team: Team! updated_at: String! } # A JIRA Oauth for a team type JiraOauth implements Node { # Assignables for a project assignables(project_id: Int!): [Hash] configured: Boolean! created_at: String! id: ID! issue_types: [Hash] projects: [Hash] site: String team: Team! updated_at: String! } # A JIRA webhook for a team type JiraWebhook implements Node { created_at: String! id: ID! last_event_received_at: String last_token_issued_at: String process_assignee_change: Boolean! process_comment_add: Boolean! process_priority_change: Boolean! process_resolution_change: Boolean! process_status_change: Boolean! team: Team! updated_at: String! } # Autogenerated input type of LeavePrivateProgram input LeavePrivateProgramInput { # A unique identifier for the client performing the mutation. clientMutationId: String handle: String! } # Autogenerated return type of LeavePrivateProgram type LeavePrivateProgramPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: [Error] me: User was_successful: Boolean! } # Autogenerated input type of LockReport input LockReportInput { # A unique identifier for the client performing the mutation. clientMutationId: String report_id: ID! } # Autogenerated return type of LockReport type LockReportPayload { activities: ActivityUnionConnection activity: ActivitiesComment # A unique identifier for the client performing the mutation. clientMutationId: String new_activity_edge: ActivityUnionEdge report: Report } # Settings for a user's Lufthansa Account type LufthansaAccount implements Node { created_at: String! first_name: String! id: ID! last_name: String! number: String! updated_at: String! } # Autogenerated input type of MarkReportAsNeedsMoreInfo input MarkReportAsNeedsMoreInfoInput { # A unique identifier for the client performing the mutation. clientMutationId: String message: String! report_id: ID! } # Autogenerated return type of MarkReportAsNeedsMoreInfo type MarkReportAsNeedsMoreInfoPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: [Error] query: Query report: Report report_id: ID! was_successful: Boolean! } # Autogenerated input type of MarkReportAsNoise input MarkReportAsNoiseInput { # A unique identifier for the client performing the mutation. clientMutationId: String message: String! report_id: ID! } # Autogenerated return type of MarkReportAsNoise type MarkReportAsNoisePayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: [Error] query: Query report: Report was_successful: Boolean! } # Autogenerated input type of MarkReportAsSignal input MarkReportAsSignalInput { # A unique identifier for the client performing the mutation. clientMutationId: String message: String report_id: ID! } # Autogenerated return type of MarkReportAsSignal type MarkReportAsSignalPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: [Error] query: Query report: Report was_successful: Boolean! } type Mutation { acceptInvitation(input: AcceptInvitationInput!): AcceptInvitationPayload acknowledgeProgramHealthAcknowledgement(input: AcknowledgeProgramHealthAcknowledgementMutationInput!): AcknowledgeProgramHealthAcknowledgementMutationPayload archiveStructuredScope(input: ArchiveStructuredScopeInput!): ArchiveStructuredScopePayload createActivityComment(input: CreateActivityCommentInput!): CreateActivityCommentPayload createBounty(input: CreateBountyInput!): CreateBountyPayload createBountySuggestion(input: CreateBountySuggestionInput!): CreateBountySuggestionPayload createCoinbasePayoutPreference(input: CreateCoinbasePayoutPreferenceInput!): CreateCoinbasePayoutPreferencePayload createCurrencycloudBankTransferPayoutPreference(input: CreateCurrencycloudBankTransferPayoutPreferenceInput!): CreateCurrencycloudBankTransferPayoutPreferencePayload createJiraOauthUrl(input: CreateJiraOauthUrlInput!): CreateJiraOauthUrlPayload createJiraWebhookToken(input: CreateJiraWebhookTokenInput!): CreateJiraWebhookTokenPayload createMailingAddress(input: CreateMailingAddressInput!): CreateMailingAddressPayload createOrUpdateHackeroneToJiraEventsConfiguration(input: CreateOrUpdateHackeroneToJiraEventsConfigurationInput!): CreateOrUpdateHackeroneToJiraEventsConfigurationPayload createOrUpdateJiraIntegration(input: CreateOrUpdateJiraIntegrationInput!): CreateOrUpdateJiraIntegrationPayload createPaypalPreference(input: CreatePaypalPreferenceInput!): CreatePaypalPreferencePayload createSlackPipeline(input: CreateSlackPipelineInput!): CreateSlackPipelinePayload createStructuredScope(input: CreateStructuredScopeInput!): CreateStructuredScopePayload createSurveyAnswer(input: CreateSurveyAnswerInput!): CreateSurveyAnswerPayload createSwag(input: CreateSwagInput!): CreateSwagPayload createTaxForm(input: CreateTaxFormInput!): CreateTaxFormPayload createUserBountiesReport(input: CreateUserBountiesReportInput!): CreateUserBountiesReportPayload createUserLufthansaAccount(input: CreateUserLufthansaAccountInput!): CreateUserLufthansaAccountPayload deleteBiDirectionalJiraIntegration(input: DeleteBiDirectionalJiraIntegrationInput!): DeleteBiDirectionalJiraIntegrationPayload deleteJiraWebhook(input: DeleteJiraWebhookInput!): DeleteJiraWebhookPayload deletePhabricatorIntegration(input: DeletePhabricatorIntegrationInput!): DeletePhabricatorIntegrationPayload deleteSlackPipeline(input: DeleteSlackPipelineInput!): DeleteSlackPipelinePayload deleteTeamMember(input: DeleteTeamMemberInput!): DeleteTeamMemberPayload deleteTeamSlackIntegration(input: DeleteTeamSlackIntegrationInput!): DeleteTeamSlackIntegrationPayload deleteUserLufthansaAccount(input: DeleteUserLufthansaAccountInput!): DeleteUserLufthansaAccountPayload disableTwoFactorAuthentication(input: DisableTwoFactorAuthenticationInput!): DisableTwoFactorAuthenticationPayload dismissProgramHealthAcknowledgement(input: DismissProgramHealthAcknowledgementMutationInput!): DismissProgramHealthAcknowledgementMutationPayload enableTwoFactorAuthentication(input: EnableTwoFactorAuthenticationInput!): EnableTwoFactorAuthenticationPayload enableUser(input: EnableUserInput!): EnableUserPayload enrollForProgram(input: EnrollForProgramInput!): EnrollForProgramPayload generateMfaOtp(input: GenerateMfaOtpInput!): GenerateMfaOtpPayload generateTaxFormUrl(input: GenerateTaxFormUrlInput!): GenerateTaxFormUrlPayload leavePrivateProgram(input: LeavePrivateProgramInput!): LeavePrivateProgramPayload lockReport(input: LockReportInput!): LockReportPayload markReportAsNeedsMoreInfo(input: MarkReportAsNeedsMoreInfoInput!): MarkReportAsNeedsMoreInfoPayload markReportAsNoise(input: MarkReportAsNoiseInput!): MarkReportAsNoisePayload markReportAsSignal(input: MarkReportAsSignalInput!): MarkReportAsSignalPayload rejectInvitation(input: RejectInvitationInput!): RejectInvitationPayload updateAutomaticInvites(input: UpdateAutomaticInvitesInput!): UpdateAutomaticInvitesPayload updateBackupCodes(input: UpdateBackupCodesInput!): UpdateBackupCodesPayload updateInvitationPreferences(input: UpdateInvitationPreferencesInput!): UpdateInvitationPreferencesPayload updateJiraWebhook(input: UpdateJiraWebhookInput!): UpdateJiraWebhookPayload updateMe(input: UpdateMeInput!): UpdateMePayload updatePhabricatorIntegration(input: UpdatePhabricatorIntegrationInput!): UpdatePhabricatorIntegrationPayload updateReportStateToNeedsMoreInfo(input: UpdateReportStateToNeedsMoreInfoInput!): UpdateReportStateToNeedsMoreInfoPayload updateReportStructuredScope(input: UpdateReportStructuredScopeInput!): UpdateReportStructuredScopePayload updateSlackPipeline(input: UpdateSlackPipelineInput!): UpdateSlackPipelinePayload updateSlackUser(input: UpdateSlackUserInput!): UpdateSlackUserPayload updateStructuredScope(input: UpdateStructuredScopeInput!): UpdateStructuredScopePayload updateTeamFancySlackIntegration(input: UpdateTeamFancySlackIntegrationInput!): UpdateTeamFancySlackIntegrationPayload updateTeamMemberVisibility(input: UpdateTeamMemberVisibilityInput!): UpdateTeamMemberVisibilityPayload updateTeamSubmissionState(input: UpdateTeamSubmissionStateInput!): UpdateTeamSubmissionStatePayload updateTeamSubscription(input: UpdateTeamSubscriptionInput!): UpdateTeamSubscriptionPayload updateTeamWeakness(input: UpdateTeamWeaknessInput!): UpdateTeamWeaknessPayload updateUserEmail(input: UpdateUserEmailInput!): UpdateUserEmailPayload updateUserLufthansaAccount(input: UpdateUserLufthansaAccountInput!): UpdateUserLufthansaAccountPayload updateUserPassword(input: UpdateUserPasswordInput!): UpdateUserPasswordPayload } # An object with an ID. interface Node { # ID of the object. id: ID! } # Possible directions for sorting a collection enum OrderDirection { ASC DESC } # Information about pagination in a connection. type PageInfo { # When paginating forwards, the cursor to continue. endCursor: String # When paginating forwards, are there more items? hasNextPage: Boolean! # When paginating backwards, are there more items? hasPreviousPage: Boolean! # When paginating backwards, the cursor to continue. startCursor: String } # The connection type for User. type ParticipantConnection { # A list of edges. edges: [ParticipantWithReputationEdge] # Information to aid in pagination. pageInfo: PageInfo! total_count: Int! year_range: [Int] } # An edge in a connection. type ParticipantWithReputationEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: User # The participant's rank within the team rank: Int # The participant's reputation within the team reputation: Int } # A interface for the common fields on an Payout Preference interface PayoutPreferenceInterface { _id: ID! default: Boolean id: ID! } # A user can have payout preferences for different payment services union PayoutPreferenceUnion = CoinbasePayoutPreferenceType | CurrencycloudBankTransferPayoutPreferenceType | HackeronePayrollPayoutPreferenceType | PaypalPayoutPreferenceType # A Paypal Payout Preference type PaypalPayoutPreferenceType implements Node, PayoutPreferenceInterface { _id: ID! default: Boolean email: String id: ID! } # All available permissions enum PermissionEnum { program_management } # A Phabricator integration for a team type PhabricatorIntegration implements Node { base_url: String created_at: String! description: String id: ID! process_h1_comment_added: Boolean! process_h1_status_change: Boolean! process_phabricator_comment_added: Boolean! process_phabricator_status_change: Boolean! project_tags: String team: Team! title: String updated_at: String! } # Different possible profile picture sizes enum ProfilePictureSizes { # 110x110 large # 82x82 medium # 62x62 small # 260x260 xtralarge } # A program_health_acknowledgement for a team_member type ProgramHealthAcknowledgement implements Node { acknowledged: Boolean created_at: String dismissed: Boolean id: ID! reason: ProgramHealthAcknowledgementReasonEnum team_member: TeamMember! } # The connection type for ProgramHealthAcknowledgement. type ProgramHealthAcknowledgementConnection { # A list of edges. edges: [ProgramHealthAcknowledgementEdge] # Information to aid in pagination. pageInfo: PageInfo! total_count: Int! } # An edge in a connection. type ProgramHealthAcknowledgementEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: ProgramHealthAcknowledgement } # reason which reflect the state of a program health acknowledgement enum ProgramHealthAcknowledgementReasonEnum { failed ok paused review } # Root entity of the Hackerone Schema type Query implements Node { bank_transfer_reference: BankTransferReference clusters( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int order_by: ClusterOrder = {direction: ASC, field: BROWSING_FRIENDLY} ): ClusterConnection external_program(handle: String!): ExternalProgram id: ID! me: User # Fetches an object given its ID. node( # ID of the object. id: ID! ): Node query: Query! report(id: Int!): Report @deprecated(reason: "Query for a Report node at the root level is not recommended.") reports( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String database_id: Int = null # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int # Relay classic does not have support for starting paginationsomewhere in the # middle, see https://github.com/facebook/relay/issues/1312 Workaround is to # add a page argument till Relay supports this limit: Int = null order_by: ReportOrderInput = {direction: DESC, field: id} # Relay classic does not have support for starting paginationsomewhere in the # middle, see https://github.com/facebook/relay/issues/1312 Workaround is to # add a page argument till Relay supports this page: Int = null pre_submission_review_states: [ReportPreSubmissionReviewStateEnum] = null substate: ReportStateEnum without_scope: Boolean = null ): ReportConnection resource(url: URI): ResourceInterface session: Session severity_calculator: SeverityCalculator surveys( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String category: SurveyCategoryEnum # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): SurveyConnectionType team(handle: String!): Team @deprecated(reason: "Query for a Team node at the root level is not recommended. Ref T12456") teams( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String enrollable: Boolean # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int order_by: TeamOrderInput permissions: [PermissionEnum] = [] ): TeamConnection user(username: String!): User @deprecated(reason: "Query for a User node at the root level is not recommended. Ref T12456") users( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): UserConnection } # Autogenerated input type of RejectInvitation input RejectInvitationInput { # A unique identifier for the client performing the mutation. clientMutationId: String handle: String! } # Autogenerated return type of RejectInvitation type RejectInvitationPayload { # A unique identifier for the client performing the mutation. clientMutationId: String me: User } # A HackerOne report type Report implements Node, ResourceInterface { _id: ID! activities( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int not_types: [ActivityTypes] order_by: ActivityOrderInput types: [ActivityTypes] # A timestamp encoded as a string that. When provided, only activities with a # updated_at greater than this value will be resolved. Example: # activities(updated_at_after: "2017-11-30 13:37:12 UTC") updated_at_after: String ): ActivityConnection allow_singular_disclosure_after: String allow_singular_disclosure_at: String assignee: AssigneeUnion attachments: [Attachment] bounties: [Bounty] bounty_awarded_at: String bug_reporter_agreed_on_going_public_at: String cloned_from: Report closed_at: String comments_closed: Boolean created_at: String! cve_ids: [String] disclosed_at: String first_program_activity_at: String i_can_anc_review: Boolean id: ID! latest_activity_at: String latest_public_activity_at: String latest_public_activity_of_reporter_at: String latest_public_activity_of_team_at: String mediation_requested_at: String original_report: Report pre_submission_review_state: String reference: ID reference_link: String reporter: User severity: Severity singular_disclosure_allowed: Boolean singular_disclosure_disabled: Boolean stage: String stale: Boolean state: String structured_scope: StructuredScope substate: String! suggested_bounty: String summaries: [Summary] swag: [Swag] swag_awarded_at: String team: Team team_member_agreed_on_going_public_at: String title: String triaged_at: String # A pre-submission trigger that notified the hacker before submission. This # field is only present for reports filed after February 14, 2016. triggered_pre_submission_trigger: Trigger url: URI! vulnerability_information: String vulnerability_information_html: String weakness: Weakness } # A interface for the common fields on an HackerOne Report Activity interface ReportActivityInterface { attachments: [Attachment] automated_response: Boolean genius_execution_id: ID @deprecated(reason: "This is about to be replaced by .genius_execution") report: Report } # The connection type for Report. type ReportConnection { # Groups and counts reports by the severity rating count_by_severity: [CountBySeverity]! # A list of edges. edges: [ReportEdge] # Information to aid in pagination. pageInfo: PageInfo! total_count: Int! } # An edge in a connection. type ReportEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: Report } # Filters which can be used to query reports enum ReportFilterEnum { assigned assigned_to_me bounty_awarded going_public_team going_public_user hacker_requested_mediation ineligible_for_bounty needs_first_program_response no_bounty_awarded no_swag_awarded not_public public reporter_is_active stale swag_awarded unassigned } # Fields on which a collection of reports can be ordered enum ReportOrderField { created_at id latest_activity_at } input ReportOrderInput { direction: OrderDirection! field: ReportOrderField! } # Pre submission review states a report can be in enum ReportPreSubmissionReviewStateEnum { pre_submission_accepted pre_submission_needs_more_info pre_submission_pending pre_submission_rejected } # States a report can be in enum ReportStateEnum { closed duplicate informative needs_more_info new not_applicable open pre_submission resolved spam triaged } # The connection type for User. type ReporterConnection { # A list of edges. edges: [UserEdge] # Information to aid in pagination. pageInfo: PageInfo! total_count: Int! } # Represents a type that can be retrieved by a URL. interface ResourceInterface { url: URI! } # A HackerOne session type Session implements Node { csrf_token: String! @deprecated(reason: "This token is used to support legacy operations in HackerOne's frontend") id: ID! } # A HackerOne severity for a report type Severity implements Node { _id: ID! attack_complexity: SeverityAttackComplexityEnum attack_vector: SeverityAttackVectorEnum author_type: SeverityAuthorEnum availability: SeverityAvailabilityEnum confidentiality: SeverityConfidentialityEnum created_at: String id: ID! integrity: SeverityIntegrityEnum privileges_required: SeverityPrivilegesRequiredEnum rating: SeverityRatingEnum scope: SeverityScopeEnum score: Float user_id: Int user_interaction: SeverityUserInteractionEnum } # Severity attack complexity enum SeverityAttackComplexityEnum { high low } # Severity attack vector enum SeverityAttackVectorEnum { adjacent local network physical } # Severity author enum SeverityAuthorEnum { Team User } # Severity availability enum SeverityAvailabilityEnum { high low none } # Calculate CVSS Severity score and rating type SeverityCalculator implements Node { calculated_max_severity(availability_requirement: SeveritySecurityRequirementEnum, confidentiality_requirement: SeveritySecurityRequirementEnum, integrity_requirement: SeveritySecurityRequirementEnum): SeverityRatingEnum id: ID! } # Severity confidentiality enum SeverityConfidentialityEnum { high low none } # Severity integrity enum SeverityIntegrityEnum { high low none } # Severity privileges required enum SeverityPrivilegesRequiredEnum { high low none } # Severity rating enum SeverityRatingEnum { critical high low medium none } # Severity scope enum SeverityScopeEnum { changed unchanged } # Severity security requirement rating enum SeveritySecurityRequirementEnum { high low medium none } # Severity user interaction enum SeverityUserInteractionEnum { none required } # SLA types enum SlaTypeEnum { first_program_response report_triage } # Slack channel type SlackChannel implements Node { # ID of the object. id: ID! name: String! } # A Slack integration for a team type SlackIntegration implements Node { channel: String @deprecated(reason: "this field is not used in our new Slack integration") channels: [SlackChannel] id: ID! should_fetch_slack_channels: Boolean! should_fetch_slack_users: Boolean! team: Team! team_url: String! users: [SlackUser] } # A Slack Pipeline Configuration for notifications type SlackPipeline implements Node, ResourceInterface { # The primary key from the database _id: ID! channel: String! descriptive_label: String id: ID! notification_report_agreed_on_going_public: Boolean! notification_report_assignee_changed: Boolean! notification_report_became_public: Boolean! notification_report_bounty_paid: Boolean! notification_report_bounty_suggested: Boolean! notification_report_bug_closed_as_spam: Boolean! notification_report_bug_duplicate: Boolean! notification_report_bug_informative: Boolean! notification_report_bug_needs_more_info: Boolean! notification_report_bug_new: Boolean! notification_report_bug_not_applicable: Boolean! notification_report_closed_as_resolved: Boolean! notification_report_comments_closed: Boolean! notification_report_created: Boolean! notification_report_internal_comment_added: Boolean! notification_report_manually_disclosed: Boolean! notification_report_not_eligible_for_bounty: Boolean! notification_report_public_comment_added: Boolean! notification_report_reopened: Boolean! notification_report_swag_awarded: Boolean! notification_report_triaged: Boolean! team: Team! updated_at: String! url: URI! } # The connection type for SlackPipeline. type SlackPipelineConnection { # A list of edges. edges: [SlackPipelineEdge] # Information to aid in pagination. pageInfo: PageInfo! total_count: Int! } # An edge in a connection. type SlackPipelineEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: SlackPipeline } # Slack user type SlackUser implements Node { # The id provided by Slack _id: String! avatar_small: String! deleted: Boolean! email: String! # ID of the object. id: ID! name: String! real_name: String } # A static participant for a team type StaticParticipant implements Node { _id: ID! avatar(size: ProfilePictureSizes!): String! bio: String external_url: String id: ID! name: String! year: String } # The connection type for StaticParticipant. type StaticParticipantConnection { # A list of edges. edges: [StaticParticipantEdge] # Information to aid in pagination. pageInfo: PageInfo! total_count: Int! } # An edge in a connection. type StaticParticipantEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: StaticParticipant } # A defined scope of a HackerOne program type StructuredScope implements Node, ResourceInterface { _id: ID! archived_at: String asset_identifier: String! asset_type: StructuredScopedAssetTypeEnum availability_requirement: SeveritySecurityRequirementEnum confidentiality_requirement: SeveritySecurityRequirementEnum created_at: String! eligible_for_bounty: Boolean eligible_for_submission: Boolean id: ID! instruction: String integrity_requirement: SeveritySecurityRequirementEnum max_severity: SeverityRatingEnum reference: String rendered_instruction: String reports( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): ReportConnection structured_scope_versions( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): StructuredScopeVersionConnection team: Team updated_at: String url: URI! } # An edge in a connection. type StructuredScopeEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: StructuredScope } # A versioned log of a scope of a HackerOne program type StructuredScopeVersion implements Node, ResourceInterface { _id: ID! archived_at: String created_at: String! id: ID! instruction: String max_severity: String team: Team url: URI! } # The connection type for StructuredScopeVersion. type StructuredScopeVersionConnection { # A list of edges. edges: [StructuredScopeVersionEdge] # Information to aid in pagination. pageInfo: PageInfo! } # An edge in a connection. type StructuredScopeVersionEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: StructuredScopeVersion } # Structured Scope asset type enum enum StructuredScopedAssetTypeEnum { APPLE_STORE_APP_ID CIDR DOWNLOADABLE_EXECUTABLES GOOGLE_PLAY_APP_ID HARDWARE OTHER OTHER_APK OTHER_IPA SOURCE_CODE TESTFLIGHT URL WINDOWS_APP_STORE_APP_ID } # The connection type for StructuredScope. type StructuredScopesConnection { # A list of edges. edges: [StructuredScopeEdge] max_updated_at: String # Information to aid in pagination. pageInfo: PageInfo! total_count: Int! } # Submission states enum SubmissionStateEnum { disabled open paused } # Team subscription action enum enum SubscriptionActionEnum { subscribe_to_all unsubscribe_from_all } # A HackerOne summary for a report type Summary implements Node { _id: ID! category: String! @deprecated(reason: "The implementation of this field contains hard to reason about polymorphism") content: String! created_at: String! id: ID! updated_at: String! user: User! } # A HackerOne survey type Survey implements Node, ResourceInterface { category: String! id: ID! question: String! structured_responses( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): SurveyStructuredResponseConnectionType url: URI! } # Survey categories enum SurveyCategoryEnum { invitation_rejection } # The connection type for Survey. type SurveyConnectionType { # A list of edges. edges: [SurveyEdge] # Information to aid in pagination. pageInfo: PageInfo! total_count: Int! } # An edge in a connection. type SurveyEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: Survey } # Prepared survey response reasons type SurveyStructuredResponse implements Node { _id: ID! enabled: Boolean! helper_text: String id: ID! reason: String! survey: Survey } # The connection type for SurveyStructuredResponse. type SurveyStructuredResponseConnectionType { # A list of edges. edges: [SurveyStructuredResponseEdge] # Information to aid in pagination. pageInfo: PageInfo! total_count: Int! } # An edge in a connection. type SurveyStructuredResponseEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: SurveyStructuredResponse } # A HackerOne swag awarded for a report type Swag implements Node { _id: ID! address: Address @deprecated(reason: "This field is deprecated. The preferred way to access this data is using swag.user.address.") created_at: String id: ID! report: Report sent: Boolean! team: Team user: User } # The connection type for Swag. type SwagConnection { # A list of edges. edges: [SwagEdge] # Information to aid in pagination. pageInfo: PageInfo! } # An edge in a connection. type SwagEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: Swag } # A tax form for a user type TaxForm implements Node { created_at: String! hello_sign_client_id: String id: ID! signed_at: String status: TaxFormStateEnum! type: TaxFormTypeEnum! url: String } # Status of a tax form enum TaxFormStateEnum { expired needs_review rejected requested unavailable valid } # Type of a tax form enum TaxFormTypeEnum { W8BEN W8BENE W9 W9Corporate } # A HackerOne team type Team implements Node, ResourceInterface { # The primary key from the database _id: ID! about: String abuse: Boolean activities( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int not_types: [ActivityTypes] order_by: ActivityOrderInput types: [ActivityTypes] # A timestamp encoded as a string that. When provided, only activities with a # updated_at greater than this value will be resolved. Example: # activities(updated_at_after: "2017-11-30 13:37:12 UTC") updated_at_after: String ): ActivityConnection automatic_invites: Boolean base_bounty: Int bug_count: Int child_teams( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): TeamConnection common_responses( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int order_by: CommonResponseOrder = {direction: ASC, field: title} ): CommonResponseConnection created_at: String currency: String fancy_slack_integration: Boolean fancy_slack_integration_enabled: Boolean! first_response_time: Float hackerone_to_jira_events_configuration: HackeroneToJiraEventsConfiguration handle: String! has_avatar: Boolean has_policy: Boolean i_can_create_jira_webhook: Boolean i_can_destroy_jira_webhook: Boolean! i_can_view_base_bounty: Boolean i_can_view_jira_integration: Boolean i_can_view_jira_webhook: Boolean i_can_view_phabricator_integration: Boolean i_can_view_program_health: Boolean! i_can_view_reports_resolved: Boolean i_can_view_weaknesses: Boolean i_cannot_create_jira_webhook_reasons: [TeamCannotCreateJiraWebhookReasons]! id: ID! inbox_views( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int order_by: TeamInboxViewOrder = {direction: ASC, field: position} visible: Boolean ): TeamInboxViewConnection internet_bug_bounty: Boolean jira_integration: JiraIntegration jira_oauth: JiraOauth jira_phase_3_enabled: Boolean! jira_webhook: JiraWebhook maximum_number_of_team_mediation_requests: Float name: String! new_staleness_threshold: Int offers_bounties: Boolean offers_swag: Boolean open_soft_launch_invitations( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): InvitationUnionConnection @deprecated(reason: "This should be a generic invitation connection.\n Used interim until generic invitation type is defined") participants( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int limit: Int = null year: Int = null ): ParticipantConnection phabricator_integration: PhabricatorIntegration policy_html: String profile_picture(size: ProfilePictureSizes!): String! report_submission_form_intro: String report_template: String reporters( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): ReporterConnection reports( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String database_id: Int = null # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int # Relay classic does not have support for starting paginationsomewhere in the # middle, see https://github.com/facebook/relay/issues/1312 Workaround is to # add a page argument till Relay supports this limit: Int = null order_by: ReportOrderInput = {direction: DESC, field: id} # Relay classic does not have support for starting paginationsomewhere in the # middle, see https://github.com/facebook/relay/issues/1312 Workaround is to # add a page argument till Relay supports this page: Int = null pre_submission_review_states: [ReportPreSubmissionReviewStateEnum] = null state: ReportStateEnum substate: ReportStateEnum violates_minimum_sla: SlaTypeEnum without_scope: Boolean = null ): ReportConnection review_requested_at: String sla_failed_count: Int sla_missed_count: Int slack_integration: SlackIntegration slack_pipelines( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): SlackPipelineConnection state: TeamState! static_participants( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): StaticParticipantConnection structured_scopes( # Returns the elements in the list that come after the specified cursor. after: String archived: Boolean = null asset_type: String = null # Returns the elements in the list that come before the specified cursor. before: String eligible_for_submission: Boolean = null # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int search: String = null ): StructuredScopesConnection submission_state: SubmissionStateEnum! target_signal: Float team_member_groups( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): TeamMemberGroupConnection team_members( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int order_by: TeamMemberOrder = {direction: ASC, field: username} ): TeamMemberConnection triage_active: Boolean triage_time: Float triaged_staleness_threshold: Int twitter_handle: String updated_at: String url: URI! weaknesses( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String cluster_id: ID = null # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int order_by: WeaknessOrder = {direction: ASC, field: name} search: String = null team_weakness_state: [TeamWeaknessStates] = [enabled, disabled, hidden] ): TeamWeaknessConnection website: String whitelisted_hackers( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): ReporterConnection } # Different reasons why a team cannot create a jira webhook enum TeamCannotCreateJiraWebhookReasons { CANNOT_VIEW FEATURE_GATED PROGRAM_PERMISSION_REQUIRED } # The connection type for Team. type TeamConnection { # A list of edges. edges: [TeamEdge] # Information to aid in pagination. pageInfo: PageInfo! total_count: Int! } # An edge in a connection. type TeamEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: Team } # A team report filter preset type TeamInboxView implements Node { _id: ID! assigned_to_group_ids: [Int!]! assigned_to_user_ids: [Int!]! built_in: Boolean! created_at: String! filters: [ReportFilterEnum!]! hackathons: [Int!]! id: ID! key: String! name: String! position: Int! reporter_ids: [Int!]! severities: [String!]! substates: [ReportStateEnum!]! team: Team! text_query: String! updated_at: String! visible: Boolean! } # The connection type for TeamInboxView. type TeamInboxViewConnection { # A list of edges. edges: [TeamInboxViewEdge] # Information to aid in pagination. pageInfo: PageInfo! total_count: Int! } # An edge in a connection. type TeamInboxViewEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: TeamInboxView } input TeamInboxViewOrder { direction: OrderDirection! field: TeamInboxViewOrderField! } # Fields on which a collection of team inbox views can be ordered enum TeamInboxViewOrderField { position } # A HackerOne team member type TeamMember implements Node { # The primary key from the database _id: ID! auto_subscribe: Boolean concealed: Boolean created_at: String! i_can_leave_team: Boolean! id: ID! permissions: [String]! slack_user_id: String team: Team! user: User! } # The connection type for TeamMember. type TeamMemberConnection { # A list of edges. edges: [TeamMemberEdge] # Information to aid in pagination. pageInfo: PageInfo! total_count: Int! } # An edge in a connection. type TeamMemberEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: TeamMember } # A HackerOne team member group type TeamMemberGroup implements Node { _id: ID! created_at: String # ID of the object. id: ID! name: String! permissions: [String]! } # The connection type for TeamMemberGroup. type TeamMemberGroupConnection { # A list of edges. edges: [TeamMemberGroupEdge] # Information to aid in pagination. pageInfo: PageInfo! } # An edge in a connection. type TeamMemberGroupEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: TeamMemberGroup } input TeamMemberOrder { direction: OrderDirection! field: TeamMemberOrderField! } # Fields on which a collection of team members can be ordered enum TeamMemberOrderField { username } # Fields on which a collection of Teams can be ordered enum TeamOrderField { missing_pressure name } input TeamOrderInput { direction: OrderDirection! field: TeamOrderField! } # Different possible team states enum TeamState { da_mode inactive public_mode sandboxed soft_launched } # Team configuration of a weakness type TeamWeakness implements Node { id: ID! instruction: String state: TeamWeaknessStates team: Team! weakness: Weakness! } # The connection type for Weakness. type TeamWeaknessConnection { # A list of edges. edges: [TeamWeaknessEdge] # Information to aid in pagination. pageInfo: PageInfo! total_count: Int! } # An edge in a connection. type TeamWeaknessEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: Weakness team_weakness: TeamWeakness } # Possible states of how a weakness can be configured for a team enum TeamWeaknessStates { disabled enabled hidden } # A HackerOne trigger type Trigger implements Node { _id: ID! id: ID! title: String! } # Tshirt size enum TshirtSizeEnum { M_Large M_Medium M_Small M_XLarge M_XXLarge W_Large W_Medium W_Small W_XLarge W_XXLarge } # Represents a RFC compliant URI string. It is often used to fetch an object. scalar URI # Autogenerated input type of UpdateAutomaticInvites input UpdateAutomaticInvitesInput { # A unique identifier for the client performing the mutation. clientMutationId: String enabled: Boolean! handle: String! } # Autogenerated return type of UpdateAutomaticInvites type UpdateAutomaticInvitesPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash team: Team } # Autogenerated input type of UpdateBackupCodes input UpdateBackupCodesInput { backup_codes: [String]! # A unique identifier for the client performing the mutation. clientMutationId: String password: String! signature: String! totp_code: String! totp_secret: String! } # Autogenerated return type of UpdateBackupCodes type UpdateBackupCodesPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: [String] me: User } # Autogenerated input type of UpdateInvitationPreferences input UpdateInvitationPreferencesInput { # A unique identifier for the client performing the mutation. clientMutationId: String invitation_preference: InvitationPreferenceTypeEnum! } # Autogenerated return type of UpdateInvitationPreferences type UpdateInvitationPreferencesPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash me: User } # Autogenerated input type of UpdateJiraWebhook input UpdateJiraWebhookInput { # A unique identifier for the client performing the mutation. clientMutationId: String jira_webhook_id: ID! process_assignee_change: Boolean process_comment_add: Boolean process_priority_change: Boolean process_resolution_change: Boolean process_status_change: Boolean } # Autogenerated return type of UpdateJiraWebhook type UpdateJiraWebhookPayload { # A unique identifier for the client performing the mutation. clientMutationId: String jira_webhook: JiraWebhook! } # Autogenerated input type of UpdateMe input UpdateMeInput { # A unique identifier for the client performing the mutation. clientMutationId: String tshirt_size: String! } # Autogenerated return type of UpdateMe type UpdateMePayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash me: User } # Autogenerated input type of UpdatePhabricatorIntegration input UpdatePhabricatorIntegrationInput { api_token: String base_url: String # A unique identifier for the client performing the mutation. clientMutationId: String description: String process_h1_comment_added: Boolean process_h1_status_change: Boolean process_phabricator_comment_added: Boolean process_phabricator_status_change: Boolean team_id: ID! title: String } # Autogenerated return type of UpdatePhabricatorIntegration type UpdatePhabricatorIntegrationPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash team: Team } # Autogenerated input type of UpdateReportStateToNeedsMoreInfo input UpdateReportStateToNeedsMoreInfoInput { # A unique identifier for the client performing the mutation. clientMutationId: String message: String! report_id: ID! } # Autogenerated return type of UpdateReportStateToNeedsMoreInfo type UpdateReportStateToNeedsMoreInfoPayload { # A unique identifier for the client performing the mutation. clientMutationId: String report: Report } # Autogenerated input type of UpdateReportStructuredScope input UpdateReportStructuredScopeInput { # A unique identifier for the client performing the mutation. clientMutationId: String report_id: ID! structured_scope_id: ID } # Autogenerated return type of UpdateReportStructuredScope type UpdateReportStructuredScopePayload { # A unique identifier for the client performing the mutation. clientMutationId: String report: Report } # Autogenerated input type of UpdateSlackPipeline input UpdateSlackPipelineInput { channel: String! # A unique identifier for the client performing the mutation. clientMutationId: String descriptive_label: String notification_report_agreed_on_going_public: Boolean! notification_report_assignee_changed: Boolean! notification_report_became_public: Boolean! notification_report_bounty_paid: Boolean! notification_report_bounty_suggested: Boolean! notification_report_bug_closed_as_spam: Boolean! notification_report_bug_duplicate: Boolean! notification_report_bug_informative: Boolean! notification_report_bug_needs_more_info: Boolean! notification_report_bug_new: Boolean! notification_report_bug_not_applicable: Boolean! notification_report_closed_as_resolved: Boolean! notification_report_comments_closed: Boolean! notification_report_created: Boolean! notification_report_internal_comment_added: Boolean! notification_report_manually_disclosed: Boolean! notification_report_not_eligible_for_bounty: Boolean! notification_report_public_comment_added: Boolean! notification_report_reopened: Boolean! notification_report_swag_awarded: Boolean! notification_report_triaged: Boolean! slack_pipeline_id: ID! } # Autogenerated return type of UpdateSlackPipeline type UpdateSlackPipelinePayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash slack_pipeline: SlackPipeline! } # Autogenerated input type of UpdateSlackUser input UpdateSlackUserInput { # A unique identifier for the client performing the mutation. clientMutationId: String slack_user_id: String! team_member_id: ID! } # Autogenerated return type of UpdateSlackUser type UpdateSlackUserPayload { # A unique identifier for the client performing the mutation. clientMutationId: String team_member: TeamMember! } # Autogenerated input type of UpdateStructuredScope input UpdateStructuredScopeInput { asset_identifier: String availability_requirement: String # A unique identifier for the client performing the mutation. clientMutationId: String confidentiality_requirement: String eligible_for_bounty: Boolean eligible_for_submission: Boolean instruction: String integrity_requirement: String reference: String structured_scope_id: ID! } # Autogenerated return type of UpdateStructuredScope type UpdateStructuredScopePayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash query: Query structured_scope: StructuredScope } # Autogenerated input type of UpdateTeamFancySlackIntegration input UpdateTeamFancySlackIntegrationInput { # A unique identifier for the client performing the mutation. clientMutationId: String fancy_slack_integration: Boolean! team_id: ID! } # Autogenerated return type of UpdateTeamFancySlackIntegration type UpdateTeamFancySlackIntegrationPayload { # A unique identifier for the client performing the mutation. clientMutationId: String team: Team } # Autogenerated input type of UpdateTeamMemberVisibility input UpdateTeamMemberVisibilityInput { # A unique identifier for the client performing the mutation. clientMutationId: String concealed: Boolean! team_member_id: ID! } # Autogenerated return type of UpdateTeamMemberVisibility type UpdateTeamMemberVisibilityPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash team_member: TeamMember } # Autogenerated input type of UpdateTeamSubmissionState input UpdateTeamSubmissionStateInput { # A unique identifier for the client performing the mutation. clientMutationId: String handle: String! submission_state: SubmissionStateEnum! } # Autogenerated return type of UpdateTeamSubmissionState type UpdateTeamSubmissionStatePayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash team: Team } # Autogenerated input type of UpdateTeamSubscription input UpdateTeamSubscriptionInput { action: SubscriptionActionEnum auto_subscribe: Boolean! # A unique identifier for the client performing the mutation. clientMutationId: String team_member_id: ID! } # Autogenerated return type of UpdateTeamSubscription type UpdateTeamSubscriptionPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash team_member: TeamMember } # Autogenerated input type of UpdateTeamWeakness input UpdateTeamWeaknessInput { # A unique identifier for the client performing the mutation. clientMutationId: String instruction: String state: TeamWeaknessStates! team_weakness_id: ID! } # Autogenerated return type of UpdateTeamWeakness type UpdateTeamWeaknessPayload { # A unique identifier for the client performing the mutation. clientMutationId: String query: Query team_weakness: TeamWeakness } # Autogenerated input type of UpdateUserEmail input UpdateUserEmailInput { # A unique identifier for the client performing the mutation. clientMutationId: String email: String! password: String! } # Autogenerated return type of UpdateUserEmail type UpdateUserEmailPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: [Error] me: User was_successful: Boolean! } # Autogenerated input type of UpdateUserLufthansaAccount input UpdateUserLufthansaAccountInput { # A unique identifier for the client performing the mutation. clientMutationId: String first_name: String! last_name: String! number: String! } # Autogenerated return type of UpdateUserLufthansaAccount type UpdateUserLufthansaAccountPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash me: User } # Autogenerated input type of UpdateUserPassword input UpdateUserPasswordInput { # A unique identifier for the client performing the mutation. clientMutationId: String current_password: String! password: String! password_confirmation: String! } # Autogenerated return type of UpdateUserPassword type UpdateUserPasswordPayload { # A unique identifier for the client performing the mutation. clientMutationId: String errors: Hash me: User } # A HackerOne user type User implements Node, ResourceInterface { _id: ID! address: Address authentication_service: AuthenticationServiceEnum! badges( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): BadgesUsersConnection bio: String bounties( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String currency: CurrencyCode = null # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): BountyConnection created_at: String! demo_hacker: Boolean! disabled: Boolean! duplicate_users( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): UserConnection email: String i_can_update_username: Boolean id: ID! impact: Float impact_percentile: Float invitation_preference: InvitationPreferenceTypeEnum location: String lufthansa_account: LufthansaAccount member_of_verified_team: Boolean membership(team_handle: String!): TeamMember memberships( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): TeamMemberConnection name: String next_update_username_date: String otp_backup_codes: [String] payout_preferences: [PayoutPreferenceUnion] profile_picture(size: ProfilePictureSizes!): String! profile_pictures: Hash! @deprecated(reason: "Returns all the possible profile pictures instead of just the one you want use .profile_picture instead.") program_health_acknowledgements( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): ProgramHealthAcknowledgementConnection rank: Int remaining_reports(team_handle: String!): Int reports( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String database_id: Int = null # Returns the first _n_ elements from the list. first: Int handle: String # Returns the last _n_ elements from the list. last: Int # Relay classic does not have support for starting paginationsomewhere in the # middle, see https://github.com/facebook/relay/issues/1312 Workaround is to # add a page argument till Relay supports this limit: Int = null order_by: ReportOrderInput = {direction: DESC, field: id} # Relay classic does not have support for starting paginationsomewhere in the # middle, see https://github.com/facebook/relay/issues/1312 Workaround is to # add a page argument till Relay supports this page: Int = null pre_submission_review_states: [ReportPreSubmissionReviewStateEnum] = null substate: ReportStateEnum without_scope: Boolean = null ): ReportConnection reputation: Int signal: Float signal_percentile: Float soft_launch_invitation(team_handle: String!): InvitationsSoftLaunch swag( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): SwagConnection tax_form: TaxForm teams( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String enrollable: Boolean # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int order_by: TeamOrderInput permissions: [PermissionEnum] = [] ): TeamConnection totp_enabled: Boolean totp_supported: Boolean triage_user: Boolean tshirt_size: TshirtSizeEnum unconfirmed_email: String url: URI! username: String! website: String whitelisted_teams( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): TeamConnection } # The connection type for User. type UserConnection { # A list of edges. edges: [UserEdge] # Information to aid in pagination. pageInfo: PageInfo! total_count: Int! } # An edge in a connection. type UserEdge { # A cursor for use in pagination. cursor: String! # The item at the end of the edge. node: User } # The type of vulnerability on a HackerOne report type Weakness implements Node { _id: ID! clusters( # Returns the elements in the list that come after the specified cursor. after: String # Returns the elements in the list that come before the specified cursor. before: String # Returns the first _n_ elements from the list. first: Int # Returns the last _n_ elements from the list. last: Int ): ClusterConnection created_at: String! description: String external_id: String id: ID! name: String! updated_at: String! } input WeaknessOrder { direction: OrderDirection! field: WeaknessOrderField! } # Fields on which a collection of weaknesses can be ordered enum WeaknessOrderField { name } graphql-ruby-2.5.19/benchmark/run.rb000066400000000000000000000467761514115062600173350ustar00rootroot00000000000000# frozen_string_literal: true require "graphql" ADD_WARDEN = false require "jazz" require "benchmark/ips" require "stackprof" require "memory_profiler" require "graphql/batch" require "securerandom" module GraphQLBenchmark QUERY_STRING = GraphQL::Introspection::INTROSPECTION_QUERY DOCUMENT = GraphQL.parse(QUERY_STRING) SCHEMA = Jazz::Schema BENCHMARK_PATH = File.expand_path("../", __FILE__) CARD_SCHEMA = GraphQL::Schema.from_definition(File.read(File.join(BENCHMARK_PATH, "schema.graphql"))) ABSTRACT_FRAGMENTS = GraphQL.parse(File.read(File.join(BENCHMARK_PATH, "abstract_fragments.graphql"))) ABSTRACT_FRAGMENTS_2_QUERY_STRING = File.read(File.join(BENCHMARK_PATH, "abstract_fragments_2.graphql")) ABSTRACT_FRAGMENTS_2 = GraphQL.parse(ABSTRACT_FRAGMENTS_2_QUERY_STRING) BIG_SCHEMA = GraphQL::Schema.from_definition(File.join(BENCHMARK_PATH, "big_schema.graphql")) BIG_QUERY_STRING = File.read(File.join(BENCHMARK_PATH, "big_query.graphql")) BIG_QUERY = GraphQL.parse(BIG_QUERY_STRING) FIELDS_WILL_MERGE_SCHEMA = GraphQL::Schema.from_definition("type Query { hello: String }") FIELDS_WILL_MERGE_QUERY = GraphQL.parse("{ #{Array.new(5000, "hello").join(" ")} }") module_function def self.run(task) Benchmark.ips do |x| case task when "query" x.report("query") { SCHEMA.execute(document: DOCUMENT) } when "validate" x.report("validate - introspection ") { CARD_SCHEMA.validate(DOCUMENT) } x.report("validate - abstract fragments") { CARD_SCHEMA.validate(ABSTRACT_FRAGMENTS) } x.report("validate - abstract fragments 2") { CARD_SCHEMA.validate(ABSTRACT_FRAGMENTS_2) } x.report("validate - big query") { BIG_SCHEMA.validate(BIG_QUERY) } x.report("validate - fields will merge") { FIELDS_WILL_MERGE_SCHEMA.validate(FIELDS_WILL_MERGE_QUERY) } when "scan" require "graphql/c_parser" x.report("scan c - introspection") { GraphQL.scan_with_c(QUERY_STRING) } x.report("scan - introspection") { GraphQL.scan_with_ruby(QUERY_STRING) } x.report("scan c - fragments") { GraphQL.scan_with_c(ABSTRACT_FRAGMENTS_2_QUERY_STRING) } x.report("scan - fragments") { GraphQL.scan_with_ruby(ABSTRACT_FRAGMENTS_2_QUERY_STRING) } x.report("scan c - big query") { GraphQL.scan_with_c(BIG_QUERY_STRING) } x.report("scan - big query") { GraphQL.scan_with_ruby(BIG_QUERY_STRING) } when "parse" # Uncomment this to use the C parser: # require "graphql/c_parser" x.report("parse - introspection") { GraphQL.parse(QUERY_STRING) } x.report("parse - fragments") { GraphQL.parse(ABSTRACT_FRAGMENTS_2_QUERY_STRING) } x.report("parse - big query") { GraphQL.parse(BIG_QUERY_STRING) } else raise("Unexpected task #{task}") end end end def self.profile_parse # To profile the C parser instead: # require "graphql/c_parser" report = MemoryProfiler.report do GraphQL.parse(BIG_QUERY_STRING) GraphQL.parse(QUERY_STRING) GraphQL.parse(ABSTRACT_FRAGMENTS_2_QUERY_STRING) end report.pretty_print end def self.validate_memory FIELDS_WILL_MERGE_SCHEMA.validate(FIELDS_WILL_MERGE_QUERY) report = MemoryProfiler.report do FIELDS_WILL_MERGE_SCHEMA.validate(FIELDS_WILL_MERGE_QUERY) nil end report.pretty_print end def self.profile # Warm up any caches: SCHEMA.execute(document: DOCUMENT) # CARD_SCHEMA.validate(ABSTRACT_FRAGMENTS) res = nil result = StackProf.run(mode: :wall) do # CARD_SCHEMA.validate(ABSTRACT_FRAGMENTS) res = SCHEMA.execute(document: DOCUMENT) end StackProf::Report.new(result).print_text end def self.build_large_schema Class.new(GraphQL::Schema) do query_t = Class.new(GraphQL::Schema::Object) do graphql_name("Query") int_ts = 5.times.map do |i| int_t = Module.new do include GraphQL::Schema::Interface graphql_name "Interface#{i}" 5.times do |n2| field :"field#{n2}", String do argument :arg, String end end end field :"int_field_#{i}", int_t int_t end obj_ts = 100.times.map do |n| input_obj_t = Class.new(GraphQL::Schema::InputObject) do graphql_name("Input#{n}") argument :arg, String end obj_t = Class.new(GraphQL::Schema::Object) do graphql_name("Object#{n}") implements(*int_ts) 20.times do |n2| field :"field#{n2}", String do argument :input, input_obj_t end end field :self_field, self field :int_0_field, int_ts[0] end field :"rootfield#{n}", obj_t obj_t end 10.times do |n| union_t = Class.new(GraphQL::Schema::Union) do graphql_name "Union#{n}" possible_types(*obj_ts.sample(10)) end field :"unionfield#{n}", union_t end end query(query_t) end end def self.profile_boot Benchmark.ips do |x| x.config(time: 10) x.report("Booting large schema") { build_large_schema } end result = StackProf.run(mode: :wall, interval: 1) do build_large_schema end StackProf::Report.new(result).print_text retained_schema = nil report = MemoryProfiler.report do retained_schema = build_large_schema end report.pretty_print end SILLY_LARGE_SCHEMA = build_large_schema def self.profile_small_query_on_large_schema schema = Class.new(SILLY_LARGE_SCHEMA) Benchmark.ips do |x| x.report("Run small query") { schema.execute("{ __typename }") } end result = StackProf.run(mode: :wall, interval: 1) do schema.execute("{ __typename }") end StackProf::Report.new(result).print_text StackProf.run(mode: :wall, out: "tmp/small_query.dump", interval: 1) do schema.execute("{ __typename }") end report = MemoryProfiler.report do schema.execute("{ __typename }") end puts "\n\n" report.pretty_print end def self.profile_large_introspection schema = SILLY_LARGE_SCHEMA Benchmark.ips do |x| x.config(time: 10) x.report("Run large introspection") { schema.to_json } end result = StackProf.run(mode: :wall) do schema.to_json end StackProf::Report.new(result).print_text report = MemoryProfiler.report do schema.to_json end puts "\n\n" report.pretty_print end def self.profile_large_analysis query_str = "query {\n".dup 5.times do |n| query_str << " intField#{n} { " 20.times do |o| query_str << "...Obj#{o}Fields " end query_str << "}\n" end query_str << "}" 20.times do |o| query_str << "fragment Obj#{o}Fields on Object#{o} { " 20.times do |f| query_str << " field#{f}(arg: \"a\")\n" end query_str << " selfField { selfField { selfField { __typename } } }\n" # query_str << " int0Field { ...Int0Fields }" query_str << "}\n" end # query_str << "fragment Int0Fields on Interface0 { __typename }" query = GraphQL::Query.new(SILLY_LARGE_SCHEMA, query_str) analyzers = [ GraphQL::Analysis::AST::FieldUsage, GraphQL::Analysis::AST::QueryDepth, GraphQL::Analysis::AST::QueryComplexity ] Benchmark.ips do |x| x.report("Running introspection") { GraphQL::Analysis::AST.analyze_query(query, analyzers) } end StackProf.run(mode: :wall, out: "last-stackprof.dump", interval: 1) do GraphQL::Analysis::AST.analyze_query(query, analyzers) end result = StackProf.run(mode: :wall, interval: 1) do GraphQL::Analysis::AST.analyze_query(query, analyzers) end StackProf::Report.new(result).print_text report = MemoryProfiler.report do GraphQL::Analysis::AST.analyze_query(query, analyzers) end puts "\n\n" report.pretty_print end # Adapted from https://github.com/rmosolgo/graphql-ruby/issues/861 def self.profile_large_result schema = ProfileLargeResult::Schema document = ProfileLargeResult::ALL_FIELDS Benchmark.ips do |x| x.config(time: 10) x.report("Querying for #{ProfileLargeResult::DATA.size} objects") { schema.execute(document: document) } end result = StackProf.run(mode: :wall, interval: 1) do schema.execute(document: document) end StackProf::Report.new(result).print_text report = MemoryProfiler.report do schema.execute(document: document) end report.pretty_print end def self.profile_small_result schema = ProfileLargeResult::Schema document = GraphQL.parse <<-GRAPHQL query { foos(first: 5) { __typename id int1 int2 string1 string2 foos(first: 5) { __typename string1 string2 foo { __typename int1 } } } } GRAPHQL Benchmark.ips do |x| x.config(time: 10) x.report("Querying for #{ProfileLargeResult::DATA.size} objects") { schema.execute(document: document) } end StackProf.run(mode: :wall, interval: 1, out: "tmp/small.dump") do schema.execute(document: document) end result = StackProf.run(mode: :wall, interval: 1) do schema.execute(document: document) end StackProf::Report.new(result).print_text report = MemoryProfiler.report do schema.execute(document: document) end report.pretty_print end def self.profile_small_introspection schema = ProfileLargeResult::Schema document = GraphQL.parse(GraphQL::Introspection::INTROSPECTION_QUERY) Benchmark.ips do |x| x.config(time: 5) x.report("Introspection") { schema.execute(document: document) } end result = StackProf.run(mode: :wall, interval: 1) do schema.execute(document: document) end StackProf::Report.new(result).print_text report = MemoryProfiler.report do schema.execute(document: document) end report.pretty_print end module ProfileLargeResult def self.eager_or_proc(value) ENV["EAGER"] ? value : -> { value } end DATA_SIZE = 1000 DATA = DATA_SIZE.times.map { eager_or_proc({ id: SecureRandom.uuid, int1: SecureRandom.random_number(100000), int2: SecureRandom.random_number(100000), string1: eager_or_proc(SecureRandom.base64), string2: SecureRandom.base64, boolean1: SecureRandom.random_number(1) == 0, boolean2: SecureRandom.random_number(1) == 0, int_array: eager_or_proc(10.times.map { eager_or_proc(SecureRandom.random_number(100000)) } ), string_array: 10.times.map { SecureRandom.base64 }, boolean_array: 10.times.map { SecureRandom.random_number(1) == 0 }, }) } module Bar include GraphQL::Schema::Interface field :string_array, [String], null: false end module Baz include GraphQL::Schema::Interface implements Bar field :int_array, [Integer], null: false field :boolean_array, [Boolean], null: false end class ExampleExtension < GraphQL::Schema::FieldExtension end class FooType < GraphQL::Schema::Object implements Baz field :id, ID, null: false, extensions: [ExampleExtension] field :int1, Integer, null: false, extensions: [ExampleExtension] field :int2, Integer, null: false, extensions: [ExampleExtension] field :string1, String, null: false do argument :arg1, String, required: false argument :arg2, String, required: false argument :arg3, String, required: false argument :arg4, String, required: false end field :string2, String, null: false do argument :arg1, String, required: false argument :arg2, String, required: false argument :arg3, String, required: false argument :arg4, String, required: false end field :boolean1, Boolean, null: false do argument :arg1, String, required: false argument :arg2, String, required: false argument :arg3, String, required: false argument :arg4, String, required: false end field :boolean2, Boolean, null: false do argument :arg1, String, required: false argument :arg2, String, required: false argument :arg3, String, required: false argument :arg4, String, required: false end field :foos, [FooType], null: false, description: "Return a list of Foo objects" do argument :first, Integer, default_value: DATA_SIZE end def foos(first:) DATA.first(first) end field :foo, FooType def foo DATA.sample end end class QueryType < GraphQL::Schema::Object description "Query root of the system" field :foos, [FooType], null: false, description: "Return a list of Foo objects" do argument :first, Integer, default_value: DATA_SIZE end def foos(first:) DATA.first(first) end end class Schema < GraphQL::Schema query QueryType # use GraphQL::Dataloader lazy_resolve Proc, :call end ALL_FIELDS = GraphQL.parse <<-GRAPHQL query($skip: Boolean = false) { foos { id @skip(if: $skip) int1 int2 string1 string2 boolean1 boolean2 stringArray intArray booleanArray } } GRAPHQL end def self.profile_to_definition require_relative "./batch_loading" schema = ProfileLargeResult::Schema schema.to_definition Benchmark.ips do |x| x.report("to_definition") { schema.to_definition } end result = StackProf.run(mode: :wall, interval: 1) do schema.to_definition end StackProf::Report.new(result).print_text report = MemoryProfiler.report do schema.to_definition end report.pretty_print end def self.profile_from_definition # require "graphql/c_parser" schema_str = SILLY_LARGE_SCHEMA.to_definition Benchmark.ips do |x| x.report("from_definition") { GraphQL::Schema.from_definition(schema_str) } end result = StackProf.run(mode: :wall, interval: 1) do GraphQL::Schema.from_definition(schema_str) end StackProf::Report.new(result).print_text report = MemoryProfiler.report do GraphQL::Schema.from_definition(schema_str) end report.pretty_print end def self.profile_batch_loaders require_relative "./batch_loading" include BatchLoading document = GraphQL.parse <<-GRAPHQL { braves: team(name: "Braves") { ...TeamFields } bulls: team(name: "Bulls") { ...TeamFields } } fragment TeamFields on Team { players { team { players { team { name } } } } } GRAPHQL batch_result = GraphQLBatchSchema.execute(document: document).to_h dataloader_result = GraphQLDataloaderSchema.execute(document: document).to_h no_batch_result = GraphQLNoBatchingSchema.execute(document: document).to_h results = [batch_result, dataloader_result, no_batch_result].uniq if results.size > 1 puts "Batch result:" pp batch_result puts "Dataloader result:" pp dataloader_result puts "No-batch result:" pp no_batch_result raise "Got different results -- fix implementation before benchmarking." end Benchmark.ips do |x| x.report("GraphQL::Batch") { GraphQLBatchSchema.execute(document: document) } x.report("GraphQL::Dataloader") { GraphQLDataloaderSchema.execute(document: document) } x.report("No Batching") { GraphQLNoBatchingSchema.execute(document: document) } x.compare! end puts "========== GraphQL-Batch Memory ==============" report = MemoryProfiler.report do GraphQLBatchSchema.execute(document: document) end report.pretty_print puts "========== Dataloader Memory =================" report = MemoryProfiler.report do GraphQLDataloaderSchema.execute(document: document) end report.pretty_print puts "========== No Batch Memory ==============" report = MemoryProfiler.report do GraphQLNoBatchingSchema.execute(document: document) end report.pretty_print end def self.profile_schema_memory_footprint schema = nil report = MemoryProfiler.report do query_type = Class.new(GraphQL::Schema::Object) do graphql_name "Query" 100.times do |i| type = Class.new(GraphQL::Schema::Object) do graphql_name "Object#{i}" field :f, Integer end field "f#{i}", type end end thing_type = Class.new(GraphQL::Schema::Object) do graphql_name "Thing" field :name, String end mutation_type = Class.new(GraphQL::Schema::Object) do graphql_name "Mutation" 100.times do |i| mutation_class = Class.new(GraphQL::Schema::RelayClassicMutation) do graphql_name "Do#{i}" argument :id, "ID" field :thing, thing_type field :things, thing_type.connection_type end field "f#{i}", mutation: mutation_class end end schema = Class.new(GraphQL::Schema) do query(query_type) mutation(mutation_type) end end report.pretty_print end class StackDepthSchema < GraphQL::Schema class Thing < GraphQL::Schema::Object field :thing, self do argument :lazy, Boolean, default_value: false end def thing(lazy:) if lazy -> { :something } else :something end end field :stack_trace_depth, Integer do argument :lazy, Boolean, default_value: false end def stack_trace_depth(lazy:) get_depth = -> { graphql_caller = caller.select { |c| c.include?("graphql") } graphql_caller.size } if lazy get_depth else get_depth.call end end end class Query < GraphQL::Schema::Object field :thing, Thing def thing :something end end query(Query) lazy_resolve(Proc, :call) end def self.profile_stack_depth query_str = <<-GRAPHQL query($lazyThing: Boolean!, $lazyStackTrace: Boolean!) { thing { thing(lazy: $lazyThing) { thing(lazy: $lazyThing) { thing(lazy: $lazyThing) { thing(lazy: $lazyThing) { stackTraceDepth(lazy: $lazyStackTrace) } } } } } } GRAPHQL eager_res = StackDepthSchema.execute(query_str, variables: { lazyThing: false, lazyStackTrace: false }) lazy_res = StackDepthSchema.execute(query_str, variables: { lazyThing: true, lazyStackTrace: false }) very_lazy_res = StackDepthSchema.execute(query_str, variables: { lazyThing: true, lazyStackTrace: true }) get_depth = ->(result) { result["data"]["thing"]["thing"]["thing"]["thing"]["thing"]["stackTraceDepth"] } puts <<~RESULT Result Depth --------------------- Eager #{get_depth.call(eager_res)} Lazy #{get_depth.call(lazy_res)} Very Lazy #{get_depth.call(very_lazy_res)} RESULT end end graphql-ruby-2.5.19/benchmark/schema.graphql000066400000000000000000000033701514115062600210030ustar00rootroot00000000000000# A big schema for testing type Query { node(id: ID!): Node } interface Node { id: ID! } interface Node2 { id: ID } interface Commentable { id: ID! comments: [Comment!]! } interface Named { name: String! } type Comment implements Node { author: Player body: String! id: ID! } type Card implements Node, Commentable, Node2, Named { name: String! converted_mana_cost: Int! mana_cost: String! colors: [Color!]! power: Int toughness: Int rules_text: String! id: ID! comments: [Comment!]! } type Printing implements Node, Commentable, Node2 { card: Card! expansion: Expansion! rarity: Rarity! artist: Artist! id: ID! comments: [Comment!]! } type Expansion implements Node, Commentable, Named { name: String! code: String! printings: [Printing!]! block: Block! id: ID! comments: [Comment!]! } type Block implements Node, Commentable, Named { name: String! expansions: [Expansion!]! id: ID! comments: [Comment!]! } # Eg shard, guild, clan type Watermark implements Node, Commentable, Named { name: String! cards: [Card!]! colors: [Color!]! id: ID! comments: [Comment!]! } type Artist implements Node, Commentable, Named { name: String! printings: [Printing!]! id: ID! comments: [Comment!]! } type Player implements Node, Commentable, Named { name: String! decks: [Deck!]! id: ID! comments: [Comment!]! } type Deck implements Node, Commentable, Named { name: String! colors: [Color!]! slots: [Slot!]! id: ID! comments: [Comment!]! } type Slot implements Node, Commentable { deck: Deck! card: Card! id: ID! comments: [Comment!]! } enum Color { WHITE BLUE BLACK RED GREEN COLORLESS } enum Rarity { COMMON UNCOMMON RARE MYTHIC_RARE TIMESHIFTED } graphql-ruby-2.5.19/cop/000077500000000000000000000000001514115062600150075ustar00rootroot00000000000000graphql-ruby-2.5.19/cop/development/000077500000000000000000000000001514115062600173315ustar00rootroot00000000000000graphql-ruby-2.5.19/cop/development/context_is_passed_cop.rb000066400000000000000000000030401514115062600242320ustar00rootroot00000000000000# frozen_string_literal: true require 'rubocop' module Cop module Development class ContextIsPassedCop < RuboCop::Cop::Base MSG = <<-MSG This method also accepts `context` as an argument. Pass it so that the returned value will reflect the current query, or use another method that isn't context-dependent. MSG # These are already context-aware or else not query-related def_node_matcher :likely_query_specific_receiver?, " { (send _ {:ast_node :query :context :warden :ctx :query_ctx :query_context}) (lvar {:ast_node :query :context :warden :ctx :query_ctx :query_context}) (ivar {:@query :@context :@warden}) (send _ {:introspection_system}) } " def_node_matcher :method_doesnt_receive_second_context_argument?, <<-MATCHER (send _ {:get_field :get_argument :get_type} _) MATCHER def_node_matcher :method_doesnt_receive_first_context_argument?, <<-MATCHER (send _ {:fields :arguments :types :enum_values}) MATCHER def_node_matcher :is_enum_values_call_without_arguments?, " (send (send _ {:enum :enum_type (ivar {:@enum :@enum_type})}) {:values}) " def on_send(node) if ( method_doesnt_receive_second_context_argument?(node) || method_doesnt_receive_first_context_argument?(node) || is_enum_values_call_without_arguments?(node) ) && !likely_query_specific_receiver?(node.to_a[0]) add_offense(node) end end end end end graphql-ruby-2.5.19/cop/development/no_eval_cop.rb000066400000000000000000000011701514115062600221410ustar00rootroot00000000000000# frozen_string_literal: true require 'rubocop' module Cop module Development class NoEvalCop < RuboCop::Cop::Base MSG_TEMPLATE = "Don't use `%{eval_method_name}` which accepts strings and may result evaluating unexpected code. Use `%{exec_method_name}` instead, and pass a block." def on_send(node) case node.method_name when :module_eval, :class_eval, :instance_eval message = MSG_TEMPLATE % { eval_method_name: node.method_name, exec_method_name: node.method_name.to_s.sub("eval", "exec").to_sym } add_offense node, message: message end end end end end graphql-ruby-2.5.19/cop/development/no_focus_cop.rb000066400000000000000000000007431514115062600223360ustar00rootroot00000000000000# frozen_string_literal: true require 'rubocop' module Cop module Development # Make sure no tests are focused, from https://github.com/rubocop-hq/rubocop/issues/3773#issuecomment-420662102 class NoFocusCop < RuboCop::Cop::Base MSG = 'Remove `focus` from tests.' def_node_matcher :focused?, <<-MATCHER (send nil? :focus) MATCHER def on_send(node) return unless focused?(node) add_offense node end end end end graphql-ruby-2.5.19/cop/development/none_without_block_cop.rb000066400000000000000000000027721514115062600244230ustar00rootroot00000000000000# frozen_string_literal: true require 'rubocop' module Cop module Development # A custom Rubocop rule to catch uses of `.none?` without a block. # # @see https://github.com/rmosolgo/graphql-ruby/pull/2090 class NoneWithoutBlockCop < RuboCop::Cop::Base MSG = <<-MD Instead of `.none?` or `.any?` without a block: - Use `.empty?` to check for an empty collection (faster) - Add a block to explicitly check for `false` (more clear) Run `-a` to replace this with `%{bang}.empty?`. MD def on_block(node) # Since this method was called with a block, it can't be # a case of `.none?` without a block ignore_node(node.send_node) end def on_send(node) if !ignored_node?(node) && (node.method_name == :none? || node.method_name == :any?) && node.arguments.size == 0 add_offense(node, message: MSG % { bang: node.method_name == :none? ? "" : "!.." } ) end end def autocorrect(node) lambda do |corrector| if node.method_name == :none? corrector.replace(node.location.selector, "empty?") else # Backtrack to any chained method calls so we can insert `!` before them full_exp = node while node.parent.send_type? full_exp = node.parent end new_source = "!" + full_exp.source_range.source.sub("any?", "empty?") corrector.replace(full_exp, new_source) end end end end end end graphql-ruby-2.5.19/cop/development/trace_methods_cop.rb000066400000000000000000000061071514115062600233440ustar00rootroot00000000000000# frozen_string_literal: true require 'rubocop' module Cop module Development class TraceMethodsCop < RuboCop::Cop::Base extend RuboCop::Cop::AutoCorrector TRACE_HOOKS = [ :analyze_multiplex, :analyze_query, :authorized, :authorized_lazy, :begin_analyze_multiplex, :begin_authorized, :begin_dataloader, :begin_dataloader_source, :begin_execute_field, :begin_resolve_type, :begin_validate, :dataloader_fiber_exit, :dataloader_fiber_resume, :dataloader_fiber_yield, :dataloader_spawn_execution_fiber, :dataloader_spawn_source_fiber, :end_analyze_multiplex, :end_authorized, :end_dataloader, :end_dataloader_source, :end_execute_field, :end_resolve_type, :end_validate, :execute_field, :execute_field_lazy, :execute_multiplex, :execute_query, :execute_query_lazy, :lex, :parse, :resolve_type, :resolve_type_lazy, :validate, ] MSG = "Trace methods should call `super` to pass control to other traces" def on_def(node) if TRACE_HOOKS.include?(node.method_name) && !node.each_descendant(:super, :zsuper).any? add_offense(node) do |corrector| if node.body offset = node.loc.column + 2 corrector.insert_after(node.body.loc.expression, "\n#{' ' * offset}super") end end end end def on_module(node) if node.defined_module_name.to_s.end_with?("Trace") all_defs = [] node.body.each_child_node do |body_node| if body_node.def_type? all_defs << body_node.method_name end end missing_defs = TRACE_HOOKS - all_defs redundant_defs = [ # Not really necessary for making a good trace: :lex, :analyze_query, :execute_query, :execute_query_lazy, # Only useful for isolated event tracking: :begin_dataloader, :end_dataloader, :dataloader_fiber_exit, :dataloader_spawn_execution_fiber, :dataloader_spawn_source_fiber ] missing_defs.each do |missing_def| if all_defs.include?(:"begin_#{missing_def}") && all_defs.include?(:"end_#{missing_def}") redundant_defs << missing_def redundant_defs << :"#{missing_def}_lazy" end missing_name = missing_def.to_s if missing_name.start_with?("begin") && all_defs.include?(:"#{missing_name.sub("begin_", "")}") redundant_defs << missing_def elsif missing_name.start_with?("end") && all_defs.include?(:"#{missing_name.sub("end_", "")}") redundant_defs << missing_def end end missing_defs -= redundant_defs if missing_defs.any? add_offense(node, message: "Missing some trace hook methods:\n\n- #{missing_defs.join("\n- ")}") end end end end end end graphql-ruby-2.5.19/gemfiles/000077500000000000000000000000001514115062600160215ustar00rootroot00000000000000graphql-ruby-2.5.19/gemfiles/mongoid_8.gemfile000066400000000000000000000005041514115062600212350ustar00rootroot00000000000000# This file was generated by Appraisal source "https://rubygems.org" gem 'logger' gem "bootsnap" gem "ruby-prof", platform: :ruby gem "pry" gem "pry-stack_explorer", platform: :ruby gem "mongoid", "~> 8.0" gem "libev_scheduler" gem "evt" gem "async" gem "concurrent-ruby", "1.3.4" gem "minitest-mock" gemspec path: "../" graphql-ruby-2.5.19/gemfiles/mongoid_9.gemfile000066400000000000000000000004301514115062600212340ustar00rootroot00000000000000# This file was generated by Appraisal source "https://rubygems.org" gem "bootsnap" gem "ruby-prof", platform: :ruby gem "pry" gem "pry-stack_explorer", platform: :ruby gem "mongoid", "~> 9.0" gem "libev_scheduler" gem "evt" gem "async" gem "minitest-mock" gemspec path: "../" graphql-ruby-2.5.19/gemfiles/pronto.gemfile000066400000000000000000000001461514115062600206750ustar00rootroot00000000000000source "https://rubygems.org" gem "pronto" gem "pronto-rubocop" gem "pronto-undercover" gem "base64" graphql-ruby-2.5.19/gemfiles/rails_7.2_postgresql.gemfile000066400000000000000000000005271514115062600233420ustar00rootroot00000000000000# This file was generated by Appraisal source "https://rubygems.org" gem "bootsnap" gem "ruby-prof", platform: :ruby gem "pry" gem "pry-stack_explorer", platform: :ruby gem "rails", "~> 7.2.0", require: "rails/all" gem "pg", platform: :ruby gem "sequel" gem "evt" gem "async" gem "libev_scheduler" gem "google-protobuf" gemspec path: "../" graphql-ruby-2.5.19/gemfiles/rails_8.0.gemfile000066400000000000000000000005431514115062600210540ustar00rootroot00000000000000# This file was generated by Appraisal source "https://rubygems.org" gem "bootsnap" gem "ruby-prof", platform: :ruby gem "pry" gem "pry-stack_explorer", platform: :ruby gem "rails", "~> 8.0.0", require: "rails/all" gem "sqlite3" gem "pg", platform: :ruby gem "sequel" gem "evt" gem "async" gem "google-protobuf" gem "minitest-mock" gemspec path: "../" graphql-ruby-2.5.19/gemfiles/rails_8.1.gemfile000066400000000000000000000005431514115062600210550ustar00rootroot00000000000000# This file was generated by Appraisal source "https://rubygems.org" gem "bootsnap" gem "ruby-prof", platform: :ruby gem "pry" gem "pry-stack_explorer", platform: :ruby gem "rails", "~> 8.1.0", require: "rails/all" gem "sqlite3" gem "pg", platform: :ruby gem "sequel" gem "evt" gem "async" gem "google-protobuf" gem "minitest-mock" gemspec path: "../" graphql-ruby-2.5.19/gemfiles/rails_master.gemfile000066400000000000000000000013721514115062600220430ustar00rootroot00000000000000# This file was generated by Appraisal source "https://rubygems.org" gem "bootsnap" gem "ruby-prof", platform: :ruby gem "pry" gem "pry-stack_explorer", platform: :ruby gem "rails", github: "rails/rails", require: "rails/all", ref: "main" gem 'sqlite3' gem 'pg' gem "sequel" gem "evt" if RUBY_ENGINE == "ruby" # This doesn't work on truffle-ruby because there's no `IO::READABLE` gem "libev_scheduler" end gem "async" gem "google-protobuf" gem "redis" gem 'puma' gem 'sprockets-rails' gem 'capybara' gem 'selenium-webdriver' gem "minitest-mock" gemspec path: "../" if (cred = Bundler.settings["GEMS__GRAPHQL__PRO"]) && !cred.empty? gem "graphql-pro", source: "https://gems.graphql.pro" gem "graphql-enterprise", source: "https://gems.graphql.pro" end graphql-ruby-2.5.19/graphql-c_parser/000077500000000000000000000000001514115062600174605ustar00rootroot00000000000000graphql-ruby-2.5.19/graphql-c_parser/CHANGELOG.md000066400000000000000000000015751514115062600213010ustar00rootroot00000000000000# GraphQL::CParser ## 1.1.3 - Fix to disallow non-null sign (`!`) in fragment conditions #5347 ## 1.1.2 - Fix to handle strings with null bytes #5193 ## 1.1.1 - Add support for `Schema.max_query_string_tokens` #4929 ## 1.1.0 - Drop support for Ruby 2.7 #4899 - Reduce allocation of repeated strings for identifiers when parsing schemas #4899 ## 1.0.8 - Support directives on variable definitions, requires `graphql` 2.2.10+ #4847 ## 1.0.5 - Properly parse integers with leading zeros as Integers, not Floats #4556 ## 1.0.4 - Use UTF-8 encoding for static strings #4526 ## 1.0.3 - Raise a `ParseError` on bad Unicode escapes (like the Ruby parser) #4514 - Force UTF-8 encoding (like the Ruby parser) #4467 ## 1.0.2 - Remove `.y` and `.rl` files to avoid triggering build tasks during install ## 1.0.1 - Fix gem files (to include `ext`) ## 1.0.0 - Release GraphQL::CParser graphql-ruby-2.5.19/graphql-c_parser/Rakefile000066400000000000000000000004301514115062600211220ustar00rootroot00000000000000# frozen_string_literal: true require "bundler/gem_helper" # use a custom tag to avoid conflicting with GraphQL-Ruby tags in the same git repo class CustomGemHelper < Bundler::GemHelper def version_tag "graphql-c_parser-v#{version}" end end CustomGemHelper.install_tasks graphql-ruby-2.5.19/graphql-c_parser/ext/000077500000000000000000000000001514115062600202605ustar00rootroot00000000000000graphql-ruby-2.5.19/graphql-c_parser/ext/graphql_c_parser_ext/000077500000000000000000000000001514115062600244545ustar00rootroot00000000000000graphql-ruby-2.5.19/graphql-c_parser/ext/graphql_c_parser_ext/extconf.rb000066400000000000000000000001351514115062600264460ustar00rootroot00000000000000# frozen_string_literal: true require 'mkmf' create_makefile 'graphql/graphql_c_parser_ext' graphql-ruby-2.5.19/graphql-c_parser/ext/graphql_c_parser_ext/graphql_c_parser_ext.c000066400000000000000000000017471514115062600310250ustar00rootroot00000000000000#include "graphql_c_parser_ext.h" VALUE GraphQL_CParser_Lexer_tokenize_with_c_internal(VALUE self, VALUE query_string, VALUE fstring_identifiers, VALUE reject_numbers_followed_by_names, VALUE max_tokens) { return tokenize(query_string, RTEST(fstring_identifiers), RTEST(reject_numbers_followed_by_names), FIX2INT(max_tokens)); } VALUE GraphQL_CParser_Parser_c_parse(VALUE self) { yyparse(self, rb_ivar_get(self, rb_intern("@filename"))); return Qnil; } void Init_graphql_c_parser_ext() { VALUE GraphQL = rb_define_module("GraphQL"); VALUE CParser = rb_define_module_under(GraphQL, "CParser"); VALUE Lexer = rb_define_module_under(CParser, "Lexer"); rb_define_singleton_method(Lexer, "tokenize_with_c_internal", GraphQL_CParser_Lexer_tokenize_with_c_internal, 4); setup_static_token_variables(); VALUE Parser = rb_define_class_under(CParser, "Parser", rb_cObject); rb_define_method(Parser, "c_parse", GraphQL_CParser_Parser_c_parse, 0); initialize_node_class_variables(); } graphql-ruby-2.5.19/graphql-c_parser/ext/graphql_c_parser_ext/graphql_c_parser_ext.h000066400000000000000000000002511514115062600310170ustar00rootroot00000000000000#ifndef Graphql_ext_h #define Graphql_ext_h #include #include #include "lexer.h" #include "parser.h" void Init_graphql_c_parser_ext(); #endif graphql-ruby-2.5.19/graphql-c_parser/ext/graphql_c_parser_ext/lexer.c000066400000000000000000001534221514115062600257460ustar00rootroot00000000000000#line 1 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" #line 106 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" #line 8 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" static const char _graphql_c_lexer_trans_keys[] = { 1, 22, 4, 43, 14, 47, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 49, 4, 22, 4, 4, 4, 4, 4, 22, 4, 4, 4, 4, 14, 15, 14, 15, 10, 15, 12, 12, 0, 49, 0, 0, 1, 22, 4, 4, 4, 4, 4, 4, 4, 22, 4, 4, 4, 4, 1, 1, 14, 15, 12, 12, 10, 29, 14, 15, 12, 15, 12, 12, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 14, 46, 0 }; static const signed char _graphql_c_lexer_char_class[] = { 0, 1, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 3, 4, 5, 6, 2, 7, 2, 8, 9, 2, 10, 0, 11, 12, 13, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 16, 2, 2, 17, 2, 2, 18, 19, 19, 19, 19, 20, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 21, 22, 23, 2, 24, 2, 25, 26, 27, 28, 29, 30, 31, 32, 33, 19, 19, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 19, 45, 46, 19, 47, 48, 49, 0 }; static const short _graphql_c_lexer_index_offsets[] = { 0, 22, 62, 96, 129, 162, 195, 228, 261, 294, 327, 363, 382, 383, 384, 403, 404, 405, 407, 409, 415, 416, 466, 467, 489, 490, 491, 492, 511, 512, 513, 514, 516, 517, 537, 539, 543, 544, 577, 610, 643, 676, 709, 742, 775, 808, 841, 874, 907, 940, 973, 1006, 1039, 1072, 1105, 1138, 1171, 1204, 1237, 1270, 1303, 1336, 1369, 1402, 1435, 1468, 1501, 1534, 1567, 1600, 1633, 1666, 1699, 1732, 1765, 1798, 1831, 1864, 1897, 1930, 1963, 1996, 2029, 2062, 2095, 2128, 2161, 2194, 2227, 2260, 2293, 2326, 2359, 2392, 2425, 2458, 2491, 2524, 2557, 2590, 2623, 2656, 2689, 2722, 2755, 2788, 2821, 2854, 2887, 2920, 2953, 2986, 3019, 3052, 3085, 3118, 3151, 3184, 3217, 3250, 3283, 3316, 3349, 3382, 3415, 3448, 3481, 3514, 3547, 3580, 3613, 3646, 0 }; static const short _graphql_c_lexer_indices[] = { 0, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 4, 5, 5, 0, 0, 0, 5, 5, 0, 0, 0, 0, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 7, 7, 0, 0, 0, 7, 7, 0, 0, 0, 0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 0, 0, 0, 8, 8, 0, 0, 0, 0, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 0, 0, 0, 9, 9, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 0, 0, 0, 10, 10, 0, 0, 0, 0, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 0, 0, 0, 11, 11, 0, 0, 0, 0, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 0, 0, 0, 12, 12, 0, 0, 0, 0, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 0, 0, 0, 12, 12, 0, 0, 0, 0, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 0, 0, 1, 15, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 16, 17, 18, 19, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 16, 20, 21, 23, 23, 25, 25, 26, 26, 24, 24, 25, 25, 27, 30, 31, 29, 32, 33, 34, 35, 36, 37, 38, 29, 39, 40, 29, 41, 42, 43, 44, 45, 46, 46, 47, 29, 48, 46, 46, 46, 46, 49, 50, 51, 46, 46, 52, 46, 53, 54, 55, 46, 56, 57, 58, 59, 60, 46, 46, 46, 61, 62, 63, 30, 65, 1, 1, 66, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 14, 69, 70, 71, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 16, 72, 18, 73, 41, 42, 75, 26, 26, 76, 76, 23, 23, 76, 76, 76, 76, 77, 76, 76, 76, 76, 76, 76, 76, 76, 77, 25, 25, 75, 74, 42, 42, 78, 46, 46, 13, 13, 13, 46, 46, 13, 13, 13, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 80, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 81, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 82, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 83, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 84, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 85, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 86, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 87, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 88, 46, 46, 46, 46, 46, 46, 46, 46, 89, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 90, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 91, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 92, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 93, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 94, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 95, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 96, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 97, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 98, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 99, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 100, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 101, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 102, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 103, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 104, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 105, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 106, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 107, 108, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 109, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 110, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 111, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 112, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 113, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 114, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 115, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 116, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 117, 46, 46, 46, 118, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 119, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 120, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 121, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 122, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 123, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 124, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 125, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 126, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 127, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 128, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 129, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 130, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 131, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 132, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 133, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 134, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 135, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 136, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 137, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 138, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 139, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 140, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 141, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 142, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 143, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 144, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 145, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 146, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 147, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 148, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 149, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 150, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 151, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 152, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 153, 46, 46, 46, 46, 46, 46, 154, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 155, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 156, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 157, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 158, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 159, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 160, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 161, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 162, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 163, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 164, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 165, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 166, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 167, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 168, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 169, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 170, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 171, 46, 46, 46, 46, 46, 172, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 173, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 174, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 175, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 176, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 177, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 178, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 179, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 79, 79, 79, 46, 46, 79, 79, 79, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 180, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 0 }; static const signed char _graphql_c_lexer_index_defaults[] = { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 14, 14, 14, 14, 14, 22, 24, 24, 0, 29, 64, 1, 67, 68, 68, 14, 14, 14, 34, 65, 74, 76, 76, 74, 65, 13, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 0 }; static const short _graphql_c_lexer_cond_targs[] = { 21, 0, 21, 1, 2, 3, 6, 4, 5, 7, 8, 9, 10, 21, 11, 12, 14, 13, 25, 15, 16, 27, 21, 33, 21, 34, 18, 21, 21, 21, 22, 21, 21, 23, 30, 21, 21, 21, 21, 31, 36, 32, 35, 21, 21, 21, 37, 21, 21, 38, 46, 53, 63, 81, 88, 91, 92, 96, 105, 123, 128, 21, 21, 21, 21, 21, 24, 21, 21, 26, 21, 28, 29, 21, 21, 17, 21, 19, 20, 21, 39, 40, 41, 42, 43, 44, 45, 37, 47, 49, 48, 37, 50, 51, 52, 37, 54, 57, 55, 56, 37, 58, 59, 60, 61, 62, 37, 64, 72, 65, 66, 67, 68, 69, 70, 71, 37, 73, 75, 74, 37, 76, 77, 78, 79, 80, 37, 82, 83, 84, 85, 86, 87, 37, 89, 90, 37, 37, 93, 94, 95, 37, 97, 98, 99, 100, 101, 102, 103, 104, 37, 106, 113, 107, 110, 108, 109, 37, 111, 112, 37, 114, 115, 116, 117, 118, 119, 120, 121, 122, 37, 124, 126, 125, 37, 127, 37, 129, 130, 131, 37, 0 }; static const signed char _graphql_c_lexer_cond_actions[] = { 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 4, 5, 6, 7, 0, 0, 8, 0, 11, 0, 12, 13, 6, 0, 14, 15, 16, 17, 0, 6, 6, 6, 18, 19, 20, 21, 22, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 25, 26, 27, 28, 29, 30, 31, 0, 32, 4, 4, 33, 34, 0, 35, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 37, 0, 0, 0, 38, 0, 0, 0, 39, 0, 0, 0, 0, 40, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 43, 0, 0, 0, 0, 0, 44, 0, 0, 0, 0, 0, 0, 45, 0, 0, 46, 47, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 49, 0, 0, 0, 0, 0, 0, 50, 0, 0, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 53, 0, 54, 0, 0, 0, 55, 0 }; static const signed char _graphql_c_lexer_to_state_actions[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; static const signed char _graphql_c_lexer_from_state_actions[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; static const signed char _graphql_c_lexer_eof_trans[] = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 14, 14, 14, 14, 14, 14, 23, 25, 25, 1, 29, 65, 66, 68, 69, 69, 69, 69, 69, 74, 66, 75, 77, 77, 75, 66, 14, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 0 }; static const int graphql_c_lexer_start = 21; static const int graphql_c_lexer_first_final = 21; static const int graphql_c_lexer_error = -1; static const int graphql_c_lexer_en_main = 21; #line 108 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" #include #include #define INIT_STATIC_TOKEN_VARIABLE(token_name) \ static VALUE GraphQLTokenString##token_name; INIT_STATIC_TOKEN_VARIABLE(ON) INIT_STATIC_TOKEN_VARIABLE(FRAGMENT) INIT_STATIC_TOKEN_VARIABLE(QUERY) INIT_STATIC_TOKEN_VARIABLE(MUTATION) INIT_STATIC_TOKEN_VARIABLE(SUBSCRIPTION) INIT_STATIC_TOKEN_VARIABLE(REPEATABLE) INIT_STATIC_TOKEN_VARIABLE(RCURLY) INIT_STATIC_TOKEN_VARIABLE(LCURLY) INIT_STATIC_TOKEN_VARIABLE(RBRACKET) INIT_STATIC_TOKEN_VARIABLE(LBRACKET) INIT_STATIC_TOKEN_VARIABLE(RPAREN) INIT_STATIC_TOKEN_VARIABLE(LPAREN) INIT_STATIC_TOKEN_VARIABLE(COLON) INIT_STATIC_TOKEN_VARIABLE(VAR_SIGN) INIT_STATIC_TOKEN_VARIABLE(DIR_SIGN) INIT_STATIC_TOKEN_VARIABLE(ELLIPSIS) INIT_STATIC_TOKEN_VARIABLE(EQUALS) INIT_STATIC_TOKEN_VARIABLE(BANG) INIT_STATIC_TOKEN_VARIABLE(PIPE) INIT_STATIC_TOKEN_VARIABLE(AMP) INIT_STATIC_TOKEN_VARIABLE(SCHEMA) INIT_STATIC_TOKEN_VARIABLE(SCALAR) INIT_STATIC_TOKEN_VARIABLE(EXTEND) INIT_STATIC_TOKEN_VARIABLE(IMPLEMENTS) INIT_STATIC_TOKEN_VARIABLE(INTERFACE) INIT_STATIC_TOKEN_VARIABLE(UNION) INIT_STATIC_TOKEN_VARIABLE(ENUM) INIT_STATIC_TOKEN_VARIABLE(DIRECTIVE) INIT_STATIC_TOKEN_VARIABLE(INPUT) static VALUE GraphQL_type_str; static VALUE GraphQL_true_str; static VALUE GraphQL_false_str; static VALUE GraphQL_null_str; typedef enum TokenType { AMP, BANG, COLON, DIRECTIVE, DIR_SIGN, ENUM, ELLIPSIS, EQUALS, EXTEND, FALSE_LITERAL, FLOAT, FRAGMENT, IDENTIFIER, INPUT, IMPLEMENTS, INT, INTERFACE, LBRACKET, LCURLY, LPAREN, MUTATION, NULL_LITERAL, ON, PIPE, QUERY, RBRACKET, RCURLY, REPEATABLE, RPAREN, SCALAR, SCHEMA, STRING, SUBSCRIPTION, TRUE_LITERAL, TYPE_LITERAL, UNION, VAR_SIGN, BLOCK_STRING, QUOTED_STRING, UNKNOWN_CHAR, COMMENT, BAD_UNICODE_ESCAPE } TokenType; typedef struct Meta { int line; int col; char *query_cstr; char *pe; VALUE tokens; int dedup_identifiers; int reject_numbers_followed_by_names; int preceeded_by_number; int max_tokens; int tokens_count; } Meta; #define STATIC_VALUE_TOKEN(token_type, content_str) \ case token_type: \ token_sym = ID2SYM(rb_intern(#token_type)); \ token_content = GraphQLTokenString##token_type; \ break; #define DYNAMIC_VALUE_TOKEN(token_type) \ case token_type: \ token_sym = ID2SYM(rb_intern(#token_type)); \ token_content = rb_utf8_str_new(ts, te - ts); \ break; void emit(TokenType tt, char *ts, char *te, Meta *meta) { meta->tokens_count++; // -1 indicates that there is no limit: if (meta->max_tokens > 0 && meta->tokens_count > meta->max_tokens) { VALUE mGraphQL = rb_const_get_at(rb_cObject, rb_intern("GraphQL")); VALUE cParseError = rb_const_get_at(mGraphQL, rb_intern("ParseError")); VALUE exception = rb_funcall( cParseError, rb_intern("new"), 4, rb_str_new_cstr("This query is too large to execute."), LONG2NUM(meta->line), LONG2NUM(meta->col), rb_str_new_cstr(meta->query_cstr) ); rb_exc_raise(exception); } int quotes_length = 0; // set by string tokens below int line_incr = 0; VALUE token_sym = Qnil; VALUE token_content = Qnil; int this_token_is_number = 0; switch(tt) { STATIC_VALUE_TOKEN(ON, "on") STATIC_VALUE_TOKEN(FRAGMENT, "fragment") STATIC_VALUE_TOKEN(QUERY, "query") STATIC_VALUE_TOKEN(MUTATION, "mutation") STATIC_VALUE_TOKEN(SUBSCRIPTION, "subscription") STATIC_VALUE_TOKEN(REPEATABLE, "repeatable") STATIC_VALUE_TOKEN(RCURLY, "}") STATIC_VALUE_TOKEN(LCURLY, "{") STATIC_VALUE_TOKEN(RBRACKET, "]") STATIC_VALUE_TOKEN(LBRACKET, "[") STATIC_VALUE_TOKEN(RPAREN, ")") STATIC_VALUE_TOKEN(LPAREN, "(") STATIC_VALUE_TOKEN(COLON, ":") STATIC_VALUE_TOKEN(VAR_SIGN, "$") STATIC_VALUE_TOKEN(DIR_SIGN, "@") STATIC_VALUE_TOKEN(ELLIPSIS, "...") STATIC_VALUE_TOKEN(EQUALS, "=") STATIC_VALUE_TOKEN(BANG, "!") STATIC_VALUE_TOKEN(PIPE, "|") STATIC_VALUE_TOKEN(AMP, "&") STATIC_VALUE_TOKEN(SCHEMA, "schema") STATIC_VALUE_TOKEN(SCALAR, "scalar") STATIC_VALUE_TOKEN(EXTEND, "extend") STATIC_VALUE_TOKEN(IMPLEMENTS, "implements") STATIC_VALUE_TOKEN(INTERFACE, "interface") STATIC_VALUE_TOKEN(UNION, "union") STATIC_VALUE_TOKEN(ENUM, "enum") STATIC_VALUE_TOKEN(DIRECTIVE, "directive") STATIC_VALUE_TOKEN(INPUT, "input") // For these, the enum name doesn't match the symbol name: case TYPE_LITERAL: token_sym = ID2SYM(rb_intern("TYPE")); token_content = GraphQL_type_str; break; case TRUE_LITERAL: token_sym = ID2SYM(rb_intern("TRUE")); token_content = GraphQL_true_str; break; case FALSE_LITERAL: token_sym = ID2SYM(rb_intern("FALSE")); token_content = GraphQL_false_str; break; case NULL_LITERAL: token_sym = ID2SYM(rb_intern("NULL")); token_content = GraphQL_null_str; break; case IDENTIFIER: if (meta->reject_numbers_followed_by_names && meta->preceeded_by_number) { VALUE mGraphQL = rb_const_get_at(rb_cObject, rb_intern("GraphQL")); VALUE mCParser = rb_const_get_at(mGraphQL, rb_intern("CParser")); VALUE prev_token = rb_ary_entry(meta->tokens, -1); VALUE exception = rb_funcall( mCParser, rb_intern("prepare_number_name_parse_error"), 5, LONG2NUM(meta->line), LONG2NUM(meta->col), rb_str_new_cstr(meta->query_cstr), rb_ary_entry(prev_token, 3), rb_utf8_str_new(ts, te - ts) ); rb_exc_raise(exception); } token_sym = ID2SYM(rb_intern("IDENTIFIER")); if (meta->dedup_identifiers) { token_content = rb_enc_interned_str(ts, te - ts, rb_utf8_encoding()); } else { token_content = rb_utf8_str_new(ts, te - ts); } break; // Can't use these while we're in backwards-compat mode: // DYNAMIC_VALUE_TOKEN(INT) // DYNAMIC_VALUE_TOKEN(FLOAT) case INT: token_sym = ID2SYM(rb_intern("INT")); token_content = rb_utf8_str_new(ts, te - ts); this_token_is_number = 1; break; case FLOAT: token_sym = ID2SYM(rb_intern("FLOAT")); token_content = rb_utf8_str_new(ts, te - ts); this_token_is_number = 1; break; DYNAMIC_VALUE_TOKEN(COMMENT) case UNKNOWN_CHAR: if (ts[0] == '\0') { return; } else { token_content = rb_utf8_str_new(ts, te - ts); token_sym = ID2SYM(rb_intern("UNKNOWN_CHAR")); break; } case QUOTED_STRING: quotes_length = 1; token_content = rb_utf8_str_new(ts + quotes_length, (te - ts - (2 * quotes_length))); token_sym = ID2SYM(rb_intern("STRING")); break; case BLOCK_STRING: token_sym = ID2SYM(rb_intern("STRING")); quotes_length = 3; token_content = rb_utf8_str_new(ts + quotes_length, (te - ts - (2 * quotes_length))); line_incr = FIX2INT(rb_funcall(token_content, rb_intern("count"), 1, rb_utf8_str_new_cstr("\n"))); break; // These are used only by the parser, this is never reached case STRING: case BAD_UNICODE_ESCAPE: break; } if (token_sym != Qnil) { if (tt == BLOCK_STRING || tt == QUOTED_STRING) { VALUE mGraphQL = rb_const_get_at(rb_cObject, rb_intern("GraphQL")); VALUE mGraphQLLanguage = rb_const_get_at(mGraphQL, rb_intern("Language")); VALUE mGraphQLLanguageLexer = rb_const_get_at(mGraphQLLanguage, rb_intern("Lexer")); VALUE valid_string_pattern = rb_const_get_at(mGraphQLLanguageLexer, rb_intern("VALID_STRING")); if (tt == BLOCK_STRING) { VALUE mGraphQLLanguageBlockString = rb_const_get_at(mGraphQLLanguage, rb_intern("BlockString")); token_content = rb_funcall(mGraphQLLanguageBlockString, rb_intern("trim_whitespace"), 1, token_content); tt = STRING; } else { tt = STRING; if ( RB_TEST(rb_funcall(token_content, rb_intern("valid_encoding?"), 0)) && RB_TEST(rb_funcall(token_content, rb_intern("match?"), 1, valid_string_pattern)) ) { rb_funcall(mGraphQLLanguageLexer, rb_intern("replace_escaped_characters_in_place"), 1, token_content); if (!RB_TEST(rb_funcall(token_content, rb_intern("valid_encoding?"), 0))) { token_sym = ID2SYM(rb_intern("BAD_UNICODE_ESCAPE")); tt = BAD_UNICODE_ESCAPE; } } else { token_sym = ID2SYM(rb_intern("BAD_UNICODE_ESCAPE")); tt = BAD_UNICODE_ESCAPE; } } } VALUE token = rb_ary_new_from_args(5, token_sym, rb_int2inum(meta->line), rb_int2inum(meta->col), token_content, INT2FIX(200 + (int)tt) ); if (tt != COMMENT) { rb_ary_push(meta->tokens, token); } meta->preceeded_by_number = this_token_is_number; } // Bump the column counter for the next token meta->col += te - ts; meta->line += line_incr; } VALUE tokenize(VALUE query_rbstr, int fstring_identifiers, int reject_numbers_followed_by_names, int max_tokens) { int cs = 0; int act = 0; char *p = StringValuePtr(query_rbstr); long query_len = RSTRING_LEN(query_rbstr); char *pe = p + query_len; char *eof = pe; char *ts = 0; char *te = 0; VALUE tokens = rb_ary_new(); struct Meta meta_s = {1, 1, p, pe, tokens, fstring_identifiers, reject_numbers_followed_by_names, 0, max_tokens, 0}; Meta *meta = &meta_s; #line 987 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { cs = (int)graphql_c_lexer_start; ts = 0; te = 0; act = 0; } #line 407 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" #line 998 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { unsigned int _trans = 0; const char * _keys; const short * _inds; int _ic; _resume: {} if ( p == pe && p != eof ) goto _out; switch ( _graphql_c_lexer_from_state_actions[cs] ) { case 10: { { #line 1 "NONE" {ts = p;}} #line 1013 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } } if ( p == eof ) { if ( _graphql_c_lexer_eof_trans[cs] > 0 ) { _trans = (unsigned int)_graphql_c_lexer_eof_trans[cs] - 1; } } else { _keys = ( _graphql_c_lexer_trans_keys + ((cs<<1))); _inds = ( _graphql_c_lexer_indices + (_graphql_c_lexer_index_offsets[cs])); if ( ( (*( p))) <= 125 && ( (*( p))) >= 9 ) { _ic = (int)_graphql_c_lexer_char_class[(int)( (*( p))) - 9]; if ( _ic <= (int)(*( _keys+1)) && _ic >= (int)(*( _keys)) ) _trans = (unsigned int)(*( _inds + (int)( _ic - (int)(*( _keys)) ) )); else _trans = (unsigned int)_graphql_c_lexer_index_defaults[cs]; } else { _trans = (unsigned int)_graphql_c_lexer_index_defaults[cs]; } } cs = (int)_graphql_c_lexer_cond_targs[_trans]; if ( _graphql_c_lexer_cond_actions[_trans] != 0 ) { switch ( _graphql_c_lexer_cond_actions[_trans] ) { case 6: { { #line 1 "NONE" {te = p+1;}} #line 1051 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 26: { { #line 75 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p+1;{ #line 75 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(RCURLY, ts, te, meta); } }} #line 1064 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 24: { { #line 76 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p+1;{ #line 76 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(LCURLY, ts, te, meta); } }} #line 1077 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 17: { { #line 77 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p+1;{ #line 77 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(RPAREN, ts, te, meta); } }} #line 1090 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 16: { { #line 78 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p+1;{ #line 78 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(LPAREN, ts, te, meta); } }} #line 1103 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 23: { { #line 79 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p+1;{ #line 79 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(RBRACKET, ts, te, meta); } }} #line 1116 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 22: { { #line 80 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p+1;{ #line 80 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(LBRACKET, ts, te, meta); } }} #line 1129 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 18: { { #line 81 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p+1;{ #line 81 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(COLON, ts, te, meta); } }} #line 1142 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 32: { { #line 82 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p+1;{ #line 82 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(BLOCK_STRING, ts, te, meta); } }} #line 1155 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 2: { { #line 83 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p+1;{ #line 83 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(QUOTED_STRING, ts, te, meta); } }} #line 1168 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 14: { { #line 84 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p+1;{ #line 84 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(VAR_SIGN, ts, te, meta); } }} #line 1181 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 20: { { #line 85 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p+1;{ #line 85 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(DIR_SIGN, ts, te, meta); } }} #line 1194 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 8: { { #line 86 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p+1;{ #line 86 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(ELLIPSIS, ts, te, meta); } }} #line 1207 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 19: { { #line 87 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p+1;{ #line 87 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(EQUALS, ts, te, meta); } }} #line 1220 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 13: { { #line 88 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p+1;{ #line 88 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(BANG, ts, te, meta); } }} #line 1233 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 25: { { #line 89 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p+1;{ #line 89 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(PIPE, ts, te, meta); } }} #line 1246 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 15: { { #line 90 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p+1;{ #line 90 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(AMP, ts, te, meta); } }} #line 1259 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 12: { { #line 93 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p+1;{ #line 93 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" meta->line += 1; meta->col = 1; meta->preceeded_by_number = 0; } }} #line 1276 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 11: { { #line 104 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p+1;{ #line 104 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(UNKNOWN_CHAR, ts, te, meta); } }} #line 1289 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 34: { { #line 54 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p;p = p - 1;{ #line 54 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(INT, ts, te, meta); } }} #line 1302 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 35: { { #line 55 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p;p = p - 1;{ #line 55 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(FLOAT, ts, te, meta); } }} #line 1315 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 31: { { #line 82 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p;p = p - 1;{ #line 82 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(BLOCK_STRING, ts, te, meta); } }} #line 1328 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 30: { { #line 83 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p;p = p - 1;{ #line 83 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(QUOTED_STRING, ts, te, meta); } }} #line 1341 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 36: { { #line 91 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p;p = p - 1;{ #line 91 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(IDENTIFIER, ts, te, meta); } }} #line 1354 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 33: { { #line 92 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p;p = p - 1;{ #line 92 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(COMMENT, ts, te, meta); } }} #line 1367 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 27: { { #line 99 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p;p = p - 1;{ #line 99 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" meta->col += te - ts; meta->preceeded_by_number = 0; } }} #line 1383 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 28: { { #line 104 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {te = p;p = p - 1;{ #line 104 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(UNKNOWN_CHAR, ts, te, meta); } }} #line 1396 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 5: { { #line 54 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {p = ((te))-1; { #line 54 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(INT, ts, te, meta); } }} #line 1410 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 7: { { #line 55 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {p = ((te))-1; { #line 55 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(FLOAT, ts, te, meta); } }} #line 1424 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 1: { { #line 104 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {p = ((te))-1; { #line 104 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(UNKNOWN_CHAR, ts, te, meta); } }} #line 1438 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 3: { { #line 1 "NONE" {switch( act ) { case 3: { p = ((te))-1; { #line 56 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(ON, ts, te, meta); } break; } case 4: { p = ((te))-1; { #line 57 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(FRAGMENT, ts, te, meta); } break; } case 5: { p = ((te))-1; { #line 58 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(TRUE_LITERAL, ts, te, meta); } break; } case 6: { p = ((te))-1; { #line 59 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(FALSE_LITERAL, ts, te, meta); } break; } case 7: { p = ((te))-1; { #line 60 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(NULL_LITERAL, ts, te, meta); } break; } case 8: { p = ((te))-1; { #line 61 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(QUERY, ts, te, meta); } break; } case 9: { p = ((te))-1; { #line 62 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(MUTATION, ts, te, meta); } break; } case 10: { p = ((te))-1; { #line 63 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(SUBSCRIPTION, ts, te, meta); } break; } case 11: { p = ((te))-1; { #line 64 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(SCHEMA, ts, te, meta); } break; } case 12: { p = ((te))-1; { #line 65 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(SCALAR, ts, te, meta); } break; } case 13: { p = ((te))-1; { #line 66 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(TYPE_LITERAL, ts, te, meta); } break; } case 14: { p = ((te))-1; { #line 67 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(EXTEND, ts, te, meta); } break; } case 15: { p = ((te))-1; { #line 68 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(IMPLEMENTS, ts, te, meta); } break; } case 16: { p = ((te))-1; { #line 69 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(INTERFACE, ts, te, meta); } break; } case 17: { p = ((te))-1; { #line 70 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(UNION, ts, te, meta); } break; } case 18: { p = ((te))-1; { #line 71 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(ENUM, ts, te, meta); } break; } case 19: { p = ((te))-1; { #line 72 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(INPUT, ts, te, meta); } break; } case 20: { p = ((te))-1; { #line 73 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(DIRECTIVE, ts, te, meta); } break; } case 21: { p = ((te))-1; { #line 74 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(REPEATABLE, ts, te, meta); } break; } case 29: { p = ((te))-1; { #line 82 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(BLOCK_STRING, ts, te, meta); } break; } case 30: { p = ((te))-1; { #line 83 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(QUOTED_STRING, ts, te, meta); } break; } case 38: { p = ((te))-1; { #line 91 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" emit(IDENTIFIER, ts, te, meta); } break; } }} } #line 1604 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 47: { { #line 1 "NONE" {te = p+1;}} #line 1614 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 56 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 3;}} #line 1620 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 41: { { #line 1 "NONE" {te = p+1;}} #line 1630 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 57 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 4;}} #line 1636 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 53: { { #line 1 "NONE" {te = p+1;}} #line 1646 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 58 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 5;}} #line 1652 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 40: { { #line 1 "NONE" {te = p+1;}} #line 1662 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 59 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 6;}} #line 1668 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 46: { { #line 1 "NONE" {te = p+1;}} #line 1678 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 60 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 7;}} #line 1684 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 48: { { #line 1 "NONE" {te = p+1;}} #line 1694 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 61 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 8;}} #line 1700 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 45: { { #line 1 "NONE" {te = p+1;}} #line 1710 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 62 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 9;}} #line 1716 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 52: { { #line 1 "NONE" {te = p+1;}} #line 1726 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 63 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 10;}} #line 1732 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 51: { { #line 1 "NONE" {te = p+1;}} #line 1742 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 64 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 11;}} #line 1748 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 50: { { #line 1 "NONE" {te = p+1;}} #line 1758 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 65 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 12;}} #line 1764 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 54: { { #line 1 "NONE" {te = p+1;}} #line 1774 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 66 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 13;}} #line 1780 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 39: { { #line 1 "NONE" {te = p+1;}} #line 1790 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 67 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 14;}} #line 1796 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 42: { { #line 1 "NONE" {te = p+1;}} #line 1806 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 68 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 15;}} #line 1812 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 44: { { #line 1 "NONE" {te = p+1;}} #line 1822 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 69 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 16;}} #line 1828 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 55: { { #line 1 "NONE" {te = p+1;}} #line 1838 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 70 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 17;}} #line 1844 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 38: { { #line 1 "NONE" {te = p+1;}} #line 1854 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 71 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 18;}} #line 1860 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 43: { { #line 1 "NONE" {te = p+1;}} #line 1870 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 72 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 19;}} #line 1876 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 37: { { #line 1 "NONE" {te = p+1;}} #line 1886 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 73 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 20;}} #line 1892 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 49: { { #line 1 "NONE" {te = p+1;}} #line 1902 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 74 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 21;}} #line 1908 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 4: { { #line 1 "NONE" {te = p+1;}} #line 1918 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 82 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 29;}} #line 1924 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 29: { { #line 1 "NONE" {te = p+1;}} #line 1934 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 83 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 30;}} #line 1940 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } case 21: { { #line 1 "NONE" {te = p+1;}} #line 1950 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" { #line 91 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" {act = 38;}} #line 1956 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } } } if ( p == eof ) { if ( cs >= 21 ) goto _out; } else { switch ( _graphql_c_lexer_to_state_actions[cs] ) { case 9: { { #line 1 "NONE" {ts = 0;}} #line 1976 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.c" break; } } p += 1; goto _resume; } _out: {} } #line 408 "graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl" return tokens; } #define SETUP_STATIC_TOKEN_VARIABLE(token_name, token_content) \ GraphQLTokenString##token_name = rb_utf8_str_new_cstr(token_content); \ rb_funcall(GraphQLTokenString##token_name, rb_intern("-@"), 0); \ rb_global_variable(&GraphQLTokenString##token_name); \ #define SETUP_STATIC_STRING(var_name, str_content) \ var_name = rb_utf8_str_new_cstr(str_content); \ rb_global_variable(&var_name); \ rb_str_freeze(var_name); \ void setup_static_token_variables() { SETUP_STATIC_TOKEN_VARIABLE(ON, "on") SETUP_STATIC_TOKEN_VARIABLE(FRAGMENT, "fragment") SETUP_STATIC_TOKEN_VARIABLE(QUERY, "query") SETUP_STATIC_TOKEN_VARIABLE(MUTATION, "mutation") SETUP_STATIC_TOKEN_VARIABLE(SUBSCRIPTION, "subscription") SETUP_STATIC_TOKEN_VARIABLE(REPEATABLE, "repeatable") SETUP_STATIC_TOKEN_VARIABLE(RCURLY, "}") SETUP_STATIC_TOKEN_VARIABLE(LCURLY, "{") SETUP_STATIC_TOKEN_VARIABLE(RBRACKET, "]") SETUP_STATIC_TOKEN_VARIABLE(LBRACKET, "[") SETUP_STATIC_TOKEN_VARIABLE(RPAREN, ")") SETUP_STATIC_TOKEN_VARIABLE(LPAREN, "(") SETUP_STATIC_TOKEN_VARIABLE(COLON, ":") SETUP_STATIC_TOKEN_VARIABLE(VAR_SIGN, "$") SETUP_STATIC_TOKEN_VARIABLE(DIR_SIGN, "@") SETUP_STATIC_TOKEN_VARIABLE(ELLIPSIS, "...") SETUP_STATIC_TOKEN_VARIABLE(EQUALS, "=") SETUP_STATIC_TOKEN_VARIABLE(BANG, "!") SETUP_STATIC_TOKEN_VARIABLE(PIPE, "|") SETUP_STATIC_TOKEN_VARIABLE(AMP, "&") SETUP_STATIC_TOKEN_VARIABLE(SCHEMA, "schema") SETUP_STATIC_TOKEN_VARIABLE(SCALAR, "scalar") SETUP_STATIC_TOKEN_VARIABLE(EXTEND, "extend") SETUP_STATIC_TOKEN_VARIABLE(IMPLEMENTS, "implements") SETUP_STATIC_TOKEN_VARIABLE(INTERFACE, "interface") SETUP_STATIC_TOKEN_VARIABLE(UNION, "union") SETUP_STATIC_TOKEN_VARIABLE(ENUM, "enum") SETUP_STATIC_TOKEN_VARIABLE(DIRECTIVE, "directive") SETUP_STATIC_TOKEN_VARIABLE(INPUT, "input") SETUP_STATIC_STRING(GraphQL_type_str, "type") SETUP_STATIC_STRING(GraphQL_true_str, "true") SETUP_STATIC_STRING(GraphQL_false_str, "false") SETUP_STATIC_STRING(GraphQL_null_str, "null") } graphql-ruby-2.5.19/graphql-c_parser/ext/graphql_c_parser_ext/lexer.h000066400000000000000000000003401514115062600257410ustar00rootroot00000000000000#ifndef Graphql_lexer_h #define Graphql_lexer_h #include VALUE tokenize(VALUE query_rbstr, int fstring_identifiers, int reject_numbers_followed_by_names, int max_tokens); void setup_static_token_variables(); #endif graphql-ruby-2.5.19/graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl000066400000000000000000000363601514115062600261420ustar00rootroot00000000000000%%{ machine graphql_c_lexer; IDENTIFIER = [_A-Za-z][_0-9A-Za-z]*; NEWLINE = [\c\r\n]; BLANK = [, \t]+; COMMENT = '#' [^\n\r]*; INT = '-'? ('0'|[1-9][0-9]*); FLOAT = INT ('.'[0-9]+) (('e' | 'E')?('+' | '-')?[0-9]+)?; ON = 'on'; FRAGMENT = 'fragment'; TRUE_LITERAL = 'true'; FALSE_LITERAL = 'false'; NULL_LITERAL = 'null'; QUERY = 'query'; MUTATION = 'mutation'; SUBSCRIPTION = 'subscription'; SCHEMA = 'schema'; SCALAR = 'scalar'; TYPE_LITERAL = 'type'; EXTEND = 'extend'; IMPLEMENTS = 'implements'; INTERFACE = 'interface'; UNION = 'union'; ENUM = 'enum'; INPUT = 'input'; DIRECTIVE = 'directive'; REPEATABLE = 'repeatable'; LCURLY = '{'; RCURLY = '}'; LPAREN = '('; RPAREN = ')'; LBRACKET = '['; RBRACKET = ']'; COLON = ':'; # Could limit to hex here, but “bad unicode escape” on 0XXF is probably a # more helpful error than “unknown char” UNICODE_ESCAPE = "\\u" ([0-9A-Za-z]{4} | LCURLY [0-9A-Za-z]{4,} RCURLY); VAR_SIGN = '$'; DIR_SIGN = '@'; ELLIPSIS = '...'; EQUALS = '='; BANG = '!'; PIPE = '|'; AMP = '&'; QUOTED_STRING = ('"' ((('\\"' | ^'"') - "\\" - "\n" - "\r") | UNICODE_ESCAPE | '\\' [\\/bfnrt])* '"'); # catch-all for anything else. must be at the bottom for precedence. UNKNOWN_CHAR = /./; BLOCK_STRING = ('"""' ('\\"""' | ^'"' | '"'{1,2} ^'"')* '"'{0,2} '"""'); main := |* INT => { emit(INT, ts, te, meta); }; FLOAT => { emit(FLOAT, ts, te, meta); }; ON => { emit(ON, ts, te, meta); }; FRAGMENT => { emit(FRAGMENT, ts, te, meta); }; TRUE_LITERAL => { emit(TRUE_LITERAL, ts, te, meta); }; FALSE_LITERAL => { emit(FALSE_LITERAL, ts, te, meta); }; NULL_LITERAL => { emit(NULL_LITERAL, ts, te, meta); }; QUERY => { emit(QUERY, ts, te, meta); }; MUTATION => { emit(MUTATION, ts, te, meta); }; SUBSCRIPTION => { emit(SUBSCRIPTION, ts, te, meta); }; SCHEMA => { emit(SCHEMA, ts, te, meta); }; SCALAR => { emit(SCALAR, ts, te, meta); }; TYPE_LITERAL => { emit(TYPE_LITERAL, ts, te, meta); }; EXTEND => { emit(EXTEND, ts, te, meta); }; IMPLEMENTS => { emit(IMPLEMENTS, ts, te, meta); }; INTERFACE => { emit(INTERFACE, ts, te, meta); }; UNION => { emit(UNION, ts, te, meta); }; ENUM => { emit(ENUM, ts, te, meta); }; INPUT => { emit(INPUT, ts, te, meta); }; DIRECTIVE => { emit(DIRECTIVE, ts, te, meta); }; REPEATABLE => { emit(REPEATABLE, ts, te, meta); }; RCURLY => { emit(RCURLY, ts, te, meta); }; LCURLY => { emit(LCURLY, ts, te, meta); }; RPAREN => { emit(RPAREN, ts, te, meta); }; LPAREN => { emit(LPAREN, ts, te, meta); }; RBRACKET => { emit(RBRACKET, ts, te, meta); }; LBRACKET => { emit(LBRACKET, ts, te, meta); }; COLON => { emit(COLON, ts, te, meta); }; BLOCK_STRING => { emit(BLOCK_STRING, ts, te, meta); }; QUOTED_STRING => { emit(QUOTED_STRING, ts, te, meta); }; VAR_SIGN => { emit(VAR_SIGN, ts, te, meta); }; DIR_SIGN => { emit(DIR_SIGN, ts, te, meta); }; ELLIPSIS => { emit(ELLIPSIS, ts, te, meta); }; EQUALS => { emit(EQUALS, ts, te, meta); }; BANG => { emit(BANG, ts, te, meta); }; PIPE => { emit(PIPE, ts, te, meta); }; AMP => { emit(AMP, ts, te, meta); }; IDENTIFIER => { emit(IDENTIFIER, ts, te, meta); }; COMMENT => { emit(COMMENT, ts, te, meta); }; NEWLINE => { meta->line += 1; meta->col = 1; meta->preceeded_by_number = 0; }; BLANK => { meta->col += te - ts; meta->preceeded_by_number = 0; }; UNKNOWN_CHAR => { emit(UNKNOWN_CHAR, ts, te, meta); }; *|; }%% %% write data; #include #include #define INIT_STATIC_TOKEN_VARIABLE(token_name) \ static VALUE GraphQLTokenString##token_name; INIT_STATIC_TOKEN_VARIABLE(ON) INIT_STATIC_TOKEN_VARIABLE(FRAGMENT) INIT_STATIC_TOKEN_VARIABLE(QUERY) INIT_STATIC_TOKEN_VARIABLE(MUTATION) INIT_STATIC_TOKEN_VARIABLE(SUBSCRIPTION) INIT_STATIC_TOKEN_VARIABLE(REPEATABLE) INIT_STATIC_TOKEN_VARIABLE(RCURLY) INIT_STATIC_TOKEN_VARIABLE(LCURLY) INIT_STATIC_TOKEN_VARIABLE(RBRACKET) INIT_STATIC_TOKEN_VARIABLE(LBRACKET) INIT_STATIC_TOKEN_VARIABLE(RPAREN) INIT_STATIC_TOKEN_VARIABLE(LPAREN) INIT_STATIC_TOKEN_VARIABLE(COLON) INIT_STATIC_TOKEN_VARIABLE(VAR_SIGN) INIT_STATIC_TOKEN_VARIABLE(DIR_SIGN) INIT_STATIC_TOKEN_VARIABLE(ELLIPSIS) INIT_STATIC_TOKEN_VARIABLE(EQUALS) INIT_STATIC_TOKEN_VARIABLE(BANG) INIT_STATIC_TOKEN_VARIABLE(PIPE) INIT_STATIC_TOKEN_VARIABLE(AMP) INIT_STATIC_TOKEN_VARIABLE(SCHEMA) INIT_STATIC_TOKEN_VARIABLE(SCALAR) INIT_STATIC_TOKEN_VARIABLE(EXTEND) INIT_STATIC_TOKEN_VARIABLE(IMPLEMENTS) INIT_STATIC_TOKEN_VARIABLE(INTERFACE) INIT_STATIC_TOKEN_VARIABLE(UNION) INIT_STATIC_TOKEN_VARIABLE(ENUM) INIT_STATIC_TOKEN_VARIABLE(DIRECTIVE) INIT_STATIC_TOKEN_VARIABLE(INPUT) static VALUE GraphQL_type_str; static VALUE GraphQL_true_str; static VALUE GraphQL_false_str; static VALUE GraphQL_null_str; typedef enum TokenType { AMP, BANG, COLON, DIRECTIVE, DIR_SIGN, ENUM, ELLIPSIS, EQUALS, EXTEND, FALSE_LITERAL, FLOAT, FRAGMENT, IDENTIFIER, INPUT, IMPLEMENTS, INT, INTERFACE, LBRACKET, LCURLY, LPAREN, MUTATION, NULL_LITERAL, ON, PIPE, QUERY, RBRACKET, RCURLY, REPEATABLE, RPAREN, SCALAR, SCHEMA, STRING, SUBSCRIPTION, TRUE_LITERAL, TYPE_LITERAL, UNION, VAR_SIGN, BLOCK_STRING, QUOTED_STRING, UNKNOWN_CHAR, COMMENT, BAD_UNICODE_ESCAPE } TokenType; typedef struct Meta { int line; int col; char *query_cstr; char *pe; VALUE tokens; int dedup_identifiers; int reject_numbers_followed_by_names; int preceeded_by_number; int max_tokens; int tokens_count; } Meta; #define STATIC_VALUE_TOKEN(token_type, content_str) \ case token_type: \ token_sym = ID2SYM(rb_intern(#token_type)); \ token_content = GraphQLTokenString##token_type; \ break; #define DYNAMIC_VALUE_TOKEN(token_type) \ case token_type: \ token_sym = ID2SYM(rb_intern(#token_type)); \ token_content = rb_utf8_str_new(ts, te - ts); \ break; void emit(TokenType tt, char *ts, char *te, Meta *meta) { meta->tokens_count++; // -1 indicates that there is no limit: if (meta->max_tokens > 0 && meta->tokens_count > meta->max_tokens) { VALUE mGraphQL = rb_const_get_at(rb_cObject, rb_intern("GraphQL")); VALUE cParseError = rb_const_get_at(mGraphQL, rb_intern("ParseError")); VALUE exception = rb_funcall( cParseError, rb_intern("new"), 4, rb_str_new_cstr("This query is too large to execute."), LONG2NUM(meta->line), LONG2NUM(meta->col), rb_str_new_cstr(meta->query_cstr) ); rb_exc_raise(exception); } int quotes_length = 0; // set by string tokens below int line_incr = 0; VALUE token_sym = Qnil; VALUE token_content = Qnil; int this_token_is_number = 0; switch(tt) { STATIC_VALUE_TOKEN(ON, "on") STATIC_VALUE_TOKEN(FRAGMENT, "fragment") STATIC_VALUE_TOKEN(QUERY, "query") STATIC_VALUE_TOKEN(MUTATION, "mutation") STATIC_VALUE_TOKEN(SUBSCRIPTION, "subscription") STATIC_VALUE_TOKEN(REPEATABLE, "repeatable") STATIC_VALUE_TOKEN(RCURLY, "}") STATIC_VALUE_TOKEN(LCURLY, "{") STATIC_VALUE_TOKEN(RBRACKET, "]") STATIC_VALUE_TOKEN(LBRACKET, "[") STATIC_VALUE_TOKEN(RPAREN, ")") STATIC_VALUE_TOKEN(LPAREN, "(") STATIC_VALUE_TOKEN(COLON, ":") STATIC_VALUE_TOKEN(VAR_SIGN, "$") STATIC_VALUE_TOKEN(DIR_SIGN, "@") STATIC_VALUE_TOKEN(ELLIPSIS, "...") STATIC_VALUE_TOKEN(EQUALS, "=") STATIC_VALUE_TOKEN(BANG, "!") STATIC_VALUE_TOKEN(PIPE, "|") STATIC_VALUE_TOKEN(AMP, "&") STATIC_VALUE_TOKEN(SCHEMA, "schema") STATIC_VALUE_TOKEN(SCALAR, "scalar") STATIC_VALUE_TOKEN(EXTEND, "extend") STATIC_VALUE_TOKEN(IMPLEMENTS, "implements") STATIC_VALUE_TOKEN(INTERFACE, "interface") STATIC_VALUE_TOKEN(UNION, "union") STATIC_VALUE_TOKEN(ENUM, "enum") STATIC_VALUE_TOKEN(DIRECTIVE, "directive") STATIC_VALUE_TOKEN(INPUT, "input") // For these, the enum name doesn't match the symbol name: case TYPE_LITERAL: token_sym = ID2SYM(rb_intern("TYPE")); token_content = GraphQL_type_str; break; case TRUE_LITERAL: token_sym = ID2SYM(rb_intern("TRUE")); token_content = GraphQL_true_str; break; case FALSE_LITERAL: token_sym = ID2SYM(rb_intern("FALSE")); token_content = GraphQL_false_str; break; case NULL_LITERAL: token_sym = ID2SYM(rb_intern("NULL")); token_content = GraphQL_null_str; break; case IDENTIFIER: if (meta->reject_numbers_followed_by_names && meta->preceeded_by_number) { VALUE mGraphQL = rb_const_get_at(rb_cObject, rb_intern("GraphQL")); VALUE mCParser = rb_const_get_at(mGraphQL, rb_intern("CParser")); VALUE prev_token = rb_ary_entry(meta->tokens, -1); VALUE exception = rb_funcall( mCParser, rb_intern("prepare_number_name_parse_error"), 5, LONG2NUM(meta->line), LONG2NUM(meta->col), rb_str_new_cstr(meta->query_cstr), rb_ary_entry(prev_token, 3), rb_utf8_str_new(ts, te - ts) ); rb_exc_raise(exception); } token_sym = ID2SYM(rb_intern("IDENTIFIER")); if (meta->dedup_identifiers) { token_content = rb_enc_interned_str(ts, te - ts, rb_utf8_encoding()); } else { token_content = rb_utf8_str_new(ts, te - ts); } break; // Can't use these while we're in backwards-compat mode: // DYNAMIC_VALUE_TOKEN(INT) // DYNAMIC_VALUE_TOKEN(FLOAT) case INT: token_sym = ID2SYM(rb_intern("INT")); token_content = rb_utf8_str_new(ts, te - ts); this_token_is_number = 1; break; case FLOAT: token_sym = ID2SYM(rb_intern("FLOAT")); token_content = rb_utf8_str_new(ts, te - ts); this_token_is_number = 1; break; DYNAMIC_VALUE_TOKEN(COMMENT) case UNKNOWN_CHAR: if (ts[0] == '\0') { return; } else { token_content = rb_utf8_str_new(ts, te - ts); token_sym = ID2SYM(rb_intern("UNKNOWN_CHAR")); break; } case QUOTED_STRING: quotes_length = 1; token_content = rb_utf8_str_new(ts + quotes_length, (te - ts - (2 * quotes_length))); token_sym = ID2SYM(rb_intern("STRING")); break; case BLOCK_STRING: token_sym = ID2SYM(rb_intern("STRING")); quotes_length = 3; token_content = rb_utf8_str_new(ts + quotes_length, (te - ts - (2 * quotes_length))); line_incr = FIX2INT(rb_funcall(token_content, rb_intern("count"), 1, rb_utf8_str_new_cstr("\n"))); break; // These are used only by the parser, this is never reached case STRING: case BAD_UNICODE_ESCAPE: break; } if (token_sym != Qnil) { if (tt == BLOCK_STRING || tt == QUOTED_STRING) { VALUE mGraphQL = rb_const_get_at(rb_cObject, rb_intern("GraphQL")); VALUE mGraphQLLanguage = rb_const_get_at(mGraphQL, rb_intern("Language")); VALUE mGraphQLLanguageLexer = rb_const_get_at(mGraphQLLanguage, rb_intern("Lexer")); VALUE valid_string_pattern = rb_const_get_at(mGraphQLLanguageLexer, rb_intern("VALID_STRING")); if (tt == BLOCK_STRING) { VALUE mGraphQLLanguageBlockString = rb_const_get_at(mGraphQLLanguage, rb_intern("BlockString")); token_content = rb_funcall(mGraphQLLanguageBlockString, rb_intern("trim_whitespace"), 1, token_content); tt = STRING; } else { tt = STRING; if ( RB_TEST(rb_funcall(token_content, rb_intern("valid_encoding?"), 0)) && RB_TEST(rb_funcall(token_content, rb_intern("match?"), 1, valid_string_pattern)) ) { rb_funcall(mGraphQLLanguageLexer, rb_intern("replace_escaped_characters_in_place"), 1, token_content); if (!RB_TEST(rb_funcall(token_content, rb_intern("valid_encoding?"), 0))) { token_sym = ID2SYM(rb_intern("BAD_UNICODE_ESCAPE")); tt = BAD_UNICODE_ESCAPE; } } else { token_sym = ID2SYM(rb_intern("BAD_UNICODE_ESCAPE")); tt = BAD_UNICODE_ESCAPE; } } } VALUE token = rb_ary_new_from_args(5, token_sym, rb_int2inum(meta->line), rb_int2inum(meta->col), token_content, INT2FIX(200 + (int)tt) ); if (tt != COMMENT) { rb_ary_push(meta->tokens, token); } meta->preceeded_by_number = this_token_is_number; } // Bump the column counter for the next token meta->col += te - ts; meta->line += line_incr; } VALUE tokenize(VALUE query_rbstr, int fstring_identifiers, int reject_numbers_followed_by_names, int max_tokens) { int cs = 0; int act = 0; char *p = StringValuePtr(query_rbstr); long query_len = RSTRING_LEN(query_rbstr); char *pe = p + query_len; char *eof = pe; char *ts = 0; char *te = 0; VALUE tokens = rb_ary_new(); struct Meta meta_s = {1, 1, p, pe, tokens, fstring_identifiers, reject_numbers_followed_by_names, 0, max_tokens, 0}; Meta *meta = &meta_s; %% write init; %% write exec; return tokens; } #define SETUP_STATIC_TOKEN_VARIABLE(token_name, token_content) \ GraphQLTokenString##token_name = rb_utf8_str_new_cstr(token_content); \ rb_funcall(GraphQLTokenString##token_name, rb_intern("-@"), 0); \ rb_global_variable(&GraphQLTokenString##token_name); \ #define SETUP_STATIC_STRING(var_name, str_content) \ var_name = rb_utf8_str_new_cstr(str_content); \ rb_global_variable(&var_name); \ rb_str_freeze(var_name); \ void setup_static_token_variables() { SETUP_STATIC_TOKEN_VARIABLE(ON, "on") SETUP_STATIC_TOKEN_VARIABLE(FRAGMENT, "fragment") SETUP_STATIC_TOKEN_VARIABLE(QUERY, "query") SETUP_STATIC_TOKEN_VARIABLE(MUTATION, "mutation") SETUP_STATIC_TOKEN_VARIABLE(SUBSCRIPTION, "subscription") SETUP_STATIC_TOKEN_VARIABLE(REPEATABLE, "repeatable") SETUP_STATIC_TOKEN_VARIABLE(RCURLY, "}") SETUP_STATIC_TOKEN_VARIABLE(LCURLY, "{") SETUP_STATIC_TOKEN_VARIABLE(RBRACKET, "]") SETUP_STATIC_TOKEN_VARIABLE(LBRACKET, "[") SETUP_STATIC_TOKEN_VARIABLE(RPAREN, ")") SETUP_STATIC_TOKEN_VARIABLE(LPAREN, "(") SETUP_STATIC_TOKEN_VARIABLE(COLON, ":") SETUP_STATIC_TOKEN_VARIABLE(VAR_SIGN, "$") SETUP_STATIC_TOKEN_VARIABLE(DIR_SIGN, "@") SETUP_STATIC_TOKEN_VARIABLE(ELLIPSIS, "...") SETUP_STATIC_TOKEN_VARIABLE(EQUALS, "=") SETUP_STATIC_TOKEN_VARIABLE(BANG, "!") SETUP_STATIC_TOKEN_VARIABLE(PIPE, "|") SETUP_STATIC_TOKEN_VARIABLE(AMP, "&") SETUP_STATIC_TOKEN_VARIABLE(SCHEMA, "schema") SETUP_STATIC_TOKEN_VARIABLE(SCALAR, "scalar") SETUP_STATIC_TOKEN_VARIABLE(EXTEND, "extend") SETUP_STATIC_TOKEN_VARIABLE(IMPLEMENTS, "implements") SETUP_STATIC_TOKEN_VARIABLE(INTERFACE, "interface") SETUP_STATIC_TOKEN_VARIABLE(UNION, "union") SETUP_STATIC_TOKEN_VARIABLE(ENUM, "enum") SETUP_STATIC_TOKEN_VARIABLE(DIRECTIVE, "directive") SETUP_STATIC_TOKEN_VARIABLE(INPUT, "input") SETUP_STATIC_STRING(GraphQL_type_str, "type") SETUP_STATIC_STRING(GraphQL_true_str, "true") SETUP_STATIC_STRING(GraphQL_false_str, "false") SETUP_STATIC_STRING(GraphQL_null_str, "null") } graphql-ruby-2.5.19/graphql-c_parser/ext/graphql_c_parser_ext/parser.c000066400000000000000000004143651514115062600261310ustar00rootroot00000000000000/* A Bison parser, made by GNU Bison 3.8.2. */ /* Bison implementation for Yacc-like parsers in C Copyright (C) 1984, 1989-1990, 2000-2015, 2018-2021 Free Software Foundation, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ /* As a special exception, you may create a larger work that contains part or all of the Bison parser skeleton and distribute that work under terms of your choice, so long as that work isn't itself a parser generator using the skeleton or a modified version thereof as a parser skeleton. Alternatively, if you modify or redistribute the parser skeleton itself, you may (at your option) remove this special exception, which will cause the skeleton and the resulting Bison output files to be licensed under the GNU General Public License without this special exception. This special exception was added by the Free Software Foundation in version 2.2 of Bison. */ /* C LALR(1) parser skeleton written by Richard Stallman, by simplifying the original so-called "semantic" parser. */ /* DO NOT RELY ON FEATURES THAT ARE NOT DOCUMENTED in the manual, especially those whose name start with YY_ or yy_. They are private implementation details that can be changed or removed. */ /* All symbols defined below should begin with yy or YY, to avoid infringing on user name space. This should be done even for local variables, as they might otherwise be expanded by user macros. There are some unavoidable exceptions within include files to define necessary library symbols; they are noted "INFRINGES ON USER NAME SPACE" below. */ /* Identify Bison output, and Bison version. */ #define YYBISON 30802 /* Bison version string. */ #define YYBISON_VERSION "3.8.2" /* Skeleton name. */ #define YYSKELETON_NAME "yacc.c" /* Pure parsers. */ #define YYPURE 2 /* Push parsers. */ #define YYPUSH 0 /* Pull parsers. */ #define YYPULL 1 /* First part of user prologue. */ #line 5 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" // C Declarations #include #define YYSTYPE VALUE int yylex(YYSTYPE *, VALUE, VALUE); void yyerror(VALUE, VALUE, const char*); static VALUE GraphQL_Language_Nodes_NONE; static VALUE r_string_query; #define MAKE_AST_NODE(node_class_name, nargs, ...) rb_funcall(GraphQL_Language_Nodes_##node_class_name, rb_intern("from_a"), nargs + 1, filename,__VA_ARGS__) #define SETUP_NODE_CLASS_VARIABLE(node_class_name) static VALUE GraphQL_Language_Nodes_##node_class_name; SETUP_NODE_CLASS_VARIABLE(Argument) SETUP_NODE_CLASS_VARIABLE(Directive) SETUP_NODE_CLASS_VARIABLE(Document) SETUP_NODE_CLASS_VARIABLE(Enum) SETUP_NODE_CLASS_VARIABLE(Field) SETUP_NODE_CLASS_VARIABLE(FragmentDefinition) SETUP_NODE_CLASS_VARIABLE(FragmentSpread) SETUP_NODE_CLASS_VARIABLE(InlineFragment) SETUP_NODE_CLASS_VARIABLE(InputObject) SETUP_NODE_CLASS_VARIABLE(ListType) SETUP_NODE_CLASS_VARIABLE(NonNullType) SETUP_NODE_CLASS_VARIABLE(NullValue) SETUP_NODE_CLASS_VARIABLE(OperationDefinition) SETUP_NODE_CLASS_VARIABLE(TypeName) SETUP_NODE_CLASS_VARIABLE(VariableDefinition) SETUP_NODE_CLASS_VARIABLE(VariableIdentifier) SETUP_NODE_CLASS_VARIABLE(ScalarTypeDefinition) SETUP_NODE_CLASS_VARIABLE(ObjectTypeDefinition) SETUP_NODE_CLASS_VARIABLE(InterfaceTypeDefinition) SETUP_NODE_CLASS_VARIABLE(UnionTypeDefinition) SETUP_NODE_CLASS_VARIABLE(EnumTypeDefinition) SETUP_NODE_CLASS_VARIABLE(InputObjectTypeDefinition) SETUP_NODE_CLASS_VARIABLE(EnumValueDefinition) SETUP_NODE_CLASS_VARIABLE(DirectiveDefinition) SETUP_NODE_CLASS_VARIABLE(DirectiveLocation) SETUP_NODE_CLASS_VARIABLE(FieldDefinition) SETUP_NODE_CLASS_VARIABLE(InputValueDefinition) SETUP_NODE_CLASS_VARIABLE(SchemaDefinition) SETUP_NODE_CLASS_VARIABLE(ScalarTypeExtension) SETUP_NODE_CLASS_VARIABLE(ObjectTypeExtension) SETUP_NODE_CLASS_VARIABLE(InterfaceTypeExtension) SETUP_NODE_CLASS_VARIABLE(UnionTypeExtension) SETUP_NODE_CLASS_VARIABLE(EnumTypeExtension) SETUP_NODE_CLASS_VARIABLE(InputObjectTypeExtension) SETUP_NODE_CLASS_VARIABLE(SchemaExtension) #line 124 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" # ifndef YY_CAST # ifdef __cplusplus # define YY_CAST(Type, Val) static_cast (Val) # define YY_REINTERPRET_CAST(Type, Val) reinterpret_cast (Val) # else # define YY_CAST(Type, Val) ((Type) (Val)) # define YY_REINTERPRET_CAST(Type, Val) ((Type) (Val)) # endif # endif # ifndef YY_NULLPTR # if defined __cplusplus # if 201103L <= __cplusplus # define YY_NULLPTR nullptr # else # define YY_NULLPTR 0 # endif # else # define YY_NULLPTR ((void*)0) # endif # endif /* Debug traces. */ #ifndef YYDEBUG # define YYDEBUG 0 #endif #if YYDEBUG extern int yydebug; #endif /* Token kinds. */ #ifndef YYTOKENTYPE # define YYTOKENTYPE enum yytokentype { YYEMPTY = -2, YYEOF = 0, /* "end of file" */ YYerror = 256, /* error */ YYUNDEF = 257, /* "invalid token" */ AMP = 200, /* AMP */ BANG = 201, /* BANG */ COLON = 202, /* COLON */ DIRECTIVE = 203, /* DIRECTIVE */ DIR_SIGN = 204, /* DIR_SIGN */ ENUM = 205, /* ENUM */ ELLIPSIS = 206, /* ELLIPSIS */ EQUALS = 207, /* EQUALS */ EXTEND = 208, /* EXTEND */ FALSE_LITERAL = 209, /* FALSE_LITERAL */ FLOAT = 210, /* FLOAT */ FRAGMENT = 211, /* FRAGMENT */ IDENTIFIER = 212, /* IDENTIFIER */ INPUT = 213, /* INPUT */ IMPLEMENTS = 214, /* IMPLEMENTS */ INT = 215, /* INT */ INTERFACE = 216, /* INTERFACE */ LBRACKET = 217, /* LBRACKET */ LCURLY = 218, /* LCURLY */ LPAREN = 219, /* LPAREN */ MUTATION = 220, /* MUTATION */ NULL_LITERAL = 221, /* NULL_LITERAL */ ON = 222, /* ON */ PIPE = 223, /* PIPE */ QUERY = 224, /* QUERY */ RBRACKET = 225, /* RBRACKET */ RCURLY = 226, /* RCURLY */ REPEATABLE = 227, /* REPEATABLE */ RPAREN = 228, /* RPAREN */ SCALAR = 229, /* SCALAR */ SCHEMA = 230, /* SCHEMA */ STRING = 231, /* STRING */ SUBSCRIPTION = 232, /* SUBSCRIPTION */ TRUE_LITERAL = 233, /* TRUE_LITERAL */ TYPE_LITERAL = 234, /* TYPE_LITERAL */ UNION = 235, /* UNION */ VAR_SIGN = 236 /* VAR_SIGN */ }; typedef enum yytokentype yytoken_kind_t; #endif /* Token kinds. */ #define YYEMPTY -2 #define YYEOF 0 #define YYerror 256 #define YYUNDEF 257 #define AMP 200 #define BANG 201 #define COLON 202 #define DIRECTIVE 203 #define DIR_SIGN 204 #define ENUM 205 #define ELLIPSIS 206 #define EQUALS 207 #define EXTEND 208 #define FALSE_LITERAL 209 #define FLOAT 210 #define FRAGMENT 211 #define IDENTIFIER 212 #define INPUT 213 #define IMPLEMENTS 214 #define INT 215 #define INTERFACE 216 #define LBRACKET 217 #define LCURLY 218 #define LPAREN 219 #define MUTATION 220 #define NULL_LITERAL 221 #define ON 222 #define PIPE 223 #define QUERY 224 #define RBRACKET 225 #define RCURLY 226 #define REPEATABLE 227 #define RPAREN 228 #define SCALAR 229 #define SCHEMA 230 #define STRING 231 #define SUBSCRIPTION 232 #define TRUE_LITERAL 233 #define TYPE_LITERAL 234 #define UNION 235 #define VAR_SIGN 236 /* Value type. */ #if ! defined YYSTYPE && ! defined YYSTYPE_IS_DECLARED typedef int YYSTYPE; # define YYSTYPE_IS_TRIVIAL 1 # define YYSTYPE_IS_DECLARED 1 #endif int yyparse (VALUE parser, VALUE filename); /* Symbol kind. */ enum yysymbol_kind_t { YYSYMBOL_YYEMPTY = -2, YYSYMBOL_YYEOF = 0, /* "end of file" */ YYSYMBOL_YYerror = 1, /* error */ YYSYMBOL_YYUNDEF = 2, /* "invalid token" */ YYSYMBOL_AMP = 3, /* AMP */ YYSYMBOL_BANG = 4, /* BANG */ YYSYMBOL_COLON = 5, /* COLON */ YYSYMBOL_DIRECTIVE = 6, /* DIRECTIVE */ YYSYMBOL_DIR_SIGN = 7, /* DIR_SIGN */ YYSYMBOL_ENUM = 8, /* ENUM */ YYSYMBOL_ELLIPSIS = 9, /* ELLIPSIS */ YYSYMBOL_EQUALS = 10, /* EQUALS */ YYSYMBOL_EXTEND = 11, /* EXTEND */ YYSYMBOL_FALSE_LITERAL = 12, /* FALSE_LITERAL */ YYSYMBOL_FLOAT = 13, /* FLOAT */ YYSYMBOL_FRAGMENT = 14, /* FRAGMENT */ YYSYMBOL_IDENTIFIER = 15, /* IDENTIFIER */ YYSYMBOL_INPUT = 16, /* INPUT */ YYSYMBOL_IMPLEMENTS = 17, /* IMPLEMENTS */ YYSYMBOL_INT = 18, /* INT */ YYSYMBOL_INTERFACE = 19, /* INTERFACE */ YYSYMBOL_LBRACKET = 20, /* LBRACKET */ YYSYMBOL_LCURLY = 21, /* LCURLY */ YYSYMBOL_LPAREN = 22, /* LPAREN */ YYSYMBOL_MUTATION = 23, /* MUTATION */ YYSYMBOL_NULL_LITERAL = 24, /* NULL_LITERAL */ YYSYMBOL_ON = 25, /* ON */ YYSYMBOL_PIPE = 26, /* PIPE */ YYSYMBOL_QUERY = 27, /* QUERY */ YYSYMBOL_RBRACKET = 28, /* RBRACKET */ YYSYMBOL_RCURLY = 29, /* RCURLY */ YYSYMBOL_REPEATABLE = 30, /* REPEATABLE */ YYSYMBOL_RPAREN = 31, /* RPAREN */ YYSYMBOL_SCALAR = 32, /* SCALAR */ YYSYMBOL_SCHEMA = 33, /* SCHEMA */ YYSYMBOL_STRING = 34, /* STRING */ YYSYMBOL_SUBSCRIPTION = 35, /* SUBSCRIPTION */ YYSYMBOL_TRUE_LITERAL = 36, /* TRUE_LITERAL */ YYSYMBOL_TYPE_LITERAL = 37, /* TYPE_LITERAL */ YYSYMBOL_UNION = 38, /* UNION */ YYSYMBOL_VAR_SIGN = 39, /* VAR_SIGN */ YYSYMBOL_YYACCEPT = 40, /* $accept */ YYSYMBOL_start = 41, /* start */ YYSYMBOL_document = 42, /* document */ YYSYMBOL_definitions_list = 43, /* definitions_list */ YYSYMBOL_definition = 44, /* definition */ YYSYMBOL_executable_definition = 45, /* executable_definition */ YYSYMBOL_operation_definition = 46, /* operation_definition */ YYSYMBOL_operation_type = 47, /* operation_type */ YYSYMBOL_operation_name_opt = 48, /* operation_name_opt */ YYSYMBOL_variable_definitions_opt = 49, /* variable_definitions_opt */ YYSYMBOL_variable_definitions_list = 50, /* variable_definitions_list */ YYSYMBOL_variable_definition = 51, /* variable_definition */ YYSYMBOL_default_value_opt = 52, /* default_value_opt */ YYSYMBOL_selection_list = 53, /* selection_list */ YYSYMBOL_selection = 54, /* selection */ YYSYMBOL_selection_set = 55, /* selection_set */ YYSYMBOL_selection_set_opt = 56, /* selection_set_opt */ YYSYMBOL_field = 57, /* field */ YYSYMBOL_arguments_opt = 58, /* arguments_opt */ YYSYMBOL_arguments_list = 59, /* arguments_list */ YYSYMBOL_argument = 60, /* argument */ YYSYMBOL_literal_value = 61, /* literal_value */ YYSYMBOL_input_value = 62, /* input_value */ YYSYMBOL_null_value = 63, /* null_value */ YYSYMBOL_variable = 64, /* variable */ YYSYMBOL_list_value = 65, /* list_value */ YYSYMBOL_list_value_list = 66, /* list_value_list */ YYSYMBOL_enum_name = 67, /* enum_name */ YYSYMBOL_enum_value = 68, /* enum_value */ YYSYMBOL_object_value = 69, /* object_value */ YYSYMBOL_object_value_list_opt = 70, /* object_value_list_opt */ YYSYMBOL_object_value_list = 71, /* object_value_list */ YYSYMBOL_object_value_field = 72, /* object_value_field */ YYSYMBOL_object_literal_value = 73, /* object_literal_value */ YYSYMBOL_object_literal_value_list_opt = 74, /* object_literal_value_list_opt */ YYSYMBOL_object_literal_value_list = 75, /* object_literal_value_list */ YYSYMBOL_object_literal_value_field = 76, /* object_literal_value_field */ YYSYMBOL_directives_list_opt = 77, /* directives_list_opt */ YYSYMBOL_directives_list = 78, /* directives_list */ YYSYMBOL_directive = 79, /* directive */ YYSYMBOL_name = 80, /* name */ YYSYMBOL_schema_keyword = 81, /* schema_keyword */ YYSYMBOL_name_without_on = 82, /* name_without_on */ YYSYMBOL_fragment_spread = 83, /* fragment_spread */ YYSYMBOL_inline_fragment = 84, /* inline_fragment */ YYSYMBOL_fragment_definition = 85, /* fragment_definition */ YYSYMBOL_fragment_name_opt = 86, /* fragment_name_opt */ YYSYMBOL_type = 87, /* type */ YYSYMBOL_nullable_type = 88, /* nullable_type */ YYSYMBOL_type_system_definition = 89, /* type_system_definition */ YYSYMBOL_schema_definition = 90, /* schema_definition */ YYSYMBOL_operation_type_definition_list_opt = 91, /* operation_type_definition_list_opt */ YYSYMBOL_operation_type_definition_list = 92, /* operation_type_definition_list */ YYSYMBOL_operation_type_definition = 93, /* operation_type_definition */ YYSYMBOL_type_definition = 94, /* type_definition */ YYSYMBOL_description = 95, /* description */ YYSYMBOL_description_opt = 96, /* description_opt */ YYSYMBOL_scalar_type_definition = 97, /* scalar_type_definition */ YYSYMBOL_object_type_definition = 98, /* object_type_definition */ YYSYMBOL_implements_opt = 99, /* implements_opt */ YYSYMBOL_interfaces_list = 100, /* interfaces_list */ YYSYMBOL_legacy_interfaces_list = 101, /* legacy_interfaces_list */ YYSYMBOL_input_value_definition = 102, /* input_value_definition */ YYSYMBOL_input_value_definition_list = 103, /* input_value_definition_list */ YYSYMBOL_arguments_definitions_opt = 104, /* arguments_definitions_opt */ YYSYMBOL_field_definition = 105, /* field_definition */ YYSYMBOL_field_definition_list_opt = 106, /* field_definition_list_opt */ YYSYMBOL_field_definition_list = 107, /* field_definition_list */ YYSYMBOL_interface_type_definition = 108, /* interface_type_definition */ YYSYMBOL_pipe_opt = 109, /* pipe_opt */ YYSYMBOL_union_members = 110, /* union_members */ YYSYMBOL_union_type_definition = 111, /* union_type_definition */ YYSYMBOL_enum_type_definition = 112, /* enum_type_definition */ YYSYMBOL_enum_value_definition = 113, /* enum_value_definition */ YYSYMBOL_enum_value_definitions = 114, /* enum_value_definitions */ YYSYMBOL_input_object_type_definition = 115, /* input_object_type_definition */ YYSYMBOL_directive_definition = 116, /* directive_definition */ YYSYMBOL_directive_repeatable_opt = 117, /* directive_repeatable_opt */ YYSYMBOL_directive_locations = 118, /* directive_locations */ YYSYMBOL_type_system_extension = 119, /* type_system_extension */ YYSYMBOL_schema_extension = 120, /* schema_extension */ YYSYMBOL_type_extension = 121, /* type_extension */ YYSYMBOL_scalar_type_extension = 122, /* scalar_type_extension */ YYSYMBOL_object_type_extension = 123, /* object_type_extension */ YYSYMBOL_interface_type_extension = 124, /* interface_type_extension */ YYSYMBOL_union_type_extension = 125, /* union_type_extension */ YYSYMBOL_enum_type_extension = 126, /* enum_type_extension */ YYSYMBOL_input_object_type_extension = 127, /* input_object_type_extension */ YYSYMBOL_NamedTypeForCondition = 128 /* NamedTypeForCondition */ }; typedef enum yysymbol_kind_t yysymbol_kind_t; #ifdef short # undef short #endif /* On compilers that do not define __PTRDIFF_MAX__ etc., make sure and (if available) are included so that the code can choose integer types of a good width. */ #ifndef __PTRDIFF_MAX__ # include /* INFRINGES ON USER NAME SPACE */ # if defined __STDC_VERSION__ && 199901 <= __STDC_VERSION__ # include /* INFRINGES ON USER NAME SPACE */ # define YY_STDINT_H # endif #endif /* Narrow types that promote to a signed type and that can represent a signed or unsigned integer of at least N bits. In tables they can save space and decrease cache pressure. Promoting to a signed type helps avoid bugs in integer arithmetic. */ #ifdef __INT_LEAST8_MAX__ typedef __INT_LEAST8_TYPE__ yytype_int8; #elif defined YY_STDINT_H typedef int_least8_t yytype_int8; #else typedef signed char yytype_int8; #endif #ifdef __INT_LEAST16_MAX__ typedef __INT_LEAST16_TYPE__ yytype_int16; #elif defined YY_STDINT_H typedef int_least16_t yytype_int16; #else typedef short yytype_int16; #endif /* Work around bug in HP-UX 11.23, which defines these macros incorrectly for preprocessor constants. This workaround can likely be removed in 2023, as HPE has promised support for HP-UX 11.23 (aka HP-UX 11i v2) only through the end of 2022; see Table 2 of . */ #ifdef __hpux # undef UINT_LEAST8_MAX # undef UINT_LEAST16_MAX # define UINT_LEAST8_MAX 255 # define UINT_LEAST16_MAX 65535 #endif #if defined __UINT_LEAST8_MAX__ && __UINT_LEAST8_MAX__ <= __INT_MAX__ typedef __UINT_LEAST8_TYPE__ yytype_uint8; #elif (!defined __UINT_LEAST8_MAX__ && defined YY_STDINT_H \ && UINT_LEAST8_MAX <= INT_MAX) typedef uint_least8_t yytype_uint8; #elif !defined __UINT_LEAST8_MAX__ && UCHAR_MAX <= INT_MAX typedef unsigned char yytype_uint8; #else typedef short yytype_uint8; #endif #if defined __UINT_LEAST16_MAX__ && __UINT_LEAST16_MAX__ <= __INT_MAX__ typedef __UINT_LEAST16_TYPE__ yytype_uint16; #elif (!defined __UINT_LEAST16_MAX__ && defined YY_STDINT_H \ && UINT_LEAST16_MAX <= INT_MAX) typedef uint_least16_t yytype_uint16; #elif !defined __UINT_LEAST16_MAX__ && USHRT_MAX <= INT_MAX typedef unsigned short yytype_uint16; #else typedef int yytype_uint16; #endif #ifndef YYPTRDIFF_T # if defined __PTRDIFF_TYPE__ && defined __PTRDIFF_MAX__ # define YYPTRDIFF_T __PTRDIFF_TYPE__ # define YYPTRDIFF_MAXIMUM __PTRDIFF_MAX__ # elif defined PTRDIFF_MAX # ifndef ptrdiff_t # include /* INFRINGES ON USER NAME SPACE */ # endif # define YYPTRDIFF_T ptrdiff_t # define YYPTRDIFF_MAXIMUM PTRDIFF_MAX # else # define YYPTRDIFF_T long # define YYPTRDIFF_MAXIMUM LONG_MAX # endif #endif #ifndef YYSIZE_T # ifdef __SIZE_TYPE__ # define YYSIZE_T __SIZE_TYPE__ # elif defined size_t # define YYSIZE_T size_t # elif defined __STDC_VERSION__ && 199901 <= __STDC_VERSION__ # include /* INFRINGES ON USER NAME SPACE */ # define YYSIZE_T size_t # else # define YYSIZE_T unsigned # endif #endif #define YYSIZE_MAXIMUM \ YY_CAST (YYPTRDIFF_T, \ (YYPTRDIFF_MAXIMUM < YY_CAST (YYSIZE_T, -1) \ ? YYPTRDIFF_MAXIMUM \ : YY_CAST (YYSIZE_T, -1))) #define YYSIZEOF(X) YY_CAST (YYPTRDIFF_T, sizeof (X)) /* Stored state numbers (used for stacks). */ typedef yytype_int16 yy_state_t; /* State numbers in computations. */ typedef int yy_state_fast_t; #ifndef YY_ # if defined YYENABLE_NLS && YYENABLE_NLS # if ENABLE_NLS # include /* INFRINGES ON USER NAME SPACE */ # define YY_(Msgid) dgettext ("bison-runtime", Msgid) # endif # endif # ifndef YY_ # define YY_(Msgid) Msgid # endif #endif #ifndef YY_ATTRIBUTE_PURE # if defined __GNUC__ && 2 < __GNUC__ + (96 <= __GNUC_MINOR__) # define YY_ATTRIBUTE_PURE __attribute__ ((__pure__)) # else # define YY_ATTRIBUTE_PURE # endif #endif #ifndef YY_ATTRIBUTE_UNUSED # if defined __GNUC__ && 2 < __GNUC__ + (7 <= __GNUC_MINOR__) # define YY_ATTRIBUTE_UNUSED __attribute__ ((__unused__)) # else # define YY_ATTRIBUTE_UNUSED # endif #endif /* Suppress unused-variable warnings by "using" E. */ #if ! defined lint || defined __GNUC__ # define YY_USE(E) ((void) (E)) #else # define YY_USE(E) /* empty */ #endif /* Suppress an incorrect diagnostic about yylval being uninitialized. */ #if defined __GNUC__ && ! defined __ICC && 406 <= __GNUC__ * 100 + __GNUC_MINOR__ # if __GNUC__ * 100 + __GNUC_MINOR__ < 407 # define YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN \ _Pragma ("GCC diagnostic push") \ _Pragma ("GCC diagnostic ignored \"-Wuninitialized\"") # else # define YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN \ _Pragma ("GCC diagnostic push") \ _Pragma ("GCC diagnostic ignored \"-Wuninitialized\"") \ _Pragma ("GCC diagnostic ignored \"-Wmaybe-uninitialized\"") # endif # define YY_IGNORE_MAYBE_UNINITIALIZED_END \ _Pragma ("GCC diagnostic pop") #else # define YY_INITIAL_VALUE(Value) Value #endif #ifndef YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN # define YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN # define YY_IGNORE_MAYBE_UNINITIALIZED_END #endif #ifndef YY_INITIAL_VALUE # define YY_INITIAL_VALUE(Value) /* Nothing. */ #endif #if defined __cplusplus && defined __GNUC__ && ! defined __ICC && 6 <= __GNUC__ # define YY_IGNORE_USELESS_CAST_BEGIN \ _Pragma ("GCC diagnostic push") \ _Pragma ("GCC diagnostic ignored \"-Wuseless-cast\"") # define YY_IGNORE_USELESS_CAST_END \ _Pragma ("GCC diagnostic pop") #endif #ifndef YY_IGNORE_USELESS_CAST_BEGIN # define YY_IGNORE_USELESS_CAST_BEGIN # define YY_IGNORE_USELESS_CAST_END #endif #define YY_ASSERT(E) ((void) (0 && (E))) #if 1 /* The parser invokes alloca or malloc; define the necessary symbols. */ # ifdef YYSTACK_USE_ALLOCA # if YYSTACK_USE_ALLOCA # ifdef __GNUC__ # define YYSTACK_ALLOC __builtin_alloca # elif defined __BUILTIN_VA_ARG_INCR # include /* INFRINGES ON USER NAME SPACE */ # elif defined _AIX # define YYSTACK_ALLOC __alloca # elif defined _MSC_VER # include /* INFRINGES ON USER NAME SPACE */ # define alloca _alloca # else # define YYSTACK_ALLOC alloca # if ! defined _ALLOCA_H && ! defined EXIT_SUCCESS # include /* INFRINGES ON USER NAME SPACE */ /* Use EXIT_SUCCESS as a witness for stdlib.h. */ # ifndef EXIT_SUCCESS # define EXIT_SUCCESS 0 # endif # endif # endif # endif # endif # ifdef YYSTACK_ALLOC /* Pacify GCC's 'empty if-body' warning. */ # define YYSTACK_FREE(Ptr) do { /* empty */; } while (0) # ifndef YYSTACK_ALLOC_MAXIMUM /* The OS might guarantee only one guard page at the bottom of the stack, and a page size can be as small as 4096 bytes. So we cannot safely invoke alloca (N) if N exceeds 4096. Use a slightly smaller number to allow for a few compiler-allocated temporary stack slots. */ # define YYSTACK_ALLOC_MAXIMUM 4032 /* reasonable circa 2006 */ # endif # else # define YYSTACK_ALLOC YYMALLOC # define YYSTACK_FREE YYFREE # ifndef YYSTACK_ALLOC_MAXIMUM # define YYSTACK_ALLOC_MAXIMUM YYSIZE_MAXIMUM # endif # if (defined __cplusplus && ! defined EXIT_SUCCESS \ && ! ((defined YYMALLOC || defined malloc) \ && (defined YYFREE || defined free))) # include /* INFRINGES ON USER NAME SPACE */ # ifndef EXIT_SUCCESS # define EXIT_SUCCESS 0 # endif # endif # ifndef YYMALLOC # define YYMALLOC malloc # if ! defined malloc && ! defined EXIT_SUCCESS void *malloc (YYSIZE_T); /* INFRINGES ON USER NAME SPACE */ # endif # endif # ifndef YYFREE # define YYFREE free # if ! defined free && ! defined EXIT_SUCCESS void free (void *); /* INFRINGES ON USER NAME SPACE */ # endif # endif # endif #endif /* 1 */ #if (! defined yyoverflow \ && (! defined __cplusplus \ || (defined YYSTYPE_IS_TRIVIAL && YYSTYPE_IS_TRIVIAL))) /* A type that is properly aligned for any stack member. */ union yyalloc { yy_state_t yyss_alloc; YYSTYPE yyvs_alloc; }; /* The size of the maximum gap between one aligned stack and the next. */ # define YYSTACK_GAP_MAXIMUM (YYSIZEOF (union yyalloc) - 1) /* The size of an array large to enough to hold all stacks, each with N elements. */ # define YYSTACK_BYTES(N) \ ((N) * (YYSIZEOF (yy_state_t) + YYSIZEOF (YYSTYPE)) \ + YYSTACK_GAP_MAXIMUM) # define YYCOPY_NEEDED 1 /* Relocate STACK from its old location to the new one. The local variables YYSIZE and YYSTACKSIZE give the old and new number of elements in the stack, and YYPTR gives the new location of the stack. Advance YYPTR to a properly aligned location for the next stack. */ # define YYSTACK_RELOCATE(Stack_alloc, Stack) \ do \ { \ YYPTRDIFF_T yynewbytes; \ YYCOPY (&yyptr->Stack_alloc, Stack, yysize); \ Stack = &yyptr->Stack_alloc; \ yynewbytes = yystacksize * YYSIZEOF (*Stack) + YYSTACK_GAP_MAXIMUM; \ yyptr += yynewbytes / YYSIZEOF (*yyptr); \ } \ while (0) #endif #if defined YYCOPY_NEEDED && YYCOPY_NEEDED /* Copy COUNT objects from SRC to DST. The source and destination do not overlap. */ # ifndef YYCOPY # if defined __GNUC__ && 1 < __GNUC__ # define YYCOPY(Dst, Src, Count) \ __builtin_memcpy (Dst, Src, YY_CAST (YYSIZE_T, (Count)) * sizeof (*(Src))) # else # define YYCOPY(Dst, Src, Count) \ do \ { \ YYPTRDIFF_T yyi; \ for (yyi = 0; yyi < (Count); yyi++) \ (Dst)[yyi] = (Src)[yyi]; \ } \ while (0) # endif # endif #endif /* !YYCOPY_NEEDED */ /* YYFINAL -- State number of the termination state. */ #define YYFINAL 79 /* YYLAST -- Last index in YYTABLE. */ #define YYLAST 820 /* YYNTOKENS -- Number of terminals. */ #define YYNTOKENS 40 /* YYNNTS -- Number of nonterminals. */ #define YYNNTS 89 /* YYNRULES -- Number of rules. */ #define YYNRULES 185 /* YYNSTATES -- Number of states. */ #define YYNSTATES 314 /* YYMAXUTOK -- Last valid token kind. */ #define YYMAXUTOK 257 /* YYTRANSLATE(TOKEN-NUM) -- Symbol number corresponding to TOKEN-NUM as returned by yylex, with out-of-bounds checking. */ #define YYTRANSLATE(YYX) \ (0 <= (YYX) && (YYX) <= YYMAXUTOK \ ? YY_CAST (yysymbol_kind_t, yytranslate[YYX]) \ : YYSYMBOL_YYUNDEF) /* YYTRANSLATE[TOKEN-NUM] -- Symbol number corresponding to TOKEN-NUM as returned by yylex. */ static const yytype_int8 yytranslate[] = { 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2 }; #if YYDEBUG /* YYRLINE[YYN] -- Source line where rule number YYN was defined. */ static const yytype_int16 yyrline[] = { 0, 105, 105, 107, 121, 122, 125, 126, 127, 130, 131, 134, 145, 156, 169, 170, 171, 174, 175, 178, 179, 182, 183, 186, 198, 199, 202, 203, 206, 207, 208, 211, 214, 215, 218, 229, 242, 243, 246, 247, 250, 260, 261, 262, 263, 264, 265, 266, 267, 268, 271, 272, 273, 275, 283, 292, 293, 296, 297, 300, 301, 302, 303, 305, 314, 323, 324, 327, 328, 331, 342, 351, 352, 355, 356, 359, 370, 371, 374, 375, 377, 387, 388, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 405, 406, 407, 408, 409, 410, 414, 424, 433, 444, 456, 457, 460, 461, 464, 471, 480, 481, 482, 485, 498, 499, 502, 506, 511, 516, 517, 518, 519, 520, 521, 523, 526, 527, 530, 542, 556, 557, 558, 559, 562, 570, 576, 584, 589, 603, 604, 607, 608, 611, 625, 626, 629, 630, 631, 634, 648, 649, 652, 660, 665, 678, 691, 703, 704, 707, 720, 734, 735, 738, 739, 743, 744, 747, 758, 770, 771, 772, 773, 774, 775, 777, 787, 799, 811, 820, 831, 840, 851, 860, 871 }; #endif /** Accessing symbol of state STATE. */ #define YY_ACCESSING_SYMBOL(State) YY_CAST (yysymbol_kind_t, yystos[State]) #if 1 /* The user-facing name of the symbol whose (internal) number is YYSYMBOL. No bounds checking. */ static const char *yysymbol_name (yysymbol_kind_t yysymbol) YY_ATTRIBUTE_UNUSED; static const char * yysymbol_name (yysymbol_kind_t yysymbol) { static const char *const yy_sname[] = { "end of file", "error", "invalid token", "AMP", "BANG", "COLON", "DIRECTIVE", "DIR_SIGN", "ENUM", "ELLIPSIS", "EQUALS", "EXTEND", "FALSE_LITERAL", "FLOAT", "FRAGMENT", "IDENTIFIER", "INPUT", "IMPLEMENTS", "INT", "INTERFACE", "LBRACKET", "LCURLY", "LPAREN", "MUTATION", "NULL_LITERAL", "ON", "PIPE", "QUERY", "RBRACKET", "RCURLY", "REPEATABLE", "RPAREN", "SCALAR", "SCHEMA", "STRING", "SUBSCRIPTION", "TRUE_LITERAL", "TYPE_LITERAL", "UNION", "VAR_SIGN", "$accept", "start", "document", "definitions_list", "definition", "executable_definition", "operation_definition", "operation_type", "operation_name_opt", "variable_definitions_opt", "variable_definitions_list", "variable_definition", "default_value_opt", "selection_list", "selection", "selection_set", "selection_set_opt", "field", "arguments_opt", "arguments_list", "argument", "literal_value", "input_value", "null_value", "variable", "list_value", "list_value_list", "enum_name", "enum_value", "object_value", "object_value_list_opt", "object_value_list", "object_value_field", "object_literal_value", "object_literal_value_list_opt", "object_literal_value_list", "object_literal_value_field", "directives_list_opt", "directives_list", "directive", "name", "schema_keyword", "name_without_on", "fragment_spread", "inline_fragment", "fragment_definition", "fragment_name_opt", "type", "nullable_type", "type_system_definition", "schema_definition", "operation_type_definition_list_opt", "operation_type_definition_list", "operation_type_definition", "type_definition", "description", "description_opt", "scalar_type_definition", "object_type_definition", "implements_opt", "interfaces_list", "legacy_interfaces_list", "input_value_definition", "input_value_definition_list", "arguments_definitions_opt", "field_definition", "field_definition_list_opt", "field_definition_list", "interface_type_definition", "pipe_opt", "union_members", "union_type_definition", "enum_type_definition", "enum_value_definition", "enum_value_definitions", "input_object_type_definition", "directive_definition", "directive_repeatable_opt", "directive_locations", "type_system_extension", "schema_extension", "type_extension", "scalar_type_extension", "object_type_extension", "interface_type_extension", "union_type_extension", "enum_type_extension", "input_object_type_extension", "NamedTypeForCondition", YY_NULLPTR }; return yy_sname[yysymbol]; } #endif #define YYPACT_NINF (-249) #define yypact_value_is_default(Yyn) \ ((Yyn) == YYPACT_NINF) #define YYTABLE_NINF (-148) #define yytable_value_is_error(Yyn) \ 0 /* YYPACT[STATE-NUM] -- Index in YYTABLE of the portion describing STATE-NUM. */ static const yytype_int16 yypact[] = { 148, 179, 749, 485, -249, -249, 14, -249, -249, 45, -249, 83, -249, -249, -249, 716, -249, -249, -249, -249, -249, 120, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, 716, 716, 716, 716, 14, 716, 716, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, 21, 584, -249, -249, 518, -249, -249, 11, -249, -249, -249, 716, 47, 14, -249, -249, -249, 58, -249, 74, 716, 716, 716, 716, 716, 716, 14, 14, 65, 14, 75, 27, 65, 14, 716, 716, 84, 14, -249, -249, 716, 716, 14, 93, 174, -249, -249, 95, 14, 716, 14, 14, 65, 14, 65, 14, 111, 27, 122, 27, 317, 14, 14, 174, 14, 140, 102, -249, 14, 14, 617, -249, -249, 93, 650, -249, 149, 84, -249, 155, 72, -249, 716, 48, -249, 84, 144, 146, 151, 14, -249, 14, 160, 143, 143, 716, 208, 171, 716, 158, 118, 158, 162, 84, 84, 551, 14, -249, -249, 418, -249, -249, 716, -249, -249, 181, -249, -249, -249, 143, 154, 143, 143, 158, 158, 162, 782, -249, -11, 716, -249, -10, -249, 171, 716, -249, 15, -249, -249, -249, -249, 716, 165, -249, -249, -249, 84, -249, -249, -249, -249, 350, 716, -249, -249, -249, -249, 716, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, 683, 88, -249, 169, 35, 55, -249, -249, 165, 14, -249, -249, 194, -249, -249, -249, 716, -249, 69, -249, 716, -249, -249, -249, 384, 173, 716, -249, 176, 716, -249, 195, -249, 683, -249, 196, 203, -249, 716, -249, -249, -249, 683, 144, -249, -249, -249, -249, -249, -249, -249, 205, -249, -249, 209, 418, 185, 452, 14, -249, -249, 192, 196, 214, 418, 452, -249, -249, -249, 716, -249, -249, 716, 14, 683, -249, -249, -249, 14, -249 }; /* YYDEFACT[STATE-NUM] -- Default reduction number in state STATE-NUM. Performed when YYTABLE does not specify something else to do. Zero means the default is an error. */ static const yytype_uint8 yydefact[] = { 127, 0, 105, 0, 15, 14, 76, 126, 16, 0, 2, 127, 4, 6, 9, 17, 10, 7, 111, 112, 128, 0, 120, 121, 122, 123, 124, 125, 113, 8, 166, 167, 170, 171, 172, 173, 174, 175, 0, 0, 0, 0, 76, 0, 0, 91, 89, 92, 97, 93, 95, 90, 86, 87, 98, 94, 84, 83, 96, 85, 88, 99, 100, 106, 0, 76, 82, 13, 0, 26, 28, 36, 81, 29, 30, 0, 115, 77, 78, 1, 5, 19, 18, 0, 0, 0, 0, 0, 0, 0, 76, 76, 131, 0, 0, 169, 131, 76, 0, 0, 0, 76, 12, 27, 0, 0, 76, 36, 0, 114, 79, 0, 76, 0, 76, 76, 131, 76, 131, 76, 0, 182, 0, 184, 0, 76, 176, 0, 76, 0, 180, 185, 76, 76, 0, 103, 101, 36, 0, 38, 0, 32, 80, 0, 0, 117, 0, 0, 21, 0, 142, 0, 0, 76, 129, 76, 0, 127, 127, 0, 135, 133, 134, 145, 0, 145, 151, 0, 0, 0, 76, 37, 39, 0, 33, 35, 0, 116, 118, 0, 20, 22, 11, 127, 162, 127, 127, 145, 145, 151, 0, 158, 127, 0, 140, 127, 135, 132, 0, 138, 127, 178, 168, 177, 152, 0, 179, 104, 102, 31, 32, 45, 41, 59, 42, 0, 65, 53, 60, 43, 44, 0, 61, 50, 40, 46, 51, 48, 63, 47, 52, 49, 62, 119, 0, 127, 163, 0, 127, 127, 150, 130, 155, 76, 181, 159, 0, 183, 141, 136, 0, 148, 127, 153, 0, 34, 55, 57, 0, 0, 66, 67, 0, 72, 73, 0, 54, 0, 109, 24, 107, 143, 0, 156, 160, 157, 0, 142, 146, 149, 154, 56, 58, 64, 68, 0, 70, 74, 0, 0, 0, 0, 76, 108, 164, 161, 24, 0, 0, 0, 50, 69, 110, 71, 25, 23, 0, 76, 0, 75, 165, 139, 76, 144 }; /* YYPGOTO[NTERM-NUM]. */ static const yytype_int16 yypgoto[] = { -249, -249, -249, -249, 211, -249, -249, 0, -249, -249, -249, 77, -70, 94, -67, -90, 17, -249, -98, -249, 91, -248, -165, -249, -249, -249, -249, 40, -249, -249, -249, -249, -29, -249, -249, -249, -28, 23, -36, -63, -13, -168, 1, -249, -249, -249, -249, -238, -249, -249, -249, -249, 107, -127, -249, -249, 4, -249, -249, -76, 80, -249, -183, -18, -41, -12, -152, -249, -249, -249, 54, -249, -249, -185, 60, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, -249, 147 }; /* YYDEFGOTO[NTERM-NUM]. */ static const yytype_int16 yydefgoto[] = { 0, 9, 10, 11, 12, 13, 14, 61, 81, 112, 147, 148, 292, 68, 69, 174, 175, 70, 106, 138, 139, 223, 301, 225, 226, 227, 258, 228, 229, 230, 259, 260, 261, 231, 262, 263, 264, 76, 77, 78, 71, 62, 72, 73, 74, 16, 64, 269, 270, 17, 18, 109, 144, 145, 19, 20, 193, 22, 23, 125, 161, 162, 194, 195, 184, 251, 201, 252, 24, 205, 206, 25, 26, 191, 192, 27, 28, 237, 295, 29, 30, 31, 32, 33, 34, 35, 36, 37, 132 }; /* YYTABLE[YYPACT[STATE-NUM]] -- What to do in state STATE-NUM. If positive, shift that token. If negative, reduce the rule whose number is the opposite. If YYTABLE_NINF, syntax error. */ static const yytype_int16 yytable[] = { 15, 103, 82, 63, 21, 232, 95, 245, 224, 142, 135, 15, 248, 203, 110, 21, 104, 178, 244, 247, 128, 75, 232, 7, 7, 90, 91, 92, 93, 290, 96, 97, 110, 105, 75, 240, 241, 178, 296, 170, 153, 300, 155, 304, -147, 79, 98, 232, -77, 7, 257, 309, 248, 245, 121, 123, 248, 126, 110, 182, 110, 130, 107, 110, 273, 94, 101, 110, 108, 7, 312, 114, 115, 116, 117, 118, 119, 207, 208, 180, 111, 113, 124, -3, 274, 131, 131, 146, 100, 7, 232, 137, 140, 282, 1, 4, 127, 2, 278, 5, 150, 177, 103, 7, 3, 134, 4, 8, 143, 75, 5, 160, -77, 120, 122, 105, 6, 7, 8, 271, 129, 232, 7, 232, 136, 140, 83, 143, 84, 141, 232, 232, 157, 179, 146, 149, 85, 151, 152, 86, 154, 4, 156, 158, 143, 5, 196, 202, 163, 199, 166, 165, 87, 8, 173, 167, 168, 88, 89, 1, 176, 190, 2, 233, 143, 235, 183, 185, 239, 3, 189, 4, 186, 222, 198, 5, 187, 7, 188, 200, 246, 6, 7, 8, 236, 249, 234, 38, 204, 190, 222, 254, 253, 210, 272, 39, 190, 4, 40, 276, 289, 5, 283, 265, 250, 286, 291, 293, 266, 8, 298, 41, 42, 302, 299, 222, 43, 44, 306, 308, -137, 268, 80, -137, 181, -137, 307, 255, 169, 172, 243, 284, -137, -137, 164, 287, 297, 277, -137, 197, 279, 280, 190, 242, -137, 238, 133, 285, 0, 0, 288, 0, 0, 0, 268, 0, 250, 0, 222, 294, 0, 0, 0, 268, 0, 0, 275, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 222, 288, 222, 0, 310, 0, 268, 0, 0, 222, 222, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 305, 0, 0, 0, 0, 159, 0, 0, 45, 0, 46, 0, 0, 47, 48, 311, 49, 50, 51, 52, 313, 53, 0, 0, 0, 4, 54, 66, 0, 5, 0, 0, 55, 0, 56, 57, 0, 8, 58, 59, 60, 45, 0, 46, 0, 0, 47, 211, 212, 49, 213, 51, 52, 214, 53, 215, 216, 0, 4, 217, 218, 0, 5, 256, 0, 55, 0, 56, 57, 219, 8, 220, 59, 60, 221, 45, 0, 46, 0, 0, 47, 211, 212, 49, 213, 51, 52, 214, 53, 215, 216, 0, 4, 217, 218, 0, 5, 281, 0, 55, 0, 56, 57, 219, 8, 220, 59, 60, 221, 45, 0, 46, 0, 0, 47, 211, 212, 49, 213, 51, 52, 214, 53, 215, 216, 0, 4, 217, 218, 0, 5, 0, 0, 55, 0, 56, 57, 219, 8, 220, 59, 60, 221, 45, 0, 46, 0, 0, 47, 211, 212, 49, 213, 51, 52, 214, 53, 215, 303, 0, 4, 217, 218, 0, 5, 0, 0, 55, 0, 56, 57, 219, 8, 220, 59, 60, 45, 0, 46, 65, 0, 47, 48, 0, 49, 50, 51, 52, 0, 53, 0, 0, 0, 4, 54, 66, 0, 5, 0, 67, 55, 0, 56, 57, 0, 8, 58, 59, 60, 45, 0, 46, 65, 0, 47, 48, 0, 49, 50, 51, 52, 0, 53, 0, 0, 0, 4, 54, 66, 0, 5, 0, 102, 55, 0, 56, 57, 0, 8, 58, 59, 60, 45, 0, 46, 65, 0, 47, 48, 0, 49, 50, 51, 52, 0, 53, 0, 0, 0, 4, 54, 66, 0, 5, 0, 209, 55, 0, 56, 57, 0, 8, 58, 59, 60, 45, 75, 46, 0, 0, 47, 48, 0, 49, 50, 51, 52, 0, 53, 0, 0, 0, 4, 54, 99, 0, 5, 0, 0, 55, 0, 56, 57, 0, 8, 58, 59, 60, 45, 0, 46, 65, 0, 47, 48, 0, 49, 50, 51, 52, 0, 53, 0, 0, 0, 4, 54, 66, 0, 5, 0, 0, 55, 0, 56, 57, 0, 8, 58, 59, 60, 45, 0, 46, 0, 0, 47, 48, 0, 49, 50, 51, 52, 0, 53, 0, 0, 0, 4, 54, 66, 0, 5, 0, 0, 55, 171, 56, 57, 0, 8, 58, 59, 60, 45, 0, 46, 0, 0, 47, 48, 0, 49, 50, 51, 52, 0, 53, 267, 0, 0, 4, 54, 66, 0, 5, 0, 0, 55, 0, 56, 57, 0, 8, 58, 59, 60, 45, 0, 46, 0, 0, 47, 48, 0, 49, 50, 51, 52, 0, 53, 0, 0, 0, 4, 54, 66, 0, 5, 0, 0, 55, 0, 56, 57, 0, 8, 58, 59, 60, 45, 0, 46, 0, 0, 47, 48, 0, 49, 50, 51, 52, 0, 53, 0, 0, 0, 4, 54, 0, 0, 5, 0, 0, 55, 0, 56, 57, 0, 8, 58, 59, 60, 45, 0, 46, 0, 0, 47, 0, 0, 49, 213, 51, 52, 0, 53, 0, 0, 0, 4, 0, 218, 0, 5, 0, 0, 55, 0, 56, 57, 0, 8, 0, 59, 60 }; static const yytype_int16 yycheck[] = { 0, 68, 15, 2, 0, 173, 42, 192, 173, 107, 100, 11, 195, 165, 77, 11, 5, 144, 29, 29, 96, 7, 190, 34, 34, 38, 39, 40, 41, 267, 43, 44, 95, 22, 7, 187, 188, 164, 276, 137, 116, 289, 118, 291, 29, 0, 25, 215, 21, 34, 215, 299, 235, 238, 90, 91, 239, 93, 121, 149, 123, 97, 75, 126, 29, 42, 65, 130, 21, 34, 308, 84, 85, 86, 87, 88, 89, 167, 168, 31, 22, 7, 17, 0, 29, 98, 99, 39, 65, 34, 258, 104, 105, 258, 11, 23, 21, 14, 29, 27, 113, 29, 169, 34, 21, 21, 23, 35, 108, 7, 27, 124, 10, 90, 91, 22, 33, 34, 35, 31, 97, 289, 34, 291, 101, 138, 6, 127, 8, 106, 298, 299, 21, 146, 39, 112, 16, 114, 115, 19, 117, 23, 119, 21, 144, 27, 159, 29, 125, 162, 10, 128, 32, 35, 5, 132, 133, 37, 38, 11, 5, 157, 14, 176, 164, 183, 22, 21, 186, 21, 10, 23, 21, 173, 3, 27, 153, 34, 155, 21, 193, 33, 34, 35, 30, 198, 5, 8, 26, 185, 190, 26, 205, 170, 25, 16, 192, 23, 19, 5, 5, 27, 29, 216, 200, 29, 10, 4, 221, 35, 5, 32, 33, 28, 5, 215, 37, 38, 26, 5, 12, 234, 11, 15, 147, 17, 296, 210, 134, 138, 190, 260, 24, 25, 127, 263, 277, 250, 30, 159, 252, 254, 238, 189, 36, 185, 99, 260, -1, -1, 263, -1, -1, -1, 267, -1, 252, -1, 258, 272, -1, -1, -1, 276, -1, -1, 243, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 289, 303, 291, -1, 306, -1, 308, -1, -1, 298, 299, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 292, -1, -1, -1, -1, 3, -1, -1, 6, -1, 8, -1, -1, 11, 12, 307, 14, 15, 16, 17, 312, 19, -1, -1, -1, 23, 24, 25, -1, 27, -1, -1, 30, -1, 32, 33, -1, 35, 36, 37, 38, 6, -1, 8, -1, -1, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, -1, 23, 24, 25, -1, 27, 28, -1, 30, -1, 32, 33, 34, 35, 36, 37, 38, 39, 6, -1, 8, -1, -1, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, -1, 23, 24, 25, -1, 27, 28, -1, 30, -1, 32, 33, 34, 35, 36, 37, 38, 39, 6, -1, 8, -1, -1, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, -1, 23, 24, 25, -1, 27, -1, -1, 30, -1, 32, 33, 34, 35, 36, 37, 38, 39, 6, -1, 8, -1, -1, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, -1, 23, 24, 25, -1, 27, -1, -1, 30, -1, 32, 33, 34, 35, 36, 37, 38, 6, -1, 8, 9, -1, 11, 12, -1, 14, 15, 16, 17, -1, 19, -1, -1, -1, 23, 24, 25, -1, 27, -1, 29, 30, -1, 32, 33, -1, 35, 36, 37, 38, 6, -1, 8, 9, -1, 11, 12, -1, 14, 15, 16, 17, -1, 19, -1, -1, -1, 23, 24, 25, -1, 27, -1, 29, 30, -1, 32, 33, -1, 35, 36, 37, 38, 6, -1, 8, 9, -1, 11, 12, -1, 14, 15, 16, 17, -1, 19, -1, -1, -1, 23, 24, 25, -1, 27, -1, 29, 30, -1, 32, 33, -1, 35, 36, 37, 38, 6, 7, 8, -1, -1, 11, 12, -1, 14, 15, 16, 17, -1, 19, -1, -1, -1, 23, 24, 25, -1, 27, -1, -1, 30, -1, 32, 33, -1, 35, 36, 37, 38, 6, -1, 8, 9, -1, 11, 12, -1, 14, 15, 16, 17, -1, 19, -1, -1, -1, 23, 24, 25, -1, 27, -1, -1, 30, -1, 32, 33, -1, 35, 36, 37, 38, 6, -1, 8, -1, -1, 11, 12, -1, 14, 15, 16, 17, -1, 19, -1, -1, -1, 23, 24, 25, -1, 27, -1, -1, 30, 31, 32, 33, -1, 35, 36, 37, 38, 6, -1, 8, -1, -1, 11, 12, -1, 14, 15, 16, 17, -1, 19, 20, -1, -1, 23, 24, 25, -1, 27, -1, -1, 30, -1, 32, 33, -1, 35, 36, 37, 38, 6, -1, 8, -1, -1, 11, 12, -1, 14, 15, 16, 17, -1, 19, -1, -1, -1, 23, 24, 25, -1, 27, -1, -1, 30, -1, 32, 33, -1, 35, 36, 37, 38, 6, -1, 8, -1, -1, 11, 12, -1, 14, 15, 16, 17, -1, 19, -1, -1, -1, 23, 24, -1, -1, 27, -1, -1, 30, -1, 32, 33, -1, 35, 36, 37, 38, 6, -1, 8, -1, -1, 11, -1, -1, 14, 15, 16, 17, -1, 19, -1, -1, -1, 23, -1, 25, -1, 27, -1, -1, 30, -1, 32, 33, -1, 35, -1, 37, 38 }; /* YYSTOS[STATE-NUM] -- The symbol kind of the accessing symbol of state STATE-NUM. */ static const yytype_uint8 yystos[] = { 0, 11, 14, 21, 23, 27, 33, 34, 35, 41, 42, 43, 44, 45, 46, 47, 85, 89, 90, 94, 95, 96, 97, 98, 108, 111, 112, 115, 116, 119, 120, 121, 122, 123, 124, 125, 126, 127, 8, 16, 19, 32, 33, 37, 38, 6, 8, 11, 12, 14, 15, 16, 17, 19, 24, 30, 32, 33, 36, 37, 38, 47, 81, 82, 86, 9, 25, 29, 53, 54, 57, 80, 82, 83, 84, 7, 77, 78, 79, 0, 44, 48, 80, 6, 8, 16, 19, 32, 37, 38, 80, 80, 80, 80, 77, 78, 80, 80, 25, 25, 77, 82, 29, 54, 5, 22, 58, 80, 21, 91, 79, 22, 49, 7, 80, 80, 80, 80, 80, 80, 77, 78, 77, 78, 17, 99, 78, 21, 99, 77, 78, 80, 128, 128, 21, 55, 77, 80, 59, 60, 80, 77, 58, 47, 92, 93, 39, 50, 51, 77, 80, 77, 77, 99, 77, 99, 77, 21, 21, 3, 80, 100, 101, 77, 92, 77, 10, 77, 77, 53, 58, 31, 60, 5, 55, 56, 5, 29, 93, 80, 31, 51, 55, 22, 104, 21, 21, 77, 77, 10, 96, 113, 114, 96, 102, 103, 80, 100, 3, 80, 21, 106, 29, 106, 26, 109, 110, 55, 55, 29, 77, 12, 13, 15, 18, 20, 21, 24, 25, 34, 36, 39, 47, 61, 62, 63, 64, 65, 67, 68, 69, 73, 81, 80, 5, 103, 30, 117, 114, 103, 106, 106, 110, 67, 29, 113, 80, 29, 102, 80, 96, 105, 107, 80, 26, 56, 28, 62, 66, 70, 71, 72, 74, 75, 76, 80, 80, 20, 80, 87, 88, 31, 25, 29, 29, 77, 5, 80, 29, 105, 80, 28, 62, 29, 72, 80, 29, 76, 80, 5, 87, 10, 52, 4, 80, 118, 87, 104, 5, 5, 61, 62, 28, 21, 61, 77, 26, 52, 5, 61, 80, 77, 87, 77 }; /* YYR1[RULE-NUM] -- Symbol kind of the left-hand side of rule RULE-NUM. */ static const yytype_uint8 yyr1[] = { 0, 40, 41, 42, 43, 43, 44, 44, 44, 45, 45, 46, 46, 46, 47, 47, 47, 48, 48, 49, 49, 50, 50, 51, 52, 52, 53, 53, 54, 54, 54, 55, 56, 56, 57, 57, 58, 58, 59, 59, 60, 61, 61, 61, 61, 61, 61, 61, 61, 61, 62, 62, 62, 63, 64, 65, 65, 66, 66, 67, 67, 67, 67, 68, 69, 70, 70, 71, 71, 72, 73, 74, 74, 75, 75, 76, 77, 77, 78, 78, 79, 80, 80, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 82, 82, 82, 82, 82, 82, 83, 84, 84, 85, 86, 86, 87, 87, 88, 88, 89, 89, 89, 90, 91, 91, 92, 92, 93, 94, 94, 94, 94, 94, 94, 95, 96, 96, 97, 98, 99, 99, 99, 99, 100, 100, 101, 101, 102, 103, 103, 104, 104, 105, 106, 106, 107, 107, 107, 108, 109, 109, 110, 110, 111, 112, 113, 114, 114, 115, 116, 117, 117, 118, 118, 119, 119, 120, 120, 121, 121, 121, 121, 121, 121, 122, 123, 124, 125, 125, 126, 126, 127, 127, 128 }; /* YYR2[RULE-NUM] -- Number of symbols on the right-hand side of rule RULE-NUM. */ static const yytype_int8 yyr2[] = { 0, 2, 1, 1, 1, 2, 1, 1, 1, 1, 1, 5, 3, 2, 1, 1, 1, 0, 1, 0, 3, 1, 2, 6, 0, 2, 1, 2, 1, 1, 1, 3, 0, 1, 6, 4, 0, 3, 1, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3, 1, 2, 1, 1, 1, 1, 1, 3, 0, 1, 1, 2, 3, 3, 0, 1, 1, 2, 3, 0, 1, 1, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 5, 3, 6, 0, 1, 1, 2, 1, 3, 1, 1, 1, 3, 0, 3, 1, 2, 3, 1, 1, 1, 1, 1, 1, 1, 0, 1, 4, 6, 0, 3, 2, 2, 1, 3, 1, 2, 6, 1, 2, 0, 3, 6, 0, 3, 0, 1, 2, 6, 0, 1, 2, 3, 6, 7, 3, 1, 2, 7, 8, 0, 1, 1, 3, 1, 1, 6, 3, 1, 1, 1, 1, 1, 1, 4, 6, 6, 6, 4, 7, 4, 7, 4, 1 }; enum { YYENOMEM = -2 }; #define yyerrok (yyerrstatus = 0) #define yyclearin (yychar = YYEMPTY) #define YYACCEPT goto yyacceptlab #define YYABORT goto yyabortlab #define YYERROR goto yyerrorlab #define YYNOMEM goto yyexhaustedlab #define YYRECOVERING() (!!yyerrstatus) #define YYBACKUP(Token, Value) \ do \ if (yychar == YYEMPTY) \ { \ yychar = (Token); \ yylval = (Value); \ YYPOPSTACK (yylen); \ yystate = *yyssp; \ goto yybackup; \ } \ else \ { \ yyerror (parser, filename, YY_("syntax error: cannot back up")); \ YYERROR; \ } \ while (0) /* Backward compatibility with an undocumented macro. Use YYerror or YYUNDEF. */ #define YYERRCODE YYUNDEF /* Enable debugging if requested. */ #if YYDEBUG # ifndef YYFPRINTF # include /* INFRINGES ON USER NAME SPACE */ # define YYFPRINTF fprintf # endif # define YYDPRINTF(Args) \ do { \ if (yydebug) \ YYFPRINTF Args; \ } while (0) # define YY_SYMBOL_PRINT(Title, Kind, Value, Location) \ do { \ if (yydebug) \ { \ YYFPRINTF (stderr, "%s ", Title); \ yy_symbol_print (stderr, \ Kind, Value, parser, filename); \ YYFPRINTF (stderr, "\n"); \ } \ } while (0) /*-----------------------------------. | Print this symbol's value on YYO. | `-----------------------------------*/ static void yy_symbol_value_print (FILE *yyo, yysymbol_kind_t yykind, YYSTYPE const * const yyvaluep, VALUE parser, VALUE filename) { FILE *yyoutput = yyo; YY_USE (yyoutput); YY_USE (parser); YY_USE (filename); if (!yyvaluep) return; YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN YY_USE (yykind); YY_IGNORE_MAYBE_UNINITIALIZED_END } /*---------------------------. | Print this symbol on YYO. | `---------------------------*/ static void yy_symbol_print (FILE *yyo, yysymbol_kind_t yykind, YYSTYPE const * const yyvaluep, VALUE parser, VALUE filename) { YYFPRINTF (yyo, "%s %s (", yykind < YYNTOKENS ? "token" : "nterm", yysymbol_name (yykind)); yy_symbol_value_print (yyo, yykind, yyvaluep, parser, filename); YYFPRINTF (yyo, ")"); } /*------------------------------------------------------------------. | yy_stack_print -- Print the state stack from its BOTTOM up to its | | TOP (included). | `------------------------------------------------------------------*/ static void yy_stack_print (yy_state_t *yybottom, yy_state_t *yytop) { YYFPRINTF (stderr, "Stack now"); for (; yybottom <= yytop; yybottom++) { int yybot = *yybottom; YYFPRINTF (stderr, " %d", yybot); } YYFPRINTF (stderr, "\n"); } # define YY_STACK_PRINT(Bottom, Top) \ do { \ if (yydebug) \ yy_stack_print ((Bottom), (Top)); \ } while (0) /*------------------------------------------------. | Report that the YYRULE is going to be reduced. | `------------------------------------------------*/ static void yy_reduce_print (yy_state_t *yyssp, YYSTYPE *yyvsp, int yyrule, VALUE parser, VALUE filename) { int yylno = yyrline[yyrule]; int yynrhs = yyr2[yyrule]; int yyi; YYFPRINTF (stderr, "Reducing stack by rule %d (line %d):\n", yyrule - 1, yylno); /* The symbols being reduced. */ for (yyi = 0; yyi < yynrhs; yyi++) { YYFPRINTF (stderr, " $%d = ", yyi + 1); yy_symbol_print (stderr, YY_ACCESSING_SYMBOL (+yyssp[yyi + 1 - yynrhs]), &yyvsp[(yyi + 1) - (yynrhs)], parser, filename); YYFPRINTF (stderr, "\n"); } } # define YY_REDUCE_PRINT(Rule) \ do { \ if (yydebug) \ yy_reduce_print (yyssp, yyvsp, Rule, parser, filename); \ } while (0) /* Nonzero means print parse trace. It is left uninitialized so that multiple parsers can coexist. */ int yydebug; #else /* !YYDEBUG */ # define YYDPRINTF(Args) ((void) 0) # define YY_SYMBOL_PRINT(Title, Kind, Value, Location) # define YY_STACK_PRINT(Bottom, Top) # define YY_REDUCE_PRINT(Rule) #endif /* !YYDEBUG */ /* YYINITDEPTH -- initial size of the parser's stacks. */ #ifndef YYINITDEPTH # define YYINITDEPTH 200 #endif /* YYMAXDEPTH -- maximum size the stacks can grow to (effective only if the built-in stack extension method is used). Do not make this value too large; the results are undefined if YYSTACK_ALLOC_MAXIMUM < YYSTACK_BYTES (YYMAXDEPTH) evaluated with infinite-precision integer arithmetic. */ #ifndef YYMAXDEPTH # define YYMAXDEPTH 10000 #endif /* Context of a parse error. */ typedef struct { yy_state_t *yyssp; yysymbol_kind_t yytoken; } yypcontext_t; /* Put in YYARG at most YYARGN of the expected tokens given the current YYCTX, and return the number of tokens stored in YYARG. If YYARG is null, return the number of expected tokens (guaranteed to be less than YYNTOKENS). Return YYENOMEM on memory exhaustion. Return 0 if there are more than YYARGN expected tokens, yet fill YYARG up to YYARGN. */ static int yypcontext_expected_tokens (const yypcontext_t *yyctx, yysymbol_kind_t yyarg[], int yyargn) { /* Actual size of YYARG. */ int yycount = 0; int yyn = yypact[+*yyctx->yyssp]; if (!yypact_value_is_default (yyn)) { /* Start YYX at -YYN if negative to avoid negative indexes in YYCHECK. In other words, skip the first -YYN actions for this state because they are default actions. */ int yyxbegin = yyn < 0 ? -yyn : 0; /* Stay within bounds of both yycheck and yytname. */ int yychecklim = YYLAST - yyn + 1; int yyxend = yychecklim < YYNTOKENS ? yychecklim : YYNTOKENS; int yyx; for (yyx = yyxbegin; yyx < yyxend; ++yyx) if (yycheck[yyx + yyn] == yyx && yyx != YYSYMBOL_YYerror && !yytable_value_is_error (yytable[yyx + yyn])) { if (!yyarg) ++yycount; else if (yycount == yyargn) return 0; else yyarg[yycount++] = YY_CAST (yysymbol_kind_t, yyx); } } if (yyarg && yycount == 0 && 0 < yyargn) yyarg[0] = YYSYMBOL_YYEMPTY; return yycount; } #ifndef yystrlen # if defined __GLIBC__ && defined _STRING_H # define yystrlen(S) (YY_CAST (YYPTRDIFF_T, strlen (S))) # else /* Return the length of YYSTR. */ static YYPTRDIFF_T yystrlen (const char *yystr) { YYPTRDIFF_T yylen; for (yylen = 0; yystr[yylen]; yylen++) continue; return yylen; } # endif #endif #ifndef yystpcpy # if defined __GLIBC__ && defined _STRING_H && defined _GNU_SOURCE # define yystpcpy stpcpy # else /* Copy YYSRC to YYDEST, returning the address of the terminating '\0' in YYDEST. */ static char * yystpcpy (char *yydest, const char *yysrc) { char *yyd = yydest; const char *yys = yysrc; while ((*yyd++ = *yys++) != '\0') continue; return yyd - 1; } # endif #endif static int yy_syntax_error_arguments (const yypcontext_t *yyctx, yysymbol_kind_t yyarg[], int yyargn) { /* Actual size of YYARG. */ int yycount = 0; /* There are many possibilities here to consider: - If this state is a consistent state with a default action, then the only way this function was invoked is if the default action is an error action. In that case, don't check for expected tokens because there are none. - The only way there can be no lookahead present (in yychar) is if this state is a consistent state with a default action. Thus, detecting the absence of a lookahead is sufficient to determine that there is no unexpected or expected token to report. In that case, just report a simple "syntax error". - Don't assume there isn't a lookahead just because this state is a consistent state with a default action. There might have been a previous inconsistent state, consistent state with a non-default action, or user semantic action that manipulated yychar. - Of course, the expected token list depends on states to have correct lookahead information, and it depends on the parser not to perform extra reductions after fetching a lookahead from the scanner and before detecting a syntax error. Thus, state merging (from LALR or IELR) and default reductions corrupt the expected token list. However, the list is correct for canonical LR with one exception: it will still contain any token that will not be accepted due to an error action in a later state. */ if (yyctx->yytoken != YYSYMBOL_YYEMPTY) { int yyn; if (yyarg) yyarg[yycount] = yyctx->yytoken; ++yycount; yyn = yypcontext_expected_tokens (yyctx, yyarg ? yyarg + 1 : yyarg, yyargn - 1); if (yyn == YYENOMEM) return YYENOMEM; else yycount += yyn; } return yycount; } /* Copy into *YYMSG, which is of size *YYMSG_ALLOC, an error message about the unexpected token YYTOKEN for the state stack whose top is YYSSP. Return 0 if *YYMSG was successfully written. Return -1 if *YYMSG is not large enough to hold the message. In that case, also set *YYMSG_ALLOC to the required number of bytes. Return YYENOMEM if the required number of bytes is too large to store. */ static int yysyntax_error (YYPTRDIFF_T *yymsg_alloc, char **yymsg, const yypcontext_t *yyctx) { enum { YYARGS_MAX = 5 }; /* Internationalized format string. */ const char *yyformat = YY_NULLPTR; /* Arguments of yyformat: reported tokens (one for the "unexpected", one per "expected"). */ yysymbol_kind_t yyarg[YYARGS_MAX]; /* Cumulated lengths of YYARG. */ YYPTRDIFF_T yysize = 0; /* Actual size of YYARG. */ int yycount = yy_syntax_error_arguments (yyctx, yyarg, YYARGS_MAX); if (yycount == YYENOMEM) return YYENOMEM; switch (yycount) { #define YYCASE_(N, S) \ case N: \ yyformat = S; \ break default: /* Avoid compiler warnings. */ YYCASE_(0, YY_("syntax error")); YYCASE_(1, YY_("syntax error, unexpected %s")); YYCASE_(2, YY_("syntax error, unexpected %s, expecting %s")); YYCASE_(3, YY_("syntax error, unexpected %s, expecting %s or %s")); YYCASE_(4, YY_("syntax error, unexpected %s, expecting %s or %s or %s")); YYCASE_(5, YY_("syntax error, unexpected %s, expecting %s or %s or %s or %s")); #undef YYCASE_ } /* Compute error message size. Don't count the "%s"s, but reserve room for the terminator. */ yysize = yystrlen (yyformat) - 2 * yycount + 1; { int yyi; for (yyi = 0; yyi < yycount; ++yyi) { YYPTRDIFF_T yysize1 = yysize + yystrlen (yysymbol_name (yyarg[yyi])); if (yysize <= yysize1 && yysize1 <= YYSTACK_ALLOC_MAXIMUM) yysize = yysize1; else return YYENOMEM; } } if (*yymsg_alloc < yysize) { *yymsg_alloc = 2 * yysize; if (! (yysize <= *yymsg_alloc && *yymsg_alloc <= YYSTACK_ALLOC_MAXIMUM)) *yymsg_alloc = YYSTACK_ALLOC_MAXIMUM; return -1; } /* Avoid sprintf, as that infringes on the user's name space. Don't have undefined behavior even if the translation produced a string with the wrong number of "%s"s. */ { char *yyp = *yymsg; int yyi = 0; while ((*yyp = *yyformat) != '\0') if (*yyp == '%' && yyformat[1] == 's' && yyi < yycount) { yyp = yystpcpy (yyp, yysymbol_name (yyarg[yyi++])); yyformat += 2; } else { ++yyp; ++yyformat; } } return 0; } /*-----------------------------------------------. | Release the memory associated to this symbol. | `-----------------------------------------------*/ static void yydestruct (const char *yymsg, yysymbol_kind_t yykind, YYSTYPE *yyvaluep, VALUE parser, VALUE filename) { YY_USE (yyvaluep); YY_USE (parser); YY_USE (filename); if (!yymsg) yymsg = "Deleting"; YY_SYMBOL_PRINT (yymsg, yykind, yyvaluep, yylocationp); YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN YY_USE (yykind); YY_IGNORE_MAYBE_UNINITIALIZED_END } /*----------. | yyparse. | `----------*/ int yyparse (VALUE parser, VALUE filename) { /* Lookahead token kind. */ int yychar; /* The semantic value of the lookahead symbol. */ /* Default value used for initialization, for pacifying older GCCs or non-GCC compilers. */ YY_INITIAL_VALUE (static YYSTYPE yyval_default;) YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); /* Number of syntax errors so far. */ int yynerrs = 0; yy_state_fast_t yystate = 0; /* Number of tokens to shift before error messages enabled. */ int yyerrstatus = 0; /* Refer to the stacks through separate pointers, to allow yyoverflow to reallocate them elsewhere. */ /* Their size. */ YYPTRDIFF_T yystacksize = YYINITDEPTH; /* The state stack: array, bottom, top. */ yy_state_t yyssa[YYINITDEPTH]; yy_state_t *yyss = yyssa; yy_state_t *yyssp = yyss; /* The semantic value stack: array, bottom, top. */ YYSTYPE yyvsa[YYINITDEPTH]; YYSTYPE *yyvs = yyvsa; YYSTYPE *yyvsp = yyvs; int yyn; /* The return value of yyparse. */ int yyresult; /* Lookahead symbol kind. */ yysymbol_kind_t yytoken = YYSYMBOL_YYEMPTY; /* The variables used to return semantic value and location from the action routines. */ YYSTYPE yyval; /* Buffer for error messages, and its allocated size. */ char yymsgbuf[128]; char *yymsg = yymsgbuf; YYPTRDIFF_T yymsg_alloc = sizeof yymsgbuf; #define YYPOPSTACK(N) (yyvsp -= (N), yyssp -= (N)) /* The number of symbols on the RHS of the reduced rule. Keep to zero when no symbol should be popped. */ int yylen = 0; YYDPRINTF ((stderr, "Starting parse\n")); yychar = YYEMPTY; /* Cause a token to be read. */ goto yysetstate; /*------------------------------------------------------------. | yynewstate -- push a new state, which is found in yystate. | `------------------------------------------------------------*/ yynewstate: /* In all cases, when you get here, the value and location stacks have just been pushed. So pushing a state here evens the stacks. */ yyssp++; /*--------------------------------------------------------------------. | yysetstate -- set current state (the top of the stack) to yystate. | `--------------------------------------------------------------------*/ yysetstate: YYDPRINTF ((stderr, "Entering state %d\n", yystate)); YY_ASSERT (0 <= yystate && yystate < YYNSTATES); YY_IGNORE_USELESS_CAST_BEGIN *yyssp = YY_CAST (yy_state_t, yystate); YY_IGNORE_USELESS_CAST_END YY_STACK_PRINT (yyss, yyssp); if (yyss + yystacksize - 1 <= yyssp) #if !defined yyoverflow && !defined YYSTACK_RELOCATE YYNOMEM; #else { /* Get the current used size of the three stacks, in elements. */ YYPTRDIFF_T yysize = yyssp - yyss + 1; # if defined yyoverflow { /* Give user a chance to reallocate the stack. Use copies of these so that the &'s don't force the real ones into memory. */ yy_state_t *yyss1 = yyss; YYSTYPE *yyvs1 = yyvs; /* Each stack pointer address is followed by the size of the data in use in that stack, in bytes. This used to be a conditional around just the two extra args, but that might be undefined if yyoverflow is a macro. */ yyoverflow (YY_("memory exhausted"), &yyss1, yysize * YYSIZEOF (*yyssp), &yyvs1, yysize * YYSIZEOF (*yyvsp), &yystacksize); yyss = yyss1; yyvs = yyvs1; } # else /* defined YYSTACK_RELOCATE */ /* Extend the stack our own way. */ if (YYMAXDEPTH <= yystacksize) YYNOMEM; yystacksize *= 2; if (YYMAXDEPTH < yystacksize) yystacksize = YYMAXDEPTH; { yy_state_t *yyss1 = yyss; union yyalloc *yyptr = YY_CAST (union yyalloc *, YYSTACK_ALLOC (YY_CAST (YYSIZE_T, YYSTACK_BYTES (yystacksize)))); if (! yyptr) YYNOMEM; YYSTACK_RELOCATE (yyss_alloc, yyss); YYSTACK_RELOCATE (yyvs_alloc, yyvs); # undef YYSTACK_RELOCATE if (yyss1 != yyssa) YYSTACK_FREE (yyss1); } # endif yyssp = yyss + yysize - 1; yyvsp = yyvs + yysize - 1; YY_IGNORE_USELESS_CAST_BEGIN YYDPRINTF ((stderr, "Stack size increased to %ld\n", YY_CAST (long, yystacksize))); YY_IGNORE_USELESS_CAST_END if (yyss + yystacksize - 1 <= yyssp) YYABORT; } #endif /* !defined yyoverflow && !defined YYSTACK_RELOCATE */ if (yystate == YYFINAL) YYACCEPT; goto yybackup; /*-----------. | yybackup. | `-----------*/ yybackup: /* Do appropriate processing given the current state. Read a lookahead token if we need one and don't already have one. */ /* First try to decide what to do without reference to lookahead token. */ yyn = yypact[yystate]; if (yypact_value_is_default (yyn)) goto yydefault; /* Not known => get a lookahead token if don't already have one. */ /* YYCHAR is either empty, or end-of-input, or a valid lookahead. */ if (yychar == YYEMPTY) { YYDPRINTF ((stderr, "Reading a token\n")); yychar = yylex (&yylval, parser, filename); } if (yychar <= YYEOF) { yychar = YYEOF; yytoken = YYSYMBOL_YYEOF; YYDPRINTF ((stderr, "Now at end of input.\n")); } else if (yychar == YYerror) { /* The scanner already issued an error message, process directly to error recovery. But do not keep the error token as lookahead, it is too special and may lead us to an endless loop in error recovery. */ yychar = YYUNDEF; yytoken = YYSYMBOL_YYerror; goto yyerrlab1; } else { yytoken = YYTRANSLATE (yychar); YY_SYMBOL_PRINT ("Next token is", yytoken, &yylval, &yylloc); } /* If the proper action on seeing token YYTOKEN is to reduce or to detect an error, take that action. */ yyn += yytoken; if (yyn < 0 || YYLAST < yyn || yycheck[yyn] != yytoken) goto yydefault; yyn = yytable[yyn]; if (yyn <= 0) { if (yytable_value_is_error (yyn)) goto yyerrlab; yyn = -yyn; goto yyreduce; } /* Count tokens shifted since error; after three, turn off error status. */ if (yyerrstatus) yyerrstatus--; /* Shift the lookahead token. */ YY_SYMBOL_PRINT ("Shifting", yytoken, &yylval, &yylloc); yystate = yyn; YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN *++yyvsp = yylval; YY_IGNORE_MAYBE_UNINITIALIZED_END /* Discard the shifted token. */ yychar = YYEMPTY; goto yynewstate; /*-----------------------------------------------------------. | yydefault -- do the default action for the current state. | `-----------------------------------------------------------*/ yydefault: yyn = yydefact[yystate]; if (yyn == 0) goto yyerrlab; goto yyreduce; /*-----------------------------. | yyreduce -- do a reduction. | `-----------------------------*/ yyreduce: /* yyn is the number of a rule to reduce with. */ yylen = yyr2[yyn]; /* If YYLEN is nonzero, implement the default value of the action: '$$ = $1'. Otherwise, the following line sets YYVAL to garbage. This behavior is undocumented and Bison users should not rely upon it. Assigning to YYVAL unconditionally makes the parser a bit smaller, and it avoids a GCC warning that YYVAL may be used uninitialized. */ yyval = yyvsp[1-yylen]; YY_REDUCE_PRINT (yyn); switch (yyn) { case 2: /* start: document */ #line 105 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { rb_ivar_set(parser, rb_intern("@result"), yyvsp[0]); } #line 1929 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 3: /* document: definitions_list */ #line 107 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { VALUE position_source = rb_ary_entry(yyvsp[0], 0); VALUE line, col; if (RB_TEST(position_source)) { line = rb_funcall(position_source, rb_intern("line"), 0); col = rb_funcall(position_source, rb_intern("col"), 0); } else { line = INT2FIX(1); col = INT2FIX(1); } yyval = MAKE_AST_NODE(Document, 3, line, col, yyvsp[0]); } #line 1946 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 4: /* definitions_list: definition */ #line 121 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = rb_ary_new_from_args(1, yyvsp[0]); } #line 1952 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 5: /* definitions_list: definitions_list definition */ #line 122 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { rb_ary_push(yyval, yyvsp[0]); } #line 1958 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 11: /* operation_definition: operation_type operation_name_opt variable_definitions_opt directives_list_opt selection_set */ #line 134 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(OperationDefinition, 7, rb_ary_entry(yyvsp[-4], 1), rb_ary_entry(yyvsp[-4], 2), rb_ary_entry(yyvsp[-4], 3), (RB_TEST(yyvsp[-3]) ? rb_ary_entry(yyvsp[-3], 3) : Qnil), yyvsp[-2], yyvsp[-1], yyvsp[0] ); } #line 1974 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 12: /* operation_definition: LCURLY selection_list RCURLY */ #line 145 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(OperationDefinition, 7, rb_ary_entry(yyvsp[-2], 1), rb_ary_entry(yyvsp[-2], 2), r_string_query, Qnil, GraphQL_Language_Nodes_NONE, GraphQL_Language_Nodes_NONE, yyvsp[-1] ); } #line 1990 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 13: /* operation_definition: LCURLY RCURLY */ #line 156 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(OperationDefinition, 7, rb_ary_entry(yyvsp[-1], 1), rb_ary_entry(yyvsp[-1], 2), r_string_query, Qnil, GraphQL_Language_Nodes_NONE, GraphQL_Language_Nodes_NONE, GraphQL_Language_Nodes_NONE ); } #line 2006 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 17: /* operation_name_opt: %empty */ #line 174 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = Qnil; } #line 2012 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 19: /* variable_definitions_opt: %empty */ #line 178 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = GraphQL_Language_Nodes_NONE; } #line 2018 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 20: /* variable_definitions_opt: LPAREN variable_definitions_list RPAREN */ #line 179 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = yyvsp[-1]; } #line 2024 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 21: /* variable_definitions_list: variable_definition */ #line 182 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = rb_ary_new_from_args(1, yyvsp[0]); } #line 2030 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 22: /* variable_definitions_list: variable_definitions_list variable_definition */ #line 183 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { rb_ary_push(yyval, yyvsp[0]); } #line 2036 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 23: /* variable_definition: VAR_SIGN name COLON type default_value_opt directives_list_opt */ #line 186 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(VariableDefinition, 6, rb_ary_entry(yyvsp[-5], 1), rb_ary_entry(yyvsp[-5], 2), rb_ary_entry(yyvsp[-4], 3), yyvsp[-2], yyvsp[-1], yyvsp[0] ); } #line 2051 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 24: /* default_value_opt: %empty */ #line 198 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = Qnil; } #line 2057 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 25: /* default_value_opt: EQUALS literal_value */ #line 199 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = yyvsp[0]; } #line 2063 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 26: /* selection_list: selection */ #line 202 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = rb_ary_new_from_args(1, yyvsp[0]); } #line 2069 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 27: /* selection_list: selection_list selection */ #line 203 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { rb_ary_push(yyval, yyvsp[0]); } #line 2075 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 31: /* selection_set: LCURLY selection_list RCURLY */ #line 211 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = yyvsp[-1]; } #line 2081 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 32: /* selection_set_opt: %empty */ #line 214 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = rb_ary_new(); } #line 2087 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 34: /* field: name COLON name arguments_opt directives_list_opt selection_set_opt */ #line 218 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(Field, 7, rb_ary_entry(yyvsp[-5], 1), rb_ary_entry(yyvsp[-5], 2), rb_ary_entry(yyvsp[-5], 3), // alias rb_ary_entry(yyvsp[-3], 3), // name yyvsp[-2], // args yyvsp[-1], // directives yyvsp[0] // subselections ); } #line 2103 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 35: /* field: name arguments_opt directives_list_opt selection_set_opt */ #line 229 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(Field, 7, rb_ary_entry(yyvsp[-3], 1), rb_ary_entry(yyvsp[-3], 2), Qnil, // alias rb_ary_entry(yyvsp[-3], 3), // name yyvsp[-2], // args yyvsp[-1], // directives yyvsp[0] // subselections ); } #line 2119 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 36: /* arguments_opt: %empty */ #line 242 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = GraphQL_Language_Nodes_NONE; } #line 2125 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 37: /* arguments_opt: LPAREN arguments_list RPAREN */ #line 243 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = yyvsp[-1]; } #line 2131 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 38: /* arguments_list: argument */ #line 246 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = rb_ary_new_from_args(1, yyvsp[0]); } #line 2137 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 39: /* arguments_list: arguments_list argument */ #line 247 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { rb_ary_push(yyval, yyvsp[0]); } #line 2143 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 40: /* argument: name COLON input_value */ #line 250 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(Argument, 4, rb_ary_entry(yyvsp[-2], 1), rb_ary_entry(yyvsp[-2], 2), rb_ary_entry(yyvsp[-2], 3), yyvsp[0] ); } #line 2156 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 41: /* literal_value: FLOAT */ #line 260 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = rb_funcall(rb_ary_entry(yyvsp[0], 3), rb_intern("to_f"), 0); } #line 2162 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 42: /* literal_value: INT */ #line 261 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = rb_funcall(rb_ary_entry(yyvsp[0], 3), rb_intern("to_i"), 0); } #line 2168 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 43: /* literal_value: STRING */ #line 262 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = rb_ary_entry(yyvsp[0], 3); } #line 2174 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 44: /* literal_value: TRUE_LITERAL */ #line 263 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = Qtrue; } #line 2180 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 45: /* literal_value: FALSE_LITERAL */ #line 264 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = Qfalse; } #line 2186 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 53: /* null_value: NULL_LITERAL */ #line 275 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(NullValue, 3, rb_ary_entry(yyvsp[0], 1), rb_ary_entry(yyvsp[0], 2), rb_ary_entry(yyvsp[0], 3) ); } #line 2198 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 54: /* variable: VAR_SIGN name */ #line 283 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(VariableIdentifier, 3, rb_ary_entry(yyvsp[-1], 1), rb_ary_entry(yyvsp[-1], 2), rb_ary_entry(yyvsp[0], 3) ); } #line 2210 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 55: /* list_value: LBRACKET RBRACKET */ #line 292 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = GraphQL_Language_Nodes_NONE; } #line 2216 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 56: /* list_value: LBRACKET list_value_list RBRACKET */ #line 293 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = yyvsp[-1]; } #line 2222 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 57: /* list_value_list: input_value */ #line 296 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = rb_ary_new_from_args(1, yyvsp[0]); } #line 2228 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 58: /* list_value_list: list_value_list input_value */ #line 297 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { rb_ary_push(yyval, yyvsp[0]); } #line 2234 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 63: /* enum_value: enum_name */ #line 305 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(Enum, 3, rb_ary_entry(yyvsp[0], 1), rb_ary_entry(yyvsp[0], 2), rb_ary_entry(yyvsp[0], 3) ); } #line 2246 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 64: /* object_value: LCURLY object_value_list_opt RCURLY */ #line 314 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(InputObject, 3, rb_ary_entry(yyvsp[-2], 1), rb_ary_entry(yyvsp[-2], 2), yyvsp[-1] ); } #line 2258 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 65: /* object_value_list_opt: %empty */ #line 323 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = GraphQL_Language_Nodes_NONE; } #line 2264 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 67: /* object_value_list: object_value_field */ #line 327 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = rb_ary_new_from_args(1, yyvsp[0]); } #line 2270 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 68: /* object_value_list: object_value_list object_value_field */ #line 328 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { rb_ary_push(yyval, yyvsp[0]); } #line 2276 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 69: /* object_value_field: name COLON input_value */ #line 331 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(Argument, 4, rb_ary_entry(yyvsp[-2], 1), rb_ary_entry(yyvsp[-2], 2), rb_ary_entry(yyvsp[-2], 3), yyvsp[0] ); } #line 2289 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 70: /* object_literal_value: LCURLY object_literal_value_list_opt RCURLY */ #line 342 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(InputObject, 3, rb_ary_entry(yyvsp[-2], 1), rb_ary_entry(yyvsp[-2], 2), yyvsp[-1] ); } #line 2301 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 71: /* object_literal_value_list_opt: %empty */ #line 351 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = GraphQL_Language_Nodes_NONE; } #line 2307 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 73: /* object_literal_value_list: object_literal_value_field */ #line 355 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = rb_ary_new_from_args(1, yyvsp[0]); } #line 2313 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 74: /* object_literal_value_list: object_literal_value_list object_literal_value_field */ #line 356 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { rb_ary_push(yyval, yyvsp[0]); } #line 2319 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 75: /* object_literal_value_field: name COLON literal_value */ #line 359 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(Argument, 4, rb_ary_entry(yyvsp[-2], 1), rb_ary_entry(yyvsp[-2], 2), rb_ary_entry(yyvsp[-2], 3), yyvsp[0] ); } #line 2332 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 76: /* directives_list_opt: %empty */ #line 370 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = GraphQL_Language_Nodes_NONE; } #line 2338 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 78: /* directives_list: directive */ #line 374 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = rb_ary_new_from_args(1, yyvsp[0]); } #line 2344 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 79: /* directives_list: directives_list directive */ #line 375 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { rb_ary_push(yyval, yyvsp[0]); } #line 2350 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 80: /* directive: DIR_SIGN name arguments_opt */ #line 377 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(Directive, 4, rb_ary_entry(yyvsp[-2], 1), rb_ary_entry(yyvsp[-2], 2), rb_ary_entry(yyvsp[-1], 3), yyvsp[0] ); } #line 2363 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 101: /* fragment_spread: ELLIPSIS name_without_on directives_list_opt */ #line 414 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(FragmentSpread, 4, rb_ary_entry(yyvsp[-2], 1), rb_ary_entry(yyvsp[-2], 2), rb_ary_entry(yyvsp[-1], 3), yyvsp[0] ); } #line 2376 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 102: /* inline_fragment: ELLIPSIS ON NamedTypeForCondition directives_list_opt selection_set */ #line 424 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(InlineFragment, 5, rb_ary_entry(yyvsp[-4], 1), rb_ary_entry(yyvsp[-4], 2), yyvsp[-2], yyvsp[-1], yyvsp[0] ); } #line 2390 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 103: /* inline_fragment: ELLIPSIS directives_list_opt selection_set */ #line 433 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(InlineFragment, 5, rb_ary_entry(yyvsp[-2], 1), rb_ary_entry(yyvsp[-2], 2), Qnil, yyvsp[-1], yyvsp[0] ); } #line 2404 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 104: /* fragment_definition: FRAGMENT fragment_name_opt ON NamedTypeForCondition directives_list_opt selection_set */ #line 444 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(FragmentDefinition, 6, rb_ary_entry(yyvsp[-5], 1), rb_ary_entry(yyvsp[-5], 2), yyvsp[-4], yyvsp[-2], yyvsp[-1], yyvsp[0] ); } #line 2419 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 105: /* fragment_name_opt: %empty */ #line 456 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = Qnil; } #line 2425 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 106: /* fragment_name_opt: name_without_on */ #line 457 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = rb_ary_entry(yyvsp[0], 3); } #line 2431 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 108: /* type: nullable_type BANG */ #line 461 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(NonNullType, 3, rb_funcall(yyvsp[-1], rb_intern("line"), 0), rb_funcall(yyvsp[-1], rb_intern("col"), 0), yyvsp[-1]); } #line 2437 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 109: /* nullable_type: name */ #line 464 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(TypeName, 3, rb_ary_entry(yyvsp[0], 1), rb_ary_entry(yyvsp[0], 2), rb_ary_entry(yyvsp[0], 3) ); } #line 2449 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 110: /* nullable_type: LBRACKET type RBRACKET */ #line 471 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(ListType, 3, rb_funcall(yyvsp[-1], rb_intern("line"), 0), rb_funcall(yyvsp[-1], rb_intern("col"), 0), yyvsp[-1] ); } #line 2461 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 114: /* schema_definition: SCHEMA directives_list_opt operation_type_definition_list_opt */ #line 485 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(SchemaDefinition, 6, rb_ary_entry(yyvsp[-2], 1), rb_ary_entry(yyvsp[-2], 2), // TODO use static strings: rb_hash_aref(yyvsp[0], rb_str_new_cstr("query")), rb_hash_aref(yyvsp[0], rb_str_new_cstr("mutation")), rb_hash_aref(yyvsp[0], rb_str_new_cstr("subscription")), yyvsp[-1] ); } #line 2477 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 115: /* operation_type_definition_list_opt: %empty */ #line 498 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = rb_hash_new(); } #line 2483 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 116: /* operation_type_definition_list_opt: LCURLY operation_type_definition_list RCURLY */ #line 499 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = yyvsp[-1]; } #line 2489 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 117: /* operation_type_definition_list: operation_type_definition */ #line 502 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = rb_hash_new(); rb_hash_aset(yyval, rb_ary_entry(yyvsp[0], 0), rb_ary_entry(yyvsp[0], 1)); } #line 2498 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 118: /* operation_type_definition_list: operation_type_definition_list operation_type_definition */ #line 506 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { rb_hash_aset(yyval, rb_ary_entry(yyvsp[0], 0), rb_ary_entry(yyvsp[0], 1)); } #line 2506 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 119: /* operation_type_definition: operation_type COLON name */ #line 511 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = rb_ary_new_from_args(2, rb_ary_entry(yyvsp[-2], 3), rb_ary_entry(yyvsp[0], 3)); } #line 2514 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 127: /* description_opt: %empty */ #line 526 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = Qnil; } #line 2520 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 129: /* scalar_type_definition: description_opt SCALAR name directives_list_opt */ #line 530 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(ScalarTypeDefinition, 5, rb_ary_entry(yyvsp[-2], 1), rb_ary_entry(yyvsp[-2], 2), rb_ary_entry(yyvsp[-1], 3), // TODO see get_description for reading a description from comments (RB_TEST(yyvsp[-3]) ? rb_ary_entry(yyvsp[-3], 3) : Qnil), yyvsp[0] ); } #line 2535 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 130: /* object_type_definition: description_opt TYPE_LITERAL name implements_opt directives_list_opt field_definition_list_opt */ #line 542 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(ObjectTypeDefinition, 7, rb_ary_entry(yyvsp[-4], 1), rb_ary_entry(yyvsp[-4], 2), rb_ary_entry(yyvsp[-3], 3), yyvsp[-2], // implements // TODO see get_description for reading a description from comments (RB_TEST(yyvsp[-5]) ? rb_ary_entry(yyvsp[-5], 3) : Qnil), yyvsp[-1], yyvsp[0] ); } #line 2552 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 131: /* implements_opt: %empty */ #line 556 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = GraphQL_Language_Nodes_NONE; } #line 2558 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 132: /* implements_opt: IMPLEMENTS AMP interfaces_list */ #line 557 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = yyvsp[0]; } #line 2564 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 133: /* implements_opt: IMPLEMENTS interfaces_list */ #line 558 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = yyvsp[0]; } #line 2570 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 134: /* implements_opt: IMPLEMENTS legacy_interfaces_list */ #line 559 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = yyvsp[0]; } #line 2576 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 135: /* interfaces_list: name */ #line 562 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { VALUE new_name = MAKE_AST_NODE(TypeName, 3, rb_ary_entry(yyvsp[0], 1), rb_ary_entry(yyvsp[0], 2), rb_ary_entry(yyvsp[0], 3) ); yyval = rb_ary_new_from_args(1, new_name); } #line 2589 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 136: /* interfaces_list: interfaces_list AMP name */ #line 570 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { VALUE new_name = MAKE_AST_NODE(TypeName, 3, rb_ary_entry(yyvsp[0], 1), rb_ary_entry(yyvsp[0], 2), rb_ary_entry(yyvsp[0], 3)); rb_ary_push(yyval, new_name); } #line 2598 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 137: /* legacy_interfaces_list: name */ #line 576 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { VALUE new_name = MAKE_AST_NODE(TypeName, 3, rb_ary_entry(yyvsp[0], 1), rb_ary_entry(yyvsp[0], 2), rb_ary_entry(yyvsp[0], 3) ); yyval = rb_ary_new_from_args(1, new_name); } #line 2611 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 138: /* legacy_interfaces_list: legacy_interfaces_list name */ #line 584 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { rb_ary_push(yyval, MAKE_AST_NODE(TypeName, 3, rb_ary_entry(yyvsp[0], 1), rb_ary_entry(yyvsp[0], 2), rb_ary_entry(yyvsp[0], 3))); } #line 2619 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 139: /* input_value_definition: description_opt name COLON type default_value_opt directives_list_opt */ #line 589 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(InputValueDefinition, 7, rb_ary_entry(yyvsp[-4], 1), rb_ary_entry(yyvsp[-4], 2), rb_ary_entry(yyvsp[-4], 3), yyvsp[-2], yyvsp[-1], // TODO see get_description for reading a description from comments (RB_TEST(yyvsp[-5]) ? rb_ary_entry(yyvsp[-5], 3) : Qnil), yyvsp[0] ); } #line 2636 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 140: /* input_value_definition_list: input_value_definition */ #line 603 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = rb_ary_new_from_args(1, yyvsp[0]); } #line 2642 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 141: /* input_value_definition_list: input_value_definition_list input_value_definition */ #line 604 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { rb_ary_push(yyval, yyvsp[0]); } #line 2648 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 142: /* arguments_definitions_opt: %empty */ #line 607 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = GraphQL_Language_Nodes_NONE; } #line 2654 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 143: /* arguments_definitions_opt: LPAREN input_value_definition_list RPAREN */ #line 608 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = yyvsp[-1]; } #line 2660 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 144: /* field_definition: description_opt name arguments_definitions_opt COLON type directives_list_opt */ #line 611 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(FieldDefinition, 7, rb_ary_entry(yyvsp[-4], 1), rb_ary_entry(yyvsp[-4], 2), rb_ary_entry(yyvsp[-4], 3), yyvsp[-1], // TODO see get_description for reading a description from comments (RB_TEST(yyvsp[-5]) ? rb_ary_entry(yyvsp[-5], 3) : Qnil), yyvsp[-3], yyvsp[0] ); } #line 2677 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 145: /* field_definition_list_opt: %empty */ #line 625 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = GraphQL_Language_Nodes_NONE; } #line 2683 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 146: /* field_definition_list_opt: LCURLY field_definition_list RCURLY */ #line 626 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = yyvsp[-1]; } #line 2689 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 147: /* field_definition_list: %empty */ #line 629 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = GraphQL_Language_Nodes_NONE; } #line 2695 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 148: /* field_definition_list: field_definition */ #line 630 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = rb_ary_new_from_args(1, yyvsp[0]); } #line 2701 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 149: /* field_definition_list: field_definition_list field_definition */ #line 631 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { rb_ary_push(yyval, yyvsp[0]); } #line 2707 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 150: /* interface_type_definition: description_opt INTERFACE name implements_opt directives_list_opt field_definition_list_opt */ #line 634 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(InterfaceTypeDefinition, 7, rb_ary_entry(yyvsp[-4], 1), rb_ary_entry(yyvsp[-4], 2), rb_ary_entry(yyvsp[-3], 3), // TODO see get_description for reading a description from comments (RB_TEST(yyvsp[-5]) ? rb_ary_entry(yyvsp[-5], 3) : Qnil), yyvsp[-2], yyvsp[-1], yyvsp[0] ); } #line 2724 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 151: /* pipe_opt: %empty */ #line 648 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = GraphQL_Language_Nodes_NONE; } #line 2730 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 152: /* pipe_opt: PIPE */ #line 649 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = GraphQL_Language_Nodes_NONE; } #line 2736 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 153: /* union_members: pipe_opt name */ #line 652 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { VALUE new_member = MAKE_AST_NODE(TypeName, 3, rb_ary_entry(yyvsp[0], 1), rb_ary_entry(yyvsp[0], 2), rb_ary_entry(yyvsp[0], 3) ); yyval = rb_ary_new_from_args(1, new_member); } #line 2749 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 154: /* union_members: union_members PIPE name */ #line 660 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { rb_ary_push(yyval, MAKE_AST_NODE(TypeName, 3, rb_ary_entry(yyvsp[0], 1), rb_ary_entry(yyvsp[0], 2), rb_ary_entry(yyvsp[0], 3))); } #line 2757 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 155: /* union_type_definition: description_opt UNION name directives_list_opt EQUALS union_members */ #line 665 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(UnionTypeDefinition, 6, rb_ary_entry(yyvsp[-4], 1), rb_ary_entry(yyvsp[-4], 2), rb_ary_entry(yyvsp[-3], 3), yyvsp[0], // types // TODO see get_description for reading a description from comments (RB_TEST(yyvsp[-5]) ? rb_ary_entry(yyvsp[-5], 3) : Qnil), yyvsp[-2] ); } #line 2773 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 156: /* enum_type_definition: description_opt ENUM name directives_list_opt LCURLY enum_value_definitions RCURLY */ #line 678 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(EnumTypeDefinition, 6, rb_ary_entry(yyvsp[-5], 1), rb_ary_entry(yyvsp[-5], 2), rb_ary_entry(yyvsp[-4], 3), // TODO see get_description for reading a description from comments (RB_TEST(yyvsp[-6]) ? rb_ary_entry(yyvsp[-6], 3) : Qnil), yyvsp[-3], yyvsp[-1] ); } #line 2789 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 157: /* enum_value_definition: description_opt enum_name directives_list_opt */ #line 691 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(EnumValueDefinition, 5, rb_ary_entry(yyvsp[-1], 1), rb_ary_entry(yyvsp[-1], 2), rb_ary_entry(yyvsp[-1], 3), // TODO see get_description for reading a description from comments (RB_TEST(yyvsp[-2]) ? rb_ary_entry(yyvsp[-2], 3) : Qnil), yyvsp[0] ); } #line 2804 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 158: /* enum_value_definitions: enum_value_definition */ #line 703 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = rb_ary_new_from_args(1, yyvsp[0]); } #line 2810 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 159: /* enum_value_definitions: enum_value_definitions enum_value_definition */ #line 704 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { rb_ary_push(yyval, yyvsp[0]); } #line 2816 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 160: /* input_object_type_definition: description_opt INPUT name directives_list_opt LCURLY input_value_definition_list RCURLY */ #line 707 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(InputObjectTypeDefinition, 6, rb_ary_entry(yyvsp[-5], 1), rb_ary_entry(yyvsp[-5], 2), rb_ary_entry(yyvsp[-4], 3), // TODO see get_description for reading a description from comments (RB_TEST(yyvsp[-6]) ? rb_ary_entry(yyvsp[-6], 3) : Qnil), yyvsp[-3], yyvsp[-1] ); } #line 2832 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 161: /* directive_definition: description_opt DIRECTIVE DIR_SIGN name arguments_definitions_opt directive_repeatable_opt ON directive_locations */ #line 720 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(DirectiveDefinition, 7, rb_ary_entry(yyvsp[-6], 1), rb_ary_entry(yyvsp[-6], 2), rb_ary_entry(yyvsp[-4], 3), (RB_TEST(yyvsp[-2]) ? Qtrue : Qfalse), // repeatable // TODO see get_description for reading a description from comments (RB_TEST(yyvsp[-7]) ? rb_ary_entry(yyvsp[-7], 3) : Qnil), yyvsp[-3], yyvsp[0] ); } #line 2849 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 162: /* directive_repeatable_opt: %empty */ #line 734 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = Qnil; } #line 2855 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 163: /* directive_repeatable_opt: REPEATABLE */ #line 735 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = Qtrue; } #line 2861 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 164: /* directive_locations: name */ #line 738 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = rb_ary_new_from_args(1, MAKE_AST_NODE(DirectiveLocation, 3, rb_ary_entry(yyvsp[0], 1), rb_ary_entry(yyvsp[0], 2), rb_ary_entry(yyvsp[0], 3))); } #line 2867 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 165: /* directive_locations: directive_locations PIPE name */ #line 739 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { rb_ary_push(yyval, MAKE_AST_NODE(DirectiveLocation, 3, rb_ary_entry(yyvsp[0], 1), rb_ary_entry(yyvsp[0], 2), rb_ary_entry(yyvsp[0], 3))); } #line 2873 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 168: /* schema_extension: EXTEND SCHEMA directives_list_opt LCURLY operation_type_definition_list RCURLY */ #line 747 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(SchemaExtension, 6, rb_ary_entry(yyvsp[-5], 1), rb_ary_entry(yyvsp[-5], 2), // TODO use static strings: rb_hash_aref(yyvsp[-1], rb_str_new_cstr("query")), rb_hash_aref(yyvsp[-1], rb_str_new_cstr("mutation")), rb_hash_aref(yyvsp[-1], rb_str_new_cstr("subscription")), yyvsp[-3] ); } #line 2889 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 169: /* schema_extension: EXTEND SCHEMA directives_list */ #line 758 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(SchemaExtension, 6, rb_ary_entry(yyvsp[-2], 1), rb_ary_entry(yyvsp[-2], 2), Qnil, Qnil, Qnil, yyvsp[0] ); } #line 2904 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 176: /* scalar_type_extension: EXTEND SCALAR name directives_list */ #line 777 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(ScalarTypeExtension, 4, rb_ary_entry(yyvsp[-3], 1), rb_ary_entry(yyvsp[-3], 2), rb_ary_entry(yyvsp[-1], 3), yyvsp[0] ); } #line 2917 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 177: /* object_type_extension: EXTEND TYPE_LITERAL name implements_opt directives_list_opt field_definition_list_opt */ #line 787 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(ObjectTypeExtension, 6, rb_ary_entry(yyvsp[-5], 1), rb_ary_entry(yyvsp[-5], 2), rb_ary_entry(yyvsp[-3], 3), yyvsp[-2], // implements yyvsp[-1], yyvsp[0] ); } #line 2932 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 178: /* interface_type_extension: EXTEND INTERFACE name implements_opt directives_list_opt field_definition_list_opt */ #line 799 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(InterfaceTypeExtension, 6, rb_ary_entry(yyvsp[-5], 1), rb_ary_entry(yyvsp[-5], 2), rb_ary_entry(yyvsp[-3], 3), yyvsp[-2], yyvsp[-1], yyvsp[0] ); } #line 2947 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 179: /* union_type_extension: EXTEND UNION name directives_list_opt EQUALS union_members */ #line 811 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(UnionTypeExtension, 5, rb_ary_entry(yyvsp[-5], 1), rb_ary_entry(yyvsp[-5], 2), rb_ary_entry(yyvsp[-3], 3), yyvsp[0], // types yyvsp[-2] ); } #line 2961 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 180: /* union_type_extension: EXTEND UNION name directives_list */ #line 820 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(UnionTypeExtension, 5, rb_ary_entry(yyvsp[-3], 1), rb_ary_entry(yyvsp[-3], 2), rb_ary_entry(yyvsp[-1], 3), GraphQL_Language_Nodes_NONE, // types yyvsp[0] ); } #line 2975 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 181: /* enum_type_extension: EXTEND ENUM name directives_list_opt LCURLY enum_value_definitions RCURLY */ #line 831 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(EnumTypeExtension, 5, rb_ary_entry(yyvsp[-6], 1), rb_ary_entry(yyvsp[-6], 2), rb_ary_entry(yyvsp[-4], 3), yyvsp[-3], yyvsp[-1] ); } #line 2989 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 182: /* enum_type_extension: EXTEND ENUM name directives_list */ #line 840 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(EnumTypeExtension, 5, rb_ary_entry(yyvsp[-3], 1), rb_ary_entry(yyvsp[-3], 2), rb_ary_entry(yyvsp[-1], 3), yyvsp[0], GraphQL_Language_Nodes_NONE ); } #line 3003 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 183: /* input_object_type_extension: EXTEND INPUT name directives_list_opt LCURLY input_value_definition_list RCURLY */ #line 851 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(InputObjectTypeExtension, 5, rb_ary_entry(yyvsp[-6], 1), rb_ary_entry(yyvsp[-6], 2), rb_ary_entry(yyvsp[-4], 3), yyvsp[-3], yyvsp[-1] ); } #line 3017 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 184: /* input_object_type_extension: EXTEND INPUT name directives_list */ #line 860 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { yyval = MAKE_AST_NODE(InputObjectTypeExtension, 5, rb_ary_entry(yyvsp[-3], 1), rb_ary_entry(yyvsp[-3], 2), rb_ary_entry(yyvsp[-1], 3), yyvsp[0], GraphQL_Language_Nodes_NONE ); } #line 3031 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; case 185: /* NamedTypeForCondition: name */ #line 872 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" { /* This action creates a TypeName AST node. $1 (yyvsp[0] in C) refers to the semantic value of 'name'. The MAKE_AST_NODE macro is used, consistent with other rules. 'name' (represented by $1) provides an array: [filename, line, col, name_string] */ yyval = MAKE_AST_NODE(TypeName, 3, rb_ary_entry(yyvsp[0], 1), /* line from name token */ rb_ary_entry(yyvsp[0], 2), /* col from name token */ rb_ary_entry(yyvsp[0], 3) /* name string itself */ ); } #line 3047 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" break; #line 3051 "graphql-c_parser/ext/graphql_c_parser_ext/parser.c" default: break; } /* User semantic actions sometimes alter yychar, and that requires that yytoken be updated with the new translation. We take the approach of translating immediately before every use of yytoken. One alternative is translating here after every semantic action, but that translation would be missed if the semantic action invokes YYABORT, YYACCEPT, or YYERROR immediately after altering yychar or if it invokes YYBACKUP. In the case of YYABORT or YYACCEPT, an incorrect destructor might then be invoked immediately. In the case of YYERROR or YYBACKUP, subsequent parser actions might lead to an incorrect destructor call or verbose syntax error message before the lookahead is translated. */ YY_SYMBOL_PRINT ("-> $$ =", YY_CAST (yysymbol_kind_t, yyr1[yyn]), &yyval, &yyloc); YYPOPSTACK (yylen); yylen = 0; *++yyvsp = yyval; /* Now 'shift' the result of the reduction. Determine what state that goes to, based on the state we popped back to and the rule number reduced by. */ { const int yylhs = yyr1[yyn] - YYNTOKENS; const int yyi = yypgoto[yylhs] + *yyssp; yystate = (0 <= yyi && yyi <= YYLAST && yycheck[yyi] == *yyssp ? yytable[yyi] : yydefgoto[yylhs]); } goto yynewstate; /*--------------------------------------. | yyerrlab -- here on detecting error. | `--------------------------------------*/ yyerrlab: /* Make sure we have latest lookahead translation. See comments at user semantic actions for why this is necessary. */ yytoken = yychar == YYEMPTY ? YYSYMBOL_YYEMPTY : YYTRANSLATE (yychar); /* If not already recovering from an error, report this error. */ if (!yyerrstatus) { ++yynerrs; { yypcontext_t yyctx = {yyssp, yytoken}; char const *yymsgp = YY_("syntax error"); int yysyntax_error_status; yysyntax_error_status = yysyntax_error (&yymsg_alloc, &yymsg, &yyctx); if (yysyntax_error_status == 0) yymsgp = yymsg; else if (yysyntax_error_status == -1) { if (yymsg != yymsgbuf) YYSTACK_FREE (yymsg); yymsg = YY_CAST (char *, YYSTACK_ALLOC (YY_CAST (YYSIZE_T, yymsg_alloc))); if (yymsg) { yysyntax_error_status = yysyntax_error (&yymsg_alloc, &yymsg, &yyctx); yymsgp = yymsg; } else { yymsg = yymsgbuf; yymsg_alloc = sizeof yymsgbuf; yysyntax_error_status = YYENOMEM; } } yyerror (parser, filename, yymsgp); if (yysyntax_error_status == YYENOMEM) YYNOMEM; } } if (yyerrstatus == 3) { /* If just tried and failed to reuse lookahead token after an error, discard it. */ if (yychar <= YYEOF) { /* Return failure if at end of input. */ if (yychar == YYEOF) YYABORT; } else { yydestruct ("Error: discarding", yytoken, &yylval, parser, filename); yychar = YYEMPTY; } } /* Else will try to reuse lookahead token after shifting the error token. */ goto yyerrlab1; /*---------------------------------------------------. | yyerrorlab -- error raised explicitly by YYERROR. | `---------------------------------------------------*/ yyerrorlab: /* Pacify compilers when the user code never invokes YYERROR and the label yyerrorlab therefore never appears in user code. */ if (0) YYERROR; ++yynerrs; /* Do not reclaim the symbols of the rule whose action triggered this YYERROR. */ YYPOPSTACK (yylen); yylen = 0; YY_STACK_PRINT (yyss, yyssp); yystate = *yyssp; goto yyerrlab1; /*-------------------------------------------------------------. | yyerrlab1 -- common code for both syntax error and YYERROR. | `-------------------------------------------------------------*/ yyerrlab1: yyerrstatus = 3; /* Each real token shifted decrements this. */ /* Pop stack until we find a state that shifts the error token. */ for (;;) { yyn = yypact[yystate]; if (!yypact_value_is_default (yyn)) { yyn += YYSYMBOL_YYerror; if (0 <= yyn && yyn <= YYLAST && yycheck[yyn] == YYSYMBOL_YYerror) { yyn = yytable[yyn]; if (0 < yyn) break; } } /* Pop the current state because it cannot handle the error token. */ if (yyssp == yyss) YYABORT; yydestruct ("Error: popping", YY_ACCESSING_SYMBOL (yystate), yyvsp, parser, filename); YYPOPSTACK (1); yystate = *yyssp; YY_STACK_PRINT (yyss, yyssp); } YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN *++yyvsp = yylval; YY_IGNORE_MAYBE_UNINITIALIZED_END /* Shift the error token. */ YY_SYMBOL_PRINT ("Shifting", YY_ACCESSING_SYMBOL (yyn), yyvsp, yylsp); yystate = yyn; goto yynewstate; /*-------------------------------------. | yyacceptlab -- YYACCEPT comes here. | `-------------------------------------*/ yyacceptlab: yyresult = 0; goto yyreturnlab; /*-----------------------------------. | yyabortlab -- YYABORT comes here. | `-----------------------------------*/ yyabortlab: yyresult = 1; goto yyreturnlab; /*-----------------------------------------------------------. | yyexhaustedlab -- YYNOMEM (memory exhaustion) comes here. | `-----------------------------------------------------------*/ yyexhaustedlab: yyerror (parser, filename, YY_("memory exhausted")); yyresult = 2; goto yyreturnlab; /*----------------------------------------------------------. | yyreturnlab -- parsing is finished, clean up and return. | `----------------------------------------------------------*/ yyreturnlab: if (yychar != YYEMPTY) { /* Make sure we have latest lookahead translation. See comments at user semantic actions for why this is necessary. */ yytoken = YYTRANSLATE (yychar); yydestruct ("Cleanup: discarding lookahead", yytoken, &yylval, parser, filename); } /* Do not reclaim the symbols of the rule whose action triggered this YYABORT or YYACCEPT. */ YYPOPSTACK (yylen); YY_STACK_PRINT (yyss, yyssp); while (yyssp != yyss) { yydestruct ("Cleanup: popping", YY_ACCESSING_SYMBOL (+*yyssp), yyvsp, parser, filename); YYPOPSTACK (1); } #ifndef yyoverflow if (yyss != yyssa) YYSTACK_FREE (yyss); #endif if (yymsg != yymsgbuf) YYSTACK_FREE (yymsg); return yyresult; } #line 885 "graphql-c_parser/ext/graphql_c_parser_ext/parser.y" // Custom functions int yylex (YYSTYPE *lvalp, VALUE parser, VALUE filename) { VALUE next_token_idx_rb_int = rb_ivar_get(parser, rb_intern("@next_token_index")); int next_token_idx = FIX2INT(next_token_idx_rb_int); VALUE tokens = rb_ivar_get(parser, rb_intern("@tokens")); VALUE next_token = rb_ary_entry(tokens, next_token_idx); if (!RB_TEST(next_token)) { return YYEOF; } rb_ivar_set(parser, rb_intern("@next_token_index"), INT2FIX(next_token_idx + 1)); VALUE token_type_rb_int = rb_ary_entry(next_token, 4); int next_token_type = FIX2INT(token_type_rb_int); if (next_token_type == 241) { // BAD_UNICODE_ESCAPE VALUE mGraphQL = rb_const_get_at(rb_cObject, rb_intern("GraphQL")); VALUE mCParser = rb_const_get_at(mGraphQL, rb_intern("CParser")); VALUE bad_unicode_error = rb_funcall( mCParser, rb_intern("prepare_bad_unicode_error"), 1, parser ); rb_exc_raise(bad_unicode_error); } *lvalp = next_token; return next_token_type; } void yyerror(VALUE parser, VALUE filename, const char *msg) { VALUE mGraphQL = rb_const_get_at(rb_cObject, rb_intern("GraphQL")); VALUE mCParser = rb_const_get_at(mGraphQL, rb_intern("CParser")); VALUE rb_message = rb_str_new_cstr(msg); VALUE exception = rb_funcall( mCParser, rb_intern("prepare_parse_error"), 2, rb_message, parser ); rb_exc_raise(exception); } #define INITIALIZE_NODE_CLASS_VARIABLE(node_class_name) \ rb_global_variable(&GraphQL_Language_Nodes_##node_class_name); \ GraphQL_Language_Nodes_##node_class_name = rb_const_get_at(mGraphQLLanguageNodes, rb_intern(#node_class_name)); void initialize_node_class_variables() { VALUE mGraphQL = rb_const_get_at(rb_cObject, rb_intern("GraphQL")); VALUE mGraphQLLanguage = rb_const_get_at(mGraphQL, rb_intern("Language")); VALUE mGraphQLLanguageNodes = rb_const_get_at(mGraphQLLanguage, rb_intern("Nodes")); rb_global_variable(&GraphQL_Language_Nodes_NONE); GraphQL_Language_Nodes_NONE = rb_ary_new(); rb_ary_freeze(GraphQL_Language_Nodes_NONE); rb_global_variable(&r_string_query); r_string_query = rb_str_new_cstr("query"); rb_str_freeze(r_string_query); INITIALIZE_NODE_CLASS_VARIABLE(Argument) INITIALIZE_NODE_CLASS_VARIABLE(Directive) INITIALIZE_NODE_CLASS_VARIABLE(Document) INITIALIZE_NODE_CLASS_VARIABLE(Enum) INITIALIZE_NODE_CLASS_VARIABLE(Field) INITIALIZE_NODE_CLASS_VARIABLE(FragmentDefinition) INITIALIZE_NODE_CLASS_VARIABLE(FragmentSpread) INITIALIZE_NODE_CLASS_VARIABLE(InlineFragment) INITIALIZE_NODE_CLASS_VARIABLE(InputObject) INITIALIZE_NODE_CLASS_VARIABLE(ListType) INITIALIZE_NODE_CLASS_VARIABLE(NonNullType) INITIALIZE_NODE_CLASS_VARIABLE(NullValue) INITIALIZE_NODE_CLASS_VARIABLE(OperationDefinition) INITIALIZE_NODE_CLASS_VARIABLE(TypeName) INITIALIZE_NODE_CLASS_VARIABLE(VariableDefinition) INITIALIZE_NODE_CLASS_VARIABLE(VariableIdentifier) INITIALIZE_NODE_CLASS_VARIABLE(ScalarTypeDefinition) INITIALIZE_NODE_CLASS_VARIABLE(ObjectTypeDefinition) INITIALIZE_NODE_CLASS_VARIABLE(InterfaceTypeDefinition) INITIALIZE_NODE_CLASS_VARIABLE(UnionTypeDefinition) INITIALIZE_NODE_CLASS_VARIABLE(EnumTypeDefinition) INITIALIZE_NODE_CLASS_VARIABLE(InputObjectTypeDefinition) INITIALIZE_NODE_CLASS_VARIABLE(EnumValueDefinition) INITIALIZE_NODE_CLASS_VARIABLE(DirectiveDefinition) INITIALIZE_NODE_CLASS_VARIABLE(DirectiveLocation) INITIALIZE_NODE_CLASS_VARIABLE(FieldDefinition) INITIALIZE_NODE_CLASS_VARIABLE(InputValueDefinition) INITIALIZE_NODE_CLASS_VARIABLE(SchemaDefinition) INITIALIZE_NODE_CLASS_VARIABLE(ScalarTypeExtension) INITIALIZE_NODE_CLASS_VARIABLE(ObjectTypeExtension) INITIALIZE_NODE_CLASS_VARIABLE(InterfaceTypeExtension) INITIALIZE_NODE_CLASS_VARIABLE(UnionTypeExtension) INITIALIZE_NODE_CLASS_VARIABLE(EnumTypeExtension) INITIALIZE_NODE_CLASS_VARIABLE(InputObjectTypeExtension) INITIALIZE_NODE_CLASS_VARIABLE(SchemaExtension) } graphql-ruby-2.5.19/graphql-c_parser/ext/graphql_c_parser_ext/parser.h000066400000000000000000000002141514115062600261160ustar00rootroot00000000000000#ifndef Graphql_parser_h #define Graphql_parser_h int yyparse(VALUE parser, VALUE filename); void initialize_node_class_variables(); #endif graphql-ruby-2.5.19/graphql-c_parser/ext/graphql_c_parser_ext/parser.y000066400000000000000000000716531514115062600261560ustar00rootroot00000000000000%require "3.8" %define api.pure full %define parse.error detailed %{ // C Declarations #include #define YYSTYPE VALUE int yylex(YYSTYPE *, VALUE, VALUE); void yyerror(VALUE, VALUE, const char*); static VALUE GraphQL_Language_Nodes_NONE; static VALUE r_string_query; #define MAKE_AST_NODE(node_class_name, nargs, ...) rb_funcall(GraphQL_Language_Nodes_##node_class_name, rb_intern("from_a"), nargs + 1, filename,__VA_ARGS__) #define SETUP_NODE_CLASS_VARIABLE(node_class_name) static VALUE GraphQL_Language_Nodes_##node_class_name; SETUP_NODE_CLASS_VARIABLE(Argument) SETUP_NODE_CLASS_VARIABLE(Directive) SETUP_NODE_CLASS_VARIABLE(Document) SETUP_NODE_CLASS_VARIABLE(Enum) SETUP_NODE_CLASS_VARIABLE(Field) SETUP_NODE_CLASS_VARIABLE(FragmentDefinition) SETUP_NODE_CLASS_VARIABLE(FragmentSpread) SETUP_NODE_CLASS_VARIABLE(InlineFragment) SETUP_NODE_CLASS_VARIABLE(InputObject) SETUP_NODE_CLASS_VARIABLE(ListType) SETUP_NODE_CLASS_VARIABLE(NonNullType) SETUP_NODE_CLASS_VARIABLE(NullValue) SETUP_NODE_CLASS_VARIABLE(OperationDefinition) SETUP_NODE_CLASS_VARIABLE(TypeName) SETUP_NODE_CLASS_VARIABLE(VariableDefinition) SETUP_NODE_CLASS_VARIABLE(VariableIdentifier) SETUP_NODE_CLASS_VARIABLE(ScalarTypeDefinition) SETUP_NODE_CLASS_VARIABLE(ObjectTypeDefinition) SETUP_NODE_CLASS_VARIABLE(InterfaceTypeDefinition) SETUP_NODE_CLASS_VARIABLE(UnionTypeDefinition) SETUP_NODE_CLASS_VARIABLE(EnumTypeDefinition) SETUP_NODE_CLASS_VARIABLE(InputObjectTypeDefinition) SETUP_NODE_CLASS_VARIABLE(EnumValueDefinition) SETUP_NODE_CLASS_VARIABLE(DirectiveDefinition) SETUP_NODE_CLASS_VARIABLE(DirectiveLocation) SETUP_NODE_CLASS_VARIABLE(FieldDefinition) SETUP_NODE_CLASS_VARIABLE(InputValueDefinition) SETUP_NODE_CLASS_VARIABLE(SchemaDefinition) SETUP_NODE_CLASS_VARIABLE(ScalarTypeExtension) SETUP_NODE_CLASS_VARIABLE(ObjectTypeExtension) SETUP_NODE_CLASS_VARIABLE(InterfaceTypeExtension) SETUP_NODE_CLASS_VARIABLE(UnionTypeExtension) SETUP_NODE_CLASS_VARIABLE(EnumTypeExtension) SETUP_NODE_CLASS_VARIABLE(InputObjectTypeExtension) SETUP_NODE_CLASS_VARIABLE(SchemaExtension) %} %param {VALUE parser} %param {VALUE filename} // YACC Declarations %token AMP 200 %token BANG 201 %token COLON 202 %token DIRECTIVE 203 %token DIR_SIGN 204 %token ENUM 205 %token ELLIPSIS 206 %token EQUALS 207 %token EXTEND 208 %token FALSE_LITERAL 209 %token FLOAT 210 %token FRAGMENT 211 %token IDENTIFIER 212 %token INPUT 213 %token IMPLEMENTS 214 %token INT 215 %token INTERFACE 216 %token LBRACKET 217 %token LCURLY 218 %token LPAREN 219 %token MUTATION 220 %token NULL_LITERAL 221 %token ON 222 %token PIPE 223 %token QUERY 224 %token RBRACKET 225 %token RCURLY 226 %token REPEATABLE 227 %token RPAREN 228 %token SCALAR 229 %token SCHEMA 230 %token STRING 231 %token SUBSCRIPTION 232 %token TRUE_LITERAL 233 %token TYPE_LITERAL 234 %token UNION 235 %token VAR_SIGN 236 %type NamedTypeForCondition %% // YACC Rules start: document { rb_ivar_set(parser, rb_intern("@result"), $1); } document: definitions_list { VALUE position_source = rb_ary_entry($1, 0); VALUE line, col; if (RB_TEST(position_source)) { line = rb_funcall(position_source, rb_intern("line"), 0); col = rb_funcall(position_source, rb_intern("col"), 0); } else { line = INT2FIX(1); col = INT2FIX(1); } $$ = MAKE_AST_NODE(Document, 3, line, col, $1); } definitions_list: definition { $$ = rb_ary_new_from_args(1, $1); } | definitions_list definition { rb_ary_push($$, $2); } definition: executable_definition | type_system_definition | type_system_extension executable_definition: operation_definition | fragment_definition operation_definition: operation_type operation_name_opt variable_definitions_opt directives_list_opt selection_set { $$ = MAKE_AST_NODE(OperationDefinition, 7, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($1, 3), (RB_TEST($2) ? rb_ary_entry($2, 3) : Qnil), $3, $4, $5 ); } | LCURLY selection_list RCURLY { $$ = MAKE_AST_NODE(OperationDefinition, 7, rb_ary_entry($1, 1), rb_ary_entry($1, 2), r_string_query, Qnil, GraphQL_Language_Nodes_NONE, GraphQL_Language_Nodes_NONE, $2 ); } | LCURLY RCURLY { $$ = MAKE_AST_NODE(OperationDefinition, 7, rb_ary_entry($1, 1), rb_ary_entry($1, 2), r_string_query, Qnil, GraphQL_Language_Nodes_NONE, GraphQL_Language_Nodes_NONE, GraphQL_Language_Nodes_NONE ); } operation_type: QUERY | MUTATION | SUBSCRIPTION operation_name_opt: /* none */ { $$ = Qnil; } | name variable_definitions_opt: /* none */ { $$ = GraphQL_Language_Nodes_NONE; } | LPAREN variable_definitions_list RPAREN { $$ = $2; } variable_definitions_list: variable_definition { $$ = rb_ary_new_from_args(1, $1); } | variable_definitions_list variable_definition { rb_ary_push($$, $2); } variable_definition: VAR_SIGN name COLON type default_value_opt directives_list_opt { $$ = MAKE_AST_NODE(VariableDefinition, 6, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($2, 3), $4, $5, $6 ); } default_value_opt: /* none */ { $$ = Qnil; } | EQUALS literal_value { $$ = $2; } selection_list: selection { $$ = rb_ary_new_from_args(1, $1); } | selection_list selection { rb_ary_push($$, $2); } selection: field | fragment_spread | inline_fragment selection_set: LCURLY selection_list RCURLY { $$ = $2; } selection_set_opt: /* none */ { $$ = rb_ary_new(); } | selection_set field: name COLON name arguments_opt directives_list_opt selection_set_opt { $$ = MAKE_AST_NODE(Field, 7, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($1, 3), // alias rb_ary_entry($3, 3), // name $4, // args $5, // directives $6 // subselections ); } | name arguments_opt directives_list_opt selection_set_opt { $$ = MAKE_AST_NODE(Field, 7, rb_ary_entry($1, 1), rb_ary_entry($1, 2), Qnil, // alias rb_ary_entry($1, 3), // name $2, // args $3, // directives $4 // subselections ); } arguments_opt: /* none */ { $$ = GraphQL_Language_Nodes_NONE; } | LPAREN arguments_list RPAREN { $$ = $2; } arguments_list: argument { $$ = rb_ary_new_from_args(1, $1); } | arguments_list argument { rb_ary_push($$, $2); } argument: name COLON input_value { $$ = MAKE_AST_NODE(Argument, 4, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($1, 3), $3 ); } literal_value: FLOAT { $$ = rb_funcall(rb_ary_entry($1, 3), rb_intern("to_f"), 0); } | INT { $$ = rb_funcall(rb_ary_entry($1, 3), rb_intern("to_i"), 0); } | STRING { $$ = rb_ary_entry($1, 3); } | TRUE_LITERAL { $$ = Qtrue; } | FALSE_LITERAL { $$ = Qfalse; } | null_value | enum_value | list_value | object_literal_value input_value: literal_value | variable | object_value null_value: NULL_LITERAL { $$ = MAKE_AST_NODE(NullValue, 3, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($1, 3) ); } variable: VAR_SIGN name { $$ = MAKE_AST_NODE(VariableIdentifier, 3, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($2, 3) ); } list_value: LBRACKET RBRACKET { $$ = GraphQL_Language_Nodes_NONE; } | LBRACKET list_value_list RBRACKET { $$ = $2; } list_value_list: input_value { $$ = rb_ary_new_from_args(1, $1); } | list_value_list input_value { rb_ary_push($$, $2); } enum_name: /* any identifier, but not "true", "false" or "null" */ IDENTIFIER | ON | operation_type | schema_keyword enum_value: enum_name { $$ = MAKE_AST_NODE(Enum, 3, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($1, 3) ); } object_value: LCURLY object_value_list_opt RCURLY { $$ = MAKE_AST_NODE(InputObject, 3, rb_ary_entry($1, 1), rb_ary_entry($1, 2), $2 ); } object_value_list_opt: /* nothing */ { $$ = GraphQL_Language_Nodes_NONE; } | object_value_list object_value_list: object_value_field { $$ = rb_ary_new_from_args(1, $1); } | object_value_list object_value_field { rb_ary_push($$, $2); } object_value_field: name COLON input_value { $$ = MAKE_AST_NODE(Argument, 4, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($1, 3), $3 ); } /* like the previous, but with literals only: */ object_literal_value: LCURLY object_literal_value_list_opt RCURLY { $$ = MAKE_AST_NODE(InputObject, 3, rb_ary_entry($1, 1), rb_ary_entry($1, 2), $2 ); } object_literal_value_list_opt: /* nothing */ { $$ = GraphQL_Language_Nodes_NONE; } | object_literal_value_list object_literal_value_list: object_literal_value_field { $$ = rb_ary_new_from_args(1, $1); } | object_literal_value_list object_literal_value_field { rb_ary_push($$, $2); } object_literal_value_field: name COLON literal_value { $$ = MAKE_AST_NODE(Argument, 4, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($1, 3), $3 ); } directives_list_opt: /* none */ { $$ = GraphQL_Language_Nodes_NONE; } | directives_list directives_list: directive { $$ = rb_ary_new_from_args(1, $1); } | directives_list directive { rb_ary_push($$, $2); } directive: DIR_SIGN name arguments_opt { $$ = MAKE_AST_NODE(Directive, 4, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($2, 3), $3 ); } name: name_without_on | ON schema_keyword: SCHEMA | SCALAR | TYPE_LITERAL | IMPLEMENTS | INTERFACE | UNION | ENUM | INPUT | DIRECTIVE | EXTEND | FRAGMENT | REPEATABLE name_without_on: IDENTIFIER | TRUE_LITERAL | FALSE_LITERAL | NULL_LITERAL | operation_type | schema_keyword fragment_spread: ELLIPSIS name_without_on directives_list_opt { $$ = MAKE_AST_NODE(FragmentSpread, 4, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($2, 3), $3 ); } inline_fragment: ELLIPSIS ON NamedTypeForCondition directives_list_opt selection_set { $$ = MAKE_AST_NODE(InlineFragment, 5, rb_ary_entry($1, 1), rb_ary_entry($1, 2), $3, $4, $5 ); } | ELLIPSIS directives_list_opt selection_set { $$ = MAKE_AST_NODE(InlineFragment, 5, rb_ary_entry($1, 1), rb_ary_entry($1, 2), Qnil, $2, $3 ); } fragment_definition: FRAGMENT fragment_name_opt ON NamedTypeForCondition directives_list_opt selection_set { $$ = MAKE_AST_NODE(FragmentDefinition, 6, rb_ary_entry($1, 1), rb_ary_entry($1, 2), $2, $4, $5, $6 ); } fragment_name_opt: /* none */ { $$ = Qnil; } | name_without_on { $$ = rb_ary_entry($1, 3); } type: nullable_type | nullable_type BANG { $$ = MAKE_AST_NODE(NonNullType, 3, rb_funcall($1, rb_intern("line"), 0), rb_funcall($1, rb_intern("col"), 0), $1); } nullable_type: name { $$ = MAKE_AST_NODE(TypeName, 3, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($1, 3) ); } | LBRACKET type RBRACKET { $$ = MAKE_AST_NODE(ListType, 3, rb_funcall($2, rb_intern("line"), 0), rb_funcall($2, rb_intern("col"), 0), $2 ); } type_system_definition: schema_definition | type_definition | directive_definition schema_definition: SCHEMA directives_list_opt operation_type_definition_list_opt { $$ = MAKE_AST_NODE(SchemaDefinition, 6, rb_ary_entry($1, 1), rb_ary_entry($1, 2), // TODO use static strings: rb_hash_aref($3, rb_str_new_cstr("query")), rb_hash_aref($3, rb_str_new_cstr("mutation")), rb_hash_aref($3, rb_str_new_cstr("subscription")), $2 ); } operation_type_definition_list_opt: /* none */ { $$ = rb_hash_new(); } | LCURLY operation_type_definition_list RCURLY { $$ = $2; } operation_type_definition_list: operation_type_definition { $$ = rb_hash_new(); rb_hash_aset($$, rb_ary_entry($1, 0), rb_ary_entry($1, 1)); } | operation_type_definition_list operation_type_definition { rb_hash_aset($$, rb_ary_entry($2, 0), rb_ary_entry($2, 1)); } operation_type_definition: operation_type COLON name { $$ = rb_ary_new_from_args(2, rb_ary_entry($1, 3), rb_ary_entry($3, 3)); } type_definition: scalar_type_definition | object_type_definition | interface_type_definition | union_type_definition | enum_type_definition | input_object_type_definition description: STRING description_opt: /* none */ { $$ = Qnil; } | description scalar_type_definition: description_opt SCALAR name directives_list_opt { $$ = MAKE_AST_NODE(ScalarTypeDefinition, 5, rb_ary_entry($2, 1), rb_ary_entry($2, 2), rb_ary_entry($3, 3), // TODO see get_description for reading a description from comments (RB_TEST($1) ? rb_ary_entry($1, 3) : Qnil), $4 ); } object_type_definition: description_opt TYPE_LITERAL name implements_opt directives_list_opt field_definition_list_opt { $$ = MAKE_AST_NODE(ObjectTypeDefinition, 7, rb_ary_entry($2, 1), rb_ary_entry($2, 2), rb_ary_entry($3, 3), $4, // implements // TODO see get_description for reading a description from comments (RB_TEST($1) ? rb_ary_entry($1, 3) : Qnil), $5, $6 ); } implements_opt: /* none */ { $$ = GraphQL_Language_Nodes_NONE; } | IMPLEMENTS AMP interfaces_list { $$ = $3; } | IMPLEMENTS interfaces_list { $$ = $2; } | IMPLEMENTS legacy_interfaces_list { $$ = $2; } interfaces_list: name { VALUE new_name = MAKE_AST_NODE(TypeName, 3, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($1, 3) ); $$ = rb_ary_new_from_args(1, new_name); } | interfaces_list AMP name { VALUE new_name = MAKE_AST_NODE(TypeName, 3, rb_ary_entry($3, 1), rb_ary_entry($3, 2), rb_ary_entry($3, 3)); rb_ary_push($$, new_name); } legacy_interfaces_list: name { VALUE new_name = MAKE_AST_NODE(TypeName, 3, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($1, 3) ); $$ = rb_ary_new_from_args(1, new_name); } | legacy_interfaces_list name { rb_ary_push($$, MAKE_AST_NODE(TypeName, 3, rb_ary_entry($2, 1), rb_ary_entry($2, 2), rb_ary_entry($2, 3))); } input_value_definition: description_opt name COLON type default_value_opt directives_list_opt { $$ = MAKE_AST_NODE(InputValueDefinition, 7, rb_ary_entry($2, 1), rb_ary_entry($2, 2), rb_ary_entry($2, 3), $4, $5, // TODO see get_description for reading a description from comments (RB_TEST($1) ? rb_ary_entry($1, 3) : Qnil), $6 ); } input_value_definition_list: input_value_definition { $$ = rb_ary_new_from_args(1, $1); } | input_value_definition_list input_value_definition { rb_ary_push($$, $2); } arguments_definitions_opt: /* none */ { $$ = GraphQL_Language_Nodes_NONE; } | LPAREN input_value_definition_list RPAREN { $$ = $2; } field_definition: description_opt name arguments_definitions_opt COLON type directives_list_opt { $$ = MAKE_AST_NODE(FieldDefinition, 7, rb_ary_entry($2, 1), rb_ary_entry($2, 2), rb_ary_entry($2, 3), $5, // TODO see get_description for reading a description from comments (RB_TEST($1) ? rb_ary_entry($1, 3) : Qnil), $3, $6 ); } field_definition_list_opt: /* none */ { $$ = GraphQL_Language_Nodes_NONE; } | LCURLY field_definition_list RCURLY { $$ = $2; } field_definition_list: /* none - this is not actually valid but graphql-ruby used to print this */ { $$ = GraphQL_Language_Nodes_NONE; } | field_definition { $$ = rb_ary_new_from_args(1, $1); } | field_definition_list field_definition { rb_ary_push($$, $2); } interface_type_definition: description_opt INTERFACE name implements_opt directives_list_opt field_definition_list_opt { $$ = MAKE_AST_NODE(InterfaceTypeDefinition, 7, rb_ary_entry($2, 1), rb_ary_entry($2, 2), rb_ary_entry($3, 3), // TODO see get_description for reading a description from comments (RB_TEST($1) ? rb_ary_entry($1, 3) : Qnil), $4, $5, $6 ); } pipe_opt: /* none */ { $$ = GraphQL_Language_Nodes_NONE; } | PIPE { $$ = GraphQL_Language_Nodes_NONE; } union_members: pipe_opt name { VALUE new_member = MAKE_AST_NODE(TypeName, 3, rb_ary_entry($2, 1), rb_ary_entry($2, 2), rb_ary_entry($2, 3) ); $$ = rb_ary_new_from_args(1, new_member); } | union_members PIPE name { rb_ary_push($$, MAKE_AST_NODE(TypeName, 3, rb_ary_entry($3, 1), rb_ary_entry($3, 2), rb_ary_entry($3, 3))); } union_type_definition: description_opt UNION name directives_list_opt EQUALS union_members { $$ = MAKE_AST_NODE(UnionTypeDefinition, 6, rb_ary_entry($2, 1), rb_ary_entry($2, 2), rb_ary_entry($3, 3), $6, // types // TODO see get_description for reading a description from comments (RB_TEST($1) ? rb_ary_entry($1, 3) : Qnil), $4 ); } enum_type_definition: description_opt ENUM name directives_list_opt LCURLY enum_value_definitions RCURLY { $$ = MAKE_AST_NODE(EnumTypeDefinition, 6, rb_ary_entry($2, 1), rb_ary_entry($2, 2), rb_ary_entry($3, 3), // TODO see get_description for reading a description from comments (RB_TEST($1) ? rb_ary_entry($1, 3) : Qnil), $4, $6 ); } enum_value_definition: description_opt enum_name directives_list_opt { $$ = MAKE_AST_NODE(EnumValueDefinition, 5, rb_ary_entry($2, 1), rb_ary_entry($2, 2), rb_ary_entry($2, 3), // TODO see get_description for reading a description from comments (RB_TEST($1) ? rb_ary_entry($1, 3) : Qnil), $3 ); } enum_value_definitions: enum_value_definition { $$ = rb_ary_new_from_args(1, $1); } | enum_value_definitions enum_value_definition { rb_ary_push($$, $2); } input_object_type_definition: description_opt INPUT name directives_list_opt LCURLY input_value_definition_list RCURLY { $$ = MAKE_AST_NODE(InputObjectTypeDefinition, 6, rb_ary_entry($2, 1), rb_ary_entry($2, 2), rb_ary_entry($3, 3), // TODO see get_description for reading a description from comments (RB_TEST($1) ? rb_ary_entry($1, 3) : Qnil), $4, $6 ); } directive_definition: description_opt DIRECTIVE DIR_SIGN name arguments_definitions_opt directive_repeatable_opt ON directive_locations { $$ = MAKE_AST_NODE(DirectiveDefinition, 7, rb_ary_entry($2, 1), rb_ary_entry($2, 2), rb_ary_entry($4, 3), (RB_TEST($6) ? Qtrue : Qfalse), // repeatable // TODO see get_description for reading a description from comments (RB_TEST($1) ? rb_ary_entry($1, 3) : Qnil), $5, $8 ); } directive_repeatable_opt: /* nothing */ { $$ = Qnil; } | REPEATABLE { $$ = Qtrue; } directive_locations: name { $$ = rb_ary_new_from_args(1, MAKE_AST_NODE(DirectiveLocation, 3, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($1, 3))); } | directive_locations PIPE name { rb_ary_push($$, MAKE_AST_NODE(DirectiveLocation, 3, rb_ary_entry($3, 1), rb_ary_entry($3, 2), rb_ary_entry($3, 3))); } type_system_extension: schema_extension | type_extension schema_extension: EXTEND SCHEMA directives_list_opt LCURLY operation_type_definition_list RCURLY { $$ = MAKE_AST_NODE(SchemaExtension, 6, rb_ary_entry($1, 1), rb_ary_entry($1, 2), // TODO use static strings: rb_hash_aref($5, rb_str_new_cstr("query")), rb_hash_aref($5, rb_str_new_cstr("mutation")), rb_hash_aref($5, rb_str_new_cstr("subscription")), $3 ); } | EXTEND SCHEMA directives_list { $$ = MAKE_AST_NODE(SchemaExtension, 6, rb_ary_entry($1, 1), rb_ary_entry($1, 2), Qnil, Qnil, Qnil, $3 ); } type_extension: scalar_type_extension | object_type_extension | interface_type_extension | union_type_extension | enum_type_extension | input_object_type_extension scalar_type_extension: EXTEND SCALAR name directives_list { $$ = MAKE_AST_NODE(ScalarTypeExtension, 4, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($3, 3), $4 ); } object_type_extension: EXTEND TYPE_LITERAL name implements_opt directives_list_opt field_definition_list_opt { $$ = MAKE_AST_NODE(ObjectTypeExtension, 6, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($3, 3), $4, // implements $5, $6 ); } interface_type_extension: EXTEND INTERFACE name implements_opt directives_list_opt field_definition_list_opt { $$ = MAKE_AST_NODE(InterfaceTypeExtension, 6, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($3, 3), $4, $5, $6 ); } union_type_extension: EXTEND UNION name directives_list_opt EQUALS union_members { $$ = MAKE_AST_NODE(UnionTypeExtension, 5, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($3, 3), $6, // types $4 ); } | EXTEND UNION name directives_list { $$ = MAKE_AST_NODE(UnionTypeExtension, 5, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($3, 3), GraphQL_Language_Nodes_NONE, // types $4 ); } enum_type_extension: EXTEND ENUM name directives_list_opt LCURLY enum_value_definitions RCURLY { $$ = MAKE_AST_NODE(EnumTypeExtension, 5, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($3, 3), $4, $6 ); } | EXTEND ENUM name directives_list { $$ = MAKE_AST_NODE(EnumTypeExtension, 5, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($3, 3), $4, GraphQL_Language_Nodes_NONE ); } input_object_type_extension: EXTEND INPUT name directives_list_opt LCURLY input_value_definition_list RCURLY { $$ = MAKE_AST_NODE(InputObjectTypeExtension, 5, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($3, 3), $4, $6 ); } | EXTEND INPUT name directives_list { $$ = MAKE_AST_NODE(InputObjectTypeExtension, 5, rb_ary_entry($1, 1), rb_ary_entry($1, 2), rb_ary_entry($3, 3), $4, GraphQL_Language_Nodes_NONE ); } NamedTypeForCondition: name { /* This action creates a TypeName AST node. $1 (yyvsp[0] in C) refers to the semantic value of 'name'. The MAKE_AST_NODE macro is used, consistent with other rules. 'name' (represented by $1) provides an array: [filename, line, col, name_string] */ $$ = MAKE_AST_NODE(TypeName, 3, rb_ary_entry($1, 1), /* line from name token */ rb_ary_entry($1, 2), /* col from name token */ rb_ary_entry($1, 3) /* name string itself */ ); } ; %% // Custom functions int yylex (YYSTYPE *lvalp, VALUE parser, VALUE filename) { VALUE next_token_idx_rb_int = rb_ivar_get(parser, rb_intern("@next_token_index")); int next_token_idx = FIX2INT(next_token_idx_rb_int); VALUE tokens = rb_ivar_get(parser, rb_intern("@tokens")); VALUE next_token = rb_ary_entry(tokens, next_token_idx); if (!RB_TEST(next_token)) { return YYEOF; } rb_ivar_set(parser, rb_intern("@next_token_index"), INT2FIX(next_token_idx + 1)); VALUE token_type_rb_int = rb_ary_entry(next_token, 4); int next_token_type = FIX2INT(token_type_rb_int); if (next_token_type == 241) { // BAD_UNICODE_ESCAPE VALUE mGraphQL = rb_const_get_at(rb_cObject, rb_intern("GraphQL")); VALUE mCParser = rb_const_get_at(mGraphQL, rb_intern("CParser")); VALUE bad_unicode_error = rb_funcall( mCParser, rb_intern("prepare_bad_unicode_error"), 1, parser ); rb_exc_raise(bad_unicode_error); } *lvalp = next_token; return next_token_type; } void yyerror(VALUE parser, VALUE filename, const char *msg) { VALUE mGraphQL = rb_const_get_at(rb_cObject, rb_intern("GraphQL")); VALUE mCParser = rb_const_get_at(mGraphQL, rb_intern("CParser")); VALUE rb_message = rb_str_new_cstr(msg); VALUE exception = rb_funcall( mCParser, rb_intern("prepare_parse_error"), 2, rb_message, parser ); rb_exc_raise(exception); } #define INITIALIZE_NODE_CLASS_VARIABLE(node_class_name) \ rb_global_variable(&GraphQL_Language_Nodes_##node_class_name); \ GraphQL_Language_Nodes_##node_class_name = rb_const_get_at(mGraphQLLanguageNodes, rb_intern(#node_class_name)); void initialize_node_class_variables() { VALUE mGraphQL = rb_const_get_at(rb_cObject, rb_intern("GraphQL")); VALUE mGraphQLLanguage = rb_const_get_at(mGraphQL, rb_intern("Language")); VALUE mGraphQLLanguageNodes = rb_const_get_at(mGraphQLLanguage, rb_intern("Nodes")); rb_global_variable(&GraphQL_Language_Nodes_NONE); GraphQL_Language_Nodes_NONE = rb_ary_new(); rb_ary_freeze(GraphQL_Language_Nodes_NONE); rb_global_variable(&r_string_query); r_string_query = rb_str_new_cstr("query"); rb_str_freeze(r_string_query); INITIALIZE_NODE_CLASS_VARIABLE(Argument) INITIALIZE_NODE_CLASS_VARIABLE(Directive) INITIALIZE_NODE_CLASS_VARIABLE(Document) INITIALIZE_NODE_CLASS_VARIABLE(Enum) INITIALIZE_NODE_CLASS_VARIABLE(Field) INITIALIZE_NODE_CLASS_VARIABLE(FragmentDefinition) INITIALIZE_NODE_CLASS_VARIABLE(FragmentSpread) INITIALIZE_NODE_CLASS_VARIABLE(InlineFragment) INITIALIZE_NODE_CLASS_VARIABLE(InputObject) INITIALIZE_NODE_CLASS_VARIABLE(ListType) INITIALIZE_NODE_CLASS_VARIABLE(NonNullType) INITIALIZE_NODE_CLASS_VARIABLE(NullValue) INITIALIZE_NODE_CLASS_VARIABLE(OperationDefinition) INITIALIZE_NODE_CLASS_VARIABLE(TypeName) INITIALIZE_NODE_CLASS_VARIABLE(VariableDefinition) INITIALIZE_NODE_CLASS_VARIABLE(VariableIdentifier) INITIALIZE_NODE_CLASS_VARIABLE(ScalarTypeDefinition) INITIALIZE_NODE_CLASS_VARIABLE(ObjectTypeDefinition) INITIALIZE_NODE_CLASS_VARIABLE(InterfaceTypeDefinition) INITIALIZE_NODE_CLASS_VARIABLE(UnionTypeDefinition) INITIALIZE_NODE_CLASS_VARIABLE(EnumTypeDefinition) INITIALIZE_NODE_CLASS_VARIABLE(InputObjectTypeDefinition) INITIALIZE_NODE_CLASS_VARIABLE(EnumValueDefinition) INITIALIZE_NODE_CLASS_VARIABLE(DirectiveDefinition) INITIALIZE_NODE_CLASS_VARIABLE(DirectiveLocation) INITIALIZE_NODE_CLASS_VARIABLE(FieldDefinition) INITIALIZE_NODE_CLASS_VARIABLE(InputValueDefinition) INITIALIZE_NODE_CLASS_VARIABLE(SchemaDefinition) INITIALIZE_NODE_CLASS_VARIABLE(ScalarTypeExtension) INITIALIZE_NODE_CLASS_VARIABLE(ObjectTypeExtension) INITIALIZE_NODE_CLASS_VARIABLE(InterfaceTypeExtension) INITIALIZE_NODE_CLASS_VARIABLE(UnionTypeExtension) INITIALIZE_NODE_CLASS_VARIABLE(EnumTypeExtension) INITIALIZE_NODE_CLASS_VARIABLE(InputObjectTypeExtension) INITIALIZE_NODE_CLASS_VARIABLE(SchemaExtension) } graphql-ruby-2.5.19/graphql-c_parser/graphql-c_parser.gemspec000066400000000000000000000021071514115062600242570ustar00rootroot00000000000000# frozen_string_literal: true $LOAD_PATH.push File.expand_path("../lib", __FILE__) require "graphql/c_parser/version" require "date" Gem::Specification.new do |s| s.name = "graphql-c_parser" s.version = GraphQL::CParser::VERSION s.date = Date.today.to_s s.summary = "A parser for GraphQL, implemented as a C extension" s.homepage = "https://github.com/rmosolgo/graphql-ruby" s.authors = ["Robert Mosolgo"] s.email = ["rdmosolgo@gmail.com"] s.license = "MIT" s.required_ruby_version = ">= 3.0.0" s.metadata = { "homepage_uri" => "https://graphql-ruby.org", "changelog_uri" => "https://github.com/rmosolgo/graphql-ruby/blob/master/graphql-c_parser/CHANGELOG.md", "source_code_uri" => "https://github.com/rmosolgo/graphql-ruby", "bug_tracker_uri" => "https://github.com/rmosolgo/graphql-ruby/issues", "mailing_list_uri" => "https://buttondown.email/graphql-ruby", } s.files = Dir["{lib,ext}/**/*.{rb,h,c}"] s.extensions << "ext/graphql_c_parser_ext/extconf.rb" s.add_dependency "graphql", ">= 2.2.10" end graphql-ruby-2.5.19/graphql-c_parser/lib/000077500000000000000000000000001514115062600202265ustar00rootroot00000000000000graphql-ruby-2.5.19/graphql-c_parser/lib/graphql-c_parser.rb000066400000000000000000000000711514115062600240030ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/c_parser" graphql-ruby-2.5.19/graphql-c_parser/lib/graphql/000077500000000000000000000000001514115062600216645ustar00rootroot00000000000000graphql-ruby-2.5.19/graphql-c_parser/lib/graphql/c_parser.rb000066400000000000000000000121401514115062600240050ustar00rootroot00000000000000# frozen_string_literal: true require "graphql" require "graphql/c_parser/version" require "graphql/graphql_c_parser_ext" module GraphQL module CParser def self.parse(query_str, filename: nil, trace: GraphQL::Tracing::NullTrace, max_tokens: nil) Parser.parse(query_str, filename: filename, trace: trace, max_tokens: max_tokens) end def self.parse_file(filename) contents = File.read(filename) parse(contents, filename: filename) end def self.tokenize_with_c(str) reject_numbers_followed_by_names = GraphQL.respond_to?(:reject_numbers_followed_by_names) && GraphQL.reject_numbers_followed_by_names tokenize_with_c_internal(str, false, reject_numbers_followed_by_names) end def self.prepare_parse_error(message, parser) query_str = parser.query_string filename = parser.filename if message.start_with?("memory exhausted") return GraphQL::ParseError.new("This query is too large to execute.", nil, nil, query_str, filename: filename) end token = parser.tokens[parser.next_token_index - 1] if token # There might not be a token if it's a comments-only string line = token[1] col = token[2] if line && col location_str = " at [#{line}, #{col}]" if !message.include?(location_str) message += location_str end end if !message.include?("end of file") message.sub!(/, unexpected ([a-zA-Z ]+)(,| at)/, ", unexpected \\1 (#{token[3].inspect})\\2") end end GraphQL::ParseError.new(message, line, col, query_str, filename: filename) end def self.prepare_number_name_parse_error(line, col, query_str, number_part, name_part) raise GraphQL::ParseError.new("Name after number is not allowed (in `#{number_part}#{name_part}`)", line, col, query_str) end def self.prepare_bad_unicode_error(parser) token = parser.tokens[parser.next_token_index - 1] line = token[1] col = token[2] GraphQL::ParseError.new( "Parse error on bad Unicode escape sequence: #{token[3].inspect} (error) at [#{line}, #{col}]", line, col, parser.query_string, filename: parser.filename ) end module Lexer def self.tokenize(graphql_string, intern_identifiers: false, max_tokens: nil) if !(graphql_string.encoding == Encoding::UTF_8 || graphql_string.ascii_only?) graphql_string = graphql_string.dup.force_encoding(Encoding::UTF_8) end if !graphql_string.valid_encoding? return [ [ :BAD_UNICODE_ESCAPE, 1, 1, graphql_string, 241 # BAD_UNICODE_ESCAPE in lexer.rl ] ] end reject_numbers_followed_by_names = GraphQL.respond_to?(:reject_numbers_followed_by_names) && GraphQL.reject_numbers_followed_by_names # -1 indicates that there is no limit lexer_max_tokens = max_tokens.nil? ? -1 : max_tokens tokenize_with_c_internal(graphql_string, intern_identifiers, reject_numbers_followed_by_names, lexer_max_tokens) end end class Parser def self.parse(query_str, filename: nil, trace: GraphQL::Tracing::NullTrace, max_tokens: nil) self.new(query_str, filename, trace, max_tokens).result end def self.parse_file(filename) contents = File.read(filename) parse(contents, filename: filename) end def initialize(query_string, filename, trace, max_tokens) if query_string.nil? raise GraphQL::ParseError.new("No query string was present", nil, nil, query_string) end @query_string = query_string @filename = filename @tokens = nil @next_token_index = 0 @result = nil @trace = trace @intern_identifiers = false @max_tokens = max_tokens end def result if @result.nil? @tokens = @trace.lex(query_string: @query_string) do GraphQL::CParser::Lexer.tokenize(@query_string, intern_identifiers: @intern_identifiers, max_tokens: @max_tokens) end @trace.parse(query_string: @query_string) do c_parse @result end end @result end def tokens_count result @tokens.length end attr_reader :tokens, :next_token_index, :query_string, :filename end class SchemaParser < Parser def initialize(*args) super @intern_identifiers = true end end end def self.scan_with_c(graphql_string) GraphQL::CParser::Lexer.tokenize(graphql_string) end def self.parse_with_c(string, filename: nil, trace: GraphQL::Tracing::NullTrace) if string.nil? raise GraphQL::ParseError.new("No query string was present", nil, nil, string) end document = GraphQL::CParser.parse(string, filename: filename, trace: trace) if document.definitions.size == 0 raise GraphQL::ParseError.new("Unexpected end of document", 1, 1, string) end document end self.default_parser = GraphQL::CParser end graphql-ruby-2.5.19/graphql-c_parser/lib/graphql/c_parser/000077500000000000000000000000001514115062600234625ustar00rootroot00000000000000graphql-ruby-2.5.19/graphql-c_parser/lib/graphql/c_parser/version.rb000066400000000000000000000001371514115062600254750ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module CParser VERSION = "1.1.3" end end graphql-ruby-2.5.19/graphql-ruby.png000066400000000000000000000103401514115062600173470ustar00rootroot00000000000000PNG  IHDRgOۢsBIT|d pHYsaa?itEXtSoftwarewww.inkscape.org<]IDATxkTՕ֣i@^VuKv!Hdia,A2fEdYc|A,i߈(N&jpdfT59Uz=hn=.tϚszw9)f6I2"&{R6ZkL~")J=MC*O͋Y) v)VކT(>ރXPX_4\yMwD˳qYh 脯PZtW/Z{ˡrl ܑG$zi Ҿm#cCt3/؛MkRlɟP۸3RlQik<&^@juJ; p&FK@ 0㿺dyC!fP+kf|]Ʒh#J16XYzQ6Xkl!ӬO2 ŋ4F]qII[5-z(vQ!$dx+ .Tm YsiAhiR7?wZ0@$'ކI YPiv$*,)IbC_L_?brGKĊ;%g]iil~ 1 Cb. /V]]ўWH%TN$"i 2֏HfƦ`SJ { ]Pg5Я\ič7M9OEO5D|KM̋)^ RASQpIJL{9^fsw&Q.Pd(PZg>IfyxυLi>-^!'6^E@J  z|op:×W?<iٲ3R_a5l%fؿu_˞VM,bPe]ibbi.Iq׮.Пfur?._pPDeoR#(69S9\wS_ݶ#[&m_)2~]>5u?4\| ߀oQ(6p$fy0^ZCCoRNg+ǹ@/õpf_hN:=^?.iĽ Wv Hy`SngߟKvn$h?ygx;^Εz7+߽?zfR^$CR:"G@zSܦac{ꊄ <_I(T,Ćj,e*Z_pi&hi/vw۪igtF$dҠs">;&Nw겅~xXŠ jCX;/leXnLLvU  /WN^bX&,诳v"ƌy1KA_LoX^pŽPLT~n 4 ^EJuB8cZ^i:,yhGN;Mвbx}/}.GlBJ ?DڜHΰ^0EDpUE,ٽ5/z6ۉ\FUK[ԓr+.FeJ/;> zfljA X݂Ph— wh}rZj~"*w7 Hm?O/ZO5i9/ z}+{Z4:CTBXS 0*\^ԚZpM9h I`s V˜c)*R(CzGEfyF!& `Qb@a[/K.L5MIq >D^Y f2Bϴ#̤v#Vl8+0%l̄BDpu? I++W] ݅|,R1PJգ|=-ٟWg}I13! G.3btEQ'1D%j&@ki,eYhK菠O*48J ^֘EԒHm;Q~kùH}ٴai- ӺŬxUc6HobPP[[x ӣWzS찕aVDac$Dy@BUcŪQgۧ dF۟VN@X]ԦBUeL(_Cʑ@t)@&/9mj}JѵDžM5b:n'_ 4FB9pM>eMWu$>5i=8̳T C4@tý׀^+ٝ<W^Lb, 1z~Z(K 6 BeMO% ٛzVh R0jd^#6Eo:~uÜf|C61*\ ,ştEC 4D/X%(QӰ.R|$fvttHV(p[*ZY7yӌoPdjK3R)V qUB 1:߸g5wK*A%SuBuwmކ{ ${k8%ow @>1dġ79Jx!?'nቮ"FDo()l^>語hCmU'bn&493鬫z|!}ij*U\1_>[Y7yqCgOe*\WT #xHr3vnGΝdr 2i|UE\_*1e7x1.7EB/V҇JU{-`%W4^Ttyż+[ԛ+M pA5޳6Se-߭e0( *F G Ö9E͘3m %$}`%NC=w [~䨥Ce*rz/^e(H^8mG9hah-;N[1#bb,qu^_ [HRF&2xN3+|%僤®4=Wz"MTa#u}%-T߰nP|)rVq`g)A9kV`j~pĺ;>A2 zʛhG#tLȁ &^IYYlv"3ʺCǺ#|S7 :\8f!(u_sqͱFB1WĸtCOqIHs#}ǚ8@AGƯT$;3fIB:?7hᘓ=[1#8 ]mu|< 1pלX__'*IuU.TʳP@%WՁK+APnb@gbЗbú.)*ܻãW4_,ĚvutWAL\U'c E ȊЀ[cb@?9iz^;:6D>ʜq@gx@,AAGnj/LP̋z]Ҩݕ)& } !rH#Ϣ(gMd@~!v/L $}}(ʳMe໵VN %["|C 1(1(qs2~ BK& Jr K@ѿ6P0(Фc.}@"̎-9͟K59ݜ3YؒgB6pk}0eQ>F:q}+ r4IENDB`graphql-ruby-2.5.19/graphql-ruby.svg000066400000000000000000000024041514115062600173640ustar00rootroot00000000000000 graphql-ruby-2.5.19/graphql.gemspec000066400000000000000000000043401514115062600172320ustar00rootroot00000000000000# frozen_string_literal: true $LOAD_PATH.push File.expand_path("../lib", __FILE__) require "graphql/version" require "date" Gem::Specification.new do |s| s.name = "graphql" s.version = GraphQL::VERSION s.date = Date.today.to_s s.summary = "A GraphQL language and runtime for Ruby" s.description = "A plain-Ruby implementation of GraphQL." s.homepage = "https://github.com/rmosolgo/graphql-ruby" s.authors = ["Robert Mosolgo"] s.email = ["rdmosolgo@gmail.com"] s.license = "MIT" s.required_ruby_version = ">= 2.7.0" s.metadata = { "homepage_uri" => "https://graphql-ruby.org", "changelog_uri" => "https://github.com/rmosolgo/graphql-ruby/blob/master/CHANGELOG.md", "source_code_uri" => "https://github.com/rmosolgo/graphql-ruby", "bug_tracker_uri" => "https://github.com/rmosolgo/graphql-ruby/issues", "mailing_list_uri" => "https://buttondown.email/graphql-ruby", "rubygems_mfa_required" => "true", } s.files = Dir["{lib}/**/*", "MIT-LICENSE", "readme.md", ".yardopts"] s.add_runtime_dependency "base64" s.add_runtime_dependency "fiber-storage" s.add_runtime_dependency "logger" s.add_development_dependency "benchmark-ips" s.add_development_dependency "concurrent-ruby", "~>1.0" s.add_development_dependency "google-protobuf" s.add_development_dependency "graphql-batch" s.add_development_dependency "memory_profiler" s.add_development_dependency "minitest" s.add_development_dependency "minitest-focus" s.add_development_dependency "minitest-reporters" s.add_development_dependency "ostruct" s.add_development_dependency "rake" s.add_development_dependency 'rake-compiler' s.add_development_dependency "rubocop" s.add_development_dependency "simplecov" s.add_development_dependency "simplecov-lcov" s.add_development_dependency "undercover" # website stuff s.add_development_dependency "jekyll" s.add_development_dependency "jekyll-sass-converter", "~>2.2" s.add_development_dependency "yard" s.add_development_dependency "jekyll-algolia" s.add_development_dependency "jekyll-redirect-from" s.add_development_dependency "m", "~> 1.5.0" s.add_development_dependency "mutex_m" s.add_development_dependency "webrick" end graphql-ruby-2.5.19/guides/000077500000000000000000000000001514115062600155065ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/CNAME000066400000000000000000000000211514115062600162450ustar00rootroot00000000000000graphql-ruby.org graphql-ruby-2.5.19/guides/_config.yml000066400000000000000000000012331514115062600176340ustar00rootroot00000000000000title: GraphQL Ruby baseurl: "" url: "https://graphql-ruby.org" exclude: - .gitignore keep_files: ["api-doc", ".git"] # Build settings markdown: kramdown highlighter: rouge kramdown: auto_ids: true hard_wrap: false input: GFM defaults: - scope: path: "" values: layout: "default" fullwidth: true algolia: application_id: '8VO8708WUV' index_name: 'prod_graphql_ruby' settings: searchableAttributes: - section - title - headings - content customRanking: - desc(title) - desc(headings) - desc(content) plugins: - jekyll-algolia - jekyll-redirect-from graphql-ruby-2.5.19/guides/_layouts/000077500000000000000000000000001514115062600173455ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/_layouts/default.html000066400000000000000000000064101514115062600216600ustar00rootroot00000000000000 {% if page.section contains "GraphQL" %} {{ page.section }} - {{ page.title }} {% else %} GraphQL - {{ page.title }} {% endif %}
{{ content }}
graphql-ruby-2.5.19/guides/_layouts/doc_stub.html000066400000000000000000000002501514115062600220320ustar00rootroot00000000000000 {{ content }} graphql-ruby-2.5.19/guides/_layouts/guide.html000066400000000000000000000055141514115062600213350ustar00rootroot00000000000000--- layout: default --- {% if page.experimental %}

⚠ Experimental ⚠

This feature may get big changes in future releases. Check the changelog for update notes.

{% endif %} {% if page.pro %}

⚡️ Pro Feature ⚡️ This feature is bundled with GraphQL-Pro.

{% endif %} {% if page.enterprise %}

🌟 Enterprise Feature 🌟 This feature is bundled with GraphQL-Enterprise.

{% endif %}

{{ page.title }}

{% table_of_contents %}
{{ content }}
graphql-ruby-2.5.19/guides/_plugins/000077500000000000000000000000001514115062600173265ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/_plugins/api_doc.rb000066400000000000000000000157041514115062600212600ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../../lib/graphql/version" require "kramdown" module GraphQLSite API_DOC_ROOT = "/api-doc/#{GraphQL::VERSION}/" module APIDoc def api_doc(input) if !input.start_with?("GraphQL") ruby_ident = "GraphQL::#{input}" else ruby_ident = input end doc_path = ruby_ident .gsub("::", "/") # namespaces .sub(/#(.+)$/, "#\\1-instance_method") # instance methods .sub(/\.(.+)$/, "#\\1-class_method") # class methods %|#{input}| end def link_to_img(img_path, img_title) full_img_path = "#{@context.registers[:site].baseurl}#{img_path}" <<-HTML #{img_title} HTML end end class APIDocRoot < Liquid::Tag def render(context) API_DOC_ROOT end end class CalloutBlock < Liquid::Block def initialize(tag_name, callout_class, tokens) super @callout_class = callout_class.strip end def render(context) raw_text = super site = context.registers[:site] converter = site.find_converter_instance(::Jekyll::Converters::Markdown) rendered_text = converter.convert(raw_text) heading = case @callout_class when "warning" "⚠ Heads up!" else raise ArgumentError, "Unhandled callout class: #{@callout_class.inspect}" end %|

#{heading}

#{rendered_text}
| end end class OpenAnIssue < Liquid::Tag def initialize(tag_name, issue_info, tokens) title, body = issue_info.split(",") # remove whitespace and quotes if value is present @title = strip_arg(title) @body = strip_arg(body) end def render(context) %|open an issue| end private def strip_arg(text) text && text.strip[1..-2] end end # Build a URL relative to `site.baseurl`, # asserting that the page exists. class InternalLink < Liquid::Tag GUIDES_ROOT = "guides/" def initialize(tag_name, guide_info, tokens) text, path = guide_info.split(",") # remove whitespace and quotes if value is present @text = strip_arg(text) @path = strip_arg(path) if @path && @path.start_with?("/") @path = @path[1..-1] end if !exist?(@path) raise "Internal link failed, couldn't find file for: #{path}" end end def render(context) <<-HTML.chomp #{@text} HTML end private def strip_arg(text) text && text.strip[1..-2] end POSSIBLE_EXTENSIONS = [".html", ".md"] def exist?(path) filepath = GUIDES_ROOT + path.split("#").first filepath = filepath.sub(".html", "") POSSIBLE_EXTENSIONS.any? { |ext| File.exist?(filepath + ext) } end end class TableOfContents < Liquid::Tag def render(context) headers = context["page"]["content"].scan(/^##+[^\n]+$/m) section_count = 0 current_table = header_table = [nil] prev_depth = nil headers.each do |h| header_hashes = h.match(/^#+/)[0] depth = header_hashes.size if depth == 2 section_count += 1 end text = h.gsub(/^#+ /, "") target = text.downcase .gsub(/[^a-z0-9_]+/, "-") .sub(/-$/, "") .sub(/^-/, "") rendered_text = Kramdown::Document.new(text, auto_ids: false) .to_html .sub("

", "") .sub("

", "") # remove wrapping added by kramdown if prev_depth if prev_depth > depth # outdent current_table = current_table[0] elsif prev_depth < depth # indent new_table = [current_table] current_table[-1][-1] = new_table current_table = new_table else # same depth end end current_table << [rendered_text, target, []] prev_depth = depth end table_html = "".dup render_table_into_html(table_html, header_table) html = <<~HTML

Contents

#{table_html}
HTML if section_count == 0 if headers.any? full_path = "guides/#{context["page"]["path"]}" warn("No sections identified for #{full_path} -- make sure it's using `## ...` for section headings.") end "" else html end end private def render_table_into_html(html_str, table) html_str << "
    " table.each_with_index do |entry, idx| if idx == 0 next # parent reference end rendered_text, target, child_table = *entry html_str << "
  1. " html_str << "#{rendered_text}" if child_table.any? render_table_into_html(html_str, child_table) end html_str << "
  2. " end html_str << "
" end end end Liquid::Template.register_filter(GraphQLSite::APIDoc) Liquid::Template.register_tag("api_doc_root", GraphQLSite::APIDocRoot) Liquid::Template.register_tag("open_an_issue", GraphQLSite::OpenAnIssue) Liquid::Template.register_tag("internal_link", GraphQLSite::InternalLink) Liquid::Template.register_tag("table_of_contents", GraphQLSite::TableOfContents) Liquid::Template.register_tag('callout', GraphQLSite::CalloutBlock) Jekyll::Hooks.register :site, :pre_render do |site| section_pages = Hash.new { |h, k| h[k] = [] } section_names = [] site.pages.each do |page| this_section = page.data["section"] if this_section this_section_pages = section_pages[this_section] this_section_pages << page this_section_pages.sort_by! { |page| page.data["index"] || 100 } page.data["section_pages"] = this_section_pages section_names << this_section end end section_names.compact! section_names.uniq! all_sections = [] section_names.each do |section_name| all_sections << { "name" => section_name, "overview_page" => section_pages[section_name].first, } end sorted_section_names = site.pages.find { |p| p.data["title"] == "Guides Index" }.data["sections"].map { |s| s["name"] } all_sections.sort_by! { |s| sorted_section_names.index(s["name"]) } site.data["all_sections"] = all_sections end module Jekyll module Algolia module Hooks def self.before_indexing_each(record, node, context) record = record.dup record.delete(:section_pages) record end end end end graphql-ruby-2.5.19/guides/_sass/000077500000000000000000000000001514115062600166165ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/_sass/reset.scss000066400000000000000000000021021514115062600206300ustar00rootroot00000000000000/* https://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 License: none (public domain) */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline; } /* HTML5 display-role reset for older browsers */ article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { display: block; } body { line-height: 1; } ol, ul { list-style: none; } blockquote, q { quotes: none; } blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; } table { border-collapse: collapse; border-spacing: 0; } graphql-ruby-2.5.19/guides/_tasks/000077500000000000000000000000001514115062600167725ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/_tasks/site.rb000066400000000000000000000137371514115062600202760ustar00rootroot00000000000000# frozen_string_literal: true require "yard" require "webrick" namespace :apidocs do desc "Fetch a gem version from RubyGems, build the docs" task :gen_version, [:version] do |t, args| # GITHUB_REF comes from GitHub Actions version = args[:version] || ENV["GITHUB_REF"] || raise("A version is required") puts "Building docs for #{version}" # GitHub Actions gives the full tag name if version.start_with?("refs/tags/") version = version[10..-1] end if version.start_with?("v") version = version[1..-1] end Dir.mktmpdir do puts "Fetching graphql-#{version}" system("gem fetch graphql --version=#{version}") system("gem unpack graphql-#{version}.gem") system("rm graphql-#{version}.gem") Dir.chdir("graphql-#{version}") do # Copy it into gh-pages for publishing # and locally for previewing push_dest = File.expand_path("../gh-pages/api-doc/#{version}") local_dest = File.expand_path("../guides/_site/api-doc/#{version}") puts "Creating directories: #{push_dest.inspect}, #{local_dest.inspect}" FileUtils.mkdir_p(push_dest) FileUtils.mkdir_p(local_dest) system("yardoc") puts "Copying from #{Dir.pwd}/doc to #{push_dest}" copy_entry "doc", push_dest puts "Copying from #{Dir.pwd}/doc to #{local_dest}" copy_entry "doc", local_dest end end puts "Successfully generated docs for #{version}" end end namespace :site do desc "View the documentation site locally" task serve: [] do # if you need api docs, add `:build_doc` to the list of dependencies require "jekyll" options = { "source" => File.expand_path("guides"), "destination" => File.expand_path("guides/_site"), "watch" => true, "serving" => true } # Generate the site in server mode. puts "Running Jekyll..." Jekyll::Commands::Build.process(options) Jekyll::Commands::Serve.process(options) end desc "Get the gh-pages branch locally, make sure it's up-to-date" task :fetch_latest do # Ensure the gh-pages dir exists so we can generate into it. puts "Checking for gh-pages dir..." unless File.exist?("./gh-pages") puts "Creating gh-pages dir..." sh "git clone git@github.com:rmosolgo/graphql-ruby gh-pages" end # Ensure latest gh-pages branch history. Dir.chdir("gh-pages") do sh "git checkout gh-pages" sh "git pull origin gh-pages" end end desc "Remove all generated HTML (making space to re-generate)" task :clean_html do # Proceed to purge all files in case we removed a file in this release. puts "Cleaning gh-pages directory..." purge_exclude = [ 'gh-pages/.', 'gh-pages/..', 'gh-pages/.git', 'gh-pages/.gitignore', 'gh-pages/api-doc', ] FileList["gh-pages/{*,.*}"].exclude(*purge_exclude).each do |path| sh "rm -rf #{path}" end end desc "Build guides/ into gh-pages/ with Jekyll" task :build_html do # Copy site to gh-pages dir. puts "Building site into gh-pages branch..." ENV['JEKYLL_ENV'] = 'production' require "jekyll" Jekyll::Commands::Build.process({ "source" => File.expand_path("guides"), "destination" => File.expand_path("gh-pages"), "sass" => { "style" => "compressed" } }) File.write('gh-pages/.nojekyll', "Prevent GitHub from running Jekyll") end desc "Commit new docs" task :commit_changes do puts "Committing and pushing to GitHub Pages..." sha = `git rev-parse HEAD`.strip Dir.chdir('gh-pages') do system "git status" system "git add ." system "git status" system "git commit --allow-empty -m 'Updating to #{sha}.'" end end desc "Push docs to gh-pages branch" task :push_commit do Dir.chdir('gh-pages') do sh "git push origin gh-pages" end end desc "Commit the local site to the gh-pages branch and publish to GitHub Pages" task publish: [:build_doc, :update_search_index, :fetch_latest, :clean_html, :build_html, :commit_changes, :push_commit] YARD::Rake::YardocTask.new(:prepare_yardoc) task build_doc: :prepare_yardoc do require_relative "../../lib/graphql/version" def to_rubydoc_url(path) "/api-doc/#{GraphQL::VERSION}/" + path .gsub("::", "/") # namespaces .sub(/#(.+)$/, "#\\1-instance_method") # instance methods .sub(/\.(.+)$/, "#\\1-class_method") # class methods end DOC_TEMPLATE = <<-PAGE --- layout: doc_stub search: true title: %{title} url: %{url} rubydoc_url: %{url} doc_stub: true --- %{documentation} PAGE puts "Preparing YARD docs @ v#{GraphQL::VERSION} for search index..." registry = YARD::Registry.load!(".yardoc") files_target = "guides/yardoc" FileUtils.rm_rf(files_target) FileUtils.mkdir_p(files_target) # Get docs for all classes and modules docs = registry.all(:class, :module) docs.each do |code_object| begin # Skip private classes and modules if code_object.visibility == :private next end rubydoc_url = to_rubydoc_url(code_object.path) page_content = DOC_TEMPLATE % { title: code_object.path, url: rubydoc_url, documentation: code_object.format.gsub(/-{2,}/, " ").gsub(/^\s+/, ""), } filename = code_object.path.gsub(/\W+/, "_") filepath = "guides/yardoc/#{filename}.md" File.write(filepath, page_content) rescue StandardError => err puts "Error on: #{code_object.path}" puts err puts err.backtrace end end puts "Wrote #{docs.size} YARD docs to #{files_target}." end desc "Update the Algolia search index used for graphql-ruby.org" task :update_search_index do if !ENV["ALGOLIA_API_KEY"] warn("Can't update search index without ALGOLIA_API_KEY; Search will be out-of-date.") else system("bundle exec jekyll algolia push --source=./guides") end end end graphql-ruby-2.5.19/guides/authorization/000077500000000000000000000000001514115062600204065ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/authorization/authorization.md000066400000000000000000000131541514115062600236340ustar00rootroot00000000000000--- layout: guide search: true section: Authorization title: Authorization desc: During execution, check if the current user has permission to access retrieved objects. index: 3 --- While a query is running, you can check each object to see whether the current user is authorized to interact with that object. If the user is _not_ authorized, you can handle the case with an error. ## Adding Authorization Checks Schema members have `authorized?` methods which will be called during execution: - Type classes have `.authorized?(object, context)` class methods - Fields have `#authorized?(object, args, context)` instance methods - Arguments have `#authorized?(object, arg_value, context)` instance methods - Mutations and Resolvers have `.authorized?(object, context)` class methods and `#authorized?(args)` instance methods - Enum values have `#authorized?(context)` instance methods These methods are called with: - `object`: the object from your application which was returned from a field - `args`/`arg_value`: The arguments for a field, or the value of an argument - `context`: the query context, based on the hash passed as `context:` #### Object Authorization When you implement this method to return `false`, the query will be halted, for example: ```ruby class Types::Friendship < Types::BaseObject # You can only see the details on a `Friendship` # if you're one of the people involved in it. def self.authorized?(object, context) super && (object.to_friend == context[:viewer] || object.from_friend == context[:viewer]) end end ``` (Always call `super` to get the default checks, too.) Now, whenever an object of type `Friendship` is going to be returned to the client, it will first go through the `.authorized?` method. If that method returns false, the field will get `nil` instead of the original object, and you may handle that case with an error (see below). #### Field Authorization Field `#authorized?` methods are called before resolving a field, for example: ```ruby class Types::BaseField < GraphQL::Schema::Field # Pass `field ..., require_admin: true` to reject non-admin users from a given field def initialize(*args, require_admin: false, **kwargs, &block) @require_admin = require_admin super(*args, **kwargs, &block) end def authorized?(obj, args, ctx) # if `require_admin:` was given, then require the current user to be an admin super && (@require_admin ? ctx[:viewer]&.admin? : true) end end ``` For this to work, the base field class must be {% internal_link "configured with other GraphQL types", "/type_definitions/extensions.html#customizing-fields" %}. #### Argument Authorization Argument `#authorized?` hooks are called before resolving the field that the argument belongs to. For example: ```ruby class Types::BaseArgument < GraphQL::Schema::Argument def initialize(*args, require_logged_in: false, **kwargs, &block) @require_logged_in = require_logged_in super(*args, **kwargs, &block) end def authorized?(obj, arg_value, ctx) super && if @require_logged_in ctx[:viewer].present? else true end end end ``` For this to work, the base argument class must be {% internal_link "configured with other GraphQL types", "/type_definitions/extensions.html#customizing-arguments" %}. ## Mutation Authorization See mutations/mutation_authorization.html#can-this-user-perform-this-action {% internal_link "Mutation Authorization", "/mutations/mutation_authorization.html#can-this-user-perform-this-action" %}) in the Mutation Guides. ## Enum Value Authorization {{ "GraphQL::Schema::EnumValue#authorized?" | api_doc }} is called when client input is received and when the schema returns values to the client. For authorizing input, if a value's `#authorized?` method returns false, then a {{ "GraphQL::UnauthorizedEnumValueError" | api_doc }} is raised. It passed to your schema's `.unauthorized_object` hook, where you can handle it another way if you want. For authorizing return values, if an outgoing value's `#authorized?` method returns false, then a {{ "GraphQL::Schema::Enum::UnresolvedValueError" | api_doc }} is raised, which crashes the query. In this case, you should modify your field or resolver to _not_ return this value to an unauthorized viewer. (In this case, the error isn't returned to the viewer because the viewer can't do anything about it -- it's a developer-facing issue instead.) ## Handling Unauthorized Objects By default, GraphQL-Ruby silently replaces unauthorized objects with `nil`, as if they didn't exist. You can customize this behavior by implementing {{ "Schema.unauthorized_object" | api_doc }} in your schema class, for example: ```ruby class MySchema < GraphQL::Schema # Override this hook to handle cases when `authorized?` returns false for an object: def self.unauthorized_object(error) # Add a top-level error to the response instead of returning nil: raise GraphQL::ExecutionError, "An object of type #{error.type.graphql_name} was hidden due to permissions" end end ``` Now, the custom hook will be called instead of the default one. If `.unauthorized_object` returns a non-`nil` object (and doesn't `raise` an error), then that object will be used in place of the unauthorized object. A similar hook is available for unauthorized fields: ```ruby class MySchema < GraphQL::Schema # Override this hook to handle cases when `authorized?` returns false for a field: def self.unauthorized_field(error) # Add a top-level error to the response instead of returning nil: raise GraphQL::ExecutionError, "The field #{error.field.graphql_name} on an object of type #{error.type.graphql_name} was hidden due to permissions" end end ``` graphql-ruby-2.5.19/guides/authorization/can_can_integration.md000066400000000000000000000307131514115062600247210ustar00rootroot00000000000000--- layout: guide search: true section: Authorization title: CanCan Integration desc: Hook up GraphQL to CanCan abilities index: 4 pro: true --- [GraphQL::Pro](https://graphql.pro) includes an integration for powering GraphQL authorization with [CanCan](https://github.com/CanCanCommunity/cancancan). __Why bother?__ You _could_ put your authorization code in your GraphQL types themselves, but writing a separate authorization layer gives you a few advantages: - Since the authorization code isn't embedded in GraphQL, you can use the same logic in non-GraphQL (or legacy) parts of the app. - The authorization logic can be tested in isolation, so your end-to-end GraphQL tests don't have to cover as many possibilities. ## Getting Started __NOTE__: Requires the latest gems, so make sure your `Gemfile` has: ```ruby # For CanCanIntegration: gem "graphql-pro", ">=1.7.11" # For list scoping: gem "graphql", ">=1.8.7" ``` Then, `bundle install`. Whenever you run queries, include `:current_user` in the context: ```ruby context = { current_user: current_user, # ... } MySchema.execute(..., context: context) ``` ### Rails Generator If your schema files follow the same convention as `rails generate graphql:install`, then you can install the CanCan integration with a Rails generator: ```bash $ rails generate graphql:cancan:install ``` This will insert all the necessary `include ...`s described below. Alternatively, check the docs below to mix in `CanCanIntegration`'s modules. ## Authorizing Objects For each object type, you can assign a required action for Ruby objects of that type. To get started, include the `ObjectIntegration` in your base object class: ```ruby # app/graphql/types/base_object.rb class Types::BaseObject < GraphQL::Schema::Object # Add the CanCan integration: include GraphQL::Pro::CanCanIntegration::ObjectIntegration # By default, require `can :read, ...` can_can_action(:read) # Or, to require no permissions by default: # can_can_action(nil) end ``` Now, anyone fetching an object will need `can :read, ...` for that object. CanCan configurations are inherited, and can be overridden in subclasses. For example, to allow _all_ viewers to see the `Query` root type: ```ruby class Types::Query < Types::BaseObject # Allow anyone to see the query root can_can_action nil end ``` ### Bypassing CanCan `can_can_action(nil)` will override any inherited configuration and skip CanCan checks for an object, field, argument or mutation. ### Handling Unauthorized Objects When any CanCan check returns `false`, the unauthorized object is passed to {{ "Schema.unauthorized_object" | api_doc }}, as described in {% internal_link "Handling unauthorized objects", "/authorization/authorization#handling-unauthorized-objects" %}. ## Scopes #### ActiveRecord::Relation The CanCan integration adds [CanCan's `.accessible_by`](https://github.com/cancancommunity/cancancan/wiki/Fetching-Records) to GraphQL-Ruby's {% internal_link "list scoping", "/authorization/scoping" %} To scope lists of interface or union type, include the integration in your base union class and base interface module _and_ set a base `can_can_action`, if desired: ```ruby class BaseUnion < GraphQL::Schema::Union include GraphQL::Pro::CanCanIntegration::UnionIntegration # To provide a default action for scoping lists: can_can_action :read end module BaseInterface include GraphQL::Schema::Interface include GraphQL::Pro::CanCanIntegration::InterfaceIntegration # To provide a default action for scoping lists: can_can_action :read end ``` #### Array For Arrays, the CanCan integration will use `.select { ... }` to filter items using the `can_can_action` from the lists's type. #### Bypassing scopes To allow an unscoped relation to be returned from a field, disable scoping with `scope: false`, for example: ```ruby # Allow anyone to browse the job postings field :job_postings, [Types::JobPosting], null: false, scope: false ``` ## Authorizing Fields You can also require certain checks on a field-by-field basis. First, include the integration in your base field class: ```ruby # app/graphql/types/base_field.rb class Types::BaseField < GraphQL::Schema::Field # Add the CanCan integration: include GraphQL::Pro::CanCanIntegration::FieldIntegration # By default, don't require a role at field-level: can_can_action nil end ``` If you haven't already done so, you should also hook up your base field class to your base object and base interface: ```ruby # app/graphql/types/base_object.rb class Types::BaseObject < GraphQL::Schema::Object field_class Types::BaseField end # app/graphql/types/base_interface.rb module Types::BaseInterface # ... field_class Types::BaseField end # app/graphql/mutations/base_mutation.rb class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation field_class Types::BaseField end ``` Then, you can add `can_can_action:` options to your fields: ```ruby class Types::JobPosting < Types::BaseObject # Only allow `can :review_applications, JobPosting` users # to see who has applied field :applicants, [Types::User], can_can_action: :review_applicants end ``` It will require the named action (`:review_applicants`) for the object being viewed (a `JobPosting`). ### Authorizing by attribute CanCan 3.0 added attribute-level authorization ([pull request](https://github.com/CanCanCommunity/cancancan/pull/474)). You can leverage this in your field definitions with the `can_can_attribute:` configuration: ```ruby # This will call `.can?(:read, user, :email_address)` field :email_address, String, can_can_action: :read, can_can_attribute: :email_address ``` You could also provide a _default value_ for `can_can_attribute` in your base field class: ```ruby class Types::BaseField def initialize(*args, **kwargs, &block) # pass all configs to the super class: super # Then set a new `can_can_attribute` value, if applicable if can_can_attribute.nil? && can_can_action.present? # `method_sym` is the thing GraphQL-Ruby will use to resolve this field can_can_attribute(method_sym) end end end ``` (See {{ "GraphQL::Schema::Field" | api_doc }} for the different values available for defaults.) ### Providing a Custom CanCan Subject Authorization checks are _skipped_ whenever the underlying `object` is `nil`. This can happen in root query fields, for example, when no `root_value: ...` is given. To provide a `can_can_subject` in this case, you can add it as a field configuration: ```ruby field :users, Types::User.connection_type, null: false, can_can_action: :manage, # `:all` will be used instead of `object` (which is `nil`) can_can_subject: :all ``` The configuration above will call `can?(:manage, :all)` whenever that field is requested. ## Authorizing Arguments Similar to field-level checks, you can require certain permissions to _use_ certain arguments. To do this, add the integration to your base argument class: ```ruby class Types::BaseArgument < GraphQL::Schema::Argument # Include the integration and default to no permissions required include GraphQL::Pro::CanCanIntegration::ArgumentIntegration can_can_action nil end ``` Then, make sure your base argument is hooked up to your base field and base input object: ```ruby class Types::BaseField < GraphQL::Schema::Field argument_class Types::BaseArgument # PS: see "Authorizing Fields" to make sure your base field is hooked up to objects, interfaces and mutations end class Types::BaseInputObject < GraphQL::Schema::InputObject argument_class Types::BaseArgument end class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation argument_class Types::BaseArgument end ``` Now, arguments accept a `can_can_action:` option, for example: ```ruby class Types::Company < Types::BaseObject field :employees, Types::Employee.connection_type do # Only admins can filter employees by email: argument :email, String, required: false, can_can_action: :admin end end ``` This will check for `can :admin, Company` (or a similar rule for the `company` being queried) for the current user. ## Authorizing Mutations There are a few ways to authorize GraphQL mutations with the CanCan integration: - Add a [mutation-level roles](#mutation-level-roles) - Run checks on [objects loaded by ID](#authorizing-loaded-objects) Also, you can configure [unauthorized object handling](#unauthorized-mutations) #### Setup Add `MutationIntegration` to your base mutation, for example: ```ruby class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation include GraphQL::Pro::CanCanIntegration::MutationIntegration # Also, to use argument-level authorization: argument_class Types::BaseArgument end ``` Also, you'll probably want a `BaseMutationPayload` where you can set a default role: ```ruby class Types::BaseMutationPayload < Types::BaseObject # If `BaseObject` requires some permissions, override that for mutation results. # Assume that anyone who can run a mutation can read their generated result types. can_can_action nil end ``` And hook it up to your base mutation: ```ruby class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation object_class Types::BaseMutationPayload field_class Types::BaseField end ``` #### Mutation-level roles Each mutation can have a class-level `can_can_action` which will be checked before loading objects or resolving, for example: ```ruby class Mutations::PromoteEmployee < Mutations::BaseMutation can_can_action :run_mutation end ``` In the example above, `can :run_mutation, Mutations::PromoteEmployee` will be checked before running the mutation. (The currently-running instance of `Mutations::PromoteEmployee` is passed to the ability checker.) #### Authorizing Loaded Objects Mutations can automatically load and authorize objects by ID using the `loads:` option. Beyond the normal [object reading permissions](#authorizing-objects), you can add an additional role for the specific mutation input using a `can_can_action:` option: ```ruby class Mutations::FireEmployee < Mutations::BaseMutation argument :employee_id, ID, loads: Types::Employee, can_can_action: :supervise, end ``` In the case above, the mutation will halt unless the `can :supervise, ...` check returns true. (The fetched instance of `Employee` is passed to the ability checker.) #### Unauthorized Mutations By default, an authorization failure in a mutation will raise a Ruby exception. You can customize this by implementing `#unauthorized_by_can_can(owner, value)` in your base mutation, for example: ```ruby class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation def unauthorized_by_can_can(owner, value) # No error, just return nil: nil end end ``` The method is called with: - `owner`: the `GraphQL::Schema::Argument` instance or mutation class whose role was not satisfied - `value`: the object which didn't pass for `context[:current_user]` Since it's a mutation method, you can also access `context` in that method. Whatever that method returns will be treated as an early return value for the mutation, so for example, you could return {% internal_link "errors as data", "/mutations/mutation_errors" %}: ```ruby class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation field :errors, [String] def unauthorized_by_can_can(owner, value) # Return errors as data: { errors: ["Missing required permission: #{owner.can_can_action}, can't access #{value.inspect}"] } end end ``` ## Authorizing Resolvers Resolvers are authorized just like [mutations](#authorizing-mutations), and require similar setup: ```ruby # app/graphql/resolvers/base_resolver.rb class Resolvers::BaseResolver < GraphQL::Schema::Resolver include GraphQL::Pro::CanCanIntegration::ResolverIntegration argument_class BaseArgument # can_can_action(nil) # to disable authorization by default end ``` Beyond that, see [Authorizing Mutations](#authorizing-mutations) above for further details. ## Custom Abilities Class By default, the integration will look for a top-level `::Ability` class. If you're using a different class, provide an instance ahead-of-time as `context[:can_can_ability]` For example, you could _always_ add one in your schema's `#execute` method: ```ruby class MySchema < GraphQL::Schema # Override `execute` to provide a custom Abilities instance for the CanCan integration def self.execute(*args, context: {}, **kwargs) # Assign `context[:can_can_ability]` to an instance of our custom class context[:can_can_ability] = MyAuthorization::CustomAbilitiesClass.new(context[:current_user]) super end end ``` graphql-ruby-2.5.19/guides/authorization/overview.md000066400000000000000000000136451514115062600226070ustar00rootroot00000000000000--- layout: guide search: true section: Authorization title: Overview desc: Overview of GraphQL authorization in general and an intro to the built-in framework. index: 0 --- Here's a conceptual approach to GraphQL authorization, followed by an introduction to the built-in authorization framework. Each part of the framework is described in detail in its own guide. ## Authorization: GraphQL vs REST In a REST API, the common authorization pattern is fairly simple. Before performing the requested action, the server asserts that the current client has the required permissions for that action. For example: ```ruby class PostsController < ApiController def create # First, check the client's permission level: if current_user.can?(:create_posts) # If the user is permitted, then perform the action: post = Post.create(params) render json: post else # Otherwise, return an error: render nothing: true, status: 403 end end end ``` However, this request-by-request mindset doesn't map well to GraphQL because there's only one controller and the requests that come to it may be _very_ different. To illustrate the problem: ```ruby class GraphqlController < ApplicationController def execute # What permission is required for `query_str`? # It depends on the string! So, you can't generalize at this level. if current_user.can?(:"???") MySchema.execute(query_str, context: ctx, variables: variables) end end end ``` So, what new mindset will work with a GraphQL API? For __mutations__, remember that each mutation is like an API request in itself. For example, `Posts#create` above would map to the `createPost(...)` mutation in GraphQL. So, each mutation should be authorized in its own right. For __queries__, you can think of each individual _object_ like a `GET` request to a REST API. So, each object should be authorized for reading in its own right. By applying this mindset, each part of the GraphQL query will be properly authorized before it is executed. Also, since the different units of code are each authorized on their own, you can be sure that each incoming query will be properly authorized, even if it's a brand new query that the server has never seen before. ## What About Authentication? As a reminder: - _Authentication_ is the process of determining what user is making the current request, for example, accepting a username and password, or finding a `User` in the database from `session[:current_user_id]`. - _Authorization_ is the process of verifying that the current user has permission to do something (or see something), for example, checking `admin?` status or looking up permission groups from the database. In general, authentication is _not_ addressed in GraphQL at all. Instead, your controller should get the current user based on the HTTP request (eg, an HTTP header or a cookie) and provide that information to the GraphQL query. For example: ```ruby class GraphqlController < ApplicationController def execute # Somehow get the current `User` from this HTTP request. current_user = get_logged_in_user(request) # Provide the current user in `context` for use during the query context = { current_user: current_user } MySchema.execute(query_str, context: context, ...) end end ``` After your HTTP handler has loaded the current user, you can access it via `context[:current_user]` in your GraphQL code. ## Authorization in Your Business Logic Before introducing GraphQL-specific authorization, consider the advantages of application-level authorization. (See the [GraphQL.org post](https://graphql.org/learn/authorization/) on the same topic.) For example, here's authorization mixed into the GraphQL API layer: ```ruby field :posts, [Types::Post], null: false def posts # Perform an auth check in the GraphQL field code: if context[:current_user].admin? Post.all else Post.published end end ``` The downside of this is that, when `Types::Post` is queried in other contexts, the same authorization check may not be applied. Additionally, since the authorization code is coupled with the GraphQL API, the only way to test it is via GraphQL queries, which adds some complexity to tests. Alternatively, you could move the authorization to your business logic, the `Post` class: ```ruby class Post < ActiveRecord::Base # Return the list of posts which `user` may see def self.posts_for(user) if user.admin? self.all else self.published end end end ``` Then, use this application method in your GraphQL code: ```ruby field :posts, [Types::Post], null: false def posts # Fetch the posts this user can see: Post.posts_for(context[:current_user]) end ``` In this case, `Post.posts_for(user)` could be tested _independently_ from GraphQL. Then, you have less to worry about in your GraphQL tests. As a bonus, you can use `Post.posts_for(user)` in _other_ parts of the app, too, such as the web UI or REST API. ## GraphQL-Ruby's Authorization Framework Despite the advantages of authorization at the application layer, as described above, there might be some reasons to authorize in the API layer: - Have an extra assurance that your API layer is secure - Authorize the API request _before_ running it (see "visibility" below) - Integrate with code that doesn't have authorization built-in To accomplish these, you can use GraphQL-Ruby's authorization framework. The framework has three levels, each of which is described in its own guide: - {% internal_link "Visibility", "/authorization/visibility" %} hides parts of the GraphQL schema from users who don't have full permission. - {% internal_link "Authorization", "/authorization/authorization" %} checks application objects during execution to be sure the user has permission to access them. Also, [GraphQL::Pro](https://graphql.pro) has integrations for {% internal_link "CanCan", "/authorization/can_can_integration" %} and {% internal_link "Pundit", "/authorization/pundit_integration" %}. graphql-ruby-2.5.19/guides/authorization/pundit_integration.md000066400000000000000000000376411514115062600246510ustar00rootroot00000000000000--- layout: guide search: true section: Authorization title: Pundit Integration desc: Hook up GraphQL to Pundit policies index: 4 pro: true --- [GraphQL::Pro](https://graphql.pro) includes an integration for powering GraphQL authorization with [Pundit](https://github.com/varvet/pundit) policies. __Why bother?__ You _could_ put your authorization code in your GraphQL types themselves, but writing a separate authorization layer gives you a few advantages: - Since the authorization code isn't embedded in GraphQL, you can use the same logic in non-GraphQL (or legacy) parts of the app. - The authorization logic can be tested in isolation, so your end-to-end GraphQL tests don't have to cover as many possibilities. ## Getting Started __NOTE__: Requires the latest gems, so make sure your `Gemfile` has: ```ruby # For PunditIntegration: gem "graphql-pro", ">=1.7.9" # For list scoping: gem "graphql", ">=1.8.7" ``` Then, `bundle install`. Whenever you run queries, include `:current_user` in the context: ```ruby context = { current_user: current_user, # ... } MySchema.execute(..., context: context) ``` ### Rails Generator If your schema files follow the same convention as `rails generate graphql:install`, then you can install the Pundit integration with a Rails generator: ```bash $ rails generate graphql:pundit:install ``` This will insert all the necessary `include ...`s described below. Alternatively, check the docs below to mix in `PunditIntegration`'s modules. ## Authorizing Objects You can specify Pundit roles that must be satisfied in order for viewers to see objects of a certain type. To get started, include the `ObjectIntegration` in your base object class: ```ruby # app/graphql/types/base_object.rb class Types::BaseObject < GraphQL::Schema::Object # Add the Pundit integration: include GraphQL::Pro::PunditIntegration::ObjectIntegration # By default, require staff: pundit_role :staff # Or, to require no permissions by default: # pundit_role nil end ``` Now, anyone trying to read a GraphQL object will have to pass the `#staff?` check on that object's policy. Then, each child class can override that parent configuration. For example, allow _all_ viewers to read the `Query` root: ```ruby class Types::Query < Types::BaseObject # Allow anyone to see the query root pundit_role nil end ``` #### Policies and Methods For each object returned by GraphQL, the integration matches it to a policy and method. The policy is found using [`Pundit.policy!`](https://www.rubydoc.info/gems/pundit/Pundit%2Epolicy!), which looks up a policy using the object's class name. (This can be customized, see below.) Then, GraphQL will call a method on the policy to see whether the object is permitted or not. This method is assigned in the object class, for example: ```ruby class Types::Employee < Types::BaseObject # Only show employee objects to their bosses, # or when that employee is the current viewer pundit_role :employer_or_self # ... end ``` That configuration will call `#employer_or_self?` on the corresponding Pundit policy. #### Custom Policy Class By default, the integration uses `Pundit.policy!(current_user, object)` to find a policy. You can specify a policy class using `pundit_policy_class(...)`: ```ruby class Types::Employee < Types::BaseObject pundit_policy_class(Policies::CustomEmployeePolicy) # Or, you could use a string: # pundit_policy_class("Policies::CustomEmployeePolicy") end ``` For really custom policy lookup, see [Custom Policy Lookup](#custom-policy-lookup) below. #### Bypassing Policies The integration requires that every object with a `pundit_role` has a corresponding policy class. To allow objects to _skip_ authorization, you can pass `nil` as the role: ```ruby class Types::PublicProfile < Types::BaseObject # Anyone can see this pundit_role nil end ``` #### Handling Unauthorized Objects When any Policy method returns `false`, the unauthorized object is passed to {{ "Schema.unauthorized_object" | api_doc }}, as described in {% internal_link "Handling unauthorized objects", "/authorization/authorization#handling-unauthorized-objects" %}. ## Scopes The Pundit integration adds [Pundit scopes](https://github.com/varvet/pundit#scopes) to GraphQL-Ruby's {% internal_link "list scoping", "/authorization/scoping" %} feature. Any list or connection will be scoped. If a scope is missing, the query will crash rather than risk leaking unfiltered data. To scope lists of interface or union type, include the integration in your base union class and base interface module: ```ruby class BaseUnion < GraphQL::Schema::Union include GraphQL::Pro::PunditIntegration::UnionIntegration end module BaseInterface include GraphQL::Schema::Interface include GraphQL::Pro::PunditIntegration::InterfaceIntegration end ``` #### Bypassing scopes To allow an unscoped relation to be returned from a field, disable scoping with `scope: false`, for example: ```ruby # Allow anyone to browse the job postings field :job_postings, [Types::JobPosting], null: false, scope: false ``` ## Authorizing Fields You can also require certain checks on a field-by-field basis. First, include the integration in your base field class: ```ruby # app/graphql/types/base_field.rb class Types::BaseField < GraphQL::Schema::Field # Add the Pundit integration: include GraphQL::Pro::PunditIntegration::FieldIntegration # By default, don't require a role at field-level: pundit_role nil end ``` If you haven't already done so, you should also hook up your base field class to your base object and base interface: ```ruby # app/graphql/types/base_object.rb class Types::BaseObject < GraphQL::Schema::Object field_class Types::BaseField end # app/graphql/types/base_interface.rb module Types::BaseInterface # ... field_class Types::BaseField end # app/graphql/mutations/base_mutation.rb class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation field_class Types::BaseField end ``` Then, you can add `pundit_role:` options to your fields: ```ruby class Types::JobPosting < Types::BaseObject # Allow signed-in users to browse listings pundit_role :signed_in # But, only allow `JobPostingPolicy#staff?` users to see # who has applied field :applicants, [Types::User], pundit_role: :staff end ``` It will call the named role (eg, `#staff?`) on the parent object's policy (eg `JobPostingPolicy`). #### Custom Policy Class You can override the policy class for a field using `pundit_policy_class:`, for example: ```ruby class Types::JobPosting < Types::BaseObject # Only allow `ApplicantsPolicy#staff?` users to see # who has applied field :applicants, [Types::User], pundit_role: :staff, pundit_policy_class: ApplicantsPolicy # Or with a string: # pundit_policy_class: "ApplicantsPolicy" end ``` This will initialize an `ApplicantsPolicy` with the parent object (a `Job`) and call `#staff?` on it. For really custom policy lookup, see [Custom Policy Lookup](#custom-policy-lookup) below. ## Authorizing Arguments Similar to field-level checks, you can require certain permissions to _use_ certain arguments. To do this, add the integration to your base argument class: ```ruby class Types::BaseArgument < GraphQL::Schema::Argument # Include the integration and default to no permissions required include GraphQL::Pro::PunditIntegration::ArgumentIntegration pundit_role nil end ``` Then, make sure your base argument is hooked up to your base field and base input object: ```ruby class Types::BaseField < GraphQL::Schema::Field argument_class Types::BaseArgument # PS: see "Authorizing Fields" to make sure your base field is hooked up to objects, interfaces and mutations end class Types::BaseInputObject < GraphQL::Schema::InputObject argument_class Types::BaseArgument end class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation argument_class Types::BaseArgument end ``` Now, arguments accept a `pundit_role:` option, for example: ```ruby class Types::Company < Types::BaseObject field :employees, Types::Employee.connection_type do # Only admins can filter employees by email: argument :email, String, required: false, pundit_role: :admin end end ``` The role will be called on the parent object's policy, for example `CompanyPolicy#admin?` in the case above. ## Authorizing Mutations There are a few ways to authorize GraphQL mutations with the Pundit integration: - Add a [mutation-level roles](#mutation-level-roles) - Run checks on [objects loaded by ID](#authorizing-loaded-objects) Also, you can configure [unauthorized object handling](#unauthorized-mutations) #### Setup Add `MutationIntegration` to your base mutation, for example: ```ruby class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation include GraphQL::Pro::PunditIntegration::MutationIntegration # Also, to use argument-level authorization: argument_class Types::BaseArgument end ``` Also, you'll probably want a `BaseMutationPayload` where you can set a default role: ```ruby class Types::BaseMutationPayload < Types::BaseObject # If `BaseObject` requires some permissions, override that for mutation results. # Assume that anyone who can run a mutation can read their generated result types. pundit_role nil end ``` And hook it up to your base mutation: ```ruby class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation object_class Types::BaseMutationPayload field_class Types::BaseField end ``` #### Mutation-level roles Each mutation can have a class-level `pundit_role` which will be checked before loading objects or resolving, for example: ```ruby class Mutations::PromoteEmployee < Mutations::BaseMutation pundit_role :admin end ``` In the example above, `PromoteEmployeePolicy#admin?` will be checked before running the mutation. #### Custom Policy Class By default, Pundit uses the mutation's class name to look up a policy. You can override this by defining `pundit_policy_class` on your mutation: ```ruby class Mutations::PromoteEmployee < Mutations::BaseMutation pundit_policy_class ::UserPolicy pundit_role :admin end ``` Now, the mutation will check `UserPolicy#admin?` before running. For really custom policy lookup, see [Custom Policy Lookup](#custom-policy-lookup) below. #### Authorizing Loaded Objects Mutations can automatically load and authorize objects by ID using the `loads:` option. Beyond the normal [object reading permissions](#authorizing-objects), you can add an additional role for the specific mutation input using a `pundit_role:` option: ```ruby class Mutations::FireEmployee < Mutations::BaseMutation argument :employee_id, ID, loads: Types::Employee, pundit_role: :supervisor, end ``` In the case above, the mutation will halt unless the `EmployeePolicy#supervisor?` method returns true. #### Unauthorized Mutations By default, an authorization failure in a mutation will raise a Ruby exception. You can customize this by implementing `#unauthorized_by_pundit(owner, value)` in your base mutation, for example: ```ruby class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation def unauthorized_by_pundit(owner, value) # No error, just return nil: nil end end ``` The method is called with: - `owner`: the `GraphQL::Schema::Argument` or mutation class whose role was not satisfied - `value`: the object which didn't pass for `context[:current_user]` Since it's a mutation method, you can also access `context` in that method. Whatever that method returns will be treated as an early return value for the mutation, so for example, you could return {% internal_link "errors as data", "/mutations/mutation_errors" %}: ```ruby class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation field :errors, [String] def unauthorized_by_pundit(owner, value) # Return errors as data: { errors: ["Missing required permission: #{owner.pundit_role}, can't access #{value.inspect}"] } end end ``` ## Authorizing Resolvers Resolvers are authorized just like [mutations](#authorizing-mutations), and require similar setup: ```ruby # app/graphql/resolvers/base_resolver.rb class Resolvers::BaseResolver < GraphQL::Schema::Resolver include GraphQL::Pro::PunditIntegration::ResolverIntegration argument_class BaseArgument # pundit_role nil # to disable authorization by default end ``` Beyond that, see [Authorizing Mutations](#authorizing-mutations) above for further details. ## Custom Policy Lookup By default, the integration uses `Pundit`'s top-level methods to interact with policies: - `Pundit.policy!(context[:current_user], object)` is called to find a policy instance - `Pundit.policy_scope!(context[:current_user], items)` is called to filter `items` ### Custom Policy Methods You can implement a custom lookup by defining the following methods in your schema: - `pundit_policy_class_for(object, context)` to return a policy class (or raise an error if one isn't found) - `pundit_role_for(object, context)` to return a role method (Symbol), or `nil` to bypass authorization - `scope_by_pundit_policy(context, items)` to apply a scope to `items` (or raise an error if one isn't found) Since different objects have different lifecycles, the hooks are installed slightly different ways: - Your base argument, field, and mutation classes should have _instance methods_ with those names - Your base type classes should have _class methods_ with that name Here's an example of how the custom hooks can be installed: ```ruby module CustomPolicyLookup # Lookup policies in the `SystemAdmin::` namespace for system_admin users # @return [Class] def pundit_policy_class_for(object, context) current_user = context[:current_user] if current_user.system_admin? SystemAdmin.const_get("#{object.class.name}Policy") else super end end # Require admin permissions if the object is pending_approval def pundit_role_for(object, context) if object.pending_approval? :admin else super # fall back to the normally-configured role end end end # Add policy hooks as class methods class Types::BaseObject < GraphQL::Schema::Object extend CustomPolicyLookup end class Types::BaseUnion < GraphQL::Schema::Union extend CustomPolicyLookup end module Types::BaseInterface include GraphQL::Schema::Interface # Add this as a class method that will be "inherited" by other interfaces: definition_methods do include CustomPolicyLookup end end # Add policy hooks as instance methods class Types::BaseField < GraphQL::Schema::Field include CustomPolicyLookup end class Types::BaseArgument < GraphQL::Schema::Argument include CustomPolicyLookup end class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation include CustomPolicyLookup end ``` ### One Policy Per Class Another good approach is to have one policy per class. You can implement `policy_class_for(object, context)` to look up a policy _within_ the class, for example: ```ruby class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation def policy_class_for(_object, _context) # Look up a nested `Policy` constant: self.class.const_get(:Policy) end end ``` Then, each mutation can define its policy inline, for example: ```ruby class Mutations::PromoteEmployee < Mutations::BaseMutation # This will be found by `BaseMutation.policy_class`, defined above: class Policy # ... end pundit_role :admin end ``` Now, `Mutations::PromoteEmployee::Policy#admin?` will be checked before running the mutation. ## Custom User Lookup By default, the Pundit integration looks for the current user in `context[:current_user]`. You can override this by implementing `#pundit_user` on your custom query context class. For example: ```ruby # app/graphql/query_context.rb class QueryContext < GraphQL::Query::Context def pundit_user # Lookup `context[:viewer]` instead: self[:viewer] end end ``` Then be sure to hook up your custom class in the schema: ```ruby class MySchema < GraphQL::Schema context_class(QueryContext) end ``` Then, the Pundit integration will use your `def pundit_user` to get the current user at runtime. graphql-ruby-2.5.19/guides/authorization/scoping.md000066400000000000000000000043401514115062600223730ustar00rootroot00000000000000--- layout: guide search: true section: Authorization title: Scoping desc: Filter lists to match the current viewer and context index: 4 --- _Scoping_ is a complementary consideration to authorization. Rather than checking "can this user see this thing?", scoping takes a list of items filters it to the subset which is appropriate for the current viewer and context. For similar features, see [Pundit scopes](https://github.com/varvet/pundit#scopes) and [Cancan's `.accessible_by`](https://github.com/cancancommunity/cancancan/wiki/Fetching-Records). ## `scope:` option Fields accept a `scope:` option to enable (or disable) scoping, for example: ```ruby field :products, [Types::Product], scope: true # Or field :all_products, [Types::Product], scope: false ``` For __list__ and __connection__ fields, `scope: true` is the default. For all other fields, `scope: false` is the default. You can override this by using the `scope:` option. ## `.scope_items(items, ctx)` method Type classes may implement `.scope_items(items, ctx)`. This method is called when a field has `scope: true`. For example, ```ruby field :products, [Types::Product] # has `scope: true` by default ``` Will call: ```ruby class Types::Product < Types::BaseObject def self.scope_items(items, context) # filter items here end end ``` The method should return a new list with only the appropriate items for the current `context`. ## Bypassing object-level authorization If you know that any items returned from `.scope_items` should be visible to the current client, you can skip the normal `.authorized?(obj, ctx)` checks by configuring `reauthorize_scoped_objects(false)` in your type definition. For example: ```ruby class Types::Product < Types::BaseObject # Check that singly-loaded objects are visible to the current viewer def self.authorized?(object, context) super && object.visible_to?(context[:viewer]) end # Filter any list to only include objects that are visible to the current viewer def self.scope_items(items, context) items = super(items, context) items.visible_for(context[:viewer]) end # If an object of this type was returned from `.scope_items`, # don't call `.authorized?` with it. reauthorize_scoped_objects(false) end ``` graphql-ruby-2.5.19/guides/authorization/visibility.md000066400000000000000000000204301514115062600231160ustar00rootroot00000000000000--- layout: guide search: true section: Authorization title: Visibility desc: Programmatically hide parts of the GraphQL schema from some users. index: 1 redirect_from: - /schema/limiting_visibility --- With GraphQL-Ruby, it's possible to _hide_ parts of your schema from some users. This isn't exactly part of the GraphQL spec, but it's roughly within the bounds of the spec. Here are some reasons you might want to hide parts of your schema: - You don't want non-admin users to know about administration functions of the schema. - You're developing a new feature and want to make a gradual release to only a few users first. ## Hiding Parts of the Schema To start limiting visibility of your schema, add the plugin: ```ruby class MySchema < GraphQL::Schema # ... use GraphQL::Schema::Visibility # see below for options end ``` Then, you can customize the visibility of parts of your schema by reimplementing various `visible?` methods: - Type classes have a `.visible?(context)` class method - Fields and arguments have a `#visible?(context)` instance method - Enum values have `#visible?(context)` instance method - Mutation classes have a `.visible?(context)` class method These methods are called with the query context, based on the hash you pass as `context:`. If the method returns false, then that member of the schema will be treated as though it doesn't exist for the entirety of the query. That is: - In introspection, the member will _not_ be included in the result - In normal queries, if a query references that member, it will return a validation error, since that member doesn't exist ## Visibility Profiles You can use named profiles to cache your schema's visibility modes. For example: ```ruby use GraphQL::Schema::Visibility, profiles: { # mode_name => example_context_hash public: { public: true }, beta: { public: true, beta: true }, internal_admin: { internal_admin: true } } ``` Then, you can run queries with `context[:visibility_profile]` equal to one of the pre-defined profiles. When you do, GraphQL-Ruby will create a cached set of types for named profile. `.visible?` will only be called with the context hash passed to `profiles: ...`. The profile contexts passed to `profiles` will have `visibility_profile: ...` added to them, then they're frozen by GraphQL-Ruby. ### Preloading profiles By default, GraphQL-Ruby will preload all named visibility profiles when `Rails.env.production?` is present and true. You can manually set this option by passing `use ... preload: true` (or `false`). Enable preloading in production to reduce latency of the first request to each visibility profile. Disable preloading in development to speed up application boot. ### Dynamic profiles When you provide named visibility profiles, `context[:visibility_profile]` is required for query execution. You can also permit dynamic visibility for queries which _don't_ have that key set by passing `use ..., dynamic: true`. You could use this to support backwards compatibility or when visibility calculations are too complex to predefine. When no named profiles are defined, all queries use dynamic visibility. ## Object Visibility Let's say you're working on a new feature which should remain secret for a while. You can implement `.visible?` in a type: ```ruby class Types::SecretFeature < Types::BaseObject def self.visible?(context) # only show it to users with the secret_feature enabled super && context[:viewer].feature_enabled?(:secret_feature) end end ``` (Always call `super` to inherit the default behavior.) Now, the following bits of GraphQL will return validation errors: - Fields that return `SecretFeature`, eg `query { findSecretFeature { ... } }` - Fragments on `SecretFeature`, eg `Fragment SF on SecretFeature` And in introspection: - `__schema { types { ... } }` will not include `SecretFeature` - `__type(name: "SecretFeature")` will return `nil` - Any interfaces or unions which normally include `SecretFeature` will _not_ include it - Any fields that return `SecretFeature` will be excluded from introspection ## Field Visibility ```ruby class Types::BaseField < GraphQL::Schema::Field # Pass `field ..., require_admin: true` to hide this field from non-admin users def initialize(*args, require_admin: false, **kwargs, &block) @require_admin = require_admin super(*args, **kwargs, &block) end def visible?(ctx) # if `require_admin:` was given, then require the current user to be an admin super && (@require_admin ? ctx[:viewer]&.admin? : true) end end ``` For this to work, the base field class must be {% internal_link "configured with other GraphQL types", "/type_definitions/extensions.html#customizing-fields" %}. ## Argument Visibility ```ruby class Types::BaseArgument < GraphQL::Schema::Argument # If `require_logged_in: true` is given, then this argument will be hidden from logged-out viewers def initialize(*args, require_logged_in: false, **kwargs, &block) @require_logged_in = require_logged_in super(*args, **kwargs, &block) end def visible?(ctx) super && (@require_logged_in ? ctx[:viewer].present? : true) end end ``` For this to work, the base argument class must be {% internal_link "configured with other GraphQL types", "/type_definitions/extensions.html#customizing-arguments" %}. ## Opting Out By default, GraphQL-Ruby always runs visibility checks. You can opt out of this by adding to your schema class: ```ruby class MySchema < GraphQL::Schema # ... # Opt out of GraphQL-Ruby's visibility feature: use GraphQL::Schema::AlwaysVisible end ``` For big schemas, this can be a worthwhile speed-up. ## Migration Notes {{ "GraphQL::Schema::Visibility" | api_doc }} is a _new_ implementation of visibility in GraphQL-Ruby. It has some slight differences from the previous implementation ({{ "GraphQL::Schema::Warden" | api_doc }}): - `Visibility` speeds up Rails app boot because it doesn't require all types to be loaded during boot and only loads types as they are used by queries. - `Visibility` supports predefined, reusable visibility profiles which speeds up queries using complicated `visible?` checks. - `Visibility` hides types differently in a few edge cases: - Previously, `Warden` hid interface and union types which had no possible types. `Visibility` doesn't check possible types (in order to support performance improvements), so those types must return `false` for `visible?` in the same cases where all possible types were hidden. Otherwise, that interface or union type will be visible but have no possible types. - When an object type is connected to the schema as a field return type or a union member, and also implements and interface, if the object type's _other_ connection(s) to the schema are hidden, then it won't appear as an implementer of that interface unless it's registered with `orphan_types` (either by the schema or interface). `Warden` used a "global" map of types so it could discover object types in this case, but `Visibility` doesn't have that global map. (Since time of writing, `Visibility` _does_ have some global type tracking, so maybe this could be fixed.) - When `Visibility` is used, several (Ruby-level) Schema introspection methods don't work because the caches they draw on haven't been calculated (`Schema.references_to`, `Schema.union_memberships`). If you're using these, please get in touch so that we can find a way forward. ### Migration Mode You can use `use GraphQL::Schema::Visibility, ... migration_errors: true` to enable migration mode. In this mode, GraphQL-Ruby will make visibility checks with _both_ `Visibility` and `Warden` and compare the result, raising a descriptive error when the two systems return different results. As you migrate to `Visibility`, enable this mode in test to find any unexpected discrepancies. Sometimes, there's a discrepancy that is hard to resolve but doesn't make any _real_ difference in application behavior. To address these cases, you can use these flags in `context`: - `context[:visibility_migration_running] = true` is set in the main query context. - `context[:visibility_migration_warden_running] = true` is set in the _duplicate_ context which is passed to a `Warden` instance. - If you set `context[:skip_migration_error] = true`, then no migration error will be raised for that query. You can use these flags to conditionally handle edge cases that should be ignored in testing. graphql-ruby-2.5.19/guides/changesets/000077500000000000000000000000001514115062600176325ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/changesets/definition.md000066400000000000000000000300521514115062600223040ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true enterprise: true section: GraphQL Enterprise - Changesets title: Defining Changesets desc: Creating a set of modifications to release in an API version index: 2 --- After {% internal_link "installing Changeset integrations", "/changesets/installation" %} in your schema, you can create Changesets which modify parts of the schema. Changesets extend `GraphQL::Enterprise::Changeset` and include a `release` string. Once a Changeset class is defined, it can be referenced with `added_in: ...` or `removed_in: ...` configurations in the schema. __Note:__ Before GraphQL-Enterprise 1.3.0, Changesets were configured with `modifies ...` blocks. These blocks are still supported and you can find the documentation for that API [on GitHub](https://github.com/rmosolgo/graphql-ruby/blob/v2.0.22/guides/changesets/definition.md). ## Changeset Classes This Changeset will be available to any client whose `context[:changeset_version]` is on or after `2020-12-01`: ```ruby # app/graphql/changesets/deprecate_recipe_flag.rb class Changesets::DeprecateRecipeTags < GraphQL::Enterprise::Changeset release "2020-12-01" end ``` Additionally, Changesets must be {% internal_link "released", "/changesets/releases" %} for their changes to be published. ## Publishing with `added_in:` New things can be published in a changeset by adding `added_in: SomeChangeset` to their configuration. For example, to add a new argument to a field: ```ruby field :search_recipes, [Types::Recipe] do argument :query, String argument :tags, [Types::RecipeTag], required: false, added_in: Changesets::AddRecipeTags end ``` You can also provide a _replacement_ implementation by using `added_in:`. When a new definition has the same name as an existing definition, it implicitly replaces the previous definition in new versions of the API. For example: ```ruby field :rating, Integer, "A 1-5 score for this recipe" # This definition will be superseded by the following one field :rating, Float, "A 1.0-5.0 score for this recipe", added_in: Changesets::FloatingPointRatings ``` Here, a new implementation for `rating` will be used when clients requests an API version that includes `Changesets::FloatingPointRatings`. (If the client requests a version _before_ that changeset, then the preceding implementation would be used instead.) ## Removing with `removed_in:` A `removed_in:` configuration removes something in the named changeset. For example, these enum values are replaced with more clearly-named ones: ```ruby class Types::RecipeTag < Types::BaseEnum # These are replaced by *_HEAT below: value :SPICY, removed_in: Changesets::ClarifyHeatTags value :MEDIUM, removed_in: Changesets::ClarifyHeatTags value :MILD, removed_in: Changesets::ClarifyHeatTags # These new tags are more clear: value :SPICY_HEAT, added_in: Changesets::ClarifyHeatTags value :MEDIUM_HEAT, added_in: Changesets::ClarifyHeatTags value :MILD_HEAT, added_in: Changesets::ClarifyHeatTags end ``` If something has been defined several times, a `removed_in:` configuration removes _all_ definitions: ```ruby class Mutations::SubmitRecipeRating < Mutations::BaseMutation # This is replaced in future API versions by the following argument argument :rating, Integer # This replaces the previous, but in another future version, # it is removed completely (and so is the previous one) argument :rating, Float, added_in: Changesets::FloatingPointRatings, removed_in: Changesets::RemoveRatingsCompletely end ``` ## Examples See below for the different kind of modifications you can make in a changeset: - [Fields](#fields): adding, modifying, and removing fields - [Arguments](#arguments): adding, modifying, and removing arguments - [Enum values](#enum-values): adding, modifying, and removing arguments - [Unions](#unions): adding or removing object types from a union - [Interfaces](#interfaces): adding or removing interface implementations from object types - [Types](#types): changing one type definition for another - [Runtime](#runtime): choosing a behavior at runtime based on the current request and changeset ### Fields To add or redefine a field, use `field(..., added_in: ...)`, including all configuration values for the new implementation (see {{ "GraphQL::Schema::Field#initialize" | api_doc }}). The definition given here will override the previous definition (if there was one) whenever this Changeset applies. ```ruby class Types::Recipe < Types::BaseObject # This new field is available when `context[:changeset_version]` # is on or after the release date of `AddRecipeTags` field :tags, [Types::RecipeTag], added_in: Changeset::AddRecipeTags end ``` To remove a field, add a `removed_in: ...` configuration to the last definition of the field: ```ruby class Types::Recipe < Types::BaseObject # Even after migrating to floating point values, # the "rating" feature never took off, # so we removed it entirely eventually. field :rating, Integer field :rating, Float, added_in: Changeset::FloatingPointRatings, removed_in: Changeset::RemoveRatings end ``` When a field is removed, queries that request that field will be invalid, unless the client has requested a previous API version where the field is still available. ### Arguments You can add, redefine, or remove arguments that belong to fields, input objects, or resolvers. Use `added_in: ...` to provide a new (or updated) definition for an argument, for example: ```ruby class Types::RecipesFilter < Types::BaseInputObject argument :rating, Integer # This new definition is available when # the client's `context[:changeset_version]` includes `FloatingPointRatings` argument :rating, Float, added_in: Changesets::FloatingPointRatings end ``` To remove an argument entirely, add a `removed_in: ...` configuration to the last definition. It will remove _all_ implementations for that argument. For example: ```ruby class Mutations::SubmitRating < Mutations::BaseMutation # Remove this because it's irrelevant: argument :phone_number, String, removed_in: Changesets::StopCollectingPersonalInformation end ``` When arguments are removed, the schema will reject any queries which use them unless the client has requested a previous API version where the argument is still allowed. ### Enum Values With Changesets, you can add, redefine, or remove enum values. To add a new value (or provide a new implementation for a value), include `added_in:` in the `value(...)` configuration: ```ruby class Types::RecipeTag < Types::BaseEnum # This enum will accept and return `KETO` only when the client's API version # includes `AddKetoDietSupport`'s release date. value :KETO, added_in: Changesets::AddKetoDietSupport end ``` Values can be removed with `removed_in:`, for example: ```ruby class Types::RecipeTag < Types::BaseEnum # Old API versions will serve this value; # new versions won't accept it or return it. value :GRAPEFRUIT_DIET, removed_in: Changesets::RemoveLegacyDiets end ``` When enum values are removed, they won't be accepted as input and they won't be allowed as return values from fields unless the client has requested a previous API version where those values are still allowed. ### Unions You can add to or remove from a union's possible types. To release a new union member, include `added_in:` in the `possible_types` configuration: ```ruby class Types::Cookable < Types::BaseUnion possible_types Types::Recipe, Types::Ingredient # Add this to the union when clients opt in to our new feature: possible_types Types::Cuisine, added_in: Changeset::ReleaseCuisines ``` To remove a member from a union, move it to a `possible_types` call with `removed_in: ...`: ```ruby # Stop including this in the union in new API versions: possible_types Types::Chef, removed_in: Changeset::LessChefHype ``` When a possible type is removed, it will not be associated with the union type in introspection queries or schema dumps. ### Interfaces You can add to or remove from an object type's interface definitions. To add one or more interface implementations, use `implements(..., added_in:)`. This will add the interface and its fields to the object whenever this Changeset is active, for example: ```ruby class Types::Recipe < Types::BaseObject # Add this new implementation in new API versions only: implements Types::RssSubject, added_in: Changesets::AddRssSupport end ``` To remove one or more more interface implementations, add `removed_in:` to the `implements ...` configuration, for example: ```ruby implements Types::RssSubject, added_in: Changesets::AddRssSupport, # Sadly, nobody seems to want to use this, # so we removed it all: removed_in: Changesets::RemoveRssSupport ``` When an interface implementation is removed, then the interface will not be associated with the object in introspection queries or schema dumps. Also, any fields inherited from the interface will be hidden from clients. (If the object defines the field itself, it will still be visible.) ### Types Using Changesets, it's possible to define a new type using the same name as an old type. (Only one type per name is allowed for each query, but different queries can use different types for the same name.) First, to define two types with the same name, make two different type definitions. One of them will have to use `graphql_name(...)` to specify the conflicting type name. For example, to migrate an enum type to an object type, define two types: ```ruby # app/graphql/types/legacy_recipe_flag.rb # In the old version of the schema, "recipe tags" were limited to defined set of values. # This enum was renamed from `Types::RecipeTag`, then `graphql_name("RecipeTag")` # was added for GraphQL. class Types::LegacyRecipeTag < Types::BaseEnum graphql_name "RecipeTag" # ... end ``` ```ruby # app/graphql/types/recipe_flag.rb # But in the new schema, each tag is a full-fledged object with fields of its own class Types::RecipeTag < Types::BaseObject field :name, String, null: false field :is_vegetarian, Boolean, null: false # ... end ``` Then, add or update fields or arguments to use the _new_ type instead of the old one. For example: ```diff class Types::Recipe < Types::BaseObject # Change this definition to point at the newly-renamed _legacy_ type # (It's the same type definition, but the Ruby class has a new name) - field :tags, [Types::RecipeTag] + field :tags, [Types::LegacyRecipeTag] # And add a new field for the new type: + field :tags, [Types::RecipeTag], added_in: Changesets::MigrateRecipeTagToObject end ``` With that Changeset, `Recipe.tags` will return an object type instead of an enum type. Clients requesting older versions will still receive enum values from that field. The resolver will probably need an update, too, for example: ```ruby class Types::Recipe < Types::BaseObject # Here's the original definition which returns enum values: field :tags, [Types::LegacyRecipeTag], null: false # Here's the new definition which replaces the previous one on new API versions: field :tags, [Types::RecipeTag], null: false, added_in: Changesets::MigrateRecipeTagToObject def flags all_flag_objects = object.flag_objects if Changesets::MigrateRecipeTagToObject.active?(context) # Here's the new behavior, returning full objects: all_flag_objects else # Convert this to enum values, for legacy behavior: all_flag_objects.map { |f| f.name.upcase } end end end ``` That way, legacy clients will continue to receive enum values while new clients will receive objects. ## Runtime While a query is running, you can check if a changeset applies by using its `.active?(context)` method. For example: ```ruby class Types::Recipe field :flag, Types::RecipeFlag, null: true def flag # Check if this changeset applies to the current request: if Changesets::DeprecateRecipeFlag.active?(context) Stats.count(:deprecated_recipe_flag, context[:viewer]) end # ... end end ``` Besides observability, you can use a runtime check when a resolver needs to pick a different behavior depending on the API version. After defining a changeset, add it to the schema to {% internal_link "release it", "/changesets/releases" %}. graphql-ruby-2.5.19/guides/changesets/installation.md000066400000000000000000000067541514115062600226710ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true enterprise: true section: GraphQL Enterprise - Changesets title: Installing Changesets desc: Adding Changesets to your schema index: 1 --- Changesets require some updates to the schema (to define changesets) and some updates to your controller (to receive version headers from clients). ## Schema Setup To get started with [GraphQL-Enterprise](https://graphql.pro/enterprise) Changesets, you have to add them to your schema. They're added in several places: - To support versioning arguments, add the `ArgumentIntegration` to your base argument: ```ruby # app/graphql/types/base_argument.rb class Types::BaseArgument < GraphQL::Schema::Argument include GraphQL::Enterprise::Changeset::ArgumentIntegration end ``` Also, make sure that your `BaseField`, `BaseInputObject`, `BaseResolver`, and `BaseMutation` have `argument_class(Types::BaseArgument)` configured in them. - To support versioning fields, add the `FieldIntegration` to your base field: ```ruby # app/graphql/types/base_field.rb class Types::BaseField < GraphQL::Schema::Field include GraphQL::Enterprise::Changeset::FieldIntegration argument_class(Types::BaseArgument) end ``` Also, make sure that your `BaseObject`, `BaseInterface`, and `BaseMutation` have `field_class(Types::BaseField)` configured in them. - To support versioning enum values, add the `EnumValueIntegration` to your base enum value: ```ruby # app/graphql/types/base_enum_value.rb class Types::BaseEnumValue < GraphQL::Schema::EnumValue include GraphQL::Enterprise::Changeset::EnumValueIntegration end ``` Also, make sure that your `BaseEnum` has `enum_value_class(Types::BaseEnumValue)` configured in it. - To support versioning union memberships and interface implementations, add the `TypeMembershipIntegration` to your base type membership: ```ruby # app/graphql/types/base_type_membership.rb class Types::BaseTypeMembership < GraphQL::Schema::TypeMembership include GraphQL::Enterprise::Changeset::TypeMembershipIntegration end ``` Also, make sure that your `BaseUnion` and `BaseInterface` have `type_membership_class(Types::BaseTypeMembership)` configured in it. (`TypeMembership`s are used by GraphQL-Ruby to link object types to the union types they belong to and the interfaces they implement. By using a custom type membership class, you can make objects belong (or _not_ belong) to unions or interfaces, depending on the API version.) Once those integrations are set up, you're ready to {% internal_link "write a changeset", "/changesets/definition" %} and start {% internal_link "releasing API versions", "/changesets/releases" %}! ## Controller Setup Additionally, your controller must pass `context[:changeset_version]` when running queries. To provide this, update your controller: ```ruby class GraphqlController < ApplicationController def execute context = { # ... changeset_version: request.headers["API-Version"], # <- Your header here. Choose something for API clients to pass. } result = MyAppSchema.execute(..., context: context) # ... end end ``` In the example above, `API-Version: ...` will be parsed from the incoming request and used as `context[:changeset_version]`. If `context[:changeset_version]` is `nil`, then _no_ changesets will apply to that request. Now that Changesets are installed, read on to {% internal_link "define some changesets", "/changesets/definition" %}. graphql-ruby-2.5.19/guides/changesets/overview.md000066400000000000000000000046621514115062600220320ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true enterprise: true section: GraphQL Enterprise - Changesets title: API Versioning for GraphQL-Ruby desc: Evolve your schema over time, feature-by-feature index: 0 --- Out-of-the-box, GraphQL is [versionless by design](https://graphql.org/learn/schema-design/). GraphQL's openness to extension paves the way for continuously expanding and improving an API. You can _always_ add new fields, new arguments, and new types to implement new features and customize existing behavior. However, sometimes a business case may call for a different versioning scheme. [GraphQL-Enterprise](https://graphql.pro/enterprise)'s "Changesets" enable schemas to release _any_ change -- even breaking changes -- to clients, depending on what version of the schema they're using. With Changesets, you can redefine existing fields, define new types using old names, add or remove enum values -- anything, really -- while maintaining compatibility for existing clients. ## Why Changesets? Changesets are a _complementary_ evolution technique to continuous additions. In general, additive changes (new fields, new arguments, new types) are best added right to the existing schema. But if you need to _remove_ something from the schema or redefine existing parts of the schema in non-backwards-compatible ways, Changesets provide a handy way of doing so. For example, if you add a values to an Enum, you can just add it to the existing schema: ```diff class Types::RecipeTag < Types::BaseEnum value "LOW_FAT" value "LOW_CARB" + value "VEGAN" + value "KETO" + value "GRAPEFRUIT_DIET" end ``` However, if you want to change the schema in ways that would _break_ previous queries, you can do that with a Changeset: ```ruby class Types::RecipeTag < Types::BaseEnum # Turns out this makes you sick: value "GRAPEFRUIT_DIET", removed_in: Changesets::RemoveLegacyDiets end ``` Then, only clients requesting API versions _before_ this changeset would be able to use `GRAPEFRUIT_DIET`; clients requesting newer versions could not send it as input and would not receive it in responses. (Changesets _also_ support additive changes, if you prefer to make them that way.) ## Getting Started To start using Changesets, read on: - {% internal_link "Installing Changesets", "/changesets/installation" %} - {% internal_link "Writing Changesets", "/changesets/definition" %} - {% internal_link "Releasing Changesets", "/changesets/releases" %} graphql-ruby-2.5.19/guides/changesets/releases.md000066400000000000000000000057151514115062600217670ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true enterprise: true section: GraphQL Enterprise - Changesets title: Releasing Changesets desc: Associating changes to version numbers index: 3 --- To be available to clients, Changesets added to the schema with `use GraphQL::Enterprise::Changeset::Release changesets_dir: "..."`: ```ruby class MyAppSchema < GraphQL::Schema # Add this before root types so that newly-added types are also added to the schema use GraphQL::Enterprise::Changeset::Release, changesets_dir: "app/graphql/changesets" query(...) mutation(...) subscription(...) end ``` This attaches each Changeset defined in `app/graphql/changesets/*.rb` to the schema. (It assumes Rails conventions, where an underscored file like `app/graphql/changesets/add_some_feature.rb` contains a class like `Changesets::AddSomeFeature`.) {% callout warning %} Add `GraphQL::Enterprise::Changeset::Release` _before_ hooking up your root `query(...)`, `mutation(...)`, and `subscription(...)` types. Otherwise, the schema may not find links to types in new schema versions. {% endcallout %} Alternatively, Changesets can be explicitly attached using `changesets: [...]`, for example: ```ruby class MyAppSchema < GraphQL::Schema use GraphQL::Enterprise::Changeset::Release, changesets: [ Changesets::DeprecateRecipeFlag, Changesets::RemoveRecipeFlag, ] # ... end ``` Only changesets in the directory (or in the array) will be shown to clients. The `release ...` configuration in the changeset will be compared to `context[:changeset_version]` to determine if the changeset applies to the current request. ## Inspecting Releases To preview releases, you can create schema dumps by passing `context: { changeset_version: ... }` to {{ "Schema.to_definition" | api_doc }}. For example, to see how the schema looks with `API-Version: 2021-06-01`: ```ruby schema_sdl = MyAppSchema.to_definition(context: { changeset_version: "2021-06-01"}) # The GraphQL schema definition for the schema at version "2021-06-01": puts schema_sdl ``` To make sure schema versions don't change unexpectedly, use the techniques described in the {% internal_link "Schema structure guide", "/testing/schema_structure" %}. ### Introspection Methods You can also inspect a schema's changesets programmatically. `GraphQL::Enterprise` adds a `Schema.changesets` method which returns a `Set` of changeset classes: ```ruby MySchema.changesets # # ``` Additionally, each changeset has a `.changes` method describing its modifications: ```ruby AddNewFeature.changes # [ # #, # #, # #, # ... # ] ``` Each `Change` object responds to: - `.member`, the part of the schema that was modified - `.type`, the kind of modification (`:addition` when something new is added, `:removal` when a member is removed or replaced with a new definition) graphql-ruby-2.5.19/guides/css/000077500000000000000000000000001514115062600162765ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/css/main.scss000066400000000000000000000461651514115062600201330ustar00rootroot00000000000000--- --- @use "reset"; $brand-color: #a5152a; $dark-theme-brand-color: #e5534b; $brand-color-light: #ed8090; $brand-color-extralight: #f9e8ee; $dark-theme-brand-color-extralight: #262324; $experimental-background: #fff7cf; $experimental-color: hsla(50, 100%, 32%, 0.15); $pro-color: #406db5; $pro-background: hsla(217, 100%, 29%, 0.15); $enterprise-color: #238c44; $enterprise-background: hsla(135, 97%, 25%, 0.15); $dark-theme-code-border: #aaaaaa; $code-border: #d6d6d6; $dark-theme-code-background: #1e1b1b; $code-background: #fafafa; $dark-theme-code-color: #b5b5b5; $code-color: #777777; $code-border-radius: 2px; $muted-color: #777777; $subtle-color: #aaaaaa; $font: 'Rubik', sans-serif; $code-font: 'Monaco', monospace; $faint-color: #f0f0f0; $dark-theme-faint-color: #6a6969; $font-color: black; $dark-theme-font-color: #dbdbdb; $background-color: #fafafa; $dark-theme-background-color: #422e2e; $container-color: white; $dark-theme-container-color: #424242; body { font-family: $font; background: $background-color; .dark-theme-button::after { content: "☀" } } body.dark-theme { background: $dark-theme-code-background; color: $dark-theme-font-color; .dark-theme-button::after { content: "☽" } } strong, b { font-weight: bold; } // Algolia highlights: .ais-Highlight { font-style: normal; font-weight: bold; } .dark-theme { .header { background: $dark-theme-container-color; box-shadow: 0px 0px 10px 0px black; .nav a:hover { color: $font-color; background-color: $dark-theme-brand-color; } } } .header { box-shadow: 0px 0px 10px 0px #d6d6d6; z-index: 1; position: relative; background: $container-color; .nav { $height: 30px; $margin: 10px; display: flex; flex-wrap: wrap; align-items: center; $fade-time: 0.2s; .nav-links { margin-left: auto; display: flex; } .img-link { transition: background $fade-time; img { transition: filter $fade-time; transition: -webkit-filter $fade-time; filter: brightness(1) invert(0); -webkit-filter: brightness(1) invert(0); } &:hover { img { -webkit-filter: brightness(0) invert(1); filter: brightness(0) invert(1); } } } img { height: $height; width: auto; margin: $margin; } a, span { transition: background-color $fade-time; transition: color $fade-time; padding: $margin; height: $height; display: flex; align-items: center; text-decoration: none; &:hover { background-color: $brand-color; color: white; } } } } .header-container { margin: 0px 20px 0px 20px; } .container { max-width: 1200px; margin: 0px auto; padding: 10px 20px; background: $container-color; &.fullwidth { max-width: 100%; margin: 0px 20px 0px 20px; } } .dark-theme { .container { background: $dark-theme-container-color; } } .callout { padding: 20px 20px 10px 20px; margin: 20px; border: 2px; border-radius: 10px; .heading { font-size: 20px; font-weight: bold; margin-bottom: 20px; } &.callout-warning { background-color: rgba(255, 217, 0, 0.2); border-color: rgba(255, 217, 0, 0.5); } } pre { font-family: $code-font; padding: 0.5rem; border: 1px solid $code-border; border-radius: $code-border-radius; background-color: $code-background; margin: 10px 0px; overflow-x: auto; line-height: 1.4rem; } .dark-theme { pre { background-color: $dark-theme-code-background; border: 1px solid $dark-theme-code-border; } } p, li { line-height: 1.3rem; } li { margin-left: 15px; margin-top: 5px } p, ul { margin-bottom: 20px; } ul { list-style-type: disc; list-style-position: outside; } ol { list-style: decimal; margin-left: 5px; } code { font-family: $code-font; color: $code-color; font-weight: 400; } .dark-theme code { color: $dark-theme-code-color; } .code .line-numbers { display: none; } .dark-theme a { color: $dark-theme-brand-color; border-color: $dark-theme-brand-color; code { color: $dark-theme-brand-color; } } a { color: $brand-color; border-color: $brand-color; text-decoration: none; code { color: $brand-color; } } a:hover, a:hover code { text-decoration: underline; } #readme img { display: none; } .guide-container { a.img-link { background: none; &:hover { background: none; } img { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); transition: all 0.3s cubic-bezier(.25,.8,.25,1); max-height: 300px; max-width: 100%; &:hover { box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); } } } } .cell { overflow-x: scroll; } .guides-toc { ul { list-style: none; display: flex; flex-wrap: wrap; } li { padding-bottom: 10px; padding-right: 10px; } } .guides { .guide-desc { color: $muted-color; margin-left: 5px; } ul { margin-left: 10px; list-style: none; } } @mixin doc-header($color, $background-color) { background-color: $background-color; color: $color; border-radius: $code-border-radius; padding: 10px 10px 10px 10px; margin-bottom: 10px; border: 1px solid $color; p { margin: 0px; padding: 0px; } a { color: $color; &:hover { background-color: $color; color: $background-color; } } } .experimental-header { @include doc-header($experimental-color, $experimental-background); } .pro-header { @include doc-header($pro-color, $pro-background); } .enterprise-header { @include doc-header($enterprise-color, $enterprise-background); } .dark-theme .guide-footer { background-color: $dark-theme-brand-color-extralight; } .guide-footer { background: $brand-color-extralight; margin: 25px 0px 0px 0px; padding: 10px; border-radius: $code-border-radius; } .dark-theme { .hero { .hero-part { &.shaded { background: $dark-theme-faint-color; } h2 { color: $dark-theme-brand-color; text-shadow: $dark-theme-background-color 1px 1px 1px; } } } } .hero { display: flex; flex-direction: column; justify-content: space-around; .hero-title { display: flex; justify-content: center; align-items: center; img, h1 { margin: 20px 10px 30px 10px; } } .hero-subtitle { padding: 10px 0px; p { margin: 5px auto; text-align: center; } } .hero-part { display: flex; justify-content: space-between; flex-wrap: wrap; &.shaded { background: $faint-color; } h2 { color: $brand-color; text-shadow: #cccccc 1px 1px 1px; font-size: 1.4em; } .hero-feature { padding: 15px; flex-basis: calc(50% - 60px); flex-grow: 1; } } } h1, h2, h3, h4, h5 { margin: 25px 0px 15px 0px; a { text-decoration: none; } } .guide-header { margin-bottom: 15px; } h1 { font-size: 1.5rem; } h2 { font-size: 1.3rem; } h3 { font-size: 1.2rem; } h4 { font-size: 1.1rem; } h5 { font-size: 1.05rem; } em { font-style: italic; } table { width: 100%; margin: 0px 0px 15px 0px; thead { text-align: left; border-bottom: 1px solid $subtle-color; } td, th { padding: 5px 10px 5px 0px; } } .dark-theme { .search-input { background: $dark-theme-code-background; color: $dark-theme-font-color; } .search-results-container { background-color: $dark-theme-background-color; #search-results { .search-result { &:focus, &:hover { background-color: $dark-theme-container-color; border-bottom-color: $dark-theme-brand-color; .search-title { color: $dark-theme-brand-color; } } .search-category { border: 1px solid $dark-theme-brand-color; color: $dark-theme-brand-color; } } } } } .search-input { font-size: 1em; padding: 5px; margin: 10px; border: 1px solid $subtle-color; border-radius: 3px; } .search-results-container { $bg: #eaeaea; $bg-highlight: #f9f9f9; background-color: $bg; #search-results { $fade-time: 0.1s; display: flex; flex-direction: column; max-width: 1040px; margin: 0 auto; .search-result { text-decoration: none; color: $font-color; padding: 6px 10px 0px 6px; line-height: 18px; border-bottom: 2px solid transparent; transition: border-bottom-color $fade-time; .search-title { font-weight: bold; margin-right: 8px; transition: color $fade-time; } .search-preview { color: $subtle-color; } .search-category { border: 1px solid $brand-color; border-radius: 3px; margin: 0 8px 0 0; padding: 3px; font-size: 0.7em; color: $brand-color; position: relative; top: -3px; } &:focus, &:hover { outline: none; background-color: $bg-highlight; border-bottom-color: $brand-color; .search-title { color: $brand-color; } } } } } .dark-theme ul.breadcrumb .jump-to-select { color: $dark-theme-brand-color; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='292.4' height='292.4'%3E%3Cpath fill='%23e5534b' d='M287 69.4a17.6 17.6 0 0 0-13-5.4H18.4c-5 0-9.3 1.8-12.9 5.4A17.6 17.6 0 0 0 0 82.2c0 5 1.8 9.3 5.4 12.9l128 127.9c3.6 3.6 7.8 5.4 12.8 5.4s9.2-1.8 12.8-5.4L287 95c3.5-3.5 5.4-7.8 5.4-12.8 0-5-1.9-9.2-5.5-12.8z'/%3E%3C/svg%3E"); } ul.breadcrumb { color: $muted-color; li { display: inline; list-style: none; margin: 0; } li:before { content: "»"; margin: 0px 4px 0px 2px; } li:first-child:before { content: ""; margin: 0; } .jump-to-select { box-sizing: border-box; -moz-appearance: none; -webkit-appearance: none; appearance: none; padding: 5px 20px 5px 5px; border: 1px solid $code-border; border-radius: 5px; background-color: transparent; color: $brand-color; font-size: 16px; font-weight: 500; line-height: 1.3; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='292.4' height='292.4'%3E%3Cpath fill='%23a5152a' d='M287 69.4a17.6 17.6 0 0 0-13-5.4H18.4c-5 0-9.3 1.8-12.9 5.4A17.6 17.6 0 0 0 0 82.2c0 5 1.8 9.3 5.4 12.9l128 127.9c3.6 3.6 7.8 5.4 12.8 5.4s9.2-1.8 12.8-5.4L287 95c3.5-3.5 5.4-7.8 5.4-12.8 0-5-1.9-9.2-5.5-12.8z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; background-size: 9px; cursor: default; &:hover { border-color: #777; } &:focus { border-color: #999; box-shadow: 0 0 1px 2px #6db4ff; outline: none; } option { color: black; } } } .dark-theme { .table-of-contents { background: $dark-theme-code-background; } } .table-of-contents { float: right; border: 1px solid $subtle-color; border-radius: 3px; padding: 15px; margin: 0 10px 10px 10px; width: 300px; background: $code-background; .contents-header { margin: 0 0 5px 20px; } .contents-list { margin: 0; list-style: decimal; padding-left: 5px; .contents-entry { &::marker { color: $muted-color; } .contents-entry { list-style: none; } } } } /* pygments CSS, github theme */ .highlight .hll { background-color: #ffffcc } .highlight .c { color: #999988; font-style: italic } /* Comment */ .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ .highlight .k { color: #000000; font-weight: bold } /* Keyword */ .highlight .o { color: #000000; font-weight: bold } /* Operator */ .highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */ .highlight .cp { color: #999999; font-weight: bold; font-style: italic } /* Comment.Preproc */ .highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */ .highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ .highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ .highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */ .highlight .gr { color: #aa0000 } /* Generic.Error */ .highlight .gh { color: #999999 } /* Generic.Heading */ .highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ .highlight .go { color: #888888 } /* Generic.Output */ .highlight .gp { color: #555555 } /* Generic.Prompt */ .highlight .gs { font-weight: bold } /* Generic.Strong */ .highlight .gu { color: #aaaaaa } /* Generic.Subheading */ .highlight .gt { color: #aa0000 } /* Generic.Traceback */ .highlight .kc { color: #000000; font-weight: bold } /* Keyword.Constant */ .highlight .kd { color: #000000; font-weight: bold } /* Keyword.Declaration */ .highlight .kn { color: #000000; font-weight: bold } /* Keyword.Namespace */ .highlight .kp { color: #000000; font-weight: bold } /* Keyword.Pseudo */ .highlight .kr { color: #000000; font-weight: bold } /* Keyword.Reserved */ .highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */ .highlight .m { color: #009999 } /* Literal.Number */ .highlight .s { color: #d01040 } /* Literal.String */ .highlight .na { color: #008080 } /* Name.Attribute */ .highlight .nb { color: #0086B3 } /* Name.Builtin */ .highlight .nc { color: #445588; font-weight: bold } /* Name.Class */ .highlight .no { color: #008080 } /* Name.Constant */ .highlight .nd { color: #3c5d5d; font-weight: bold } /* Name.Decorator */ .highlight .ni { color: #800080 } /* Name.Entity */ .highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */ .highlight .nf { color: #990000; font-weight: bold } /* Name.Function */ .highlight .nl { color: #990000; font-weight: bold } /* Name.Label */ .highlight .nn { color: #555555 } /* Name.Namespace */ .highlight .nt { color: #000080 } /* Name.Tag */ .highlight .nv { color: #008080 } /* Name.Variable */ .highlight .ow { color: #000000; font-weight: bold } /* Operator.Word */ .highlight .w { color: #bbbbbb } /* Text.Whitespace */ .highlight .mf { color: #009999 } /* Literal.Number.Float */ .highlight .mh { color: #009999 } /* Literal.Number.Hex */ .highlight .mi { color: #009999 } /* Literal.Number.Integer */ .highlight .mo { color: #009999 } /* Literal.Number.Oct */ .highlight .sb { color: #d01040 } /* Literal.String.Backtick */ .highlight .sc { color: #d01040 } /* Literal.String.Char */ .highlight .sd { color: #d01040 } /* Literal.String.Doc */ .highlight .s2 { color: #d01040 } /* Literal.String.Double */ .highlight .se { color: #d01040 } /* Literal.String.Escape */ .highlight .sh { color: #d01040 } /* Literal.String.Heredoc */ .highlight .si { color: #d01040 } /* Literal.String.Interpol */ .highlight .sx { color: #d01040 } /* Literal.String.Other */ .highlight .sr { color: #009926 } /* Literal.String.Regex */ .highlight .s1 { color: #d01040 } /* Literal.String.Single */ .highlight .ss { color: #990073 } /* Literal.String.Symbol */ .highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */ .highlight .vc { color: #008080 } /* Name.Variable.Class */ .highlight .vg { color: #008080 } /* Name.Variable.Global */ .highlight .vi { color: #008080 } /* Name.Variable.Instance */ .highlight .il { color: #009999 } /* Literal.Number.Integer.Long */ .dark-theme { .highlight .hll { background-color: #49483e } pre.highlight { background: #272822; color: #f8f8f2 } .highlight .c { color: #75715e } /* Comment */ .highlight .err { color: #960050; background-color: #1e0010 } /* Error */ .highlight .k { color: #66d9ef } /* Keyword */ .highlight .l { color: #ae81ff } /* Literal */ .highlight .n { color: #f8f8f2 } /* Name */ .highlight .o { color: #f92672 } /* Operator */ .highlight .p { color: #f8f8f2 } /* Punctuation */ .highlight .ch { color: #75715e } /* Comment.Hashbang */ .highlight .cm { color: #75715e } /* Comment.Multiline */ .highlight .cp { color: #75715e } /* Comment.Preproc */ .highlight .cpf { color: #75715e } /* Comment.PreprocFile */ .highlight .c1 { color: #75715e } /* Comment.Single */ .highlight .cs { color: #75715e } /* Comment.Special */ .highlight .gd { color: #f92672; background-color: #5e4343; } /* Generic.Deleted */ .highlight .ge { font-style: italic } /* Generic.Emph */ .highlight .gi { color: #a6e22e; background-color: #475547; } /* Generic.Inserted */ .highlight .gs { font-weight: bold } /* Generic.Strong */ .highlight .gu { color: #75715e } /* Generic.Subheading */ .highlight .kc { color: #66d9ef } /* Keyword.Constant */ .highlight .kd { color: #66d9ef } /* Keyword.Declaration */ .highlight .kn { color: #f92672 } /* Keyword.Namespace */ .highlight .kp { color: #66d9ef } /* Keyword.Pseudo */ .highlight .kr { color: #66d9ef } /* Keyword.Reserved */ .highlight .kt { color: #66d9ef } /* Keyword.Type */ .highlight .ld { color: #e6db74 } /* Literal.Date */ .highlight .m { color: #ae81ff } /* Literal.Number */ .highlight .s { color: #e6db74 } /* Literal.String */ .highlight .na { color: #a6e22e } /* Name.Attribute */ .highlight .nb { color: #f8f8f2 } /* Name.Builtin */ .highlight .nc { color: #a6e22e } /* Name.Class */ .highlight .no { color: #66d9ef } /* Name.Constant */ .highlight .nd { color: #a6e22e } /* Name.Decorator */ .highlight .ni { color: #f8f8f2 } /* Name.Entity */ .highlight .ne { color: #a6e22e } /* Name.Exception */ .highlight .nf { color: #a6e22e } /* Name.Function */ .highlight .nl { color: #f8f8f2 } /* Name.Label */ .highlight .nn { color: #f8f8f2 } /* Name.Namespace */ .highlight .nx { color: #a6e22e } /* Name.Other */ .highlight .py { color: #f8f8f2 } /* Name.Property */ .highlight .nt { color: #f92672 } /* Name.Tag */ .highlight .nv { color: #f8f8f2 } /* Name.Variable */ .highlight .ow { color: #f92672 } /* Operator.Word */ .highlight .w { color: #f8f8f2 } /* Text.Whitespace */ .highlight .mb { color: #ae81ff } /* Literal.Number.Bin */ .highlight .mf { color: #ae81ff } /* Literal.Number.Float */ .highlight .mh { color: #ae81ff } /* Literal.Number.Hex */ .highlight .mi { color: #ae81ff } /* Literal.Number.Integer */ .highlight .mo { color: #ae81ff } /* Literal.Number.Oct */ .highlight .sb { color: #e6db74 } /* Literal.String.Backtick */ .highlight .sc { color: #e6db74 } /* Literal.String.Char */ .highlight .sd { color: #e6db74 } /* Literal.String.Doc */ .highlight .s2 { color: #e6db74 } /* Literal.String.Double */ .highlight .se { color: #ae81ff } /* Literal.String.Escape */ .highlight .sh { color: #e6db74 } /* Literal.String.Heredoc */ .highlight .si { color: #e6db74 } /* Literal.String.Interpol */ .highlight .sx { color: #e6db74 } /* Literal.String.Other */ .highlight .sr { color: #e6db74 } /* Literal.String.Regex */ .highlight .s1 { color: #e6db74 } /* Literal.String.Single */ .highlight .ss { color: #e6db74 } /* Literal.String.Symbol */ .highlight .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */ .highlight .vc { color: #f8f8f2 } /* Name.Variable.Class */ .highlight .vg { color: #f8f8f2 } /* Name.Variable.Global */ .highlight .vi { color: #f8f8f2 } /* Name.Variable.Instance */ .highlight .il { color: #ae81ff } /* Literal.Number.Integer.Long */ } graphql-ruby-2.5.19/guides/dataloader/000077500000000000000000000000001514115062600176065ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/dataloader/adopting.md000066400000000000000000000103761514115062600217440ustar00rootroot00000000000000--- layout: guide search: true section: Dataloader title: Dataloader vs. GraphQL-Batch desc: Comparing and Contrasting Batch Loading Options index: 3 --- {{ "GraphQL::Dataloader" | api_doc }} solves the same problem as [`GraphQL::Batch`](https://github.com/shopify/graphql-batch). There are a few major differences between the modules: - __Concurrency Primitive:__ GraphQL-Batch uses `Promise`s from [`promise.rb`](https://github.com/lgierth/promise.rb); GraphQL::Dataloader uses Ruby's [`Fiber` API](https://ruby-doc.org/core-3.0.0/Fiber.html). These primitives dictate how batch loading code is written (see below for comparisons). - __Maturity:__ Frankly, GraphQL-Batch is about as old as GraphQL-Ruby, and it's been in production at Shopify, GitHub, and others for many years. GraphQL::Dataloader is new, and although Ruby has supported `Fiber`s since 1.9, they still aren't widely used. - __Scope:__ It's not currently possible to use `GraphQL::Dataloader` _outside_ GraphQL. The incentive in writing `GraphQL::Dataloader` was to leverage `Fiber`'s ability to _transparently_ pause and resume work, which removes the need for `Promise`s (and removes the resulting complexity in the code). Additionally, `GraphQL::Dataloader` should _eventually_ support Ruby 3.0's `Fiber.scheduler` API, which runs I/O in the background by default. ## Comparison: Fetching a single object In this example, a single object is batch-loaded to satisfy a GraphQL field. - With __GraphQL-Batch__, you call a loader, which returns a `Promise`: ```ruby record_promise = Loaders::Record.load(1) ``` Then, under the hood, GraphQL-Ruby manages the promise (using its `lazy_resolve` feature, upstreamed from GraphQL-Batch many years ago). GraphQL-Ruby will call `.sync` on it when no further execution is possible; `promise.rb` implements `Promise#sync` to execute the pending work. - With __GraphQL::Dataloader__, you get a source, then call `.load` on it, which may pause the current Fiber, but it returns the requested object. ```ruby dataloader.with(Sources::Record).load(1) ``` Since the requested object is (eventually) returned from `.load`, Nothing else is required. ## Comparison: Fetching objects in sequence (dependent) In this example, one object is loaded, then another object is loaded _based on_ the first one. - With __GraphQL-Batch__, `.then { ... }` is used to join dependent code blocks: ```ruby Loaders::Record.load(1).then do |record| Loaders::OtherRecord.load(record.other_record_id) end ``` That call returns a `Promise`, which is stored by GraphQL-Ruby, and finally `.sync`ed. - With __GraphQL-Dataloader__, `.load(...)` returns the requested object (after a potential `Fiber` pause), so no other method calls are necessary: ```ruby record = dataloader.with(Sources::Record).load(1) dataloader.with(Sources::OtherRecord).load(record.other_record_id) ``` ## Comparison: Fetching objects concurrently (independent) Sometimes, you need multiple _independent_ records to perform a calculation. Each record is loaded, then they're combined in some bit of work. - With __GraphQL-Batch__, `Promise.all(...)` is used to to wait for several pending loads: ```ruby promise_1 = Loaders::Record.load(1) promise_2 = Loaders::OtherRecord.load(2) Promise.all([promise_1, promise_2]).then do |record, other_record| do_something(record, other_record) end ``` If the objects are loaded from the same loader, then `.load_many` also works: ```ruby Loaders::Record.load_many([1, 2]).then do |record, other_record| do_something(record, other_record) end ``` - With __GraphQL::Dataloader__, each request is registered with `.request(...)` (which never pauses the Fiber), then data is loaded with `.load` (which will pause the Fiber as needed): ```ruby # first, make some requests request_1 = dataloader.with(Sources::Record).request(1) request_2 = dataloader.with(Sources::OtherRecord).request(2) # then, load the objects and do something record = request_1.load other_record = request_2.load do_something(record, other_record) ``` If the objects come from the same `Source`, then `.load_all` will return the objects directly: ```ruby record, other_record = dataloader.with(Sources::Record).load_all([1, 2]) do_something(record, other_record) ``` graphql-ruby-2.5.19/guides/dataloader/async_dataloader.md000066400000000000000000000054061514115062600234320ustar00rootroot00000000000000--- layout: guide search: true section: Dataloader title: Async Source Execution desc: Using AsyncDataloader to fetch external data in parallel index: 5 --- `AsyncDataloader` will run {{ "GraphQL::Dataloader::Source#fetch" | api_doc }} calls in parallel, so that external service calls (like database queries or network calls) don't have to wait in a queue. To use `AsyncDataloader`, hook it up in your schema _instead of_ `GraphQL::Dataloader`: ```diff - use GraphQL::Dataloader + use GraphQL::Dataloader::AsyncDataloader ``` __Also__, add [the `async` gem](https://github.com/socketry/async) to your project, for example: ``` bundle add async ``` Now, {{ "GraphQL::Dataloader::AsyncDataloader" | api_doc }} will create `Async::Task` instances instead of plain `Fiber`s and the `async` gem will manage parallelism. For a demonstration of this behavior, see: [https://github.com/rmosolgo/rails-graphql-async-demo](https://github.com/rmosolgo/rails-graphql-async-demo) _You can also implement {% internal_link "manual parallelism", "/dataloader/parallelism" %} using `dataloader.yield`._ ## Rails For Rails, you'll need **Rails 7.1**, which properly supports fiber-based concurrency, and you'll also want to configure Rails to use Fibers for isolation: ```ruby class Application < Rails::Application # ... config.active_support.isolation_level = :fiber end ``` ### ActiveRecord Connections You can use Dataloader's {% internal_link "Fiber lifecycle hooks", "/dataloader/dataloader#fiber-lifecycle-hooks" %} to improve ActiveRecord connection handling: - In Rails < 7.2, connections are not reused when a Fiber exits; instead, they're only reused when a request or background job finishes. You can add manual `release_connection` calls to improve this. - With `isolation_level = :fiber`, new Fibers don't inherit `connected_to ...` settings from their parent fibers. Altogether, it can be improved like this: ```ruby def get_fiber_variables vars = super # Collect the current connection config to pass on: vars[:connected_to] = { role: ActiveRecord::Base.current_role, shard: ActiveRecord::Base.current_shard, prevent_writes: ActiveRecord::Base.current_preventing_writes } vars end def set_fiber_variables(vars) connection_config = vars.delete(:connected_to) # Reset connection config from the parent fiber: ActiveRecord::Base.connecting_to(**connection_config) super(vars) end def cleanup_fiber super # Release the current connection ActiveRecord::Base.connection_pool.release_connection end ``` Modify the example according to your database configuration and abstract class hierarchy. ## Other Options You can also manually implement parallelism with Dataloader. See the {% internal_link "Dataloader Parallelism", "/dataloader/parallelism" %} guide for details. graphql-ruby-2.5.19/guides/dataloader/dataloader.md000066400000000000000000000041211514115062600222260ustar00rootroot00000000000000--- layout: guide search: true section: Dataloader title: Dataloader desc: The Dataloader orchestrates Fibers and Sources index: 2 --- {{ "GraphQL::Dataloader" | api_doc }} instances are created for each query (or multiplex) and they: - Cache {% internal_link "Source", "/dataloader/sources" %} instances for the duration of GraphQL execution - Run pending Fibers to resolve data requirements and continue GraphQL execution During a query, you can access the dataloader instance with: - {{ "GraphQL::Query::Context#dataloader" | api_doc }} (`context.dataloader`, anywhere that query context is available) - {{ "GraphQL::Schema::Object#dataloader" | api_doc }} (`dataloader` inside a resolver method) - {{ "GraphQL::Schema::Resolver#dataloader" | api_doc }} (`dataloader` inside `def resolve` of a Resolver, Mutation, or Subscription class.) ## Fiber Lifecycle Hooks Under the hood, `Dataloader` creates Fibers as-needed and uses them to run GraphQL and load data from `Source` classes. You can hook into these Fibers through several lifecycle hooks. To implement these hooks, create a custom subclass and provide new implementation for these methods: ```ruby class MyDataloader < GraphQL::Dataloader # or GraphQL::Dataloader::AsyncDataloader # ... end ``` Then, use your customized dataloader instead of the built-in one: ```diff class MySchema < GraphQL::Schema - use GraphQL::Dataloader + use MyDataloader end ``` - __{{ "GraphQL::Dataloader#get_fiber_variables" | api_doc }}__ is called before creating a Fiber. By default, it returns a hash containing the parent Fiber's variables (from `Thread.current[...]`). You can add to this hash in your own implementation of this method. - __{{ "GraphQL::Dataloader#set_fiber_variables" | api_doc }}__ is called inside the new Fiber. It's passed the hash returned from `get_fiber_variables`. You can use this method to initialize "global" state inside the new Fiber. - __{{ "GraphQL::Dataloader#cleanup_fiber" | api_doc }}__ is called just before a Dataloader Fiber exits. You can use this methods to teardown any state that you prepared in `set_fiber_variables`. graphql-ruby-2.5.19/guides/dataloader/overview.md000066400000000000000000000130261514115062600220000ustar00rootroot00000000000000--- layout: guide search: true section: Dataloader title: Overview desc: Getting started with the Fiber-based Dataloader index: 0 --- {{ "GraphQL::Dataloader" | api_doc }} provides efficient, batched access to external services, backed by Ruby's `Fiber` concurrency primitive. It has a per-query result cache and {% internal_link "AsyncDataloader", "/dataloader/async_dataloader" %} supports truly parallel execution out-of-the-box. `GraphQL::Dataloader` is inspired by [`@bessey`'s proof-of-concept](https://github.com/bessey/graphql-fiber-test/tree/no-gem-changes) and [shopify/graphql-batch](https://github.com/shopify/graphql-batch). ## Batch Loading `GraphQL::Dataloader` facilitates a two-stage approach to fetching data from external sources (like databases or APIs): - First, GraphQL fields register their data requirements (eg, object IDs or query parameters) - Then, after as many requirements have been gathered as possible, `GraphQL::Dataloader` initiates _actual_ fetches to external services That cycle is repeated during execution: data requirements are gathered until no further GraphQL fields can be executed, then `GraphQL::Dataloader` triggers external calls based on those requirements and GraphQL execution resumes. ## Fibers `GraphQL::Dataloader` uses Ruby's `Fiber`, a lightweight concurrency primitive which supports application-level scheduling _within_ a `Thread`. By using `Fiber`, `GraphQL::Dataloader` can pause GraphQL execution when data is requested, then resume execution after the data is fetched. At a high level, `GraphQL::Dataloader`'s usage of `Fiber` looks like this: - GraphQL execution is run inside a Fiber. - When that Fiber returns, if the Fiber was paused to wait for data, then GraphQL execution resumes with the _next_ (sibling) GraphQL field inside a new Fiber. - That cycle continues until no further sibling fields are available and all known Fibers are paused. - `GraphQL::Dataloader` takes the first paused Fiber and resumes it, causing the `GraphQL::Dataloader::Source` to execute its `#fetch(...)` call. That Fiber continues execution as far as it can. - Likewise, paused Fibers are resumed, causing GraphQL execution to continue, until all paused Fibers are evaluated completely. Whenever `GraphQL::Dataloader` creates a new `Fiber`, it copies each pair from `Thread.current[...]` and reassigns them inside the new `Fiber`. `AsyncDataloader`, built on top of the [`async` gem](https://github.com/socketry/async), supports parallel I/O operations (like network and database communication) via Ruby's non-blocking `Fiber.schedule` API. {% internal_link "Learn more →", "/dataloader/async_dataloader" %}. ## Getting Started To install {{ "GraphQL::Dataloader" | api_doc }}, add it to your schema with `use ...`, for example: ```ruby class MySchema < GraphQL::Schema # ... use GraphQL::Dataloader end ``` Then, inside your schema, you can request batch-loaded objects by their lookup key with `dataloader.with(...).load(...)`: ```ruby field :user, Types::User do argument :handle, String end def user(handle:) dataloader.with(Sources::UserByHandle).load(handle) end ``` Or, load several objects by passing an array of lookup keys to `.load_all(...)`: ```ruby field :is_following, Boolean, null: false do argument :follower_handle, String argument :followed_handle, String end def is_following(follower_handle:, followed_handle:) follower, followed = dataloader .with(Sources::UserByHandle) .load_all([follower_handle, followed_handle]) followed && follower && follower.follows?(followed) end ``` To prepare requests from several sources, use `.request(...)`, then call `.load` after all requests are registered: ```ruby class AddToList < GraphQL::Schema::Mutation argument :handle, String argument :list, String, as: :list_name field :list, Types::UserList def resolve(handle:, list_name:) # first, register the requests: user_request = dataloader.with(Sources::UserByHandle).request(handle) list_request = dataloader.with(Sources::ListByName, context[:viewer]).request(list_name) # then, use `.load` to wait for the external call and return the object: user = user_request.load list = list_request.load # Now, all objects are ready. list.add_user!(user) { list: list } end end ``` ### `loads:` and `object_from_id` `dataloader` is also available as `context.dataloader`, so you can use it to implement `MySchema.object_from_id`. For example: ```ruby class MySchema < GraphQL::Schema def self.object_from_id(id, ctx) model_class, database_id = IdDecoder.decode(id) ctx.dataloader.with(Sources::RecordById, model_class).load(database_id) end end ``` Then, any arguments with `loads:` will use that method to fetch objects. For example: ```ruby class FollowUser < GraphQL::Schema::Mutation argument :follow_id, ID, loads: Types::User field :followed, Types::User def resolve(follow:) # `follow` was fetched using the Schema's `object_from_id` hook context[:viewer].follow!(follow) { followed: follow } end end ``` ## Data Sources To implement batch-loading data sources, see the {% internal_link "Sources guide", "/dataloader/sources" %}. ## Parallelism You can run I/O operations in parallel with GraphQL::Dataloader. There are two approaches: - `AsyncDataloader` uses the `async` gem to automatically background I/O from `Dataloader::Source#fetch` calls. {% internal_link "Read More", "/dataloader/async_dataloader" %} - You can manually call `dataloader.yield` after starting work in the background. {% internal_link "Read More", "/dataloader/parallelism" %} graphql-ruby-2.5.19/guides/dataloader/parallelism.md000066400000000000000000000055071514115062600224440ustar00rootroot00000000000000--- layout: guide search: true section: Dataloader title: Manual Parallelism desc: Yield to Dataloader after starting work index: 7 --- You can coordinate with {{ "GraphQL::Dataloader" | api_doc }} to run tasks in the background. To do this, call `dataloader.yield` inside `Source#fetch` after kicking off your task. For example: ```ruby def fetch(ids) # somehow queue up a background query, # see examples below future_result = async_query_for(ids) # return control to the dataloader dataloader.yield # dataloader will come back here # after calling other sources, # now wait for the value future_result.value end ``` _Alternatively, you can use {% internal_link "AsyncDataloader", "/dataloader/async_dataloader" %} to automatically background I/O inside `Source#fetch` calls._ ## Example: Rails load_async You can use Rails's `load_async` method to load `ActiveRecord::Relation`s in the background. For example: ```ruby class Sources::AsyncRelationSource < GraphQL::Dataloader::Source def fetch(relations) relations.each(&:load_async) # start loading them in the background dataloader.yield # hand back to GraphQL::Dataloader relations.each(&:load) # now, wait for the result, returning the now-loaded relation end end ``` You could call that source from a GraphQL field method: ```ruby field :direct_reports, [Person] def direct_reports # prepare an ActiveRecord::Relation: direct_reports = Person.where(manager: object) # pass it off to the source: dataloader .with(Sources::AsyncRelationSource) .load(direct_reports) end ``` ## Example: Rails async calculations In a Dataloader source, you can run Rails async calculations in the background while other work continues. For example: ```ruby class Sources::DirectReportsCount < GraphQL::Dataloader::Source def fetch(users) # Start the queries in the background: promises = users.map { |u| u.direct_reports.async_count } # Return to GraphQL::Dataloader: dataloader.yield # Now return the results, waiting if necessary: promises.map(&:value) end end ``` Which could be used in a GraphQL field: ```ruby field :direct_reports_count, Int def direct_reports_count dataloader.with(Sources::DirectReportsCount).load(object) end ``` ## Example: Concurrent::Future You could use `concurrent-ruby` to put work in a background thread. For example, using `Concurrent::Future`: ```ruby class Sources::ExternalDataSource < GraphQL::Dataloader::Source def fetch(urls) # Start some I/O-intensive work: futures = urls.map do |url| Concurrent::Future.execute { # Somehow fetch and parse data: get_remote_json(url) } end # Yield back to GraphQL::Dataloader: dataloader.yield # Dataloader has done what it can, # so now return the value, waiting if necessary: futures.map(&:value) end end ``` graphql-ruby-2.5.19/guides/dataloader/sources.md000066400000000000000000000160151514115062600216160ustar00rootroot00000000000000--- layout: guide search: true section: Dataloader title: Sources desc: Batch-loading objects for GraphQL::Dataloader index: 1 --- _Sources_ are what {{ "GraphQL::Dataloader" | api_doc }} uses to fetch data from external services. ## Source Concepts Sources are classes that inherit from `GraphQL::Dataloader::Source`. A Source _must_ implement `def fetch(keys)` to return a list of objects, one for each of the given keys. A source _may_ implement `def initialize(...)` to accept other batching parameters. Sources will receive two kinds of inputs from `GraphQL::Dataloader`: - _keys_, which correspond to objects requested by the application. Keys are passed to `def fetch(keys)`, which must return an object (or `nil`) for each of `keys`, in the same order as `keys`. Under the hood, each Source instance maintains a `key => object` cache. - _batch parameters_, which are the basis of batched groups. For example, if you're loading records from different database tables, the table name would be a batch parameter. Batch parameters are given to `dataloader.with(source_class, *batch_parameters)`, and the default is _no batch parameters_. When you define a source, you should add the batch parameters to `def initialize(...)` and store them in instance variables. (`dataloader.with(source_class, *batch_parameters)` returns an instance of `source_class` with the given batch parameters -- but it might be an instance which was cached by `dataloader`.) Additionally, batch parameters are used to de-duplicate Source initializations during a query run. `.with(...)` calls that have the same batch parameters will use the same Source instance under the hood. To customize how Sources are de-duplicated, see {{ "GraphQL::Dataloader::Source.batch_key_for" | api_doc }}. ## Example: Loading Strings from Redis by Key The simplest source might fetch values based on their keys. For example: ```ruby # app/graphql/sources/redis_string.rb class Sources::RedisString < GraphQL::Dataloader::Source REDIS = Redis.new def fetch(keys) # Redis's `mget` will return a value for each key with a `nil` for any not-found key. REDIS.mget(*keys) end end ``` This loader could be used in GraphQL like this: ```ruby some_string = dataloader.with(Sources::RedisString).load("some_key") ``` Calls to `.load(key)` will be batched, and when `GraphQL::Dataloader` can't go any further, it will dispatch a call to `def fetch(keys)` above. ## Example: Loading ActiveRecord Objects by ID To fetch ActiveRecord objects by ID, the source should also accept the _model class_ as a batching parameter. For example: ```ruby # app/graphql/sources/active_record_object.rb class Sources::ActiveRecordObject < GraphQL::Dataloader::Source def initialize(model_class) @model_class = model_class end def fetch(ids) records = @model_class.where(id: ids) # return a list with `nil` for any ID that wasn't found ids.map { |id| records.find { |r| r.id == id.to_i } } end end ``` This source could be used for any `model_class`, for example: ```ruby author = dataloader.with(Sources::ActiveRecordObject, ::User).load(1) post = dataloader.with(Sources::ActiveRecordObject, ::Post).load(1) ``` ## Example: Batched Calculations Besides fetching objects, Sources can return values from batched calculations. For example, a system could batch up checks for who a user follows: ```ruby # for a given user, batch checks to see whether this user follows another user. # (The default `user.followings.where(followed_user_id: followed).exists?` would cause N+1 queries.) class Sources::UserFollowingExists < GraphQL::Dataloader::Source def initialize(user) @user = user end def fetch(handles) # Prepare a `SELECT id FROM users WHERE handle IN(...)` statement user_ids = ::User.where(handle: handles).select(:id) # And use it to filter this user's followings: followings = @user.followings.where(followed_user_id: user_ids) # Now, for followings that _actually_ hit a user, get the handles for those users: followed_users = ::User.where(id: followings.select(:followed_user_id)) # Finally, return a result set, with one entry (true or false) for each of the given `handles` handles.map { |h| !!followed_users.find { |u| u.handle == h }} end end ``` It could be used like this: ```ruby is_following = dataloader.with(Sources::UserFollowingExists, context[:viewer]).load(handle) ``` After all requests were batched, `#fetch` will return a Boolean result to `is_following`. ## Example: Loading in a background thread Inside `Source#fetch(keys)`, you can call `dataloader.yield` to return control to the Dataloader. This way, it will proceed loading other Sources (if there are any), then return the source that yielded. A simple example, spinning up a new Thread: ```ruby def fetch(keys) # spin up some work in a background thread thread = Thread.new { fetch_external_data(keys) } # return control to the dataloader dataloader.yield # at this point, # the dataloader has tried everything else and come back to this source, # so block if necessary: thread.value end ``` See the {% internal_link "parallelism guide", "/dataloader/parallelism" %} for details about this approach. ## Filling the Dataloader Cache If you load records from the database, you can use them to populate a source's cache by using {{ "Dataloader::Source#merge" | api_doc }}. For example: ```ruby # Build a `{ key => value }` map to populate the cache comments_by_id = post.comments.each_with_object({}) { |comment, hash| hash[comment.id] = comment } # Merge the map into the source's cache dataloader.with(Sources::ActiveRecordObject, Comment).merge(comments_by_id) ``` After that, any calls to `.load(id)` will use those already-loaded records if they're available. ## De-duplicating equivalent objects Sometimes, _different_ objects in the application should load the same object from `fetch`. You can customize this behavior by implementing `def result_key_for(key)` in your application. For example, to map records from your ORM to their database ID: ```ruby # Load the `created_by` person for a record from our database class CreatedBySource < GraphQL::Dataloader::Source def result_key_for(key) key.id # Use the record's ID to deduplicate different `.load` calls end # Fetch a `person` for each of `records`, based on their created_by_id def fetch(records) PersonService.find_each(records.map(&:created_by_id)) end end ``` In this case, `records` will include the _first_ object for each unique `record.id` -- subsequent records with the same `.id` will be assumed to be duplicates. Under the hood, the `Source` will cache the result based on the record's `id`. Alternatively, you could use this to make the `Source` retain each incoming object, even when they would _otherwise_ be treated as duplicates. (This would come in handy when you need `def fetch` to mutate each object). For example, to treat _every_ incoming object as distinct: ```ruby def result_key_for(record) record.object_id # even if the records are equivalent, handle each distinct Ruby object separately end ``` graphql-ruby-2.5.19/guides/dataloader/testing.md000066400000000000000000000054351514115062600216140ustar00rootroot00000000000000--- layout: guide search: true section: Dataloader title: Testing desc: Tips for testing Dataloader implementation index: 4 --- There are a few techniques for testing your {{ "GraphQL::Dataloader" | api_doc }} setup. ## Integration Tests One important feature of `Dataloader` is how it manages database access while GraphQL runs queries. You can test that by listening for database queries while running queries, for example, with ActiveRecord: ```ruby def test_active_record_queries_are_batched_and_cached # set up a listener function database_queries = 0 callback = lambda {|_name, _started, _finished, _unique_id, _payload| database_queries += 1 } query_str = <<-GRAPHQL { a1: author(id: 1) { name } a2: author(id: 2) { name } b1: book(id: 1) { author { name } } b2: book(id: 2) { author { name } } } GRAPHQL # Run the query with the listener ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do MySchema.execute(query_str) end # One query for authors, one query for books assert_equal 2, database_queries end ``` You could also make specific assertions on the queries that are run (see the [`sql.active_record` docs](https://edgeguides.rubyonrails.org/active_support_instrumentation.html#active-record)). For other frameworks and databases, check your ORM or library for instrumentation options. ## Testing Dataloader Sources You can also test `Dataloader` behavior outside of GraphQL using {{ "GraphQL::Dataloader.with_dataloading" | api_doc }}. For example, let's if you have a `Sources::ActiveRecord` source defined like so: ```ruby module Sources class User < GraphQL::Dataloader::Source def fetch(ids) records = User.where(id: ids) # return a list with `nil` for any ID that wasn't found, so the shape matches ids.map { |id| records.find { |r| r.id == id.to_i } } end end end ``` You can test it like so: ```ruby def test_it_fetches_objects_by_id user_1, user_2, user_3 = 3.times.map { User.create! } database_queries = 0 callback = lambda {|_name, _started, _finished, _unique_id, _payload| database_queries += 1 } ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do GraphQL::Dataloader.with_dataloading do |dataloader| req1 = dataloader.with(Sources::ActiveRecord).request(user_1.id) req2 = dataloader.with(Sources::ActiveRecord).request(user_2.id) req3 = dataloader.with(Sources::ActiveRecord).request(user_3.id) req4 = dataloader.with(Sources::ActiveRecord).request(-1) # Validate source's matching up of records expect(req1.load).to eq(user_1) expect(req2.load).to eq(user_2) expect(req3.load).to eq(user_3) expect(req4.load).to be_nil end end assert_equal 1, database_queries, "All users were looked up at once" end ``` graphql-ruby-2.5.19/guides/defer/000077500000000000000000000000001514115062600165735ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/defer/defer-graphiql-gif.gif000066400000000000000000002403171514115062600227260ustar00rootroot00000000000000GIF89aw! NETSCAPE2.0!,&5!*;7771D7I:L=P$-A%0B&4H+5E+3J(8L.6R+=T1:L4FX>HY/D`1Gb2Id3Kh`JMNPRTVX\\@a@dBhDkGpIsJuKyN}^`dnfphrkunxq{t~aaeegghgkjmkpnrpvtxu}y|  ORSUvx{}VWY[^``bdeghikmnp8_8`:c>j?kEpCrK{FwFxGyH|J}}KPMOPS^Vb|l뇂̕ܙ蜷H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0FI3͛8sɳϟ@ JѣHҬӧPJJիXj-t֯`ÊKٳhӪUu۷pʝKݻ*ӫ_Ͼ˟Oo(h&iğO F(VhfeB82ˈ$8K+1x,0(4*wч0##XEG5iH&L6IE84, GNf\v`rWQM#@Vp)tHM)|AFڠW硈&jݝmA4CoVjPfM'A6E^j:qꪬ&ѧШ&(Ry+6 J (a  bAN+dD +8ۮJZQnkFo  uk,JP'$0D9D0w DS)( ,'oC#4C3-+0"<' -ZFB:6:]6pKETA7;`ت -hsGnZ 76,$0wPȐ,a,و+*B۴͵6nV006-D0޴33A]7Dᬣ2Cs} s i/0l^0 l/` @ ˱ `6+mL=h:Cn~0ڱԖ1,}XAm㌓͢iCH=D$D}Ӛey}  r6+`s 7W4ͥmsl p6=@  .k;)\BH*RQxEDE p92m~ F@[ݢF1Np @ ~ HD!:H\Š$$/Q.ZRA4$ X M9a69Ңj̏I5[]K4W͗uJg̚ꪻڙl]w)< wY"NP);EWdOyW<(&y{U~y>q=z $}Kzԫ}^O~۴5tH|_B %#P= _5-9H aFo0H z18 a7P|i~ ̧2V`:W@&0z ` w)8 0 x;qYd gwȠ P ЀФYL(5cC6o!K &0 #h!2 _ P G03@Gf, FpGk؆a1299VVb ` 0 Ld}Md$A bHC- 4(`0 H-!h1Xx,È91S4w31 vp P L7*R~1d' Lhȁ aGE He8qE@QX_ Y6TvP p v PX>/X J8|d 2Pb aX^8Iq=! 0H7L4 0 `0 %MefieFw!P&)B*+9 `U~1yX+E. 񐽀 Ee}y\0ه АwT9V)E0 4Kxgo.;8MY(U`V8x)'203`2)h) O \O$I ` EXy{>0 f-fD9  N9 x落a x j)I8 UC䇞7%$.9 fIe$4* @D@D5 2 }!e0eP3  @*չ̙GT:GJIl(]2Pohʚi9o3%Ap+V&@ ,0$H9`0(WHB)* I H˜N ꇕ: fiڪz:rUIA&vb*wZ&80oc%[ڬm7>`! 11 zg`~b"r: Z=[Y0 `& $lZjKAkJ1}Bg{ [za:8D1 qkX'Z۱,- A*[Ѳ8 H 1l9]¬6[9[4?SfJ(η !}3a}qh :Xb9+ д3r~>0;'~jz qɩiejp0kqqApx`x b {d#;:[W Cf|`|)[18Qwpu@ 1w``{۲Ks#7pvY](M_Ag蟶nr8k*CʇaAs0t0 Kv0AQpk&B+4p~JpxJ Ny88kZ+芪mr0?w`l0HJ<n@ bj˹\ ("P,f鸎1=1h ()kH * {sp v@@\jF,{t@[+,pD<qfbU*ce<[f/Q^ A9܂G  ! ؙj`Kv@,L]Lɖ<\,ēl#5U@yBq괽l @Af ڍ}G00T"z*MzR)Jüة8axpOLw0hvVun>`1;@֝f|$q !r@w0a &*Jj諾Ե3=bh+=b_I6dU|WF,)ӫh*KF7W"+=.ni~=&9\rvO먮2v~,O>]unj,$g+{_Y.9MhݒRAf5)o-$%Fn9τ;Q}W}ׇZ˵^ ) 'g=zz y; H鈫|;d6_ pAb+/j x  e*1(nX[`.[Ǜ9`d(&F{ّk3~X\8 Q (@[q8e  A`~+,-,V,xH7 6DPA .4 D*qp A0?dcOr(A/# %t܂0$Ǒ?A$M +iO@ 3ͨPTPEJ@H`Fx%N92^p)!{ꅋ Re S*54Oɠʄ`J12LXBSYc vZjVAv[n[p"k7 1Pw+HRK4$̠Ri$Q_{xU8W;@o `C9Ҹc۴\SVyeuecyfÔ G$QA"q! Z!( J&zNٲ{z`Z&i v)5K_8:0Sb5H̅~d{kko|pnFeIQid @!D‘H6f0n2Bĸ2JA L]t5x!H7ݸ@I z?U2f;"#*42b8>H0|Wpw}5s aQ@ġB $Ɂ2b #b0H `2I*/t$ $ 4/xgI "B.xQ\!xOUp(4̒Aа;a{!RI}Kdb?(FQSa旳9 f֖/v&gP;$%+1@r7Zc*Q{QҪ 0HD&Rlc#HHj "-yILfRd'=IP6h}&MyJTde+]ʽZTe-myK\e/)j2$f1>_&SY:3@Ӥf5Y#ffS|0%kS%7yNtɛ'9Nx*2g=!Ni3g?uiOTs3!"v*(f<q9ނRH4 ?Qf-@*$oYU\jj04I4OTpV 1C 8a XQDzUҧZDp@xTHl`[l֬b s݂ZǸ) R )ĩ!YUĮ,e6zd &B [$/Hi$^O&0 +wmdB HতNz Ƞjv H`1!=H*+t`{]v,Z&+@ PȜB.rDj  I~㨷%/yBWܤMrm]^^(FAR#@E*T V`$!,H!Su-ܙfXßn{^ >KOG$BRk`c6Mo~ĸHGRH GQNei<9Ozg>{SH@f6e0tY"ЃԀ W AY#eu;hTdIя{$!.k'qA|,= ϲTTÃXX%KMaәBvM+ v{S|'YիߘT, :p3BkM\թ^|#"Ɇ*U ImdGkDWJ,֕LgYbx>}m'kCujt;prz$֖[BŻ&$N W(Pa +C)[:p4]oK;" #=T;{@C&#|#C Ԉ2:456z829Bw 9H;cA@ɘy9BJA,(t \B&\ DA)9#4(B;+& C1 '.B4LkBuC83\g: 5C=,t7C@29CC<#@lD1?@DDJ EBGDDJDP*NDS@NdB EVlŃ`ó[8EZ TİUtE]4/E!´EEb\[\EeL B(+,fTbt;YL\RcҨeTjtATFx3mCI *Ё ,: jr,))*҈c#R*r<tܤ`@zG̰yz| A9Aǹ`a"++ȫȾz4Ȳ(,T=$|ǒJ:aƳ +9H,*I^#ΣJ-bѢ/-JhKFK-8"**<.oC2.rJ: 1(;[ɼI̫JЁ+/ࢲ\* b/p/;?//8 [( !{0a5L,@ "̲;L)ϴƔlH>A@8HpDX{Iб11@ BS|Ka+?$6!60y+8,cٲ錧ЏNP+y3(=hE}KE}{ BςQJ zsڒ.qNS4NRSSPrP+(e$4 uٜEyyLa6X bی=T<_;Ӄ#h$9nS.Tq8r}RE,] OT*XU!Ӄ(}~񩀋<34;K8<PӇ;0?Ÿy=p#8DMEUnjTNx 9,=P1 ;yϢMh]hPs8AҭLn;[UxUv&^--xiDǁFp&VyC\]]4]#=e]m׽(]h_8H ܩOpH^/Hȥj\*p^_ ^ɩ$LBp>)X-6<+<ވ-ʣ 0``c`" i32( TKKET-Kת:H,ˈƒ4ԃxxL!֤"c_QAʹ*=$D:!͓/ldS/D$p*Ķ DNl ;$=^?9b{=;៩\e ޣ Ӄp@G2X56y8mFSeS!]=`W%n?uhh׷Kk&N ׾BC&8].;?[c{<ʳ3<-MX+Yك;Y=E m톌Ɩv*u>XqE >KZ CsM.@!xH!?꿰c([$:@Dn ўﮖ}dhp t ܄ܲ@7S qnX\qK腐Jͤq p/;= ? oX`"r#r9ὦ6vT''7:^?r1+gwR.O0'/_s8$2g,U5W!/8sYj:7;tRqSTuҨQEu߬u'u@ } - Ψ;vaq$KOtvZz}-`0^HMaVJ VJv8s^u.frmpb*p+w0ɂh>x&~Jx'h+J`b}B7Àu&Mm|й]*nKLKU0+T3P4L".759~:ȒGB@9Ĕvqy>A@Ei׬M/H=J Ki+td0 à z_*ŭzn')*+ErͨԀ1^fO+fk 4{gNe({2گP-go+||| DY6FBdԝ3Ŗ~<{_sTI^4L4'5&_(G"+ OB$|Wx(h „  hI0xpA/yW/_@*r1#BLh`㖃=yS*!<Љ#@xcT;s޹sQΩjǒ-k,ڴjײmm;ҥ{c܅ /.l0Ċ3n1Ȓ'Sl2̘F9/hC'`+ B/t1HbxPWFmD0LQe!q}3A\z*W*Qr(}*inƻas5;Á5ӯo_? 8 řgݧh-pEQq^1 %I! ECe%`B#!P>4qŵPr'c (vuPS]1BF*{ :$QJ9EZy%Yj%]z) RiQpE1p 2qD$+(t&f} 3''1 bWО}ҋѥ/dLZPzP AAr!C;.tid O y*Gt@aa0&*,_:,J;-gVy`hy;8"ph",8% aP eF /Bk/(H }B{pA^H%m@xtlwhaB,,&93kY3A =4E󅭂;jQ6=߉pE u[BFvqPBm=}6i#d4m6q7t4y}[~9kJ7N9衋>:o~:ꩫnx:]:>;~;뮸Y & ?<Ӿ;+|~K?=f3=k/B_=?>~ۛ>wl>>z3έ?'0h[XJ,~r M )+ C(-PY d z@II D-/ pj)X ~Ag!Z9 X2̃V-:Znp+ VP}2 ""QCcJ:=nS# N*rĄ*uҠ(Z+~#8.Rtƴ !ꃂ%D(0Alk z bJf@4R"a %B EK.Лi @Y@e A PU$:5yu}6y ʂaAJ$,aSCV&a*(H)a&LcUX2؛DmjXR† ]+IMƔ@)ҩS*>"ܳ cDDOP#^X<%E+Hzr 9B/R ajV#Sn)Q/Ks6jUAn U i"Ъ& x0 p!x)frA+Кecڃ"(h@? ʤ# IAFҋ7:bK`_ O 2He(?BRfJH* z|,b!/ˋ8yP 4CJb /0#`y7ot$Lׂ\G` E< 1e0)fOӬfսJE z ehE*H"|,r,,3jQ/`G:2i; I=̞͚C-j˵Io. T Nr@@@DjY'L PB GZRXl 2EE Ub`%+Z W՗}ma X42unvҩ+TP"k{CHD#:b^׽x~+`3xrB%F1X c6<2dtxnAbvsj *oMm6t-9΅[T@ [UZֶR|-lc+Kj.uuVOo:x=KYW1®ǯn;'&ϥv;7O^.?|d^#/(>[<3sx͓<;_y4S/ȯ|=s﵏u/ۼO3Ё~B&X Z$$~͏}'5l <}z%z RL2rEHDABJd-wT y-`uE @iļYZED48#(~0NŃ)D!!%#1]`9YV|R)A<# DR, 1A_o.D/i 1 2=B0Ӡ1THXQ5a6 8iE89 -@ QZA}蓹ƅ@Bk@D%%1quD EqWJb!HIUwTJ٢MLw 1Y!)A\REREǝ!@S YAWW XYHpZiU8#+"\`A^y__eYca%bV8dIeY1z1.$z%# Y0=@kK$@A ՚klqnaU/VB o%:DH @I:r)18QTLquySAw@x%d1$SrCD\I4b$B$@E#D+ё% ptKX6XhZ,zz]6f*HjA~*i*>ި*j{jƪ**ʪ]*YPY+ޱ>XkVNA+bΏRPYN+lkEP+k@j績2_B+ͫMEFN$ż'F, ,M*lZ|JMŖ%~ޤ&"bd6,Zh֬ژ,ZŦ"DpJ̞'@mbnʮDԢŎ^m.֒Pӊ^gNYa-֮-זةM[LؖlP-ZήM&ƝOg}BHnB^".~.,kZ(k綮ց.vQzO D*_fh.}[>&oe >*J B/zn/cTf/&-/bx/aצ/*+ʯƯF/ӎ>./0.o70!p/:0/ӯooz]0+10yfp / ' ǰ嵰 O 000?$o Ko1hO7+ Ol+oo1n'_:0- 1k.q31 î 22"".#7r?F$0%+%_ grn2''-(2ۖ)S0**r־2,,k--ײ.//+/2s311,2'3.63?/44OVs^366S,7ws~8s.9 93;;+<3γֳ=_->>s3@׬@c+A4&BG,C?C?FN4EE+FgnvGW+HH4t4JJ*K4ƴL*MM44OO*PuQ(RgR/5n65>5T+T?*UW51^fV W˩W555YY(Zu{[\\n5^^&_5ub`ga#a&6S.6ccc3$dGM6eqfeC4fcew}6hk6hh*ich6 klǶiw6NvʶnOom?07q ror/75s6tts_evS6wowv7y77z'zywf7}_}~w:7Z"^x$%&s?RFM8Kgl8 8 )Ӟz8u8ÏǸ̸ո^/鸐Ýy%9 9k?y]'dݒoty}9sdՑ9׎_5\999𹷠\',5OWIOwp:ׅyO,㴺s67##!z: _v"/5{nK{wS{C>2Fwmp;븛٬;{;wV;{;廾g9Ce~b/>~GOW>_>dO>wO+~x+s+⯾=>:>>>g ??k',6>G3Ck?c_v~?Ǿ#9#'??? 4xaB 6LbD)VxcF9vdH#I4yeJ+Ytҡh3iƴyE;yhPC5ziRK }jTSVzkV[)ڤY)Naɖ5{mZkٶukk\sֵ{oެ^F{kp_ 6|qz7vrdɓ1j8b͛9w4QʣI6}uꈖvlٳiwnݻyz&抶7~ym˙7w1p_'~{vIw|x҅w~}zw~g~s; < |N -Ü ܐ=0#м M<E?\P0Ssutƿd Tl"<q&|-$JtI(ܒK0,<46-&˛H:7ܓO:kN.<Q1dB}D%EjL5͏N=eR0ݔRT 5QM}VTXXuZ}[˕b dMX\=h\jJn'pkg=ݢ]ݺ%twvwo#~ ־߄k6I]|;x-[EyOUIx)WyWmfdgSo᜹+碡i|Fjg.4]Nj3{ٮ%:%_ mƳg}{Fo/:>|һ1DC8n-2'/|?Egs<uF_{:=cv)] vuދw>tߍwx'yՏoʽꑅ gxp_/W-88}_߿c>c~w~Ż!?z  8>p>R@ \N ~phan8p Q#Ԡ#PaSXCİ0Y><U9Td"fX"Y@  ;xEx-W f`#,=/Wac^Xh1!;E ؈;.^ $A C<7:#$BX1!PcjH=2<%IC$ فgQ Z(KYKpM.hK_$%/_&t-:C<"$<_1iLd^%j8\M59=l$B"&:9e6d5*aMiS5a= mgLc)P^!D(ST$CurF'\|V‡>(<;y4Y|@ykKESBj)OS '$*SfK dSaԋ UjHJfU~[iWUzQ%@zVkmYzDTmJмu)k=q,LĢsl.{t,6/ RvJ?siZtk#[َtIlkoXᒒq[,Vc.%]Nwյ`;HnseXVry-^eoΗ]}?ne_%3p. v j\W(a 7#|Z! h3?8(,b0@ }bOqz(gHf@2z@r"X@eXDSo|Xܲ@@Xb/h>a-|>}\g3$jԨBJ1]Bfk@)'Jot7Iӻ MsNuWMQ֧A\k+#=@znyM<_lVهis-l j&u8Ov:W72ܓu/ H2A/ζ7f\X౏<)^q_9q!I^r)WYr\ +yV. t&zڌ~t|]:nOUձ6oRzןqe7;Оva3uGwόUx 3|->rw 9O>aG1/zˋ~C;p( ,@ 0?A^zmh,@!%=Bb`9p,/ >A# |{ %eq,ܐdhA/ ~O} rN_}j`  pj` "pD@P o `` j O)o /Пtpo b`p4P0 É=b Xa Ih@X|JʛJ MD =^X  4pX M* p } ;nvpk.fBdA  lP`!DQf D `|@``@PԊbdN0֨`Pp!H`5#=q@Q u0 Bfc @DΨ0\}Pڬ-J PD`@Hqb11@QhB͐|ı0O0)f!$O#P }HП$O T%!sj $ff \r pD8r 0v Dr |Lo`n@#K(ά,m`h|P`0'ǒ,(2-(q ){N)RbRV*Rb!H1lp@ C0 K ۃP dh@4/(M5@5(Gqreo P-SKQ \ ,Pb 5/ |`[ /2g` )_! ڐ^gR$5\%:T0=b$;E>AM3Ml&@s)Ey*HOEIJIJUMP[PTaX[d]`~cmzzz/O9Z8_9`;e?kAj@lAnGuBqEuGxJ}I|KMOPQR]gh}"4;ABA;4"447B7FE\\QPCȋ[B[OYNYMVI8\Sd .JX&.T>!ćd>4TԸb$\, B+PPRK*j BUOOT ,ְrI!#%H~؀U.-X WG"\Ҁ)In^\` jRF>\T)bIFHLL˕%zUInJ@:s*DQ(TppaL$qZ(P`ؙ/n=!,+:  !)+%5(8%, /'* ):6$+;&.3/1<15???F.C -A&0B*3A6:D;>Q"4\%:T0=b$;E>Al&@s)Ey*Hy:RJIJUMPQQQ[PTZZZaX[d]`zzz/O9Z8_9`;e?k@lAnBqEuGxHzI|KMOPPR]@*DQQPHA6QJ?3(QE%?9QK26@<<<LO4CQFNKK/",QEOL4?QIވLQ!0<INQQ)2=$BQ-Q5?phD41ȇ G \\TPH> J$G8z{n 6os+8`p3ǠH2@Pi ,HJ5邫S#\!, ..  #(<* .'*!*;3$*:%-2/0?*2;15?6:???8v:{A,B -J 1N/;C;>T#6Y!7X#8b$@h'@n(Cx,Jp8NMGJJJJTLOQQQYPT\YZc]_c^`ntzzzADVX=` lpqloqty{,|D]nUpvsyx~VY\{be6U6Y8^bA9< #0sv2p;t;*9t$dq'E;'Hw$M28,WVM̎31ZcbpK#EwkJ;hQfEKUWq[œQOHEVeUG{; [ʔ8&$pCpJL"BdZaOy! *Q~\-8y(A%钞&%-||[Rq%w"p(j>mx$LQ1ӈ=IaԘp!mػ‡ d08E$Q*0BȇOSa@=-<![X[5TR 14Hn(R ,793P#$s┓bDCMl"y@Ms'Fl3EKyR0@ (d@bVH1Ƞ*fpb,8@/0A 7 "s1'Pæ8P 2DtT) >!QYG,Z!,#"mM  %, !*;6$+0//;&.3/1<15???8v:{&.@+5E.7H2;L6>P6@R9BT>FX?HYF.C -A&0B*3A6:D;>Q"4\%:b$;E>Al&@s)Ey*HJIJAK\UMPQQQ[PTYYYDM`FPaKSfLVhOYkPZlU^qWasYbu]fy^h{aX[d]`bk~ntzzzADVXlqmqloqty{.}L]BayWdmfphrlvnxpvu{qzx~[\be/O9Z8_9`;d@lAnBpEuGxHzI|VKMUnOPPR}ϮйĽhQDH4l(\ȰÅl4Bp3jܘ`Ǐ CIɓ(S\ɲ˗0cʜI@%N>ɳ!'pѣH*]ʴSJ5vׯ`Ê;gճ<1u#ٷpʝUNx!RSmF LI (ɲoQ#K4 bi0)#ϠCx2Z6J(SJ Ǭ` _ R0?4`( pRxLB\ @ @~"`>Ƚ S ABa7 !5p@,^})L"0/ GEo6@^-v/H> La HLFo>Hh0(@r,?*ĀO *`CAkAgL@ 17NAP  l0ҍ0HlJUF;!,FHfQ ^Y/`%QjdHPFGRR>F*L?V2̲c`@d63G擋\V`zi8?t+ ɾ|#9 P% ,#YF! /fMә&T9ÔeDdZ96+ʭ,qG7z0'!) \qgD r, 9R5{dT .T3ͮN1}+n׭321ױKAJemQ;elPѱ\)DO;7b5mh? !,vM7,6F.7H1:K6>P7@R9BT>GX?HZAJ\FPaISdMVhNXjQZmU^pV`rYbu\fx^h{aj}enfphrlvnxqz dihlp,tmx|`P@HC@+X2 sV@)&ZhR,R/P)-Di"@4tb2Pp0t2 to3O /i3sz2t1d2yt 2 v0 / f, [t )mW & * ϧ ( ٮ|./ p!,"r}&* -$3(8 ):777+5E-6H.8I1:K6>P7@R9BT>FX?HYAJ\DM`EPaKSfLUhOYkQYkX^jV`rYbu\fx^h{`j}joznt opmqz{-|S_R_dmfpirlvnxpvtyqzx~HbVY\{beYZZWήйD5$$"(:;<;:("7CJ$2;2IƂѫ6A9<9A1ǝN !M/GGpҥ#. ǰÇAEPAD!2 CLP$t`Ĩ & IP 1pe$JDyѣM9)DJJRM >ZׯȪT`ӪU+,hʝm%PW/%*KL]7Y#KS񁉇n\֩'A!=6{xׂ8hT~]aQj}h"M!jH'RnȠ4:N=( PBVJ:K>9PQhTCG-ie9$BP%H|d:3!#ÌPϘeƙ 1XC6kr 1$矪 0PL6:&Cp',B%'\BBIAyȩꪨĢ*+5Z 5 ~:뱨LDeQ0A.쵷T,b-(܆~"ŖK.VזbWZ472*1Cf|0+0,\ǥl J._ (3<9ǹs(, \2 sJ. A0d@5!Ё#smC,4(5 oL ՠ`w(\5 F54ܟ88(ؽGx0O0)Op8,4 +^J + rޙ{m0P V6饷};~v,\@u>솟b;O+M|{cy\N'V.J [o c.׏)(6>pͯpS Vne>i[B P6@R9BT>FX?HYBK]DM`FPaKSfMVhOYkQZlX^jU^qW`sYbu]fy^h{bk~jozdnfpislvnxqz@pH,ȤrL8ШtJZجvzxe4n|N<}zMtC uxogW %%! rGBɶ##q{## ڗ % sC#!% #D H.g, pNj ȑLx ˆ FXv,4vI3˂ 6,dhZr! aAfͣH,VYxJdbnJʵo#vBZl:4p9V +SSDb}{ Lp/ bi*9~t` JH3o[)RH ,XkdyZ_Q[Uk\jmvҁm?@{-UrĨ/Kϩ^"+6" Љ'Ooꥑ['"!|Àqf@UtF(߈0k_B aUZJ(b|CA4cU1xPf@^1|UW~oL_:`K̇oDp^ \,Vy_Q-i@_F"Õ V1fc ]ZtF' @m#${Rd :fqH(6fJEh饳h*j 0ޢި&@Iʪ2 ~b+ KNګ7xɊ ](ƳF攰Ab6`+nFV!/ŽS';ap #pk 'K<);|kq!<2%{2),(,X'-r54,y䜯,)}U1,tDxL.ʹ)(UUU ,uMLdyJoݪ,CiGmق"Zۡ❷޶^ͷ}#9 h}%ní9Η/N5_ 6Y:[=[:DsԏϮ'a٠뾻)B>Qo9 Ӽ<#2-yk}>>;k>[??|x?@ +@  38 r0: pB(Bא9! ¥. cҰ&B7=P <0)u#(Iap'JBg+N#H"źP-@`<F2Iֈ6BȀ;5ov d>l [: !";5" A&*N&Rn@\$(yMjFT%;By)YrE*gKmB {R&f"YLf d)9Y)}*P9ͯ|'B=AЅ}eB#jRZ$,Fe(H)ґ&(JS:ЕԠ})I%(SDpZ)NҝA>PZ C=*Ta`jSTժDjPSԫ8kM*SԬ,EkJjRԭ kGQ^ԮkDP.ԯlA+PӰDl>kOӱl;%NʞӲl85Mnӳ-3EKLҴD,UJ~ҵ"eHѶm-uEZѷOCи4Dn Bй(n +B~кnkANлoś@мDoԛ?Ͻo>Ͼo=VϿ;Cp|9Cp$ 8 sð45KKD3)CqT2wqd17)T~>̫O=rNdcnj e>b2z-wA"Ӳ*1;fMVf3_ov 69e=:.BBLg ˊe*SZ͏reʝ6/]I fUGjQZSXF}j*`+j}[ @un _:ž '[+gGan-lg-rS;~ͭ퓥[>qk\N T   Wz&@ pt+@Vo 0*83.pXݍo(LkoD 𕏬F( !, Loc!*;&.@+5E-6H2;L6?P7@R9AS>FX?HZBK]FPaKSfMViNYkQZmU^pWasYbv]fy^hzbl~ntdmfpirlvoxpvqz@pH,j:ШXآ`zxL.znp0'rnvYpx  } l İXЉ ^    t`:y GCO"JfI# CBAW Ȓȗ0P‚mZ<f s5AA5;ɴ))\]FRXRpU:p&J4;L=S:ʝ ]ՠG*[7J!h! B;Tn1\"R6<8X' ױ|6ͻ`uA ǖr@AE>@ɺuf@ ϣh81wMw"4]a#*h`u0PEHw,߂C!}9$qhPXPP ,q@y@eaHu!ƹz)ګ,+ * *%:|/J'[ *k {1ъ<2ǚ %)1 ͺ9ϮztqDm@:1ГvjqGG;2jGQE|k>lX;Hs]hW^ 'CEba\nu;b)xmR658?ָb(o[e9k9Z9yw$ũ7 m A7w/\>J -b=&dOc??/x??@ p),P QAL| {s  C.&<\R²Zy! }6t!s !Yd(Ę/QJP 0Q QI|D\#IJ|X=!`<ڡ1ZھF<yTX:&#ǍG9zn<䙂)G9n$'gvD@& IN꣍cVidzS &c)yYvv4&K:",Bwg#zq KkB!6SIyA@syyғR3O]𳟦'@I!ЁB2TZ|h)*Q(Fэ*m(HC ы#=ER})LE*ә6UFsJT >)*T/zH)Rpԥ6O-jT:՟VWiVmՙv_miXU:֓g iZ=֍oh\%:ׇ֕wMh^ ׁg`9{gbݹs6֛fd9YdVefcRvvdh9Z?}cjѸZ0bl8"Vanyֆapa8W=arI6׃`t18 V7m`vv7_x77~m_zӷ^z}^|7շvM]~Gs\7`8ov[Ͷ5jfZ6aUgYaaue&YA6bc'X!ba/&X6c~_7W~)ueq~ 1Y]G%3^N~r\.*7,Rނ,o`]54)'9dj^sO7W-J"Zٙf=ʆ> @=KhVjivjN:jkɺjVk:kkkk:ll l6Vl;mV׶f!,YL7,6F-6H.8I2;L6>P7@R:BT>FX>HYAK\DM`EPaIRdMVhNXjR\nT^pW`sYcv\fy^h{aj}dmfpislvnxqz diAC{p,UaeIPlcX qIZzE cm AJCpX0$#I"hG D VP)I IUG _aH n)~^ g_HoHC QH3G3gA z//J ] h&&O΀.!!-,xD,6F-7I+:K)>Q07H3:I8>K5?PgZk\avdzh~u`zb&GN&JN(AK&FX%OP%K^(BT$SR"ZU=@L2AU3CZ9CT>HY$Na"Qe!Ui Ym [p7Ia8Mg9Oi:Qk;Tp!aXBENDGPGIQAJ\JJRPOTSQUXTWYVX]YZDM`FPaJTfLVh@]}R\nYcu\fx^h{b\[ja^nd`ak}qfauic{ne~pflvyp{CcEfFhGkHmJpMvOzP|dnfpjtlvnxq{gjemo s v x} y}   ÀʃԆUUWZ]^`bgijpuhyk}nopquwxz}~ġɥͨЪ֮ڱ᷍㹎绐뾑Ĕǖȗ˘1*\ȰÇ#JHŋ3j@)58Iɓ(Sraʗ0cʜIS +jɳOIѣ27XӧPXChԫXa֯`ÞZuسhJTT۷nKn֛9݋KÈ_ޠb%ǐ#KL˘3k̹ϠCM Dmy G0kز5zڲ< agg2<!!>]#gM ?X)C'o>arGt@Gv Gyc0 !!!xPx"xV0[n&!@xd@|yс}0HMaeUQ@"tDDA"*d\q "QqTwfkd$`N&fG# &Ugb1G G Q9"(HCD"Zs|f!(@Gj CF$Ő}'*e,twĪyb:,.PU(sV&A }0 }Ar{aUkI5v+kWDM Q@ 0FMH 9"D+ph}D!}$C$DB!\rFbH(d "9C;tI( A7|I" b  C4 } B$J !Ä!\g\"pdC@TnE&Ep#؈ RM D(&=mI {#B0! ᇚ#B"n \ k I &P0xM|AH ^cE!ؔEDKt '(LvD@Opgv~t` Bpl`( ZckxA'F@=+@((q]O!pBUR dd(6ݥ`s+#bBq!9 Pܣ7HS &8E.(MT-T#PPL'lR"T_ Tσh)'By'π /$ ZIO0ԄA65B(a 7%@PF4:( qە a6@3[D忋t!8PMvDA F x^?"r-@%22B*@BW"fYt!FyǯҚMjWֺlgKۣPa 7-jp+6EBH~ C7+<]15 >\t .ij4h܃Ѐ7݌q˜դ73Br`734|3VBnm1;'La&6q X. K9c 39Jx!ۑAF,UA ppE(ĸ;g$FOBYUc@$p pFϷ` @ \ ,s`eL` :}E(ohB$,aAcʐ&8t<)Ё_D@RG@ _ L : wAP:TTzTM.@pb tA C怇/HGcv#r5wp WX'`-d%( 9ᐆ<ïa Fm%"d"'ݯւjXէNa;oM 8@p%I0wB.)$ WH D?Njyz6Bڰra X_l!<*CxsoԦ35DA(@p$3D \)Pywɞr'ַѣ|*:jUq7*ÒXAC̐pcV'wȭx8j;8 Ws D[pЎu7 u=lNY!Ce vD(u@ B L=+ZQ]jt: Ra? fq'`/ր8Xx( 5`X_h5\!UjA^(ᵂh]iq]%a]1haт:*؃bq@ECGƁKx!<,jI,6F5=OCL^GPbISehrlvr|,s`ĉhhl0Iixw|;ѩI!,,jI,6F-7I,:K)>Q07H3;I8>K3>QgZk\avdzh~u`zb&GN&JN(AK&FX%OP%K^(BT$SR"ZU=@L2AU3CZ9CT>HY$Na"Qe!Ui Ym [p7Ia8Mg9Oi:Qk;Tp!aXBENDGPGIQAJ\JJRPOTSQUXTWYVX]YZDM`FPaJTfLVh@]|R\nYcu\fx^h{b\[ja^nd`ak}qfauic{ne~pflvyp{CcEfFhGkHmJpMvOzP|dnfpislvnxq{gjemo s v x} y}   ÀʃԆUUWZ]^_`befhjjmnpuhyk}nopquwxz}~ġɥͨЪ֮ڱ᷍㹎绐뾑Ĕǖȗ˘H Vf\ȰÇ#JHŋ3J,‡CIɓ$iXY˗0c̛8s܉PΟ@ УHaeҧPaԫX:a% `^AŊسhӪ]˶۷pʝKݻx˷߿ .yc mpD1 P*fX#W)5ugA7 >Tj^&׽+)21 PLE2R d)ͥ`S+"bGE0q! ʇFS0╯ܢ58~3 7C&(K TA$PPJ'Q"|S ~T́|))Bu0)ps $YQ옴A.yJX $$hI05MV1h(AAds).F"#򱊝(HE*DLm(GL4B~E"RÄ0Z b-"EDAMk dH'+p UE_ t I8cE ի@\#Չ >@K< @kbİ!,-9Ԅ[βO!,ͬf7z hGKZ+\jϕu]lc*{Ѷ Wk)`1=F }F$ Ëelc q椛,e+k^"`f5X,h?CD"BLۮZǦJ!](Llo( ¶wX!7M M8F=(pAB8AfIt%Nyhg8w+b1uycȞG p5{%ΰl?2<@S}oA">0k`[*8ЅAu5A&)jxCw<Ąl5! Gm\:%gE,jDZ*FgL#Q1s@ip`8Dc?JN4!;hXWlH${ J>v&UIPR ]~="*?,t2F50}ׂYomk:w6AM%*™9!QahwFOҳ{gC̉Nuf\JKL ES}-+W~G ~ 4'ta9 r׼b?+4P_~+A${~XOϿ_2 D1g{!$x0f G&Qz#(o p*Q`qwq xq FЀ(H@ !@h!1*&wp`X `a0B4107B18*e$ &+by!d&wpupqb0^u-"qRRr c0 Ao&u耥 b!x@sCx!rzPqNQ8\pv wЂCR#/"e!EVpy9q q#$Ea(X(Z`茑hbA$NPBqXxl"G"(#!Xhиь!yvPX Yc'kPq x:gG/x(xaДa@&~'r†y@"Q Q rt'&i衔؍QYB(rm0 *%q)n`Lb옊t) is$Yp]w@Ÿ`JYP,6iH*"d$уq%HB0dG+w*2H@$P,@tq+0R' q9YY~7~z72|˗1{Ǡ1|.a|J-.QZ'!Z%j闢!<,jI,6F5=OCL^GPbISehrlvr|,s`ĉhhl0Iixw|;ѩI!,jI,6F-7I,:K)>Q07H3;I8>K2>QgZk\avdzh~u`zb&GN&JN(AK&FX%OP%K^(BT$SR"ZU=@L2AU3CZ9CT>HY$Na"Qe!Ui Ym [p7Ia8Mg9Oi:Qk;Tp>Yw!aXBENDGPGIQAJ\JJRPOTSQUXTWYVX]YZDM`FPaJTfLVh@]|R\nYbu\fx^h{b\[ja^nd`ak}qfauic{ne~pflvyp{CcEfFhGkHmJpMvOzP|Q~dnfpislvnxq{gjemo s v x} y}   ÀʃԆRTUVYZ]^_`befhjjmnpuhyk}nopquwxz}~ġɥͨЪ֮ڱ᷍㹎绐뾑Ĕǖȗ˘H Xh\ȰÇ#JHŋ3J,CIɓ$k`Y˗0c̛8s܉PΟ@ УHeҧPԫX:eJ `^aسhӪ]˶۷pʝKݻx˷߿ .m D2 P*fX#W*6g8 >Tj^f=nBnHA_jmӦ`DHA @ s/^QӪm٣G92ٛ'z3 9LSxqFf|&P@ftP݃NXCU`QED4[v'{!GuG#D,`C*t[8haG}{ &j}MhF$$hɥ(H%[vԁ%1@Di!&qB:p0QQSVk[r6Z!(`lD`FD&ZvĢ{j9zW(Qj%A }0}@W^jHdu6F+VkfDN@ @ɻA.IKI ]2DˮV8#C$EA\pF* I$]C@ r9T@C";A/޸VI"@!I dTx 4A/׽+*:1 PTE2R d*ͩ`S+"dGG0q! ʇFU@╯ܢ58~3H 7C&(K \A%RP'Q"|S ~T́)*BuP*ps $YS옴A.y$bK` %(hJ05MVޱ)&AQds*.H$򱊝/(HE*Dm(GNDB~E"RÄ0Z!r-"ELAMk H(+p UE_`t I8cE ի\+Չ ?@K< @kbİ!,-9Ԅ[‚O!ˬͬf7z hGKZ+\jϕu]lc*{Ѷ Wk)`q?F }F$ Ëelc q椛,e+k^"`f5X,h?cD#DLۮZǦJ!](Llo( ¶wX!7M N8F=(pAB8AfIt%Nyhg8w+b1uycȞG p5{%ΰl?2<@U}oA">0k`[*8Ѕu5A&*jxCw<l5! Gm\:%gE,jDZ*FQgL#Q1s@ip`XDc?JNT!;hXWlH$Ż J>v&UIPR ]~=,*?,t2F50}ׂYomk:w6AM%:™9"QahwFOҳ{gC̉NuflӞFߞ;pD;ҝٺ~*h{`\{7!`#Sb"WuH'yb:sy/:26;C;@;hJ=`TcM$0+XdrH'U6fH!f??BHfd@ffD`9tiON" sg1B%tB6h:/FCx>ma@VJT`4| P$jSTEٖE?8FFFk8kFd&pGfJ qE#~2QlHu4Gɶlܖa61VI{ITG&nq궃n;vKTo8gLT w MJPqsq 'N9qgodrƄr*w/'PiP0s s]xst%SXGSDQPRQRtHאK7u/eu Z@]waCJ0sTv;vQ SVqZwgb5@z'uwVX3xrEGWPW ay%XGQy aZ\ٕ^`b9dy,VP8Pla2jia&ty,tb)|)y,0+_QYS0~X{9,79v9_CqriBQSnYE@!YiiǚQ!zYyٛ11qY\YBp#ma9Z19!b&^YAɜQױp9B!H@ !j!1&yp`Z a0b1P7bQ*g$1 &+b{!& &7yJIK wd0`w-"qrR e0 aq&7 !z@uz!z qZ^x@T* yVY*вCR#/"g!EVr{ s@n q#$YJ\1U SʤςA$NPBsZzn"GB)#YjxpIz̹Yc'mP3**zi0 /ѭJcc`&@'r{` "q q tt'yފSb(o@ ұ*%s)ppLv) ;x[y@ [p\,I*"f$q% Hb0&G+qw*))jM `ʰpJ,`6+0(@ 뛰;[{Wq'()$+%Ӌ){λKa+!<,jI,6F5=OCL^GPbISehrlvr|,s`ĉhhl0Iixw|;ѩI!-,jI,6F-7I,:K)>Q07H4;I8>K2=QgZk\avdzh~u`zb&GN&JN(AK&FX%OP%K^(BT$SR"ZU=@L2AV3CZ9CT>HY$Na"Qe!Ui Ym [p7Ia8Mg9Oi:Qk;Tp>Yw!aXBENDGPGIQAJ\JJRPOTSQUXTWYVX]YZDM`FPaKTfLVh@]|R\nYbu\fx^h{b\[ja^nd`ak}qfauic{ne~pflvyp{CcEfFhGkHmJpMvOzP|Q~dnfpislvnxq{gjemo s v x} y}   ÀʃԆRTUVYZ\^_`befhjjmnpuhyk}nopquwxz}~ġɥͨЪ֮ڱ᷍㹎绐뾑Ĕǖȗ˘H Xh\ȰÇ#JHŋ3J,CIɓ$k`Y˗0c̛8s܉PΟ@ УHeҧPԫX:eJ `^aسhӪ]˶۷pʝKݻx˷߿ .m D2 P*fX#W*6g8 >Tj^f=nBnHA_jmӦ`DHA @ s/^QӪm٣G92ٛ'z3 9LSxqFf|&P@ftP݃NXCU`QED4[v'{!GuG#D,`C*t[8haG}{ &j}MhF$$hɥ(H%[vԁ%1@Di!&qB:p0QQSVk[r6Z!(`lD`FD&ZvĢ{j9zW(Qj%A }0}@W^jHdu6F+VkfDN@ @ɻA.IKI ]2DˮV8#C$EA\pF* I$]C@ r9T@C";A/޸VI"@!I dTx 4A/׽+*:1 PTE2R d*ͩ`S+"dGG0q! ʇFU@╯ܢ58~3H 7C&(K \A%RP'Q"|S ~T́)*BuP*ps $YS옴A.y$bK` %(hJ05MVޱ)&AQds*.H$򱊝/(HE*Dm(GNDB~E"RÄ0Z!r-"ELAMk H(+p UE_`t I8cE ի\+Չ ?@K< @kbİ!,-9Ԅ[‚O!ˬͬf7z hGKZ+\jϕu]lc*{Ѷ Wk)`q?F }F$ Ëelc q椛,e+k^"`f5X,h?cD#DLۮZǦJ!](Llo( ¶wX!7M N8F=(pAB8AfIt%Nyhg8w+b1uycȞG p5{%ΰl?2<@U}oA">0k`[*8Ѕu5A&*jxCw<l5! Gm\:%gE,jDZ*FQgL#Q1s@ip`XDc?JNT!;hXWlH$Ż J>v&UIPR ]~=,*?,t2F50}ׂYomk:w6AM%:™9"QahwFOҳ{gC̉NuflӞFߞ;pD;ҝٺ~*h{`\{7!`#Sb"WuH'yb:sy/:26;C;@;hJ=`TcM$0+XdrH'U6fH!f??BHfd@ffD`9tiON" sg1B%tB6h:/FCx>ma@VJT`4| P$jSTEٖE?8FFFk8kFd&pGfJ qE#~2QlHu4Gɶlܖa61VI{ITG&nq궃n;vKTo8gLT w MJPqsq 'N9qgodrƄr*',.PfJ3p9ׅ?QAGQ2u4Etu,%NttSRV u uu?T0T3H+jmU0Ulw5U{wV!VwQw}7?kU6 ?'WaxvWzW!N~7XEy ayzxybWhjlٖnprYuZ'w}ay 0in1Vue1]7g{շ67 ]܇};c^4HSd~~[ea]oe'G7 &y8Njia‰aǀ>0b#_bbx1; Xc@8fƒ?5(.!PGAƂqdQl׃Ch;KeFeI?\Vg i@ fedd)gCBahd8ghCVD!Clh$i֟t8DvhX<`8j3&kCG 1F_jtF3XV=W ~&t;(D1fmgz9A6ᆇ g4Pv$oɘoo#GhLEDpՈ=I0FQPf9юJ("GZrԨorXs@'Q ِGXtYŠTSquUS7YaWaڒJek1v7)h|xד9iwiw7oWwx`1[iXUa\i:;[{;aU{6p)3аRXfrJX MI?*1X(+;&۲B2q5+;"/qk/QS[ai(!Y kd:qT{XZ\۵^51qb[iYBp#maj;e1m;!b&^YA|Qױpr;B! O@ !k!1&yp`Z a0b1P7bQ*g$1 &+b{! &y wd0`w-"qrR¹ e0 aq&; !z@u{!zF q{^x@K- yп+ҷCR#/"g!ir{ s`,q#$[4l\Q+0ϢA$NPBs!n*r$tpAދ2b%AA]P\,1ly9 %1&z Qro umm[z}ly‡ y0<8<Ǝ\l"x"'л)"IIz@K'pS_ȫ\,\},d* l(¶o@ *%s)ppLP͐Ʒ:[y@)1[p,B*"f$q%@ĮHb0ɌG+s*:W˾/ Љۻ,+0(` I PR=T]V}ТQJA ѳd{AjDxm*˲q/ ul q|f!c-_mRk[M!;,jI,6F5=OCL^GPbISehrlvr|,s`ĉhhl0Iixw|;ѩI!-,jI,6F-7I,:K)>Q07H4;I8>K2=QgZk\avdzh~u`zb&GN&JN(AK&FX%OP%K^(BT$SR"ZU=@L2AV3CZ9CT>HY$Na"Qe!Ui Ym [p7Ia8Mg9Oi:Qk;Tp>Xv!aXBENDGPGIQAJ\JJRPOTSQUXTWYVX]YZDM`FPaKTfLVh@]|R\nYbu\fx^h{b\[ja^nd`ak}qfauic{ne~pflvyp{CcEfFhGkHmJpLuOzP|Q~dnfpislvnxq{gjemo s v x} y}   ÀʃԆRTUVYZ\^_`bbefghjjmnpuhyk}nopquwxz}~ġɥͨЪ֮ڱ᷍㹎绐뾑Ĕǖȗ˘H Xh\ȰÇ#JHŋ3J,CIɓ$k`Y˗0c̛8s܉PΟ@ УHeҧPԫX:eJ `^aسhӪ]˶۷pʝKݻx˷߿ .m D2 P*fX#W*6g8 >Tj^f=nBnHA_jmӦ`DHA @ s/^QӪm٣G92ٛ'z3 9LSxqFf|&P@ftP݃NXCU`QED4[v'{!GuG#D,`C*t[8haG}{ &j}MhF$$hɥ(H%[vԁ%1@Di!&qB:p0QQSVk[r6Z!(`lD`FD&ZvĢ{j9zW(Qj%A }0}@W^jHdu6F+VkfDN` `ɻA.IkI e2DˮV@$C$EA\pF + $eC< +!+ %E  C, E@ r9T@C";A/޸VI"@!I dTx$4A/+;դB T&ЇL K*;i) "H4!֛HF ქ%XH"j[vg I\ V\t-|@NH}NC ЖE$G'Qr@E Tp 'v ^t_@lk+wYbu|A_*>׽++>1 PXE2R d#+ͩ`S+"dGH0q! ʇFVH╯ܢ58~3H 7C&(K \%RP'Q"|S ~T́B)*Bu`*ps $YT옴A.y,K` %(hJ05MV**AYBds+.I$򱊝o(HE*D m(GOLB~E"RÄ0Z!r-"EPAMk $Hg)+p UE_t I8cE ի\/Չ ?@K< @kbİ!,-9Ԅ[‚O!ˬͬf7z hGKZ+\jϕu]lc*{Ѷ Wk)`?F }F$ Ëelc q椛,e+k^"`f5X,h?s#ELۮZǦJ!](Llo( ¶wX!7M N8F=(pAB8AfIt%Nyhg8w+b1uycȞG p5{%ΰl?2<@V}oA">0k`[*8Ѕu5A&%+jxCw<l5! Gm\:%gE,jDZ*FgL#Q1s@ip`hDc?JNd!;hXWlH$ J>v&UIPR ]~=4*?,t2F50}ׂYomk:w6AM%B™9#QahwFOҳ{gC̉NufӞFߞ;pD;ҝٺ~*+h{`\{7!`#Sb"WuH'yb:sy/:26;C;@;hJ=`TcM $0+XdrH'U6fH!f??BHfd@ffD`9tiON" sg1B%tB6h:/FCx>ma@VJT`4| P$jSTEٖE?8FFFk8kFd&pGfJ qE#~2QlHu4Gɶlܖa61VI{ITG&nq궃n;vKTo8gLT w MJPqsq 'N9qgodrƄr*',.PvJ3p9ׅ?QAGQ2u4Etu,%NttSRV u uu?T0T3 H+jmU0Ulw5U{wV!VwQw}7?kU6 ?'WaxvWzW!N~7XEy ayzxybWhjlٖnprYuZ'w}ay 0in1Vue1]7g{շ67 ]܇};c^4HSd~~[ea]oe'G7 &y8Njia‰aǀ>0b#_bbx1; Xc@8fƒ?5(.!P GAƂqdQl׃Ch;KeFeI?\Vg i@ fedd)gCBahd8ghCVD!Clh$i֟t8DvhX<`8j3&kCG 1F_jtF3XV=W( ~&t;(D1fmgz9A6ᆇ g4Pv$oɘoo#GhLEDpՈ=I0FQPf9юJ("GZrԨqg's@'Q ِGXtYĊTSquUS7 " aWqڒJek1v7)h|xד9iwiw7oWǮǔwX1[iXUa\ija9iAg ۰;wa1Y[w)˒03a](c}k6[r^YJpDk*:y`v7i`)7 6ȘY88)e'#& 1:٩.:4&7V:Vē@=C=2 dYIteB* QGfJQfpؠSg:gJ gi}֡6VV ֆ+=$@dC2ZDwxKOP;E1jn;Dj OktdG:Wzx6ȨF䊵ƨrڛttQ sKoԌJb;DpS!# ~>ǩN:NZJWvrNp ˫Fǐst*utRRʪʑY95tq&yv.:pDi\uwwCj:Bi7WjjUiLW9yW]YN[Ȇ|ȈȊȌȎ,VP8Aa2 :a&,8),௩,+_01Sʴ /1ʥ<"QœxǬ50XQl5l <\11q\iYBp#ma<e1<!b&л^YAMQױp<B!O@ !*m!1&yp`Z a0qўb1P7bQ#*g$1 &+b{!N &yrqs wd0` w-"qrR" e0 paq&_ϥ !z@u-}!Ӯ q8;^x@|׳ y0~-2CR#/"g!ir{$ sؖq#$]\}{M׶ A$NPBs!n*r$tAd2%]P-y׿Mϓ%1&z romPm+]zy ^ ny08>lx"'V^)"z@(M'_p1^5},*m("o@ b*%s)ppL=B߹:[Gy@5[p,!ҢB*"f$qq%ܮHb0OG+*:u-׵ ^r=,_`+0<( n~؞ھ^ļnʸL˳L|>N>ɬ>׬!<,jI,6F5=OCL^GPbISehrlvr|,s`ĉhhl0Iixw|;ѩI!,jI,6F-7I-:K)>Q07H4;I8>K2=QgZk\avdzh~u`zb&GN&JN(AK&FX%OP%K^(BT$SR"ZU=@L2AV3CZ9CT>HY$Na"Qe!Ui Ym [p7Ia8Mg9Oi:Qk;Tp>Xv!aXBENDGPGIQAJ\JJRPOTSQUXTWYVX]YZDM`FPaKTfLVh@]|R\nYbu\fx^h{b\[ja^nd`ak}qfauic{ne~pflvyp{CcEfFhGkHmJpLuOzP|P~dnfpislvnxq{gjemo s v x} y}   ÀʃԆRTUVYZ\^_`bbefghjjmnpuhyk}nopquwxz}~ġɥͨЪ֮ڱ᷍㹎绐뾑Ĕǖȗ˘H Xh\ȰÇ#JHŋ3J,CIɓ$k`Y˗0c̛8s܉PΟ@ УHeҧPԫX:eJ `^aسhӪ]˶۷pʝKݻx˷߿ .m D2 P*fX#W*6g8 >Tj^f=nBnHA_jmӦ`DHA @ s/^QӪm٣G92ٛ'z3 9LSxqFf|&P@ftP݃NXCU`QED4[v'{!GuG#D,`C*t[8haG}{ &j}MhF$$hɥ(H%[vԁ%1@Di!&qB:p0QQSVk[r6Z!(`lD`FD&ZvĢ{j9zW(Qj%A }0}@W^jHdu6F+VkfDN` `ɻA.IkI e2DˮV@$C$EA\pF + $eC< +!+ %E  C, E@ r9T@C";A/޸VI"@!I dTx$4A/+;դB T&ЇL K*;i) "H4!֛HF ქ%XH"j[vg I\ V\t-|@NH}NC ЖE$G'Qr@E Tp 'v ^t_@lk+wYbu|A_*>׽++>1 PXE2R d#+ͩ`S+"dGH0q! ʇFVH╯ܢ58~3H 7C&(K \%RP'Q"|S ~T́B)*Bu`*ps $YT옴A.y,K` %(hJ05MV**AYBds+.I$򱊝o(HE*D m(GOLB~E"RÄ0Z!r-"EPAMk $Hg)+p UE_t I8cE ի\/Չ ?@K< @kbİ!,-9Ԅ[‚O!ˬͬf7z hGKZ+\jϕu]lc*{Ѷ Wk)`?F }F$ Ëelc q椛,e+k^"`f5X,h?s#ELۮZǦJ!](Llo( ¶wX!7M N8F=(pAB8AfIt%Nyhg8w+b1uycȞG p5{%ΰl?2<@V}oA">0k`[*8Ѕu5A&%+jxCw<l5! Gm\:%gE,jDZ*FgL#Q1s@ip`hDc?JNd!;hXWlH$ J>v&UIPR ]~=4*?,t2F50}ׂYomk:w6AM%B™9#QahwFOҳ{gC̉NufӞFߞ;pD;ҝٺ~*+h{`\{7!`#Sb"WuH'yb:sy/:26;C;@;hJ=`TcM $0+XdrH'U6fH!f??BHfd@ffD`9tiON" sg1B%tB6h:/FCx>ma@VJT`4| P$jSTEٖE?8FFFk8kFd&pGfJ qE#~2QlHu4Gɶlܖa61VI{ITG&nq궃n;vKTo8gLT w MJPqsq 'N9qgodrƄr*',.PvJ3p9ׅ?QAGQ2u4Etu,%NttSRV u uu?T0T3 H+jmU0Ulw5U{wV!VwQw}7?kU6 ?'WaxvWzW!N~7XEy ayzxybWhjlٖnprYuZ'w}ay 0in1Vue1]7g{շ67 ]܇};c^4HSd~~[ea]oe'G7 &y8Njia‰aǀ>0b#_bbx1; Xc@8fƒ?5(.!P GAƂqdQl׃Ch;KeFeI?\Vg i@ fedd)gCBahd8ghCVD!Clh$i֟t8DvhX<`8j3&kCG 1F_jtF3XV=W( ~&t;(D1fmgz9A6ᆇ g4Pv$oɘoo#GhLEDpՈ=I0FQPf9юJ("GZrԨqg's@'Q ِGXtYĊTSquUS7 " aWqڒJek1v7)h|xד9iwiw7oWǮǔwX1[iXUa\ija9iAg ۰;wa1Y[w)˒03a](c}k6[r^YJpDk*:y`v7i`)7 6ȘY88)e'#& 1:٩.:4&7V:Vē@=C=2 dYIteB* QGfJQfpؠSg:gJ gi}֡6VV ֆ+=$@dC2ZDwxKOP;E1jn;Dj OktdG:Wzx6ȨF䊵ƨrڛttQ sKoԌJb;DpS!# ~>ǩN:NZJW!s|P jt Y:GRgM (e+٬uњSժj h3 w8IiUzw7檓+QJxsEbbx`QtՕyKH  Ȓ<ɔ\ɖ|ɘɚK[z[ѱAa 1WQAp;lB\]4{ѳصs*}9sUUwV|vR Ab T`C7i`HT0)e̶u cqڙyo3}c<YZø0˟8i+aB TXtl4Ч ^A[S;tfhC+. DËi+WZv(أ $VƤfԈEJkskvHbZH= !8{ 4Je=Dp $qMj o 80/`.VP8P0a2 e,t&)n,+_Q"S0*.!>4,a&8 x=50XQCK^T^V~XZ71q^^iYBp#maf>e1i>!b&`绁^YAxQױpn>B!O@ !n!1yp`Z `a0b1P7bQ*g$1 &+b{!&ypwd0`w-"qrR e0 aq& !z@u~!r q>^px@)y.ВCR#/"g!ir{s q#$1q^0/\,obA$NPBs!n*r$t0AN2"%]PX/-/y5 %1&z roqmm^zpy|/yy0?4lx"'̾)"EEz@N'O__(2,`* o(o@ *%s)ppLL-[y𒧋ExSO>4sw.~|~_02,m@ !;,jI,6F5=OCL^GPbISehrlvr|,s`ĉhhl0Iixw|;ѩI!-,jI,6F-7I-:K)>Q07H4;I8>K2=QgZk\avdzh~u`zb&GN&JN(AK&FX%OP%K^(BT$SR"ZU=@L2AV3CZ9CT>HY$Na"Qe!Ui Ym [p7Ia8Mg9Oi:Qk;Tp>Xv!aXBENDGPGIQAJ\JJRPOTSQUXTWYVX]YZDM`FPaKTfLVh@]|R\nYbu\fx^h{b\[ja^nd`ak}qfauic{ne~pflvyp{CcEfFhGkHmJpLtMwOzP|P~dnfpislvnxq{gjemo s v x} y}   ÀʃԆRTUVYZ\^_`bcefghjjmnpuhyk}nopquwxz}~ġɥͨЪ֮ڱ᷍㹎绐뾑Ĕǖȗ˘H Yh\ȰÇ#JHŋ3J,GCIɓ$kdY˗0c̛8s܉PΟ@ УHeҧPԫX:J `^q%سhӪ]˶۷pʝKݻx˷߿ .ʕm DC3 P*fX#*6g8 >Tj^>oBnHaϚ_jn`DHA @ s/^QӮmG93ٟ'z3 C9LSxqFg|&PPgtP݃NXCVdaED4[w'{!GuG#D,`C*t[8hqG}| &j }MxF$$hɥ(H%[w؁%A@Di!&qB:p0QQSVk[r6Z!(pmD`FT&ZGwĢ|j9zX(Qj%A }0}@W^jHdu6F+VkfDNp pɻA.I{I i2DˮVDD$C$ EA\pF"I+ $ %i‰C@ r9T@C";A/޸VI"!I dTx(4A/Up 'v ^t_@lk_+wYbu|A*>׽+@+@1 PZE2R dc+ͩ`S+"dH0q! ʇVL╯ܢ58~3H 7C&(K \&RP'Q"|S ~T́b)*Buh*ps $YGU옴A.y0K` %(hJ05MVD*,A]bds[+.QJ$򱊝(HE*D,m(G4PPB~E"RÄ0!r-"ERAMk DH)+p UE_t I8cE ի \1Չ ?@K< @kbİ!,-9Ԅ[ʂO!ˬͬf7z hGKZ+\jϕu]lc*{Ѷ Wk)` ?F }F$ Ëelc q椛,e+k^"`f5X,h?{$TFLۮZǦJ!](Llo( ¶wX!7M N8F=(pAB8AfIt%Nyhg8w+b1uycȞG p5{%ΰl?20k`[*8Ѕ "u5A&e+jxCw<l5! Gm\:%gE,jDZ*FgL#Q1s@ip`pDc?JNl!;hXWlH$ J>v&UIPR ]~=8*?,t2F50}ׂYomk:w6AM%F™9@#QahwFOҳ{gC̉Nufma@VJT`4} P$jSTEٖE?8FFFk8kFd&pGfJ qE#~2QlHu4Gɶlܖa61VI{ITG&nq궃n;vKTo8gLT w0MJPqsq 'N9qgodrƄr*',.PJ3p9ׅ?QAGQ2u4Etu,%NttSRV u uu?T0T3 H+jmU0Ulw5U{wV!VwQw}7?kU6 ?'WaxvWzW!N~7XEy ayzxybWhjlٖnprYuZ'w}ay 0in1Vue1]7g{շ67 ]܇};c^4HSd~~[ea]oe'G7 &y8Njia‰aǀ>0b#_bbx1; Xc@8fƒ?5(.!Q GAƂqdQl׃Ch;KeFeI?\Vg i@ fedd9gCBahd8ghCVD!Clh$i֟t8DvhX<`8j3&kCG 1F_jtF3XV=W8 ~&t;(D1fmgz9A6ᆇ g4Pv $oɘoo#GhLEDpՈ=I0FQPf9юJ("GZrԨqg's@'Q ِGXtYĊTSquUS7 " aWڒJek1v7)h|xד9iwiw7oWǮǔwX1[iXUa\ija9iAg ۰;wa1Y[w)˒03a](c}k6[r^YJpDk*:y`v7i`)7 6ȘY88)e'#& 1:٩.:4&7V:Vē@=C=2 dYIteB* QGfJQfpؠSg:gJ gi}֡6VV ֆ+=$@dC2ZDwxKOP;E1jn;Dj OktdG:Wzx6ȨF䊵ƨrڛttQ sKoԌJb;DpS!# >ǩN:NZJW!s}P jt Y:GRgM (e+٬uњSժj h3 w8IiUzw7檓+QJxsEbbx`QtՕyKH  Ȓ<ɔ\ɖ|ɘɚK[z[ѱAa 1WQAp;lB\]4{ѳصs*}9sUUwf}vR Ab T`C7i`HT0)e̶u cqڙyo3}c<YZø0˟8i+aB TXtl4Ч ^A[S;tfhC+. DËi+WZv8أ $VƤfԈEJkskvHbZH= !8{ 4Je=Dp $qMj o 80/`^igqͺm\kY10 ܜрj ?tˁv۝3M9F'ЂDf= dXJ^!]K҆hRHmWjqvAɡ bh:n Լ;/jH}iQ5 =^裥@A]jJثmaoR*U[7Gl n=H؉ԦҦȦK ~m Q4z؁JǨK K=lp-xD퍉­q$gr=.(jaupsMQt&NYuR݋T\wsݶvc MjLEr<Ϯ~7aG9g~<~R߀g߆ ^ VXZ\^YW`8!a2da&no,)v_s,y/+_O}O#t-ŋlo PTh_5Qo~g1!${0i H&Q#(r s*Q`˿qzq G0( PA0P+(0A s@,tE `B!E$YIkf`=uA|HN=Eޑig > CG Ӝ| TҥYrSlb:8 z cJy 28A# ! 3@R;G"'4%Yig9ӁI+YC_)׭oz"7A1}/g=Nr a'Rt,|=b|AXn<mp@ zn((HM= B9*7b  4H>*΋HcƲLFdIP>H7n;P=p 5MF,FÁ18=p4̐téh0! 9s/$IN@ -47ДtRԔ;BP>(8X@N&c"8B5JLʡ`u’ %BУ  r `)F% N;#3X;O`ɪtb1-ee̲hZ'` HKJ>2 @BACydK6dSVye[vecyfkfsyg{gzh宰]gPi3]y,hzkYȢPn,6lv,m ֛Ro4pmZ'w+++|s!;,jI,6F5=OCL^GPbISehrlvr|,s`ĉhhl0Iixw|;ѩI!-,/.&J!*;&.@,5E-7I-:K)>Q07H3;J8>K1=QfZk\q_avdzh~u`|c&KN(CL&FX%OP%K^(BT$RR!ZU=@L2AV3CZ9BT>FX>HY$Na"Qe!Ui Ym [p7Ia8Mg9Oi:Qk;Tp>Xv!aXBENDGPGIQAJ\JJRPOTSQUXTWYVX]YZDM`FPaKTfLVhOYk@]|R\nU^qYbu\fx^h{b\[ja^nd`ak~ntqfauic{ne~pflvyp{mqz{.}S_R_CcEfFhGkHmJpLuMwNzP|P~dnfpislvnxu{q{Hbqgjelopt v x} y|   ŁʃԆRTUVYgYZ\^_`bcefghjjmnpuhyk}nYZZWblopquwxz}~иġɥͨЪ֮ڱ᷍㹎绐뾑Ĕǖȗ˘W k%#B@ŋ3jȱǏ CIɓ%We&— :Hʛ8sɳO*Wf[ т2kɴӧPz *ѣ/*(ׯ`f*46eXjP۷p$[6bi e+߿qֵK,ޭ+^TlVʸ@urM̠Cv\Za Nͺubv]y۸:>;[oj6s բVMsd&=Im9amË(e_|>&|?gǟw h*QHq p!(ah JSDbV*UD!I.dt"JDQbO6!F+樝hD5FSg$!) CO7Tvğ*=F1.A gPcuJ*T%.]zSg@1dxy'pfO@DtҩxJ1$TdnބJf uM4i[<:Vꫫ&" +PJY+FzdTc8#2g(#JkmHBze'bm[ѸO[.3-.M7o^ )'nD=!FN\DO ; 1J5h.DA7loDpq ҈#.21A ?dtk\4BA#\F0GHI/4JQNq[:d DGG#4E 8@]Y@#yX`Ev}m}"͊3 Qb:`H#0Bw:1b:PT:茠^s^gҪGDD@!|bk-SB$H ^'14~!!BI`VFhIkZ.+)8@/T$X %RHrÅdЈ4fqR N1YumE RIF2r ^@Ƞ}_`C f! 9L!R$ HE+^2ALX"M1)ȈK|bn)EQ"ʷ"!dh#X1a 8ìKn@ !ˢɅHY&[XH^w9% 4㐀O|'r|)Jk$YF,EWdeXh`ЌTġ,$1s)" h7(4F܌4Yf$H6EҼ_#Gpf#$ƗpTj5}2E܌r I| _TDi_hЀ Z-hiCM`;r 3R箠dž)&>q]"}\`ɏ22^@}fĂ϶s(˅nb̳y^D憮^c8qp3"[hpEtW$(Gҩ݌4xG.'50g WvPCN)NWXD0n HF,lլAExSbEp ox`r@ fN 6LPd*GR0c=H)c<&tE/ @PoƤ;i."ȗ/d_[?/PB!)`1@-SqIc\ɨRB!srX(xs h*׀t€h+8HR+R/S HS&T9S=UT@DՂTJuS'f^UUW ,`ehhVl%_MxWzW~V[eXcuf!Y5Y5Y0 z`bZ&1\Gm^nvd"Ar[ev8p\w_s>N]\5]E[U U.F]AacE^uqv_u__&`q` ` m7ajaj^^h0O"Fb&b=](36; ccstdCVddeRFeVedeeBfwxdfff[l˨Jtqva qg(g|g&hqxHx1 ! 6ihiiZj1jYmr65 )6@)kUkk'rf}l]Hl`lkWxl@)mX֒>#_F  Om#n&m 膇Inօ`oooqffh'pFp`p#CBt>COp\  RpHPGq?Ir>qB֑#7[%zIui.tM:$As xsasؙZ['f[GuJfutOuSwtItXt!m^wfv!Av3/vk7g Zg&AHyw}w'xy@y'q'X7y'70x_'zAz!qz4qfz0{q{{Wg|~'iG З|'Ly-ۇZg}G~~gN*aR/#OJ ӥ+Bd"B4yȦBY ?t"VMXsڧzM!F>^z ;3E㨏 *Fc!S!xJ%aT/BzT`hDу? 9aՅHVVmeN(P(H"X8XX5AYU* U=mWl_ŖQ{Ђj(E]QQXz9^g֊V^_`TZ `Fagaкq5&bpAqc6!v8ee0eUveJe`df(q\0 =vg6hGvT i2J#)d%mY)yf,s6sYFkyk8AlD9lGylGPRym 1Y dnEni|~)sIVuyo{i֪)pFm))I 04{9ƙgg+rkr9˚/Gois q?pҙt;_BR'ʉK[* iAl'5 W%QU|wנW'ڟ y g& AJ 'W :{D4{ w}+We4J/zMf}×J֣g@~AS~"1U W—_ g,fk<nrt\qxz~ȷ1q"8S(%o!O:U6Yy& \%qgLHl!qfZج˫![Z!U(ZʮJ{~[:\_hƌ;Z \1j̊%˰aȱa[HcШb X{8&j,@0J}9Jť}-.d NJQሊW^ EޤL:^<܄|_],SmMY[,].S6J!c^-e>aF8nG=yL2I4|$x*OwaȈ^[RΩ^Ja-Ƞ:L,baT&UL¢U(aʪL#yu~Yq1Dj 8'G[I˶&ʹ X߱v9-^Ka+ hh F涜&iMjs[̘D/' @ tհ6:c=qCkmnm+O9m6.ͬνhآwˠX٫ْKGz^q) &8Ɋ *QB25v, AR$Ʉ86%QŖJ%B#A 98^0K!SQNJ5UYnMx`Wa]8 QCYg!V Hw+3/i[&\aϐ㿉%O\>n4y/fСE&驥Ufݚ0jO]Ϧ]vضu}7o5qɕ+4~zs輛{^:靡вaܽ X"BgLJ$XͣW1;d*! R*YbN$aK@ hJ(9%YjI*f&@ĶƕJƮL1 K F(Cs GHAGڀ+ LHlF 1*0E!bB+ӥ3ƾ} /Hန/J R.Iv ŋ3P%--N-3̄  `(EaATTaa!aa+$L!N&W&҈ :+2! "Zl Q8@ۄڄ8㡬JUR0R f$hA@_d7_02cp(~WdGez+[\f뺅J E8!dR471#/c ST}y2l1IYWhZ9hbs0`_yI>c:F!e+(0xGBH$a_w^"n&- c$[߽x'#y!h b#kϺF녮JMBmBWƛ+h|IBPRGhxZ0_[]ˡarʭ\d.!es2!L^{#LXՅ`? xRbbaZyRXH@mY>wK|и &( 32 ^0!˚0fE~r1 'B,L)H6ѿeB" SP`a5 O)XWQQ% XZD ($ :I/NѯɄYY!bfLJHBA QA+ AW[(RDh#1O34&I fRI&$'WI~X(RrG+KHtCNqQ,bˤrV.WTF$VÙ`dfD4`6Sӄ&*yb&32崩n:g9CNs39Tg;N;Ӹs!>Kx&BgSLD4:B%HCKdDFE,RxNC rT!1G@p!;),R-vҙ൩.TuҴ6EIT<-$qE(C! "AB ZJRr"9)P)UuJUXŦJVp+^Em-Ќ,uZsE`+[BK.WQ!d^#~va V%ZXfڄLbCX31l:"Bx迬ejveU>Kӆ-ix^@ ],vbEiD2A \.$mkcnۼmӿA#p 'uO!B"ޫ7:m()"!ЉK#1R_uKfSnx(ŠQ)Dx#~\q~5M ARr{\5=PD9)F7D%J)S2,!] Y<4MdYRt)Bd*]i n:{K:`U|YZC%dI3%'FJ+bO>U]ڪjVkv+#;@;wk:VZdI-J^cDFYhV+:Z*X]miY+0 aH6KbmJ ܎M}y5.)BN¡WQ.d!fz> ԮߴwZ^r<ׯrby :2lۈ0i@/˲x #Q%IB`)3k81 I1۝y1!k0x/WI2P#Qҕ( *C,sز!"V"3ຕ3# .6 78R3C=C?S@4(4ӍD!0H47<*#"Kc"5z56ʴ*:52JQ+NEP+3(W6z#5D ]# KQ5`aDڈc6c6h$d$lKfCPⶡq[%sK YZ7wۊrGx;*_EC|wG7yG|)G~d}GtG7H{+Hl'LtZ9ȶ9}R9`C1?ع>LH(¨#KIui#HT*0PPo<;#2;2IZ;j;*#º<+!q{Pe-aDJƫW< <:ϫK$帬2=(9 CQ5Lt9<Bj$>>>yU-b>-,K@lK+?K)ꮘ'CcN/z;@Ԛ{0,@ī*@X9@$0t10 D ǁ T KÌ [ sAAڹA D!t  ⪌&Ğ'O&cu+b)0[ 6D3DC6339 => =O7D@A$!FDOP NI#JS"MDNSWP!E7=ELVNW,.EO E36FFO:F6e.i$oNr$60 oFV"Ipu9Ǭ@UHx SnUZ&[UjU]&^UfU`&XVVcU%du 2Hu*!h%eu 8V3 o}'i83+W8W˸3v pt%Pyz3WpH0W~]xׄUXt](%Uq%Wm%nU XY08k]_2@xV8&׈gY@Y Z] -8Zٜ]QZO% p pHMڬ p8h^L hfBYAhhAHA`Ѓ?p\D\[ChF@߽mF[C_FX@ ^F߿Bp[F(Xmpi_x߅̫p:\:@4E߄5H9D֩ PEZ0Eh@B vmedn LՍ>ᄠc8cHEpeUֵ޿@@ v_F(`BF@b^6ffCv`]P FePeDPTV4Aʸ[!ƃ`Fh?X`FdcVfF~=`f1P U&]95XmYEeAVs t- FmB fx[DKK~F0_RRV X6`9.cBF]ih %pAhEw.PAhjj^Da J^vEX\8PlcH;`EkXD6rag\ʝbCb<`E d(>`D`i ^E~l .CucU9cNE8a(j HnPWXV X]k:j noF .ohn\oUl~ZoshVooVnppȍ5oXOZO}ppWp3X Y  q _qqYTq!,jI,6F5=OCL^GPbISehrlvr|,s`ĉhhl0Iixw|;ѩI!, ..!*;S4PEpK{PWbl dihlpDaxp(Ĥ0H)~u[0j-3^Cn%\8HaGuzz e9z\8 i%K lJ oIK7rDz$#ŒCz|k6uwqD6$`CcYD^$TBX9&H6M*Z24=1 (";graphql-ruby-2.5.19/guides/defer/graphiql.md000066400000000000000000000045031514115062600207260ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: GraphQL Pro - Defer title: Use with GraphiQL desc: Using @defer with the GraphiQL IDE index: 4 pro: true --- You can use `@defer` and `@stream` with [GraphiQL](https://github.com/graphql/graphiql/blob/main/packages/graphiql/README.md), an in-browser IDE. Using @defer with GraphiQL ## Incremental responses If you're using the proposed `incremental: ...` response syntax ([proposal](https://github.com/graphql/graphql-spec/pull/742), [Ruby support](/defer/setup.html#example-rails-with-apollo-client)), you'll need a custom "fetcher" function to handle the `incremental: ...` part of the response. For example: ```js import { meros } from "meros"; // for handling multipart responses const customFetcher = async function* (graphqlParams, fetcherOpts) { // Make the initial fetch var result = await fetch("/graphql", { method: "POST", body: JSON.stringify(graphqlParams), headers: { 'content-type': 'application/json', } }).then((r) => { // Use meros to turn multipart responses into streams return meros(r, { multiple: true }) }) if (!isAsyncIterable(result)) { // Return plain responses as promises return result.json() } else { // Handle multipart responses one chunk at a time for await (const chunk of result) { yield chunk.map(part => { // Move the incremental part of the response into top-level // This assumes there's only one `incremental` entry // which is currently true for GraphQL-Pro's @defer implementation var newJson = {...part.body} if (newJson.incremental) { newJson.data = newJson.incremental[0].data newJson.path = newJson.incremental[0].path delete newJson.incremental } return newJson }); } } } // Helper for checking for a multipart response: function isAsyncIterable(input) { return ( typeof input === "object" && input !== null && ( input[Symbol.toStringTag] === "AsyncGenerator" || (Symbol.asyncIterator && Symbol.asyncIterator in input) ) ); } ``` Hopefully a new GraphiQL version will support this out of the box; follow the [issue on GitHub](https://github.com/graphql/graphiql/issues/3470). graphql-ruby-2.5.19/guides/defer/overview.md000066400000000000000000000044311514115062600207650ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: GraphQL Pro - Defer title: Overview desc: What is @defer, and why use it? index: 0 pro: true --- `@defer` is a {% internal_link "directive", "/type_definitions/directives" %} for streaming GraphQL responses from the server to the client. By streaming the response, the server can send the most critical (or most available) data _first_, following up with secondary data shortly afterward. `@defer` was first described by [Lee Byron at React Europe 2015](https://youtu.be/ViXL0YQnioU?t=768) and got experimental support in [Apollo in 2018](https://blog.apollographql.com/introducing-defer-in-apollo-server-f6797c4e9d6e). `@stream` is like `@defer`, but it returns list items one at a time. Find details in the {% internal_link "Stream guide", "/defer/stream" %}. ## Example GraphQL queries can be large and complex, requiring lots of computation or dependencies on slow external services. In this example, the local server maintains an index of items ("decks"), but the item data ("cards") is actually hosted on a remote server. So, GraphQL queries must make remote calls in order to serve that data. Without `@defer`, the whole query is blocked until the last field is done resolving: {{ "https://user-images.githubusercontent.com/2231765/53442028-4a122b00-39d6-11e9-8e33-b91791bf3b98.gif" | link_to_img:"Rails without defer" }} But, we can add `@defer` to slow fields: ```diff deck { slots { quantity - card + card @defer { name price } } } ``` Then, the response will stream to the client bit by bit, so the page can load progressively: {{ "https://user-images.githubusercontent.com/2231765/53442027-4a122b00-39d6-11e9-8d7b-feb7a4f7962a.gif" | link_to_img:"Rails with defer" }} This way, clients get a snappy feel from the app even while data is still loading. View the full demo at https://github.com/rmosolgo/graphql_defer_example. ## Considerations - `@defer` adds some overhead to the response, so only apply it judiciously. - `@defer` is single-threaded. `@defer`ed fields are still evaluated in sequence, but in a chunk-by-chunk way. ## Next Steps {% internal_link "Set up your server", "/defer/setup" %} to support `@defer` or read about {% internal_link "client usage", "/defer/usage" %} of it. graphql-ruby-2.5.19/guides/defer/setup.md000066400000000000000000000124551514115062600202640ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: GraphQL Pro - Defer title: Server Setup desc: Configuring the schema and server to use @defer index: 1 pro: true --- Before using `@defer` in queries, you have to: - Update `graphql` and `graphql-pro` gems - Add `@defer` to your GraphQL schema - Update your HTTP handlers (eg, Rails controllers) to send streaming responses - Optionally, customize `@defer` to work with GraphQL-Batch You can also see a [full Rails & Apollo-Client demo](https://github.com/rmosolgo/graphql_defer_example). ## Updating the gems GraphQL-Ruby 1.9+ and GraphQL-Pro 1.10+ are required: ```ruby gem "graphql", "~>1.9" gem "graphql-pro", "~>1.10" ``` And then install them: ``` $ bundle update graphql graphql-pro ``` ## Adding `@defer` to your schema Then, add `GraphQL::Pro::Defer` to your schema as a plugin: ```ruby class MySchema < GraphQL::Schema use GraphQL::Pro::Defer end ``` This will: - Attach a {% internal_link "custom directive", "/type_definitions/directives" %} called `@defer` - Add instrumentation to queries to track deferred work and execute it later ## Sending streaming responses Many web frameworks have support for streaming responses, for example: - Rails has [ActionController::Live](https://api.rubyonrails.org/classes/ActionController/Live.html) - Sinatra has [Sinatra::Streaming](http://sinatrarb.com/contrib/streaming.html) - Hanami::Controller can [stream responses](https://github.com/hanami/controller#streamed-responses) See below for how to integrate GraphQL's deferred patches with a streaming response API. To investigate support with a web framework, please {% open_an_issue "Server support for @defer with ..." %} or email `support@graphql.pro`. ### Checking for deferrals When a query has any `@defer`ed fields, you can check for `context[:defer]`: ```ruby if context[:defer] # some fields were `@defer`ed else # normal GraphQL, no `@defer` end ``` ### Working with deferrals To handle deferrals, you can enumerate over `context[:defer]`, for example: ```ruby context[:defer].each do |deferral| # do something with the `deferral`, eg # stream_to_client(deferral.to_h) end ``` The initial result is _also_ present in the deferrals, so you can treat it just like a patch. Each deferred patch has a few methods for building a response: - `.to_h` returns a hash with `path:`, `data:`, and/or `errors:`. (There is no `path:` for the root result.) - `.to_http_multipart(incremental: true)` returns a string which works with Apollo client's `@defer` support. (Use `incremental: true` to format patches for the forthcoming spec.) - `.path` returns the path to this patch in the response - `.data` returns successfully-resolved results of the patch - `.errors` returns an array of errors, if there were any Calling `.data` or `.errors` on a deferral will resume GraphQL execution until the patch is complete. ### Example: Rails with Apollo Client In this example, a Rails controller will stream HTTP Multipart patches to the client, in Apollo Client's supported format. ```ruby class GraphqlController < ApplicationController # Support `response.stream` below: include ActionController::Live def execute # ... result = MySchema.execute(query, variables: variables, context: context, operation_name: operation_name) # Check if this is a deferred query: if (deferred = result.context[:defer]) # Required for Rack 2.2+, see https://github.com/rack/rack/issues/1619 response.headers['Last-Modified'] = Time.now.httpdate # Use built-in `stream_http_multipart` with Apollo-Client & ActionController::Live deferred.stream_http_multipart(response, incremental: true) else # Return a plain, non-deferred result render json: result end ensure # Always make sure to close the stream response.stream.close end end ``` You can also investigate a [full Rails & Apollo-Client demo](https://github.com/rmosolgo/graphql_defer_example) ## With GraphQL-Batch `GraphQL-Batch` is a third-party data loading library that wraps GraphQL-Ruby execution. Deferred resolution happens outside the normal execution flow, so to work with GraphQL-Batch, you have to customize `GraphQL::Pro::Defer` a bit. Also, you'll need GraphQL-Pro `v1.24.6` or later. Here's a custom `Defer` implementation: ```ruby # app/graphql/directives/defer.rb module Directives # Modify the library's `@defer` implementation to work with GraphQL-Batch class Defer < GraphQL::Pro::Defer def self.resolve(obj, arguments, context, &block) # While the query is running, store the batch executor to re-use later context[:graphql_batch_executor] ||= GraphQL::Batch::Executor.current super end class Deferral < GraphQL::Pro::Defer::Deferral def resolve # Before calling the deferred execution, # set GraphQL-Batch back up: prev_executor = GraphQL::Batch::Executor.current GraphQL::Batch::Executor.current ||= @context[:graphql_batch_executor] super ensure # Clean up afterward: GraphQL::Batch::Executor.current = prev_executor end end end end ``` And update your schema to use your custom defer implementation: ```ruby # Use our GraphQL-Batch-compatible defer: use Directives::Defer ``` ## Next Steps Read about {% internal_link "client usage", "/defer/usage" %} of `@defer`. graphql-ruby-2.5.19/guides/defer/stream.md000066400000000000000000000033041514115062600204100ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: GraphQL Pro - Defer title: Stream desc: Using @stream to receive list items one at a time index: 3 pro: true --- `@stream` works very much like `@defer`, except it only applies to list fields. When a field has `@stream` and it returns a list, then each item in the list is returned to the client as a patch. `@stream` is described in a [proposal to the GraphQL specification](https://github.com/graphql/graphql-wg/blob/main/rfcs/DeferStream.md). __Note:__ `@stream` was added in GraphQL-Pro 1.21.0 and requires GraphQL-Ruby 1.13.6+. ## Installation To support `@stream` in your schema, add it with `use GraphQL::Pro::Stream`: ```ruby class MySchema < GraphQL::Schema # ... use GraphQL::Pro::Stream end ``` Additionally, you should update your controller to handle deferred parts of the response. See the {% internal_link "@defer setup guide", "defer/setup#sending-streaming-responses" %} for details. (`@stream` uses the same deferral pipeline as `@defer`, so the same setup instructions apply.) ## Usage After that, you can include `@stream` in your queries, for example: ```ruby { # Send each movie in its own patch: nowPlaying @stream { title director { name } } } ``` If `@stream` is applied to non-list fields, it's ignored. `@stream` supports several arguments: - `if: Boolean = true`: when `false`, the list is _not_ streamed. Instead, all items are returned synchronously. - `label: String`: if present, the given string is returned in patches as `"label": "..."` - `initialCount: Int = 0`: this number of list items are returned synchronously. (If the list is shorter than `initialCount`, then the whole list is returned synchronously.) graphql-ruby-2.5.19/guides/defer/usage.md000066400000000000000000000021741514115062600202250ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: GraphQL Pro - Defer title: Usage desc: Using @defer on the client side index: 2 pro: true --- `@defer` is a [GraphQL directive](https://graphql.org/learn/queries/#directives) which instructs the server to execute the field in a special way: ```graphql query GetPlayerInfo($handle: String!){ player(handle: $handle) { name # Send this field later, to avoid slowing down the initial response: topScore(from: 2000, to: 2020) @defer } } ``` The directives `@skip` and `@include` are built into any GraphQL server and client, but `@defer` requires special attention. Apollo-Client [currently supports the @defer directive](https://www.apollographql.com/docs/react/data/defer/). `@defer` also accepts a `label:` option which will be included in outgoing patches when it's present in the query (eg, `@defer(label: "patch1")`). Want to use `@defer` with another client? Please {% open_an_issue "Client support for @defer with ..." %} or email `support@graphql.pro` and we'll dig in! ## Next Steps {% internal_link "Set up your server", "/defer/setup" %} to support `@defer`. graphql-ruby-2.5.19/guides/development.md000066400000000000000000000207701514115062600203600ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true title: Development section: Other desc: Hacking on GraphQL Ruby --- So, you want to hack on GraphQL Ruby! Here are some tips for getting started. - [Setup](#setup) your development environment - [Run the tests](#running-the-tests) to verify your setup - [Debug](#debugging-with-pry) with pry - [Run the benchmarks](#running-the-benchmarks) to test performance in your environment - [Coding guidelines](#coding-guidelines) for working on your contribution - Special tools for building the lexer and parser - Building and publishing the [GraphQL Ruby website](#website) - [Versioning](#versioning) describes how changes are managed and released - [Releasing](#releasing) Gem versions ## Setup Get your own copy of graphql-ruby by forking [`rmosolgo/graphql-ruby` on GitHub](https://github.com/rmosolgo/graphql-ruby) and cloning your fork. Then, install the dependencies: - Install SQLite3 and MongoDB (eg, `brew install sqlite && brew tap mongodb/brew && brew install mongodb-community`) - `bundle install` - `rake compile # If you get warnings at this step, you can ignore them.` - Optional: [Ragel](https://www.colm.net/open-source/ragel/) is required to build the lexer ## Running the Tests ### Unit tests You can run the tests with ``` bundle exec rake # tests & Rubocop bundle exec rake test # tests only ``` You can run a __specific file__ with `TEST=`: ``` bundle exec rake test TEST=spec/graphql/query_spec.rb # run tests in `query_spec.rb` only ``` You can focus on a __specific example__ with `focus`: ```ruby focus it "does something cool" do # ... end ``` Then, only `focus`ed tests will run: ``` bundle exec rake test # only the focused test will be run ``` (This is provided by `minitest-focus`.) ### Integration tests You need to pick a specific gemfile from gemfiles/ to run integration tests. For example: ``` BUNDLE_GEMFILE=gemfiles/rails_6.1.gemfile bundle install BUNDLE_GEMFILE=gemfiles/rails_6.1.gemfile bundle exec rake test TEST=spec/integration/rails/graphql/relay/array_connection_spec.rb ``` ### GraphQL-CParser tests To test the `graphql_cparser` gem, you have to build the binary first: ``` bundle exec rake build_ext ``` Then, run the test suite with `GRAPHQL_CPARSER=1`: ``` GRAPHQL_CPARSER=1 bundle exec rake test ``` (Add `TEST=` to pick a certain file.) ### Other tests There are system tests for checking ActionCable behavior, use: ``` bundle exec rake test:system ``` And JavaScript tests: ``` bundle exec rake test:js ``` ## Gemfiles, Gemfiles, Gemfiles `graphql-ruby` has several gemfiles to ensure support for various Rails versions. You can specify a gemfile with `BUNDLE_GEMFILE`, eg: ``` BUNDLE_GEMFILE=gemfiles/rails_5.gemfile bundle exec rake test ``` ## Debugging with Pry [`pry`](https://pry.github.io/) is included with GraphQL-Ruby's development setup to help with debugging. To pause execution in Ruby code, add: ```ruby binding.pry ``` Then, the program will pause and your terminal will become a Ruby REPL. Feel free to use `pry` in your development process! ## Running the Benchmarks This project includes some Rake tasks to record benchmarks: ```sh $ bundle exec rake -T | grep bench: rake bench:profile # Generate a profile of the introspection query rake bench:query # Benchmark the introspection query rake bench:validate # Benchmark validation of several queries ``` You can save results by sending the output into a file: ```sh $ bundle exec rake bench:validate > before.txt $ cat before.txt # ... # --> benchmark output here ``` If you want to check performance, create a baseline by running these tasks before your changes. Then, make your changes and run the tasks again and compare your results. Keep these points in mind when using benchmarks: - The results are hardware-specific: computers with different hardware will have different results. So don't compare your results to results from other computers. - The results are environment-specific: CPU and memory availability are affected by other processes on your computer. So try to create similar environments for your before-and-after testing. ## Coding Guidelines GraphQL-Ruby uses a thorough test suite to make sure things work reliably day-after-day. Please include tests that describe your changes, for example: - If you contribute a bug fix, include a test for the code that _was_ broken (and is now fixed) - If you contribute a feature, include tests for all intended uses of that feature - If you modify existing behavior, update the tests to cover all intended behaviors for that code Don't fret about coding style or organization. There's a minimal Rubocop config in `.rubocop.yml` which runs during CI. You can run it manually with `bundle exec rake rubocop`. ## Website To update the website, update the `.md` files in `guides/`. To preview your changes, you can serve the website locally: ``` bundle exec rake site:serve ``` Then visit `http://localhost:4000`. To publish the website with GitHub pages, run the Rake task: ``` bundle exec rake site:publish ``` ### Search Index GraphQL-Ruby's search index is powered by Algolia. To update the index, you need the API key in an environment variable: ``` $ export ALGOLIA_API_KEY=... ``` Without this key, the search index will fall out-of-sync with the website. Contact @rmosolgo to gain access to this key. ### API Docs The GraphQL-Ruby website has its own rendered version of the gem's API docs. They're pushed to GitHub pages with a special process. First, generate local copies of the docs you want to publish: ``` $ bundle exec rake apidocs:gen_version[1.8.0] # for example, generate docs that you want to publish ``` Then, check them out locally: ``` $ bundle exec rake site:serve # then visit http://localhost:4000/api-doc/1.8.0/ ``` Then, publish them as part of the whole site: ``` $ bundle exec rake site:publish ``` Finally, check your work by visiting the docs on the website. ## Versioning GraphQL-Ruby does _not_ attempt to deliver "semantic versioning" for the reasons described in `jashkenas`' s post, ["Why Semantic Versioning Isn't"](https://gist.github.com/jashkenas/cbd2b088e20279ae2c8e). Instead, the following scheme is used as a guideline: - Version numbers consist of three parts, `MAJOR.MINOR.PATCH` - __`PATCH`__ version indicates bug fixes or small features for specific use cases. Ideally, you can upgrade patch versions with only a quick skim of the changelog. - __`MINOR`__ version indicates significant additions, internal refactors, or small breaking changes. When upgrading a minor version, check the changelog for any new features or breaking changes which apply to your system. The changelog will always include an upgrade path for any breaking changes. Minor versions may also include deprecation warnings to warn about upcoming breaking changes. - __`MAJOR`__ version indicates significant breaking changes. Do not expect code to run without some modification, especially if the code yielded deprecation warnings. This policy is inspired by the [Ruby 2.1.0+ version policy](https://www.ruby-lang.org/en/news/2013/12/21/ruby-version-policy-changes-with-2-1-0/). Pull requests and issues may be tagged with a [GitHub milestone](https://github.com/rmosolgo/graphql-ruby/milestones) to denote when they'll be released. The [changelog](https://github.com/rmosolgo/graphql-ruby/blob/master/CHANGELOG.md) should always contain accurate and thorough information so that users can upgrade. If you have trouble upgrading based on the changelog, please open an issue on GitHub. ## Releasing GraphQL-Ruby doesn't have a strict release schedule. If you think it should, consider opening an issue to share your thoughts. To cut a release: - Update `CHANGELOG.md` for the new version: - Add a new heading for the new version, and paste the four categories of changes into the new section - Open the GitHub milestone corresponding to the new version - Check each pull request and put it in the category (or categories) that it belongs in - If a change affects the default behavior of GraphQL-Ruby in a disruptive way, add it to `## Breaking Changes` and include migration notes if possible - Include the PR number beside the change description for future reference - Update `lib/graphql/version.rb` with the new version number - Commit changes to master - Push changes to GitHub: `git push origin master`. GitHub Actions will update the website. - Release to RubyGems: `bundle exec rake release`. This will also push the tag to GitHub which will kick off a GitHub Actions job to update the API docs. - Celebrate 🎊 ! graphql-ruby-2.5.19/guides/errors/000077500000000000000000000000001514115062600170225ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/errors/error_handling.md000066400000000000000000000036401514115062600223440ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Errors title: Error Handling desc: Rescuing application errors from field resolvers index: 3 --- You can configure your schema to rescue application errors during field resolution. Errors during batch loading will also be rescued. Thanks to [`@exAspArk`](https://github.com/exaspark) for the [`graphql-errors`](https://github.com/exAspArk/graphql-errors) gem which inspired this behavior and [`@thiago-sydow`](https://github.com/thiago-sydow) who [suggested](https://github.com/rmosolgo/graphql-ruby/issues/2139#issuecomment-524913594) an implementation like this. ## Add error handlers Handlers are added with `rescue_from` configurations in the schema: ```ruby class MySchema < GraphQL::Schema # ... rescue_from(ActiveRecord::RecordNotFound) do |err, obj, args, ctx, field| # Raise a graphql-friendly error with a custom message raise GraphQL::ExecutionError, "#{field.type.unwrap.graphql_name} not found" end rescue_from(SearchIndex::UnavailableError) do |err, obj, args, ctx, field| # Log the error Bugsnag.notify(err) # replace it with nil nil end end ``` The handler is called with several arguments: - __`err`__ is the error that was raised during field execution, then rescued - __`obj`__ is the object which was having a field resolved against it - __`args`__ is the Hash of arguments passed to the resolver - __`ctx`__ is the query context - __`field`__ is the {{ "GraphQL::Schema::Field" | api_doc }} instance for the field where the error was rescued Inside the handler, you can: - Raise a GraphQL-friendly {{ "GraphQL::ExecutionError" | api_doc }} to return to the user - Re-raise the given `err` to crash the query and halt execution. (The error will propagate to your application, eg, the controller.) - Report some metrics from the error, if applicable - Return a new value to be used for the error case (if not raising another error) graphql-ruby-2.5.19/guides/errors/execution_errors.md000066400000000000000000000055261514115062600227530ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Errors title: Top-level "errors" desc: The top-level "errors" array and how to use it. index: 1 --- The GraphQL specification [allows for a top-level `"errors"` key](https://graphql.github.io/graphql-spec/June2018/#sec-Errors) in the response which may contain information about what went wrong during execution. For example: ```ruby { "errors" => [ ... ] } ``` The response may include _both_ `"data"` and `"errors"` in the case of a partial success: ```ruby { "data" => { ... } # parts of the query that ran successfully "errors" => [ ... ] # errors that prevented some parts of the query from running } ``` ## When to Use Top-Level Errors In general, top-level errors should only be used for exceptional circumstances when a developer should be made aware that the system had some kind of problem. For example, the GraphQL specification says that when a non-null field returns `nil`, an error should be added to the `"errors"` key. This kind of error is not recoverable by the client. Instead, something on the server should be fixed to handle this case. When you want to notify a client some kind of recoverable issue, consider making error messages part of the schema, for example, as in {% internal_link "mutation errors", "/mutations/mutation_errors" %}. ## Adding Errors to the Array In GraphQL-Ruby, you can add entries to this array by raising `GraphQL::ExecutionError` (or a subclass of it), for example: ```ruby raise GraphQL::ExecutionError, "Can't continue with this query" ``` When this error is raised, its `message` will be added to the `"errors"` key and GraphQL-Ruby will automatically add the `line`, `column` and `path` to it. So, the above error might be: ```ruby { "errors" => [ { "message" => "Can't continue with this query", "locations" => [ { "line" => 2, "column" => 10, } ], "path" => ["user", "login"], } ] } ``` ## Customizing Error JSON The default error JSON includes `"message"`, `"locations"` and `"path"`. The [forthcoming version](https://spec.graphql.org/draft/#example-fce18) of the GraphQL spec recommends putting custom data in the `"extensions"` key of the error JSON. You can customize this in two ways: - Pass `extensions:` when raising an error, for example: ```ruby raise GraphQL::ExecutionError.new("Something went wrong", extensions: { "code" => "BROKEN" }) ``` In this case, `"extensions" => { "code" => "BROKEN" }` will be added to the error JSON. - Override `#to_h` in a subclass of `GraphQL::ExecutionError`, for example: ```ruby class ServiceUnavailableError < GraphQL::ExecutionError def to_h super.merge({ "extensions" => {"code" => "SERVICE_UNAVAILABLE"} }) end end ``` Now, `"extensions" => { "code" => "SERVICE_UNAVAILABLE" }` will be added to the error JSON. graphql-ruby-2.5.19/guides/errors/overview.md000066400000000000000000000060431514115062600212150ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Errors title: Errors in GraphQL desc: A conceptual introduction to errors in GraphQL index: 0 redirect_from: - /schema/type_errors/ - /queries/error_handling/ --- There are a _lot_ of different kinds of errors in GraphQL! In this guide, we'll discuss some of the main categories and learn when they apply. ## Validation Errors Because GraphQL is strongly typed, it performs validation of all queries before executing them. If an incoming query is invalid, it isn't executed. Instead, a response is sent back with `"errors"`: ```ruby { "errors" => [ ... ] } ``` Each error has a message, line, column and path. The validation rules are part of the GraphQL specification and built into GraphQL-Ruby, so there's not really a way to customize this behavior, except to pass `validate: false` when executing a query, which skips validation altogether. You can configure your schema to stop validating after a certain number of errors by setting {{ "Schema.validate_max_errors" | api_doc }}. Also, you can add a timeout to this step with {{ "Schema.validate_timeout" | api_doc }}. ## Analysis Errors GraphQL-Ruby supports pre-execution analysis, which may return `"errors"` instead of running a query. You can find details in the {% internal_link "Analysis guide", "queries/ast_analysis" %}. ## GraphQL Invariants While GraphQL-Ruby is executing a query, some constraints must be satisfied. For example: - Non-null fields may not return `nil`. - Interface and union types must resolve objects to types that belong to that interface/union. These constraints are part of the GraphQL specification, and when they are violated, it must be addressed somehow. Read more in {% internal_link "Type Errors", "/errors/type_errors" %}. ## Top-level `"errors"` The GraphQL specification provides for a top-level `"errors"` key which may include information about errors during query execution. `"errors"` and `"data"` may _both_ be present in the case of a partial success. In your own schema, you can add to the `"errors"` key by raising `GraphQL::ExecutionError` (or subclasses of it) in your code. Read more in the {% internal_link "Execution Errors guide", "/errors/execution_errors" %}. ## Handled Errors A schema can be configured to handle certain errors during field execution with handlers that you give it, using `rescue_from`. Read more in the {% internal_link "Error Handling guide", "/errors/error_handling" %}. ## Unhandled Errors (Crashes) When a `raise`d error is not `rescue`d, the GraphQL query crashes entirely and the surrounding code (like a Rails controller) must handle the exception. For example, Rails will probably return a generic `500` page. ## Errors as Data When you want end users (human beings) to read error messages, you can express errors _in the schema_, using normal GraphQL fields and types. In this approach, errors are strongly-typed data, queryable in the schema, like any other application data. For more about this approach, see {% internal_link "Mutation Errors", "/mutations/mutation_errors" %} graphql-ruby-2.5.19/guides/errors/type_errors.md000066400000000000000000000023521514115062600217230ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Errors title: Type Errors desc: Handling type errors index: 3 --- The GraphQL specification _requires_ certain assumptions to hold true when executing a query. However, it's possible that some code would violate that assumption, resulting in a type error. Here are two type errors that you can customize in GraphQL-Ruby: - A field with `null: false` returned `nil` - A field returned a value as a union or interface, but that value couldn't be resolved to a member of that union or interface. You can specify behavior in these cases by defining a {{ "Schema.type_error" | api_doc }} hook: ```ruby class MySchema < GraphQL::Schema def self.type_error(err, query_ctx) # Handle a failed runtime type coercion end end ``` It is called with an instance of {{ "GraphQL::UnresolvedTypeError" | api_doc }} or {{ "GraphQL::InvalidNullError" | api_doc }} and the query context (a {{ "GraphQL::Query::Context" | api_doc }}). If you don't specify a hook, you get the default behavior: - Unexpected `nil`s add an error the response's `"errors"` key - Unresolved Union / Interface types raise {{ "GraphQL::UnresolvedTypeError" | api_doc }} An object that fails type resolution is treated as `nil`. graphql-ruby-2.5.19/guides/faq.md000066400000000000000000000042551514115062600166050ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true title: FAQ other: true desc: How to do common tasks --- Returning Route URLs ==================== With GraphQL there is less of a need to include resource URLs to other REST resources, however sometimes you want to use Rails routing to include a URL as one of your fields. A common use case would be to build HTML format URLs to render a link in your React UI. In that case you can pass the request to your context, so that the helpers are able to build full URLs based on the incoming host, port and protocol. Example ------- ```ruby class Types::UserType < Types::BaseObject include ActionController::UrlFor include Rails.application.routes.url_helpers # Needed by ActionController::UrlFor to extract the host, port, protocol etc. from the current request def request context[:request] end # Needed by Rails.application.routes.url_helpers, it will then use the url_options defined by ActionController::UrlFor def default_url_options {} end field :profile_url, String, null: false def profile_url user_url(object) end end # In your GraphQL controller, add the request to `context`: MySchema.execute( params[:query], variables: params[:variables], context: { request: request }, ) ``` Returning ActiveStorage blob URLs ================================= If you are using ActiveStorage and need to return a URL to an attachment blob, you will find that using `Rails.application.routes.url_helpers.rails_blob_url` alone will throw an exception since Rails won't know what host, port or protocol to use in it. You can include `ActiveStorage::SetCurrent` in your GraphQL controller to pass on this information into your resolvers. Example ======= ```ruby class GraphqlController < ApplicationController include ActiveStorage::SetCurrent ... end class Types::UserType < Types::BaseObject field :picture_url, String, null: false def picture_url Rails.application.routes.url_helpers.rails_blob_url( object.picture, protocol: ActiveStorage::Current.url_options[:protocol], host: ActiveStorage::Current.url_options[:host], port: ActiveStorage::Current.url_options[:port] ) end end ``` graphql-ruby-2.5.19/guides/fields/000077500000000000000000000000001514115062600167545ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/fields/arguments.md000066400000000000000000000121431514115062600213040ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Fields title: Arguments desc: Fields may take arguments as inputs index: 1 --- Fields can take **arguments** as input. These can be used to determine the return value (eg, filtering search results) or to modify the application state (eg, updating the database in `MutationType`). Arguments are defined with the `argument` helper. These arguments are passed as [keyword arguments](https://robots.thoughtbot.com/ruby-2-keyword-arguments) to the resolver method: ```ruby field :search_posts, [PostType], null: false do argument :category, String end def search_posts(category:) Post.where(category: category).limit(10) end ``` ## Nullability To make an argument optional, set `required: false`, and set default values for the corresponding keyword arguments: ```ruby field :search_posts, [PostType], null: false do argument :category, String, required: false end def search_posts(category: nil) if category Post.where(category: category).limit(10) else Post.all.limit(10) end end ``` Be aware that if all arguments are optional and the query does not provide any arguments, then the resolver method will be called with no arguments. To prevent an `ArgumentError` in this case, you must either specify default values for all keyword arguments (as done in the prior example) or use the double splat operator argument in the method definition. For example: ```ruby def search_posts(**args) if args[:category] Post.where(category: args[:category]).limit(10) else Post.all.limit(10) end end ``` ### Default Values Another approach is to use `default_value: value` to provide a default value for the argument if it is not supplied in the query. ```ruby field :search_posts, [PostType], null: false do argument :category, String, required: false, default_value: "Programming" end def search_posts(category:) Post.where(category: category).limit(10) end ``` Arguments with `required: false` _do_ accept `null` as inputs from clients. This can be surprising in resolver code, for example, an argument with `Integer, required: false` can sometimes be `nil`. In this case, you can use `replace_null_with_default: true` to apply the given `default_value: ...` when clients provide `null`. For example: ```ruby # Even if clients send `query: null`, the resolver will receive `"*"` for this argument: argument :query, String, required: false, default_value: "*", replace_null_with_default: true ``` Finally, `required: :nullable` will require clients to pass the argument, although it will accept `null` as a valid input. For example: ```ruby # This argument _must_ be given -- send `null` if there's no other appropriate value: argument :email_address, String, required: :nullable ``` ## Deprecation **Experimental:** __Deprecated__ arguments can be marked by adding a `deprecation_reason:` keyword argument: ```ruby field :search_posts, [PostType], null: false do argument :name, String, required: false, deprecation_reason: "Use `query` instead." argument :query, String, required: false end ``` ## Aliasing Use `as: :alternate_name` to use a different key from within your resolvers while exposing another key to clients. ```ruby field :post, PostType, null: false do argument :post_id, ID, as: :id end def post(id:) Post.find(id) end ``` ## Preprocessing Provide a `prepare` function to modify or validate the value of an argument before the field's resolver method is executed: ```ruby field :posts, [PostType], null: false do argument :start_date, String, prepare: ->(startDate, ctx) { # return the prepared argument. # raise a GraphQL::ExecutionError to halt the execution of the field and # add the exception's message to the `errors` key. } end def posts(start_date:) # use prepared start_date end ``` ## Automatic camelization Arguments that are snake_cased will be camelized in the GraphQL schema. Using the example of: ```ruby field :posts, [PostType], null: false do argument :start_year, Int end ``` The corresponding GraphQL query will look like: ```graphql { posts(startYear: 2018) { id } } ``` To disable auto-camelization, pass `camelize: false` to the `argument` method. ```ruby field :posts, [PostType], null: false do argument :start_year, Int, camelize: false end ``` Furthermore, if your argument is already camelCased, then it will remain camelized in the GraphQL schema. However, the argument will be converted to snake_case when it is passed to the resolver method: ```ruby field :posts, [PostType], null: false do argument :startYear, Int end def posts(start_year:) # ... end ``` ## Valid Argument Types Only certain types are valid for arguments: - {{ "GraphQL::Schema::Scalar" | api_doc }}, including built-in scalars (string, int, float, boolean, ID) - {{ "GraphQL::Schema::Enum" | api_doc }} - {{ "GraphQL::Schema::InputObject" | api_doc }}, which allows key-value pairs as input - {{ "GraphQL::Schema::List" | api_doc }}s of a valid input type, configured using `[...]` - {{ "GraphQL::Schema::NonNull" | api_doc }}s of a valid input type (arguments are non-null by default; use `required: false` to make optional arguments) graphql-ruby-2.5.19/guides/fields/introduction.md000066400000000000000000000237441514115062600220310ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Fields title: Introduction desc: Implement fields and resolvers with the Ruby DSL index: 0 --- Object fields expose data about that object or connect the object to other objects. You can add fields to your object types with the `field(...)` class method, for example: ```ruby field :name, String, "The unique name of this list", null: false ``` {% internal_link "Objects", "/type_definitions/objects" %} and {% internal_link "Interfaces", "/type_definitions/interfaces" %} have fields. The different elements of field definition are addressed below: - [Names](#field-names) identify the field in GraphQL - [Return types](#field-return-type) say what kind of data this field returns - [Documentation](#field-documentation) includes description, comments and deprecation notes - [Resolution behavior](#field-resolution) hooks up Ruby code to the GraphQL field - [Arguments](#field-arguments) allow fields to take input when they're queried - [Extra field metadata](#extra-field-metadata) for low-level access to the GraphQL-Ruby runtime - [Add default values for field parameters](#field-parameter-default-values) ## Field Names A field's name is provided as the first argument or as the `name:` option: ```ruby field :team_captain, ... # or: field ..., name: :team_captain ``` Under the hood, GraphQL-Ruby **camelizes** field names, so `field :team_captain, ...` would be `{ teamCaptain }` in GraphQL. You can disable this behavior by adding `camelize: false` to your field definition or to the [default field options](#field-parameter-default-values). The field's name is also used as the basis of [field resolution](#field-resolution). ## Field Return Type The second argument to `field(...)` is the return type. This can be: - A built-in GraphQL type (`Integer`, `Float`, `String`, `ID`, or `Boolean`) - A GraphQL type from your application - An _array_ of any of the above, which denotes a {% internal_link "list type", "/type_definitions/lists" %}. {% internal_link "Nullability", "/type_definitions/non_nulls" %} is expressed with the `null:` keyword: - `null: true` (default) means that the field _may_ return `nil` - `null: false` means the field is non-nullable; it may not return `nil`. If the implementation returns `nil`, GraphQL-Ruby will return an error to the client. Additionally, list types maybe nullable by adding `[..., null: true]` to the definition. Here are some examples: ```ruby field :name, String # `String`, may return a `String` or `nil` field :id, ID, null: false # `ID!`, always returns an `ID`, never `nil` field :teammates, [Types::User], null: false # `[User!]!`, always returns a list containing `User`s field :scores, [Integer, null: true] # `[Int]`, may return a list or `nil`, the list may contain a mix of `Integer`s and `nil`s ``` ## Field Documentation Fields may be documented with a __description__, __comment__ and may be __deprecated__. __Descriptions__ can be added with the `field(...)` method as a positional argument, a keyword argument, or inside the block: ```ruby # 3rd positional argument field :name, String, "The name of this thing", null: false # `description:` keyword field :name, String, null: false, description: "The name of this thing" # inside the block field :name, String, null: false do description "The name of this thing" end ``` __Comments__ can be added with the `field(...)` method as a keyword argument, or inside the block: ```ruby # `comment:` keyword field :name, String, null: false, comment: "Rename to full name" # inside the block field :name, String, null: false do comment "Rename to full name" end ``` Generates field name with comment above "Rename to full name" above. ```graphql type Foo { # Rename to full name name: String! } ``` __Deprecated__ fields can be marked by adding a `deprecation_reason:` keyword argument: ```ruby field :email, String, deprecation_reason: "Users may have multiple emails, use `User.emails` instead." ``` Fields with a `deprecation_reason:` will appear as "deprecated" in GraphiQL. ## Field Resolution In general, fields return Ruby values corresponding to their GraphQL return types. For example, a field with the return type `String` should return a Ruby string, and a field with the return type `[User!]!` should return a Ruby array with zero or more `User` objects in it. By default, fields return values by: - Trying to call a method on the underlying object; _OR_ - If the underlying object is a `Hash`, lookup a key in that hash. - An optional `:fallback_value` can be supplied that will be used if the above fail. The method name or hash key corresponds to the field name, so in this example: ```ruby field :top_score, Integer, null: false ``` The default behavior is to look for a `#top_score` method, or lookup a `Hash` key, `:top_score` (symbol) or `"top_score"` (string). You can override the method name with the `method:` keyword, or override the hash key(s) with the `hash_key:` or `dig:` keyword, for example: ```ruby # Use the `#best_score` method to resolve this field field :top_score, Integer, null: false, method: :best_score # Lookup `hash["allPlayers"]` to resolve this field field :players, [User], null: false, hash_key: "allPlayers" # Use the `#dig` method on the hash with `:nested` and `:movies` keys field :movies, [Movie], null: false, dig: [:nested, :movies] ``` To pass-through the underlying object without calling a method on it, you can use `method: :itself`: ```ruby field :player, User, null: false, method: :itself ``` This is equivalent to: ```ruby field :player, User, null: false def player object end ``` If you don't want to delegate to the underlying object, you can define a method for each field: ```ruby # Use the custom method below to resolve this field field :total_games_played, Integer, null: false def total_games_played object.games.count end ``` Inside the method, you can access some helper methods: - `object` is the underlying application object (formerly `obj` to resolve functions) - `context` is the query context (passed as `context:` when executing queries, formerly `ctx` to resolve functions) Additionally, when you define arguments (see below), they're passed to the method definition, for example: ```ruby # Call the custom method with incoming arguments field :current_winning_streak, Integer, null: false do argument :include_ties, Boolean, required: false, default_value: false end def current_winning_streak(include_ties:) # Business logic goes here end ``` As the examples above show, by default the custom method name must match the field name. If you want to use a different custom method, the `resolver_method` option is available: ```ruby # Use the custom method with a non-default name below to resolve this field field :total_games_played, Integer, null: false, resolver_method: :games_played def games_played object.games.count end ``` `resolver_method` has two main use cases: 1. resolver re-use between multiple fields 2. dealing with method conflicts (specifically if you have fields named `context` or `object`) Note that `resolver_method` _cannot_ be used in combination with `method` or `hash_key`. ## Field Arguments _Arguments_ allow fields to take input to their resolution. For example: - A `search()` field may take a `term:` argument, which is the query to use for searching, eg `search(term: "GraphQL")` - A `user()` field may take an `id:` argument, which specifies which user to find, eg `user(id: 1)` - An `attachments()` field may take a `type:` argument, which filters the result by file type, eg `attachments(type: PHOTO)` Read more in the {% internal_link "Arguments guide", "/fields/arguments" %} ## Extra Field Metadata Inside a field method, you can access some low-level objects from the GraphQL-Ruby runtime. Be warned, these APIs are subject to change, so check the changelog when updating. A few `extras` are available: - `ast_node` - `graphql_name` (the field's name) - `owner` (the type that this field belongs to) - `lookahead` (see {% internal_link "Lookahead", "/queries/lookahead" %}) - `execution_errors`, whose `#add(err_or_msg)` method should be used for adding errors - `argument_details` (Interpreter only), an instance of {{ "GraphQL::Execution::Interpreter::Arguments" | api_doc }} with argument metadata - `parent` (the previous `object` in the query) - Custom extras, see below To inject them into your field method, first, add the `extras:` option to the field definition: ```ruby field :my_field, String, null: false, extras: [:ast_node] ``` Then add `ast_node:` keyword to the method signature: ```ruby def my_field(ast_node:) # ... end ``` At runtime, the requested runtime object will be passed to the field. __Custom extras__ are also possible. Any method on your field class can be passed to `extras: [...]`, and the value will be injected into the method. For example, `extras: [:owner]` will inject the object type who owns the field. Any new methods on your custom field class may be used, too. ## Field Parameter Default Values The field method requires you to pass `null:` keyword argument to determine whether the field is nullable or not. For another field you may want to override `camelize`, which is `true` by default. You can override this behavior by adding a custom field with overwritten `camelize` option, which is `true` by default. ```ruby class CustomField < GraphQL::Schema::Field # Add `null: false` and `camelize: false` which provide default values # in case the caller doesn't pass anything for those arguments. # **kwargs is a catch-all that will get everything else def initialize(*args, null: false, camelize: false, **kwargs, &block) # Then, call super _without_ any args, where Ruby will take # _all_ the args originally passed to this method and pass it to the super method. super end end ``` To use `CustomField` in your Objects and Interfaces, you'll need to register it as a `field_class` on those classes. See [Customizing Fields](https://graphql-ruby.org/type_definitions/extensions#customizing-fields) for more information on how to do so. graphql-ruby-2.5.19/guides/fields/limits.md000066400000000000000000000014501514115062600205770ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Fields title: Limits desc: Always limit lists of items index: 4 --- ## List Fields Always limit the number of items which can be returned from a list field. For example, use a `limit:` argument and make sure it's not too big. The `prepare:` function provides a convenient place to cap the number of items: ```ruby field :items, [Types::ItemType] do # Cap the number of items at 30 argument :limit, Integer, default_value: 20, prepare: ->(limit, ctx) {[limit, 30].min} end def items(limit:) object.items.limit(limit) end ``` This way, you won't hit your database for 1000 items! ## Connections Connections accept a {% internal_link "`max_page_size` option","/pagination/using_connections#max-page-size" %} which limits the number of nodes. graphql-ruby-2.5.19/guides/fields/resolvers.md000066400000000000000000000144061514115062600213270ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Fields title: Resolvers desc: Reusable, extendable resolution logic for complex fields index: 2 redirect_from: - /fields/functions --- A {{ "GraphQL::Schema::Resolver" | api_doc }} is a container for field signature and resolution logic. It can be attached to a field with the `resolver:` keyword: ```ruby # Use the resolver class to execute this field field :pending_orders, resolver: PendingOrders ``` Under the hood, {{ "GraphQL::Schema::Mutation" | api_doc }} is a specialized subclass of `Resolver`. ## First, ask yourself ... Do you really need a `Resolver`? Putting logic in a Resolver has some downsides: - Since it's coupled to GraphQL, it's harder to test than a plain ol' Ruby object in your app - Since the base class comes from GraphQL-Ruby, it's subject to upstream changes which may require updates in your code Here are a few alternatives to consider: - Put display logic (sorting, filtering, etc.) into a plain ol' Ruby class in your app, and test that class - Hook up that object with a method, for example: ```ruby field :recommended_items, [Types::Item], null: false def recommended_items ItemRecommendation.new(user: context[:viewer]).items end ``` - If you have lots of arguments to share, use a class method to generate fields, for example: ```ruby # Generate a field which returns a filtered, sorted list of items def self.items_field(name, override_options, &block) # Prepare options default_field_options = { type: [Types::Item], null: false } field_options = default_field_options.merge(override_options) # Create the field field(name, **field_options) do argument :order_by, Types::ItemOrder, required: false argument :category, Types::ItemCategory, required: false # Allow an override block to add more arguments instance_eval(&block) if block_given? end end # Then use the generator to create a field: items_field(:recommended_items) do |field| field.argument :similar_to_product_id, ID, required: false end # Implement the field def recommended_items # ... end ``` As a matter of code organization, that class method could be put in a module and shared between different classes that need it. - If you need the _same_ logic shared between several objects, consider using a Ruby module and its `self.included` hook, for example: ```ruby module HasRecommendedItems def self.included(child_class) # attach the field here child_class.field(:recommended_items, [Types::Item], null: false) end # then implement the field def recommended_items # ... end end # Add the field to some objects: class Types::User < BaseObject include HasRecommendedItems # adds the field end ``` - If the module approach looks good to you, also consider {% internal_link "Interfaces", "/type_definitions/interfaces" %}. They also share behavior between objects (since they're just modules that get included, after all), and they expose that commonality to clients via introspection. ## When do you really need a resolver? So, if there are other, better options, why does `Resolver` exist? Here are a few specific advantages: - __Isolation__. A `Resolver` is instantiated for each call to the field, so its instance variables are private to that object. If you need to use instance variables for some reason, this helps. You have a guarantee that those values won't hang around when the work is done. - __Complex Schema Generation__. `RelayClassicMutation` (which is a `Resolver` subclass) generates input types and return types for each mutation. Using a `Resolver` class makes it easier to implement, share and extend this code generation logic. ## Using `resolver` Use the base resolver class: ```ruby module Resolvers class RecommendedItems < BaseResolver type [Types::Item], null: false description "Items this user might like" argument :order_by, Types::ItemOrder, required: false argument :category, Types::ItemCategory, required: false def resolve(order_by: nil, category: nil) # call your application logic here: recommendations = ItemRecommendation.new( viewer: context[:viewer], recommended_for: object, order_by: order_by, category: category, ) # return the list of items recommendations.items end end end ``` And attach it to your field: ```ruby class Types::User < Types::BaseObject field :recommended_items, resolver: Resolvers::RecommendedItems end ``` Since the `Resolver` lifecycle is managed by the GraphQL runtime, the best way to test it is to execute GraphQL queries and check the results. ### Nesting resolvers of the same type You may run into cyclical loading issues when using a resolver within the definition of the type the resolver returns e.g. ```ruby # app/graphql/types/query_type.rb module Types class QueryType < Types::BaseObject field :tasks, resolver: Resolvers::TasksResolver end end # app/graphql/types/task_type.rb module Types class TaskType < Types::BaseObject field :title, String, null: false field :tasks, resolver: Resolvers::TasksResolver end end # app/graphql/resolvers/tasks_resolver.rb module Resolvers class TasksResolver < BaseResolver type [Types::TaskType], null: false def resolve [] end end end ``` The above can produce the following error: `Failed to build return type for Task.tasks from nil: Unexpected type input: (NilClass)`. A simple solution is to express the type as a string in the resolver: ```ruby module Resolvers class TasksResolver < BaseResolver type "[Types::TaskType]", null: false def resolve [] end end end ``` In doing so, you can defer the loading of the type class until the nested resolver has already been loaded. # Extensions In cases when you want your resolvers to add some extensions to the field they resolve, you can use `extension` method, which accepts extension class and options. Multiple extensions can be configured for a single resolver. ```ruby class GreetingExtension < GraphQL::Schema::FieldExtension def resolve(object:, arguments:, **rest) name = yield(object, arguments) "#{options[:greeting]}, #{name}!" end end class ResolverWithExtension < BaseResolver type String, null: false extension GreetingExtension, greeting: "Hi" def resolve "Robert" end end ``` graphql-ruby-2.5.19/guides/fields/validation.md000066400000000000000000000072031514115062600214320ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Fields title: Validation desc: Rails-like validations for arguments index: 3 --- Arguments can be validated at runtime using built-in or custom validators. Validations are configured in `argument(...)` calls on fields or input objects: ```ruby argument :home_phone, String, description: "A US phone number", validates: { format: { with: /\d{3}-\d{3}-\d{4}/ } } ``` or, `validates required: { ... }` inside a `field ... do ... end` block: ```ruby field :comments, [Comment], description: "Find comments by author ID or author name" do argument :author_id, ID, required: false argument :author_name, String, required: false # Either `authorId` or `authorName` must be provided by the client, but not both: validates required: { one_of: [:author_id, :author_name] } end ``` Validations can be provided with a keyword (`validates: { ... }`) or with a method inside the configuration block (`validates ...`). ## Built-In Validations See each validator's API docs for details: - `length: { maximum: ..., minimum: ..., is: ..., within: ... }` {{ "Schema::Validator::LengthValidator" | api_doc }} - `format: { with: /.../, without: /.../ }` {{ "Schema::Validator::FormatValidator" | api_doc }} - `numericality: { greater_than:, greater_than_or_equal_to:, less_than:, less_than_or_equal_to:, other_than:, odd:, even: }` {{ "Schema::Validator::NumericalityValidator" | api_doc }} - `inclusion: { in: [...] }` {{ "Schema::Validator::InclusionValidator" | api_doc }} - `exclusion: { in: [...] }` {{ "Schema::Validator::ExclusionValidator" | api_doc }} - `required: { one_of: [...] }` {{ "Schema::Validator::RequiredValidator" | api_doc }} - `allow_blank: true|false` {{ "Schema::Validator::AllowBlankValidator" | api_doc }} - `allow_null: true|false` {{ "Schema::Validator::AllowNullValidator" | api_doc }} - `all: { ... }` {{ "Schema::Validator::AllValidator" | api_doc }} Some of the validators accept customizable messages for certain validation failures; see the API docs for examples. `allow_blank:` and `allow_null:` may affect other validations, for example: ```ruby validates: { format: { with: /\A\d{4}\Z/ }, allow_blank: true } ``` Will permit any String containing four digits, or the empty string (`""`) if Rails is loaded. (GraphQL-Ruby checks for `.blank?`, which is usually defined by Rails.) Alternatively, they can be used alone, for example: ```ruby argument :id, ID, required: false, validates: { allow_null: true } ``` Will permit any query that passes `id: null`. ## Custom Validators You can write custom validators, too. A validator is a class that extends `GraphQL::Schema::Validator`. It should implement: - `def initialize(..., **default_options)` to accept any validator-specific options and pass along the defaults to `super(**default_options)` - `def validate(object, context, value)` which is called at runtime to validate `value`. It may return a String error message or an Array of Strings. GraphQL-Ruby will add those messages to the top-level `"errors"` array along with runtime context information. Then, custom validators can be attached either: - directly, passed to `validates`, like `validates: { MyCustomValidator => { some: :options }` - by keyword, if the keyword is registered with `GraphQL::Schema::Validator.install(:custom, MyCustomValidator)`. (That would support `validates: { custom: { some: :options }})`.) Validators are initialized when the schema is constructed (at application boot), and `validate(...)` is called while executing the query. There's one `Validator` instance for each configuration on each field, argument, or input object. (`Validator` instances aren't shared.) graphql-ruby-2.5.19/guides/getting_started.md000066400000000000000000000103751514115062600212250ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true title: Getting Started section: Other desc: Start here! --- ## Installation You can install `graphql` from RubyGems by adding to your application's `Gemfile`: ```ruby # Gemfile gem "graphql" ``` Then, running `bundle install`: ```sh $ bundle install ``` ## Getting Started On Rails, you can get started with a few [GraphQL generators](https://rmosolgo.github.io/graphql-ruby/schema/generators#graphqlinstall): ```sh # Add graphql-ruby boilerplate and mount graphiql in development $ rails g graphql:install # You may need to run bundle install again, as by default graphiql-rails is added on installation. $ bundle install # Make your first object type $ rails g graphql:object Post title:String rating:Int comments:[Comment] ``` Or, you can build a GraphQL server by hand: - Define some types - Connect them to a schema - Execute queries with your schema ### Declare types Types describe objects in your application and form the basis for [GraphQL's type system](https://graphql.org/learn/schema/#type-system). ```ruby # app/graphql/types/post_type.rb module Types class PostType < Types::BaseObject description "A blog post" field :id, ID, null: false field :title, String, null: false # fields should be queried in camel-case (this will be `truncatedPreview`) field :truncated_preview, String, null: false # Fields can return lists of other objects: field :comments, [Types::CommentType], # And fields can have their own descriptions: description: "This post's comments, or null if this post has comments disabled." end end # app/graphql/types/comment_type.rb module Types class CommentType < Types::BaseObject field :id, ID, null: false field :post, PostType, null: false end end ``` ### Build a Schema Before building a schema, you have to define an [entry point to your system, the "query root"](https://graphql.org/learn/schema/#the-query-mutation-and-subscription-types): ```ruby class QueryType < GraphQL::Schema::Object description "The query root of this schema" field :post, resolver: Resolvers::PostResolver end ``` Define how this field is resolved by creating a resolver class: ```ruby # app/graphql/resolvers/post_resolver.rb module Resolvers class PostResolver < BaseResolver type Types::PostType, null: false argument :id, ID def resolve(id:) ::Post.find(id) end end end ``` Then, build a schema with `QueryType` as the query entry point: ```ruby class Schema < GraphQL::Schema query Types::QueryType end ``` This schema is ready to serve GraphQL queries! {% internal_link "Browse the guides","/guides" %} to learn about other GraphQL Ruby features. ### Execute queries You can execute queries from a query string: ```ruby query_string = " { post(id: 1) { id title truncatedPreview } }" result_hash = Schema.execute(query_string) # { # "data" => { # "post" => { # "id" => 1, # "title" => "GraphQL is nice" # "truncatedPreview" => "GraphQL is..." # } # } # } ``` See {% internal_link "Executing Queries","/queries/executing_queries" %} for more information about running queries on your schema. ## Use with Relay If you're building a backend for [Relay](https://facebook.github.io/relay/), you'll need: - A JSON dump of the schema, which you can get by sending [`GraphQL::Introspection::INTROSPECTION_QUERY`](https://github.com/rmosolgo/graphql-ruby/blob/master/lib/graphql/introspection/introspection_query.rb) - Relay-specific helpers for GraphQL, see the {% internal_link "Connection guide", "/pagination/connection_concepts" %}, {% internal_link "Mutation guide", "mutations/mutation_classes" %}, and {% internal_link "Object Identification guide", "/schema/object_identification" %}. ## Use with Apollo Client [Apollo Client](https://www.apollographql.com/) is a full featured, simple to use GraphQL client with convenient integrations for popular view layers. You don't need to do anything special to connect Apollo Client to a `graphql-ruby` server. ## Use with GraphQL.js Client [GraphQL.js Client](https://github.com/f/graphql.js) is a tiny client that is platform- and framework-agnostic. It works well with `graphql-ruby` servers, since GraphQL requests are simple query strings transported over HTTP. graphql-ruby-2.5.19/guides/graphql-ruby-dark.png000066400000000000000000000043571514115062600215610ustar00rootroot00000000000000PNG  IHDR]H PLTEUU@@f3UUII`@UUMM]FUUNN[IUDPPZKUGQQYMUIQFNNUJRGNNULRIUMPHSKPIUNSLQJUHQKUISMQKUJSNUJSIRMUKSJULSJULRIUMTKRJUMTLRKQITLSKQJTMSKQJTISLRKTJSLRKTJSMRLTKSJRLTKSJRLTKSJRJTLSKRJTLSKRJTLSKRKTJSLSKTJSLSKRKSJSLRKSJSLRKSJSLRKRLTKSJRLRJTKRJTLRKSKTJSLSKTJSLSKTKSJSKTKSJSLTKSKSLRKSKRKSKSKRLSKSKRJSKSKRJSKRKSLSKRKSJSKSKTKSLSKTKSJSKSJSLTKSKSLTKSKSJTKSKSKTLSKSKRJSKSKRKSKSKRKSLSKRKSJSKSKSKSLSKSKSKSKSKSKSKSKSLSKSKSKSKSKSKSKSJTKSKSKSLSKRKSKSKSKSKSKSKSKSKSKSKSKSKSKSKSKSKSKSKSKSK CtRNS  "#$%&')*+,-.01234679;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrsvwxy|}lbKGDkjIDATs̳k3!9r96WQZ-"RTTXWXv&D%͝#%f}y^;mzewU*U{*& .W{S{+{;OjS{)h?[nSLGnUKWsjs`a Y I+ڬ#в"C Έ+W_t|[Lt1k7G^2]d4^5E8<)Km k*Y ʶּ%,Uc}ɨ CB67g,T!=Jяa#a)fM5Hw#T7)1fȦW'klj\P'H=ɡaP$iԱÿe5`X"e0MaK` Vj|_*} ~;J#/ád[ {+À ZzFx]b`캷hO;uiCoBhe i='urXuYC߭x.N0..wWSҤOݑiwmT#e>*xeo0d4QϘ3ۍ[7eLfO(up\LR.N#I ο} HS`l3Y9`&R~MKcjfZaS~G$`Z]LQ~NH*qQ*d|[Z*4f q ދn'5Ԫ0y*l2 y*l.Y )|] l \|GgpY$@C i6JTxnUh#[uV~]$?WiW<JS/]V *6"^wr<u꼽W{.OB}D!J\vKlhxh7VAr[l x$" ;.U<UQt}v\6ChTEqCUt}v\,84DUDCWϯcQU`Q~k ,)Qэ$Wŷ8߳f$p$gs:Z* q`S^j.i( @IENDB`graphql-ruby-2.5.19/guides/graphql-ruby-icon.png000066400000000000000000000100121514115062600215510ustar00rootroot00000000000000PNG  IHDRggA\bKGD pHYsaa?itIMEJ7IDATx]}xSU&)lKIR(<Jp|DGQeR*M˂]G>gQaGyQOqgG|`gwMR*|5G{Is~&iIOy;: 0` 0` HwW %yݜGbXLa`8Ol 5|L*o%`03cVޮ $9 [LK0,滚|ky)[=aT<J+u_%侖"mw,cIM"tۓyYpl?_5~ԊpQCGiK%c( f'{ifhk%G$B \TҊxb`Jh㡂Lky]R"v$f&@(Ia2]s0n0i*XטJg-ef߰73f ^bҶPG'y}~:4Mr0|1"d7<&^߻'mНؙfd8&an dӀ^헅y@@.KJƌjr6^M~eh)jBUyx+X{T*(@4t$ !EtIkyBV(_&K,I9 ׄRq`o\LPL#A\T7'MUĸA,6E-{ Pr1f]|3C^!ձ7U i+peL}Gp.aVW3nV(i0c^t8j81 ,܁r|Y,M?Q7y{286nVe!z LN'O | poHl]qY^2U/TM"oir<䄹]R=0IL7=P<.!rBeDPMJڥ)qR?d"6jSwc=]2nEϚj;;;uUVpuh=-hjlΗ2JNݱRvB0 @czͅ@ͱ(Bi3ر6= T)&ۭIdj7K7|ޜ/$d6\]ȩ; [WGAbΈ؜YVHD_~Pm- Ak$t9La[\_譤pX#T\9v7igRtcPo:zIw'*'Ԫ'kYI(/`zׁ c!Um$VxT,81 |ikNΩ8$3!e9z;rm n% `sͱ F%9M:{ψi@nsga'8.5ʂcGb a|MI&tYow~G0PC`~s YL1QDnWI-bI0VeJ& fE ~gð1W8ךL).q QBj \͇΀*nu?̔N&EjO(PdI< #~K?´@lq>/nزGO 5=z=ewu~AcyQ%a=r5Uzs A3b"vE`][ϊA*1;ߦkzɄUKu:h"EK)*3q4n8nX;=CF[ v Mr)"Le{omx0x*X;O !G~Ym꼁$zf!CB  _-r6,98 40xDP*] "h6dTaX3|xž$O,+ꦆCGwZ::U?݇S&niJ3LT;KW%7B{:ǃ+{<d٩ uBo3k~W`]I$E!T y_C:ׁ8DՄN^kIJt;ljaЃA$oo.3TF{$彷Z~EJHXw f'YԈa0 1}"'H$Eн4@ys}2=Mߨԗˉ[;s\dA8A`Zts_ HYr̞tހNEBIlUÄU!dSwޥ iVZUA?cJcc;NMǽVhR"[t[|#qujTD@XkU>2 zwk.zfo n}'>S{:xt֭&q"GJp C 9"/C ~,IWꑘh51DpĤrzysړZ<#GGEVe DE|וQ 2W@,ndLAyޯVxEeڔ1aA*,D/@-`W?!N=P_:Q$Hl3?dߧӔBN>=d)ꬎljBK _Ngi}oVRB6C+t M mJ@vT{ꬎ t.Q$0jd"r՝L`m:gߪaꃾ YiT~1A``XάNFcO:C>+tΐÄĸ[Z;fn8mxl8V..f ݿXAֱ[B \CwSΐ3]M0Y=jٚkj?^V粐g!R&md9|8|?xW$m&%uu@L*H hi:^P 3͖ME~ן\R]z:7L!L%bShJ&Дt%&ܺ< Wȷƈӄd Al&bt9J=ް9(fUoT#C~''NА} Rț7"51UO!8 ĔYē"Ep{jb@x![wՀ}՜UGy3:^եc?i:L LA]/E6YGNʥ0ss\$?zޒ?Wͣ;-]{U dʹ(1nv;ÃZfeu0-WڕG}-M@ɖ Ѻ"UErr] ދ jub8-Q;Ɍ靿#v|)+s`жYgPPx4' _1+{4)r]t! `|] eU} 19GN߫Ŀ19IOV|1Y2ki\$&gige4 D稅Ir钖dfphw/#7Ig`z[c(r6 `YZ̽\0U Ǚ>M96"9"Hc*b+1)[ {a 0` 0`o$r{IENDB`graphql-ruby-2.5.19/guides/graphql-ruby.png000066400000000000000000000122271514115062600206350ustar00rootroot00000000000000PNG  IHDRgOۢsRGB pHYsaa?iiTXtXML:com.adobe.xmp www.inkscape.org 1 UXeIDATx\ TřgFF LC#,FMD#(p#f$g(1(̃`| fW.`{fI,țaqkwzwgw뜙zU_U_<SIv"X`\c\Gz"Y0 Z5mcSR];|ESeB=BhϵF{F65.kuBa ;)ɊIa=nr`'6pp_ ZUww`sd8u\0_6ul);Sdbs0|N/DktI01-~'50&ÄcDrgat,}N`e-˖տVsC#Bgq$#6@%afT zrXyO/yGq+ul8묪 =)|Hm ? Zk4beGvbBjm NSc|1 V^1QU2tc= 'AWwL<(X$L M5G;`m~=g@rS4" ,x3yg+[ݏ'6Ԅs&Mk|-]V2ўN퓧ORM=?>f{%N,9S5i6#Ic~_rd v< qiHJHeQZ(95Ոo2YG_$lod IbSȼ5^~2]9{X `MP?-TAmtW-D__ͣMפLʬ4E2$ i)NIS X8 /GI*N +dM9= 5Ɲ\\aRⱋ;}"8Fbs{7 5eދ/›;5ଭ#-=Wi }  BW\lϞSn M(lنHf>Oׯ} :gO¹V~0aW4E^Ӹ*_//[Ϙ1ͧ!s Ht;4zYe *vq!~ {Pڕ1zIbO rx~HܗВTڤx"@x!ކ}nI aN6v4އ9fb<5O>){dJN{m÷*iL0X|.oߵzX$EsKKׅiT{x9?^ܴ[W"h31O` k&N0Ha Aj G1DM$t#@Vћp 6("60NĬ `!M/t9QCfg.kjJ#Eξ?; 31 M_IM$qv-) 锱 \і~xFN=qE_@!y Fa\VEX\yܥ!x}~=|^`:/Ipo2DD< ,BT6=H`g:D\;mdʧ3p ODvV6oK¸zjb?9қn,[8"@#gA$0{._Xl%GnQ}aFwSDj~ .?UCu,̋1 d6# :A "Vc/bs"%M lP䬧 )L[an^y9gT>7PAK@! olЇ'Nn2}"07jɔrOe/ b[6L:Z֖*]:29fr5l| Z0|V-+mP 7#cikoF>5=e}1[j$Fje߲Np:b!"9xu?N"k?j*+)r QϏ?\Ic$Ν7؄_0pQ%~)N-^ ֞j+Oy)USpm^=돆wi0S~ξ"G B~vթcdH@c;0p;fbPW bV$7n !W8cbLK3F\f+~Z6p"&#!ݚ[{U6 &X')m4W~>Y`c|ZMpgS{MӬծCXAP?2TNU\a9dS^U6kGLX.A 野nLWb $ZȦ±A x0Ʋ[x}~eWX]c+rx1b? |L>{b ^BbԴt*{$9)AeęLgKِ$W@zJ܇_'XtQpU G/=pHgLb%T9bCS ͧW(t_M h G۔Vz .qK㘝1w(RqᏉ1OlUeʬ/ sa.C w+aJ Ws :o7z%}$I7K%Qe.=*s[=JoH*i^RXF`g<@ܥE}Q{N&ʬ˖L=[yٔ)"-5cSg}pE i*~:?Ȃ( e Q}N”96sXAjtQ1j$~dn`J$aLdcgz1NO$Cbnm B83|-'́Fg5}S%Y:D f{ DM[쵟N'.pS9 VN vpcXKOCD\-mx>T¾B88]A "W,͘Օ.-&N^yӯ̶e\6U=UűE`Pvƈo/;X4zb+S.٭Ñ{4onjs2;Er|the=^\{|SZŏK_8Lis/K s $Zh/Po +?lD)}XgdYYvƃ_.oo*e1:!yu0Yӂ\?Cc)kk-d|̓9CC h(GQx? I 1&cyҥ-%k;! 1hm6ߟvgƤ_9'}+'p3&C)Ku,RD9N>Y|^!Mg`/6mxU;6ØbSq>pFޭVr'SIENDB`graphql-ruby-2.5.19/guides/guides.html000066400000000000000000000037361514115062600176650ustar00rootroot00000000000000--- title: Guides Index sections: - name: Schema - name: Queries - name: Type Definitions - name: Authorization - name: Fields - name: Mutations - name: Errors - name: Pagination - name: Relay - name: Dataloader - name: Subscriptions - name: GraphQL Pro - name: GraphQL Pro - OperationStore - name: GraphQL Pro - Defer - name: GraphQL Enterprise - Rate Limiters - name: GraphQL Enterprise - Object Cache - name: GraphQL Enterprise - Changesets - name: JavaScript Client - name: Language Tools - name: Testing - name: Other ---

Guides

{% assign sorted_pages = site.pages | sort: "index" %} {% assign pages_with_index = ''|split:'' %} {% assign pages_without_index = ''|split:'' %} {% for guide in sorted_pages %} {% if guide.layout == "guide" %} {% if guide.index %} {% assign pages_with_index = pages_with_index | push: guide %} {% else %} {% assign pages_without_index = pages_without_index | push: guide %} {% endif %} {% endif %} {% endfor %} {% assign sorted_guides = pages_with_index | concat: pages_without_index %} {% for section in page.sections %}

{{ section.name }}

    {% for guide in sorted_guides %} {% if guide.section == section.name %}
  • {{ guide.title }} {{ guide.desc }}

  • {% endif %} {% endfor %}
{% endfor %}
graphql-ruby-2.5.19/guides/index.html000066400000000000000000000115701514115062600175070ustar00rootroot00000000000000--- title: Welcome fullwidth: false ---
GraphQL Ruby Logo

GraphQL Ruby

The graphql gem implements the GraphQL Server Specification in Ruby.

Use it to add a GraphQL API to your Ruby or Rails app.

Install the Gem

Get going fast with the graphql gem, battle-tested and trusted by GitHub, Shopify, Flexport, Chime, and Kickstarter.

{% highlight bash %} # Download the gem: bundle add graphql # Setup with Rails: rails generate graphql:install {% endhighlight %}

Define Your Schema

Describe your application with a GraphQL schema to create a self-documenting, strongly-typed API.

{% highlight ruby %} # app/graphql/types/profile_type.rb class Types::ProfileType < Types::BaseObject field :id, ID, null: false field :name, String, null: false field :avatar, Types::PhotoType end {% endhighlight %}

Serve Queries

Provide custom data to clients and extend your API with {% internal_link "mutations", "/mutations/mutation_root" %}, {% internal_link "subscriptions", "/subscriptions/overview" %}, {% internal_link "streaming responses", "/defer/overview" %}, and {% internal_link "multiplexing", "/queries/multiplex" %}.

{% highlight ruby %} # app/controllers/graphql_controller.rb result = MySchema.execute( params[:query], variables: params[:variables], context: { current_user: current_user }, ) render json: result {% endhighlight %}

Harden Your API

Confidently deploy GraphQL with GraphQL-Ruby:

  • {% internal_link "Testing helpers", "/testing/overview" %} to validate your system
  • {% internal_link "Authorization", "/authorization/overview" %} integrates with your app's permission system
  • {% internal_link "GraphQL::Dataloader", "/dataloader/overview" %} optimizes access to data sources
  • {% internal_link "Complexity limits", "/queries/complexity_and_depth" %}, {% internal_link "timeouts", "/queries/timeout" %}, and {% internal_link "rate limits", "/limiters/overview" %} to protect your server resources
  • {% internal_link "Tracing", "/queries/tracing" %} for integration with your APM or custom usage
  • {% internal_link "API versioning", "/changesets/overview" %} to roll out changes while preserving client experience
  • {% internal_link "Persisted queries", "/operation_store/overview" %} to guarantee approved API usage
  • {% internal_link "Caching", "/object_cache/overview" %} to serve repeated data requests

Integrate with Client Libraries

{% internal_link "graphql-ruby-client", "/javascript_client/overview" %} provides integration with {% internal_link "Apollo Client", "/javascript_client/apollo_subscriptions" %}, {% internal_link "Relay", "/javascript_client/relay_subscriptions" %}, {% internal_link "GraphiQL", "/javascript_client/graphiql_subscriptions" %}, {% internal_link "urql", "/javascript_client/urql_subscriptions" %}, or custom JavaScript.

Going Beyond

Customize your GraphQL API:

  • {% internal_link "Language tooling", "/language_tools/visitor/ %} for manipulating GraphQL documents
  • {% internal_link "Type system extensions", "/type_definitions/extensions/ %} for customizing your schema definition
  • {% internal_link "Query analysis", "/queries/ast_analysis" %} for ahead-of-time query inspection

Add GraphQL to your Ruby app. Get Started!

graphql-ruby-2.5.19/guides/javascript_client/000077500000000000000000000000001514115062600212125ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/javascript_client/ably_key.png000066400000000000000000003616541514115062600235360ustar00rootroot00000000000000PNG  IHDR,IZ iCCPICC ProfileHWXS[R -)7A@H! !,*v*b[ kE `ETVTޤ}|s3{duGHj',Dž0SRӘ'(Pes$"(e ܂PbM.O8+A|\#@hz"X[ @e2c6 q@b>j2BQqtrB d?xL^t[g|#1lV"r@"e?K^txSبY8YpLa*'1kA|Q,iXҾ# kl.;(bҜD%vf'(qxz2>Z(̍RY [xaLAH8İУEY hs )b5%9JGEY6bi92!q L7O2a炵d%)|$%jqyD%7 VW@ҷXǶrC^+XI6;"V9׀ 6A GAA e z{/H`1ad>A"Gy꿌hO{)-{gGl8 cJ &È!D:61]$y0;p:O7 f\{ndGWUsUy3#V?F n[Z%VikNH%"Dd1RE*H +r9\B:cy|B1j%:eh$NAh>Z.AWh5CϢWЛh'10caX ebblVaX'֋}ĉ8g6DxÛcJ vOB8!' wM  ~ll fAbO"Hv$oR M* 6NIȪdc39F{ɧɃ****1*\Y*Tv4\UVhR(ޔJ6e!rrVUUTCu@uj!ՋU?R@dzzFYhiJZ ]A-\6_RN+uu uEeGԯjhXjj5iTj׸ѯItҌ\W -VWksZ]tnFs;Dm+plRm}:Z:ttfTd` KF8#qqi(QQGum{Ѻ~<݃7u?1r=m'ߢ^whќ%g6ajohdj(2dxΰ׈agmQ1X`L?3YlfHM Z&.2=hЌb24[odgnl>|y=  EFVɖK--_XZ[YZ=YZ[W[߰!ڰlrl6۴ۢYWP;7;f11ccܶ?v`8D9,rwx5|l5c[~utuuxI)iSg[gs K|mwǕ:uk7w7wst*,mV,k#c n=bx㻼M۽;}>>|:}M|پվO~g 8>3pn ,(4$-X+81"Qi?6/5tv0BXdؚ͑Ȋ'QQ 脈 &<Fǀu1cbcH;r89q-i{V%ON&6%'MNIz63elܔ+Ԇ4RZRڮI6L:x)VSfN4UjԓԧI''M̎aW3328\?znϛ<;sm 7'7,W(ޚ>'&gwPnr !ε}%K/._qRzwߏ]u15kg]p#ƕ7;n%޺s{;;/}}Jj<,{d68yq'Owq^><ܽYs5/_ ic/E/{ڗZz͊zow?@z|d}lϤ_l4~`(ohHG 6437³C;5K.(G?aL.n qQ6 2Ҕ"tqVĢ [CH| n |ŝO&Dxf#Cm=lW pHYs%%IR$iTXtXML:com.adobe.xmp 1008 812 aʴiDOT(V[@IDATx }ScL1 EJJ! MW隆rJtM{F_xtK,DdM" H@$  H@ ݎST;fvH?xzߜ:zǾ@~lM[lE'.u~$  H@$ C`T .otwW^f+ȸ|< :o̝3DL1zӔSN^ծwaoxӫj^JW]uUfiҊ+8ƹ$  H`|Uf[h$5G~~%Y^y׿~{$  H@_}!xP_?EY$6_~_e/-+𼯵Zy(Ȁ 9#7~=W_}u{޴RK[I@$ @` ?y\;e7)M;8d>iyֱ͓KBUk]?Bz'-oI ,@mBƳ3Q 9p|U?CE30CuYC ^L3͔7 X_G^<,$0yw&Ō՟gyҢ.:h0؏y (#ċe~˲]wݕ{\,Lzꩋ=S>ƭޚ?Ԑ㴪PF+< Q(³4 ӟ4wZ1Kڼu*r 9y{$V8# o:ϚuP#M%%Y旼RDL}'ʓr`DgWDoP,+m8R8/kM)ZQ'9s9gˎvyr\%Bp/]' &wCeX":·C3k5y5Q_2Gϟ_'u#h_:|gay'NYh n4\$Ի~]{yqx#>O9up\V3<&"V\B300:Zξ0`)vM@b H J6D0p>j#fDuH#:N4p e05 䉎Lt錃3{s)?5Mo;Fe S\ʉ3'0C ' H@&0Q83VqxV\sX=೙ǃ+'`xATÃcil4Ӑgv!P<#oh0T@GtfߎU<κi>FTAC;`"()C=t2!9cv1x.1Gru#'"r,2Ɯ)C'?EE!/B˜&D(t # S53&ʲ4qtDD'fQgxlcYr,7"'EGEDM7C}u?񏳈:'Zo=GPeDfR0r O;2DP,$]G: #(By`v54p:¸oPftĆ.OΗ5S$  L4*xꌰr])>C fOl4aeVԹ?œկ%i!h73DcmcI}wO8Ӄ>qbU/wc/GgyGGx>!lK>` e>^ wܶ/Mk;Cxl׾\+ߊ{x;IL,}YBO H@$0/uLqIG]'ҋeofcNOy8 OntI97fgr7۵RL꬙c>tD( Z VC#E~ NF_,7 {f{Mr"a!fF8{2`\?CAn-,MǬ!c7Dc{i>`CxxyqUA?:⸞鐋ۗ/"#0|g8^Hpc-" ؏:ۍtu.L3e  fuk爲& H@$?}-Y,<;w*1=V^I;/E|p7Y⭎[ީg/~9IqAQ5(^Fl,|uz>oOvȳ T OG9A_?ߙqC*[zrax #۱',ǑW;0HP8nU55;ؾ1mnuuP+K"uWib?FedCgt0>N:C !:* H@&,ntD@ơ:C--d xxڱv3i5 gLM."!4Ÿqm7BIf4( !Ĭo*I0rƫhX3Zfr0L^u*$xE1^l3|uR7ؿr6&"Su0zœ^" 2dnx'CTqv?#4B^(BCE:w/: NêªjD@g2k$uKc>43Êr+-:lXƹّx;a>VC!F*[_6 eGG z1:8b>(' >~J@$ Lǻ S.X O=H,!Y~(!اYY7|2L'ƹ!3SꪼeH6CHkΗG@F,N*Q |U"^#)2DL~Iy9+}l[N|x~bձU6t@c\tP}gϪ&$  H@S&l&wC? fxX-OZ4LO46:ƒYFG>,V!z<q4 x,'g#aԬoǺ!9X{GjpF{d"y#^X"cHB_m~Ekc_^O༨̇;9QKȍ ת[}e_ʈqLHCx3!8eDBx˙Yt1rK馛}&xYf܁r$\󃍛vtftFDGf +(ٰPxro:G݋8$  H@B? #QwwuH.EJv;1Яx;!rH& !BX2Eo QPunD8s*0p&D j uf$ jf 3idžO!bAgAGb5:(RB3&]՜wJ\f}XOc==.o.$  H@N`=p<^>8giϡ)%/x'.["m$K/4]p\ #y<Ӗ@I $  H`Pp#+x]w5Ow^F=q':؁ ؈Nx7ax\ 0*A IM$  HKO,=s8.Nkj'H뉓2BU D<0vqڭI@$  H@㏀~I[9b5*wk%M0GxVs$  H@$09Lzb 1fg&c9ռ>{D̤$  H@$  E@&7$  H@$  -$  H@$ (F$  H@$ %[]$  H@$|[H$  H@$0cߣK@$  H@"o I@$  H@Ɩ~l{t H@$  H@@[mar# H@$  H@P-. H@$  H@h-Ln$ H@$  H@[ %  H@$  H@mPɍ$  H@$  H@cK@?=$  H@$  H- 0$  H@$  H`l (ǖG$  H@$  E@&7$  H@$  -$  H@$ (F$  H@$ %[]$  H@$|[H$  H@$0cߣK@$  H@"o I@$  H@Ɩ~l{t H@$  H@@[mar# H@$  H@Qꫯ{.ˉ$  H@$  H L1i)LO?}hو x>p8$  H@$034Өx_|,GGbTK˃I@$  H@&04Ӥ馛nTgJz H@$  H@+8gyQ9 ~zTNH@$  H@ƒ,2*W f" H@$  H@J@߯%yI@$  H@@_PUqz2$  H@$ЯZ$  H@$  |_'# H@$  H@J@߯%yI@$  H@@_PUqz2$  H@$ЯZ$  H@$  |_'# H@$  H@J@߯%yI@$  H@@_PUqz2$  H@$ЯZ$  H@$  |_'# H@$  H@J@߯%yI@$  H@@_PUqz2$  H@$ЯZ$  H@$  |_'# H@$  H@J@߯%yI@$  H@@_PUqz2$  H@$ЯZ$  H@$  |_'# H@$  H@J@߯%yI@$  H@@_PUqz2$  H@$ЯPO>d^Q8ZwviTSM5OrcjPg_ק)f$  H@W\t7G}4M9MozSz;ޑ/}#I6}<#E]0ot?{ɝs=wZl'?ɴzif㎉~tP6xwtgc (/}CJ[mUZr%d[^ve^iUVik^衇J[ou뮛>ύI/tQGc/8]{k_Wzֻ8:ì ~wN_rg93`up$  H@ @cx]vI?MugLgn햦nAN3YsE'|饗G?2g=q#Ն+r+j!Ҧnn֦ԧ>gat!`{vi2bc_״2яvUWM7tSuYӽ;`xEg۳>Lg^;N$  H`h~vxuu;Q{G"!yy/| .OwO3Oߟ?#k,/;vϟQ K.Pm]:C׏sa5 oxCZ;YlDt3oqcL1u6i]-~cuP~iEMs9ggy/O=\#N8!wRg*H^FY) H@%B'<Yoy[&K~ DA_6x%:80qV[m(-9::N8HxOX5 +w}o}+-re:87R𣑏+B ЙDX|0hw1ܙg9JMc-1Op$  H@㑀RA#4o喜!ft26!=ؾe}tꩧZs5''juO&:+Z2~6K/H.Bx|ۼ+oDD;!\gMou< &3x.& H@P(Bfo|٣*;W54acx_i,x8owoM|;c쩧J?^8G<3y=ƳK7?'x"[ҩFW^ye#7pCc[2BO80Vm$<v[fC&k`xdlʜehH٧;R64wIHYNF0nҫRҘ>H;ӳeYE#T!Tk7d<<۟r)x^{C,m$[lјQl/~͏992x-}MpEp#r23"?x;Ve7uY[~0::Fq?=`H'4itpoS$  g JȱpD0%YLXhqtU6/4""Y箻F|t0S{x<vX$'Bb29Fi#%[6/ LDnC-;t` <87I믿~xYeS.?`u]OtP_5>(3iNhB f1"nꆖFu x:!`D yahA9䟼/'uoS{՝Un$  H`PwP$QU*VFc=ydа,CC!=Wyl >ƸYxJ3prK^NDzSƋJDcՐ뼰 _?`q"+Cȉ@Һ-Svԍ3:a\ x:.igE"hTQ:0 wЏڱcA6\ϰ&oLuz c{A78)먻L$ ^$0U&pfI2fPU=ץǯCdUt FP8R#2+mfxef+y1MhuxgӨǘ.}N=tB]H 8aӄ# L)C-[ved~5P"ʺҪN+[ ^^_ DaRKLVFX >fG9! s7TӀUo۶{8,ONt/ N :.$  H (;(5V24TDu)R.g,-㪛F{[Y+?11s4,E!1_5ZLO0z471\!:K[" 4(m| mϤfXU/-;xr";ƌчcDE\E +\ VcrAgs8`Uk4rى/QF+_\C?܄[]tV\{Cx^q~G¹1'LX780>) H@@/PwPjP&"xc|w; n%Jǯf)^9.3ѡآ`+}VSH1τ#3=^U2}5lUVYgfgHc|@3Vh xAzxO|Um Y+ꩧ}c9.sh_: qg>xb0/i4T+J:ݮj~#Ct#>:UAc#'u{fY\#Cs#{op|9s(TBQw8pRבv$  H@H@ao6VC# 3go2cqgm<csKCtlUS 2 Gja(=J1ZzٶlX@gf C 0&(Cxb|BB19n3v>Dr^YCJ-*H"mVvfnG3(ZMVM]֕fu ~7x!:^R$5'۶(ߙIp0)C\y啵 #% yn5ۉ'xmUyuYVDTjŭ@zJkƋ!'۳"uDönC]$  |J25j AK}|78{Y#+o1yn1m{׆~щ! k'l;'̖O/A _ʱX蠃!o[v9//!QD T֕xy6˝xry晬T!^'\ %Xy}T)Î:]D}!މ*aKiezu߹7QQ::^ c=;OgZ+gGsO ފW5LXIy⚡P4T) L} 7$  H@ .O<2ULcp<@J0ҥ!`<-󠕀|sZ:Oxͪ]Q֎+]wuu}sY#<0# fml'y1'coqcg0at`;c2LE]z%FEvK3܉o(6~$  H@ㅀ%Xd'DOL3͔Ö]xp!Z3>ҤP>$G#x _SrqvEY)bG:O&o;#rFxcDk=xxK;!sOji#!Iw\N70γAP!)1\'G3iq\::U o;#6nQDcAi֕v q7&/xۏ]@ <s=y= 3ڥ:X1!u!FZ䁺ɤr;Qu\;Exo]z-k&aO(2d8]G`b=zBCG;6\7FFOѮ!In`?;5/`zʜ:yP 5$  H@I@?ѱ8ʞYd 6t$  H@O vRF㍀~$  H@m &>N'a6&oJ@$  )|`x+# H@$0M|ęAcf'6>lWLmQ? H@$  L RҞ$  H@$  4|O$  H@$ B@?QJ$  H@$ &3$  H@$0Q('JI{$  H@$=]|f^$  H@& D)iS$  H@zK@$  H@D!(%yJ@$  H@@OPty H@$  H@(=O H@$  H@i .>3/ H@$  H@~) H@$  H@=M@aI}@IDATg%  H@$  H`PO<%  H@$  H ({̼$  H@$  L RҞ$  H@$  4|O$  H@$ B@?QJ$  H@$ &3$  H@$0Q('JI{$  H@$z^?3W_MSL1E0$  H@$ @h]>gyb}b~u$ҋ/#q Ӕ$  H@$  6wꩧNO?da<>Q)F" H@$  H@M!?L38 x2K/ƹx H@$  H@@O9ѲuG$  H@$^ž$  H@$  |_'! H@$  H@N@%I@$  H@@_PE1z$  H@$^ž$  H@$  |_'! H@$  H@N@%I@$  H@@_PE1z$  H@$^ž$  H@$  |_'! H@$  H@N@%I@$  H@@_PE1z$  H@$^ž$  H@$  |_'! H@$  H@N@%I@$  H@@_PE1z$  H@$^ž$  H@$  |_'! H@$  H@N@%I@$  H@@_PE1z$  H@$^ž$  H@$  |_'! H@$  H@N@%I@$  H@@_PE1z$  H@$^ž$  H@$  |_'! H@$  H@N@%I@$  H@@_PE1z$  H@$^ž$  H@$  |_'! H@$  H@N@%I@$  H@@_PE1z$  H@$^ž$  H@$  |_'! H@$  H@N@%I@$  H@@_PE1z$  H@$^ž$  H@$  |_'! H@$  H@N@%I@$  H@@_PE1z$  H@$^ž$  H@$  |_'! H@$  H@N@%I@$  H@@_PE1z$  H@$^ž$  H@$  |_'! H@$  H@N@%I@$  H@@_PE1z$  H@$^ž$  H@$  QrK;Z{^{ 暫]#]uU?f_.$9W^y@?ӯƲUVY%9眍~$  H@$  FE{O/3Θ6x6ۤiCY6} _h~7-R[?9VX!wqӶnXv'[/$  H@$ X i%E}*;$  H@$0Dꪉ/=4묳G oxÀ5$  H@$ @`L_rn5\3/k,{.=裍>6~w ?|NrOs1ǀ $  H@$0q'/6d쑏O/xO|'  H@$  H`T K:}o{l_^_$  H@gIZ]v٥P' o}kcIcڿmo{キB -fa_y}fJL7 ;ll7*8uu WK|lgy",M6@#s5_&,ȫ {,=#{s^X[y|ꩧ5\|+)g=-2yj" H@$  H@}C` x3]VZ)uQ 'tR:S7s;t1Ǥ'x"jN;픿O;Cο5r,g=63/xW]xi}#yvmn&nFK/MvXz'sp~k]]5o&w}O?=:9+95G:([&/ΘSӟt_Eeԝ$  H@$  q+񖯲*ϴFw߽Nvio]ۯV_}/;sk*fɿ袋TSMwS#=teeceoDp V?)#|tAil3K@$  H@yVxٛsϴ.=_WҎ;X_x}Xc?f+ '>vmjfm-H{,\pzB&Pw6l6gO}_`Oihҏfy<pg/՟ ?tMWͪ%  H@$  q)[oupwxƫo!}(-oY g~_5{/4,guV7X|g4ڳ[?q1obX?X#^eX_v Dt>`l7cC9w&Ƴ5tUWC3<1@䃰z駉 $  H@$PG` <^39bRŃoۍ >U~x?яt~y1^nx&?>{kPl9^裏N/Nw;믿~z衇!) H@$  H`N6[zOxL\Ga8?akM)c>x膀\z啉fc,[n?g7駟> .HL{&Չ>O5BZӡLt|Ncuyҗ|<"'gvM$  H@D'0&]P3+<.ULl;&O/~'N=ԼPvuUdvC7|&+S~ !9~cUlfuy_z# `C"rrH@$  H@hƭGl2Z[&U_ ۩0|3y~%gY0n-"~cc6gy&jk馛6~7뮻6Ҩ#yx^z@s2$lr=y",2eՂ o}[]zWuU7|sb@\s솀'kD0Wʻ3{;߅1a:?~r!'?I^_'໙y9|>4L?;C ) H@$  H ; x㎗+V0+x/5fK,DG$ѹc x_:h`Rwjx 7ɯ+Zae]\w H@$  H@}K/<뮻l`0gf{lM6_|żptP}7놀g{7'袋WUo^!]w l |7,/ kUuLhWNp[DIh$  H@@_:]{U<>6lx?90&km_nxyep8r&a&X^kau!cLZAɡbv;"b{?%  H@$  *RKՖ]U3g?Y袋3O:s' U؟O&+; x g?r!}?qgO䳙U) nY^mQؼ[;V$  H@gyށrf8=ߌ#of x(B4n}1^Yǻ_~ح;֓Άn"M5T|}@{F:<.O7XEG a$  H@$ЏM<]zkg6 /t` /p"|1N;؎Pzf'}^Wc:wpܟ}#(w˯£S!o!|LVԈSO=uV&$  H@$  ;| O:ϻKkex<ݟg<WژP.o^əgEO?]yMnjZ x4tDs1(Lk 7to$  H@$   P>QL3M/76xK;M8c-ܒl&Y=N?yykk[nrU{.ϮwOw#-y{.첼b-~Ԧ ǿ/OϳG|'cXZ.$  H@$ &0* vXJO?}o3 ՙ05gn믟gFwuWzG2Αyo!7$  H@$=^#}:V_}<ƞo5;HŴ%  H@$  Lt ^Z? /ao" H@$  H@G@?z͑.xy_5O>m_⮱/$  H@$0#x|_N뭷^W> 3$&c\sΙxīx]c=Yf%) H@$  H@L@?p_~ymžW.vM$  H@Ǝ~؏ɑy\;tw6<̓z릩v$  H@$ !(z}::]u剘) H@$  H@~( H@$  H@O@eH@$  H@ )J@$  H@@P~z$  H@$0('@!{$  H@$_$  H@$  L PȞ$  H@$  >|g  H@$  H@~( H@$  H@O@eH@$  H@ )J@$  H@@P~z$  H@$0('@!{$  H@$_$  H@$  L PȞ$  H@$  >|g  H@$  H@~( H@$  H@O@eH@$  H@ )J@$  H@@P~z$  H@$0('@!{$  H@$_$  H@$  LBN30Cb)&rOQ$  H@$0tc"?;#M{ozgҔSN7E]4-iKSO=dgtI'|0 |+O?78-C㙀t:*ްz56_8{uM+Bc_Z/lXO#8"/?G~4s}s#|Igw|Z+cQ8~t7#if/RK 7ܐ}`8㆞{HG {7s9'gu,`m^`hǖ xַ`6쳧6,})|;<W _dvcI^ȇn^ʊe?|zQ6^<~p[c5Ҷn8,zhO}*暍u|iua?~6/?+ک]Za?77:rZ0t O;Cسz̉nΨvm<=Wy'/<޷׿S#G,Q_Pm;^sLs:9"bYfI믿~W}8uNgbhk^ .A)Qv[:y}ߟ<Fc.K?OOonʫ7SC+x9:}،;:Ҍ`؃/ꏖfd_n]O{,/JϺ :*cYAig:ޣw;dMG>B0^:k6>ؤ/MsδRK=g6l4æU-(RsOd( ˠL3P@1^yb0c10ЬՆ oM|lK^rx&K\pe3P9-Ju#ot.""?1|_.yH;>ZLNrŵsD+j&3fijwkGp;r_/,uVáؗm3xV~8Evx6w}w?'_by9f\5o_)c2"|_ͫxf3P+qNFs(<˺$vtrױixH$q^eUҎ;,ɖ!y|Gc.@c.zaT}o铟do{NN{yMuY:V Ue=˃I:BcuOC""8x:x5YyF}'aD0qc11^~iQ x^OzgcpO#%[lx\xu5y^x=|+\!nIΆ³~6l=ئ2x3 ':#6G#Ær}uCH\StP$U?Z`Q317YD0Rѩx8>@b?uc K?kg&ڸeM'VmI?m*6vqiRvm$? >Pg=7p3*g,#oXly}J<[ y1F=/gvNG xz|W^3kuC)/je-D:xei^¦r! %/)^Ή:چCPi/=ӉO<q.h<\ݹܺ`;?u:NkzTZM@' · /0ϛ:K1\a- [#9WDs\s\7eS)~>{g^Y0u&Ͻ7lӮstq/': ϨG; ڢi3wta3gmX7GZO(!Eq7u .U>IvOviFʰ]XN1y.Eά@)<ڜ\_u6g8)sS)hG⨋6uuJSy=lZ;i5^V*Qڤ+n*?ZJyAV|Y V/})7*bx`?!y晹מ^#GC1` )Qe(}nL&ģ l2ip YTn x1>6lYo e>n卲S8l;O BbX7:)BnqõiB=Ú{HU{GMUS.s78vh>JÇy 'z뭳)t̼ؤ6u)0:a3tdz,sl /u7zJyAV<"DFY=>"MF0{β0Ôz@Ǫ r24v$|e>e'ou͞I5彶NPZ;W7d\w;gjcq!~a<ϹQÈfB7o8b(Uglu{USyNN=:C'iV:׻ޕ;j36 )5}|fTDkҡ ~jf}btbEGxSסGz"j3,' },/.]#* '&<aPIΩd)K < `PM0C6_gAA\iq+,L+½6͘r [xyX `Y /lxW Tg%¾W O 86zjS+pD it5\?8FG[\ hDP|>ﵯnd,"2X?~T[zy@FFEV]mo&dz:2?GpfٖN{Q e<&Gtqd[tOu+*y7=3d~lmmy iۺ 'l}BV ׸FzMQƐ6!({,?lj c' b?zb[' Q00M{Pe('h8uC6n4-i粘fkh|=CX*5<m|GDq5EEd.>Z=sLmޯ;бJ_!*QM%dA&k백8Æ}  V >e$F߷# Kk!"4(FIbVe-|d8ucB*Qۜ:j{O*'S3k˅0Hrћa: Efq>phҩ m锿ٱD:#9xFi!e^r[=i?ɶwOuw8e=M~iNDC 5r4mX .Q\mL[;=Q|/ڰXЋFY"[<". gp#V'+w<{I? 1"0f>Sq]ms xxǚUFyհ4AB AsQGէ!L6o ֔XP7^( VF#z WFЙF]aJNIub uOg>'88Jc.?kKpPI9}iXy;bƭ K۽ϫm(k|p\tBi:A9:h0ο9*MF +=\T[ޔ ۧ_Cw2!.mmqΟmkWd)$_8E?&<Ɛ. Nh0Sh'峏G~yx1 Aǽw&<_ssy㤽T}.v>g:=/V[}"pv|e_]27ŵx < >N)&D;~qM g8'E}9> x.|P0~BAP05/ޑo,CDaja2?mH6>y#]4[9U;yܹ#׽5s݄1O9?y!oxv /$p"U F:guZhb1RZ: V4Q'h6_uBfU~amo調]y<=0`sxlQ9v摞qh8yJ9XٱRc)v"#Z v x=dmm6EDz䅺F|J1 +[w<L綶IsT.KWnD[/ްmNKS!Q u geQ:_׽+,@8=ya^DDRPF9*εq\/b%jN\,kqB"MFUFŢ}Xq L-!_pŽ +9R'$HϾuK[&m֖8nS'#N{E9̼T޹̳UNzP';?ƳqMgpA1(0_6yk|b|V)b%,_ui9fIzhx'5 IDATW8ʩ>{=?I{)TM|sylЯ _+Ơ N'E,%@'F{Gs-)gQ dl+y//%qtpYGH 2`K†xio:L42X!KAoMƹ4c}:9/Ms ClбAx`2Bk 2isr:Ǎy] `qzyA>q g.DgzF=gU63/v}C/~w[;iBqiԩ&C#x6b xz=lܶU'~9ZݨM:<;,mvt.l'2I^nq\GنJJ{@)"tXRe1ʋ_x22GQ|gU,5Fs,7((bQ8~3z|܃:M> x#ynjnl !tٱ1}BF{&qM GXMn|0"It&ɟN,:³ZbҘdܾ/,~tqnC{܄͝@)P]|#{!PvD}p!Hyh kYrl0;`hu9szMH`˵em,SZ8G~R_X/v^tGmsrT7jp\N>GzD;08lԺ^B65: LQopLRfL%{Q8FGvt0ƈrex+H!7#w9+l-4>[wHma2iv q9~C #Eym7`t_}9bS~y8kj $1?hF l252.|;As]\2e~Oii}=4➥]o8>s{9ژ|y .Ym;u) Жir9Gݡ`-{m{@o) H@$  H@{S_΍nte='$  H@$  L~^zig?_z뭫ۮ}ݧ2uvu|k~6|է>zZm?^`6;ն YgUs1a|3wq$_$  H@&YY]vunwxE6ڿN?ë:} 'P|`<՟j->-7ʗO|!R׾c=FIc$  H@$  X > ƫ& ..: 7ܰb{M6l͇(į,oZwu{o'$  H`uLw_&7p8k$  6}s./ԛvmu{]/ϯW/V+?^~_u[r1ԧ_o~}黟I&wէ?zc؊ cL _=qP$  H`(GG,G<bXzq[ ?nK)WsN=%r~s\“k!0m#HG?k*1s;ݩ?5OOsI{ֵU3{ 567AHgM7]cQ8—&RKY5?)鶕 eG}!/\k%6yaʜs< Ssͱ?̙fیD:."?4;M,=:>kDs_җ-آzЕ5I@$#x\"v$KW3jtOR%s9 [Zw [ֱQw*" jtNYxGED@fϢr̷řg<" ڎ8ZX!p| ?:wcS#>B0۰g#V!dxz7FQ'5 g"9~+_~zЃz>׷N;TOK'3 1>Suk 0D 1̸Nzn!:Xq uF9[_3Ϭ*9 ʟ2zPm\;IhZ`XOzғV^3eu7N+859"O,~VۘO9PO6g~wm6;*7jZKu˽}vaή)vc_Оm^Q40 F;9KM<j6yL ,>sχϾgqF<$  H@] ʦc{XI ?Ӟ5`uGֿΆQC,~Bq,'3PFd<7w=`C0&U*Q A|S.A&Mkub 8XQ8g1G!3#?y*b# BX8\#wZ ޸&uEqywY?Tu툷,"gKz5c#V; ξSF؟}Yԕ_5?10b!4/Q`c^0c1ctXUb 'Έb߳XI?"eNbD:[IPOp`!/mVN ¬,(w=,}iFT9Xq/.Q%;,w]+~ϵAs/S_g0Rﳐc=*NYs9ʹ9/u& vmWGIA{ OxB\; a t.M$  H@C@?BYdO(Ig:a1etvDtgE8A(٣*̫Dt7eOGTvͣ/yKUs:mu#)d]pՙ bc[̝ 5B~2ypr0 #pPFytc™k]E!x9gX䉴<:Qvwo}[{jF3 A?G,@8?!yHwdKDrDe;u A`-0x,`B]!aC#1yE)!fEobE~z}G}%# dE9Q*e38r["]"*8LЖ 4"⩯DD&#yp~qΔO8SGh3 'rJnk82G|SG;C{DűyD~ؗ-A\sL܋\'M$ %lt8!D0LR#eqɨ6 Pi׃dYM}mqsDH,ӍQ*Ÿn 13񈤛uGnkf b|}>B[qF)&ξ,#ʏ4q~ bǏ[q8ġR&DI` cMʚ{yhkVߏt|׻UG܍ QRmLDbuDp]A A$E@$  H`(G(,~%GDAt0#|t򘷎}l䋑C:!aFLean9'DNuّ%M.1{y?tۨ.sSY ˣ̣Fc]9ˣ#Р -D@a|S º=ILO"lil/-#Kqa䃈 -·x,~}ϣ}9cchh8i`@&ˣ18D? ^32r=è,ï -uXSE. &Y8~y|wPq/2Ҏ@dg;ލgZij;\oX5iڢp #YفciD2`[߷Ll$  0IzXJ.PF١R1…el\χXcy:E2F'm>FXP,"|fwS #P!ێQ{F'.&sB1F-V)eTw:c[Lhc]1Jyj!r>m`3I8b617z"e7rj q,_t™sDS4,BaCdǽf8042Oy*mM&7" OǃÀX'ʸt5E= :Oۚ&G"zفC4PȍxLgXLǠsTN>~B:c}#"x?%  H@XZ xOBj!m'm#T!Lfϗ!xFG6ez$8/&8&_#tF੟0l|I]3l@eiG b,~ Hv?֕f>߈ ` YƼsg )`' 9pxIeD/czC㳭`iO H@f~2go7aN8!u x!lo~8 !n_#_/N Vሊ4&>7"0"ġ@$#2b߈r^:5/0{,0kW(1 A;Φ~ģJ- 'NH,惗ɯmɽS!lK XÉB>8k7K]3eoNhkʈ>cϱ3G'B^ۚõetM$ !$<N~v-0^PCT1tcO2m9a,0җ4#{6wz^)g'^D0GqD-|HՖcui =.bBDa4Bn0dNjQyFY7$Ftn!P8'a0PvʗE"xX/>V_>0JcOd΁r44u0^Z=,Ӗ7CTqޘ˝aȄ>|y.l 9,q*hW_5<618?;<8?NFn(F]@"^?3a(̼|RD`iÈ5{IԥHc=|DZ?ʜO$^UB|21f7#al',1@M4!X~ 0"aդ|3b1^ a*8b:n#tSLQ'ASLwJ!i?"Z-T;Bc.B7yqrPa`QD AC'L,DƜnN+>sY|Q8 ?ٸNFJ'0LBq%);lj娆8G' +d߱9)x *ޠQ1~]F@~H<.,摏ZIN\#F9yőB~{^ܒ<0]OJ#φS vib& H@PP&VdӕfI Ͼy䑫EC9bn3҆"<!' ǍIO[G5 {Br\GB,^|-̲,r682]4j\#7Fb9:6 DIO6‘Q~ +?GXfٳN;-FDاmN~D*(A!# 9`}bnh%Sdt7"! \ˁ3);5FQ^,R CRWqӔ }5`G8zeS$DPWBEDM_0?Q).}By!&I7?]'0Gvv&Ϥq/L#y-nQ#6+OwBkS3ִ^H}#ra˔V'0?{ABV@ k"SM) H@lP@y ٌyEԌjtHI^ UF%D5:"h7{>q݌ez2!5kkSM'{9d#ϱO"eAfp8``M9} r?)|l=B܇f0J!3)ߨsUaD%ΔRv߶-&'f>EpoV܀1my%]eJUa2gm& H@|PG93,#tfB]$f L#i6;񓚠ZDTO>h՟%  H@1 +3:XeFTB]GO#%MaLa`t cGB5 ,D,}D]$  H@/S=F$V_{ ޘ?[ܙkGZMֶYӾ& H@$ &S̿6efR1w0me L:Ȩ'i&VgQH\qg3Hba$  H@!05B<.(< 8=V@ھǚ7 H@$  t礈xVeUeOg2F,>|$  H@$  H`*৑aӔ$  H@$  D X^$  H@$  $  H@$ H@Kk$  H@$ #"3$  H@$ (Wb{$  H@$0wsWdfX$  H@V"J,uY$  H@掀~ K@$  H@J$_5K@$  H@P]a H@$  H@X+Խf H@$  H@; +23, H@$  H@+~%, H@$  H@sG@?wEf%  H@$  H`%PR%  H@$  H`(̰$  H@$  D X^$  H@$  $  H@$ H@Kk$  H@$ #"3$  H@$ (Wb{$  H@$0wsWdfX$  H@V"J,uY$  H@掀~ K@$  H@J$_5K@$  H@P]a H@$  H@X+Խf H@$  H@; +23, H@$  H@+~%, H@$  H@sG`U.9EՕ;jՅSFz{Z{-z.$  H@$  ,5 xߏީ_kZk <[=$  H@$  L???ĴߘPOn$  H@$ y$05͖|= pt^$  H@$  H` LM-+dIUWU\rI/y=$  H@$   1gu1T_ת /˫\:V[mU~Wm&cc=\o<裫 6ؠy9|gug7^ATO6lë3$  H@F'YgU|E]Ԛº[/w}Fp衇VC8j7%D_]ve]zjvXL{i"V;bDӳW(/D`ժUu{%1m4TL^ʔ|_~W[lEnM,&+ FxfwK/D4<#niʾk\cN|1~ 64"WsN6,K/?{]f7kC9s$.lZ>+tWVߨmݶGܝMnrO%pj7K`I\pՙgY;'Zwi<#ɝ|u)wi?H<@IDAT vSy92R"O(JP6Ӝy߷ɐB(C $!syw}{s3|{5]Z{F( noqKuH4J͒cߙң7\8s~,f{6뮴椞go}[[wߝ㬲*i櫌íiy O}*?ͿO>K'/I_}z򓟜]w|TsseK/4|iKzxgɯ*Uv|o?\y啉qƘ_ywF`H8_'=)QCR-W%X#fj ow!0o}g3#015:x{t;LOgk-0=y}3i?yZaFEOL6l~vT`y?!>(򖷤]wuw=PG}tO{һ馛K^ oxCzߜ8o~3zӗ>M %x'9眣ףWUxG:|HSZ|swy 3&âӟ+^17*uBItg&C{Te0F` 25nO}s@\@`k Olnt '$Yd wYNe)-wiAYIs\bڄ[1cF:sXE*_Xİ^uU]vl)CR Z ox_fe-ܒfΜ9ށr[og4l[-sϝA,>, #:YOXʂp}g\s͕h'mCx"bVZQ)K,D\ ?>Т.wҾx /SN /xAV{I[5)[w sꊕOICK VZ)W#C4:) r~x -^ ?<%=qhvJ6/' TiK¤n1MWoqư1v# aΨ4}"]p]i*kyeR3a+(*C~ ۶e$9ɂ!ϣx6l@"% qL{,^LAlZ6QEg"[l1c9&כ¨>pc1” ]r'}CN@]hƈO}+_e[c ֹ;+LzF뮻.{EW|=3 B$> 0mk;ڽ & e:*'{TzNQyUe"ݽ;cpEϙM6$S 1P0Jj[FLW_=KRG̯Q 5M(-6\Xg1:w=bna\D#֜2?}> YS5\3g03`,vaTU\% x0GB+\|zB;!dGbDu/{*>^WDz9sKe"KjH|дoFM_V4bY+L^xO`pg=k>ֶO*+F >!1rmF #ʏz1DMTضxUWat]``3/`[e2OL("gӿK\ 1.)HML _w:?GbZ>/[fGfs1dL?ϊw,\#n%Ux'A π_ӻψu]E-3y_N"L0LJn_V _Bfm}5]SB N x4`ҀtJj*3yaae$-WLܻDq8g7)o,x1#d 7!cz&Ψ'^ KM\;찜L s<>,"PmF( P п .@N#^{#0#8+yHa^ ]D`h{8V2A~`†#i7UL4}9²uq ㎐>` 6bL](*)}qg N%>s }K u>ؤE0"Nq4g|]vQ`c LGDC~ۤ\v\Ɣ*QFҠo6y}(%w!g^(ȗNs(eq[ȟ{~^7:7(*-sQoZ /JmHl#/Ì])~H5yMv>hӷȠmQigp7i3ڂ9+K? E0" }m̉TWIQV0`gKiF0o!| eL<ҚI.A*.?ΖGا?W2OuGآ1k K&kmVEш!bL"cɆ/Ї43Ug((# ^yOD>_"?MX7}PP^J!tQ&g|ߝJe~xXg?YN{Ϲկ~>pgEh)a`6% ' UTgzݶ? ^w}h0xs#DPŀEJa?]Nq˸[+ B,"K.&BIߦlL)Cuਨ)&=H[AŏEkeaHu!b_* 5m.:3pۏ:4r|Ń9Pnfr=**xnH j~NH+'P,2*/+J_s6c4Pk L*y1o(2nмCJy"+aUfSQH`TyXvOX"]#DA|S^^-l c-w:$&̙7P0dAؖR|4Y˭g#̂-u"}oq\e=͡}2G\+;HUkQҿc,Kx٥=vy.}+BmemmKm橱#T%wq^Lb2-iqNJ6}Hq' QI'xfѦm"y%6uz!0z\| Fi *YxvSҎ]c2ƾoUʊUU`UX=\y3 RW^q1FQ[;/#T)X ]N":̽. ƑRCe-7eqga|x0Q]o(Ȑ.0ʔ &f2 ԡ,,e~WqXq$Lx^H{`AIb,&Vi>bA-4qܤx+7 $>H~bF9k5s$KA2 BBb$).eT߆GEMh4wrX/)4ی( 82 KigVB8Faxa.!DB=aNjF@CXPcuu@fE%?;kGE/Ǐ~QCRMVc*c68O6Q*nɊdF M5 [!lGD(fPUͺ*c>l[xF,`GuT~sq\ xq*7׀J+H/Hr K]!sb{pOiJNY?e`EM)hISyqŢ`y!|CJW`%k, ~ߝofir<,Uq`g+}`m(jkZ$pA -;k1=4Շ=`V\K_&&eW4BNoZ6 Ae>w(V7S,Q_uS'r@易 zpkVzQ^P pbJi'``,^裮x@SDʤ(y+6TW*`/ #]ˈ tMhNO{8$-w}wI(qb;y2 Y 10ʔ7R/B~ԕ.`x_Kv8D(g kOMǓ0c %5˕|?"]jڷˍ0BKIw蟌 Z'{9 +?eg ^kD O?E8DBa G¢W]CikuQ0=bl Sp5HK?"> ʣ*+ހqX%DSʏ7#jg"7|RP ȶRQ# v9QL_&,{OR{]oJ/7~DNXR~\51^ս[D eU=늧 Nr`.5F+;y׼U TlM-Zߙ'RF,(䘧!a,#Qƒ;@Au^PtYcb;FbغqE"kV&  w.`LUQX_0(mL }k*w/D YY s!xPh~Xo,}" kk՗zĥNu`^EbW]ۣ*.hJSj3OW~D< q#He,GCgR>S\ʬ0<_Gi~Kw8OJ<&gLT x1ͽS[.Ms-¨L2\" \+ LEI D\@C}kڿ2(86g@w8!+ҏA_GMR)RŢO|"?&=YYܰf4˴~kUΪ~+R,.e QxVn!DزraxW61B+U ENGuN߲"ju/ŋ%Q`~DW*171XMY4Ѵ 7h'sJiLîyՋ_2I8 l(c 42RoIҗHA0oʭr/f`a~qQsKR_**cnQFE%de)=.7eb|AUKeZ ( PL1R'~(G 8!/;v/% u{rU~EEk˒Ye>P[]WXʀ Cp'"O֥Odܰ6SV&o@QǸCĸgEU.־*Qv".xZ ;?Fux`~P6kdfyz)m'0:s OpU>?x⟰GbXO}&\ #犎 JBbң7<~7R FĚ4dX>!$|L#PG?{ ,pVE}+_ɯ8˭*,Ϙ@X!+y5ҭR<`E5 կ~5[(z{ߛ]xw؈5C'χ<~c%A8i^~ur^湘>/-B<G+rU]a)p1lbO!u i1&х臋DK`U&_1} U{cxOy{TO$N|D*YL8[FW_n܂2^e#Or_UcTBB(sGT"V`DAg0|F<؎XPA˜ )efU2R&0Q"\|q,Ia8j\mj^7 I!~Jz{}kg)c]g1檽/XpfKLGC0?[/egGO$4XmYM 4>p>c}ДaOOop0y\N,4Lt#mF$&U]EcAu)g?mޏ FMU=JWq'E EF;?B aOkUP8ЯahQRqW`'m4W2'1,Qu54zY0H.ĄUSVӜE7%Dnxܱmy4]e"\[TTF˭Uy戺qTڦr㡋'2Mti]^ 0uVOQr#b=Ci  xG\Sz[M7« t>-=s<5} 쵏]K]\t< o? SID'~,nA ߋ.}240X2Рd1 eBd1CPfpBu< 0A"T%sjOȑ'-]wʎ;B:` [Q{7d\fcUE'o^ƒC, C!<d:Ì`Չ=Feyi eR-0(sA'HaQ%LmPT@Kh,DXDN['tmݳ8!|ƈE@,d`N@ɜHO[л L08Kbcċ1D[AqM)xV P(LtxXN%5-2' }aFxĆyl)tdq*GVk?R %K&-YQ~Q"km,kb2+Yx>ݦcTgT̽kP0}@xÈȼ`?}>&B1ϘG)+}/bAEɇQ_,}̵R2oS3'+FbH˻(*7%w3zuT1'z *W13jδB8Ac}6I|@~Mצ.7)Vec>A#hox) ӴO*R2Z#"(*_XC$ぅWī85]$>*e<<5~$fDQ0Gi^,U!1Oǃ jgrHQu>иJg) 9/*GI( %xɫXwl}*,9&Y /7s&[M Ui Q ~헅zÆy$G8Ԯ1A"aT Jt4xro&$b4Ptc…YQ9hQ ,VX^'KȢ?(Զlđ,*,hl⮥6u HxVy*(~~Y#0O-Nݵ nĕ =@{36ԷK {}o)q)aa([$YWBۼfyK!RtY+e|P'"#N6e_9[;c%3)B,s#2((3~CJKmYj '1#JHB <ŶH}{a)ƭD"}q2Tn*ߺRDbde>uA82<k=fMqN_sk>hӷ_ʭSK1G 5 C2N>zWS+y*_PHk`a-c^MkD1&X~xq#TuFH ex .٥#+ _*PH;A?)]xBeY>C:B23#߈8zϵ%G`x Br|JA M& }̄AˎBòC%wF-0 ӅF$&51Fbc?$֐*7i¢Pl0XfX8^~x|W}"tq{ayIz΢&TS^j?bьB'biĚ-b`q#ݍ#! ) $ .` Q&+qnt游e$ HyM3g4Ũc/"n-[[lB kI/}wsOxkK.$MZ~3#`0F#` 9- /Ӟ,!.KvZ^۰4"XTER~^fm6pC7M{Aq_^:@Dvm?aT9%U'#qHy$tMFm'YZ^WC)?9{7kfr-G}Jg}vBȌL\s_%`׼&6h=pNZHXbi_*oGm<ze x&jswg+ 7ܐ~`|G~G,>(cڶ'mv'ge.0F#`0CsNpV,X](!!D#*XZ"\"3-m;N?X@Qk?^CQ%n \`b|_m{Ŷ+)B7ڶ< Uy0F#`0 :8 z-eFm=XmJ_}_hYY{xxw1^XipS*W9"~}mS  )UF#`0F ٣ai} B)\sM:SdqpgFEc9"{ݜWp]"So>=Fq=GMHG~;!PXo}E;Qt\nr;~/Z{I93>xj'CL(-wyYIߴ?\meQ2Qf'|bBY@[ʾv pVɷ33f4? Re#`0F@x ARP#Bd4 =79THYJ8yG!K+>e3]P^ߧH`DY8k]{R.|r{ni 7llE@{j _*82]9%jNї z$||:+M:]AQF V-p `/BQ@xv%%Pi/iﯽڹ۴'Q&|°0F#`0  ;w]'Ҍ:;x*/^<"<(+V]Ly8+!dFXSiiVo Dt]S`XKm{":6Ԧ X)+ڂѦ<k0F#`D` K=L(TS{ja2#`0F{:@IDAT#08,[:&5Tt㇪0F#`0X'=^~N~7#`0F#`@J ?az*L0F#`0FBd!|0F#`0F@| #`0F#`d!`~wF#`0F#`Z `Xj0F#`0F`?Y;_#`0F#`0-,5F#`0F#0YX,䝯0F#`0FXo#`0F#`,,O#`0F#`@ ,A0F#`0FL' yk0F#`0F[F#`0F#`&  𓅼5F#`0F# -rP#`0F#`0B#`0F#`h`90F#`0FBd!|0F#`0F@| #`0F#`d!`~wF#`0F#`Z `Xj0F#`0F`?Y;_#`0F#`0-,5F#`0F#0YX,䝯0F#`0FXo#`0F#`,,O#`0F#`@ ,A0F#`0FL' yk0F#`0F[F#`0F#`&  𓅼5F#`0F# -rP#`0F#`0B#`0F#`h`90F#`0FB`̙3ӽޛy iN;oZtE&#`0F#`0Äx;#=c\s͕Zj) 33F#`0FA#00N=Р_ϟ_|w~h0F#`0F`:"00np^Z?}5F#`0F#0뭷N*8.̍0F#`0F';ym9FmwMLoy[r[/}%ηtggx̂#`0F#`}g̘cn =гtAh/|c' /C2g0F#`XvA?X8Zkա3\n:뤕VZizV¥6F#`[p׿_>sX]]#  p% x2'XiN௺ꪄ2,vuiypi%'?9/k;zꫯNkfz뭧tY]8#P?tYgX 򕯬 V#rlIp +@`,`z԰});$!ä_җNmGqD}_:#9 ZyP|F/{챉ۘ!Ԃ^\r%fWN-\x >c:~uQ{Ioy Ӝx̝%rHCh ӧ=ii*BD!еLT&#N:)]{O}vӶ倗Óe/{Y6ȴF`ߡ!2jiy晭d7tSv+|͗M74-"*_J^zi|W]u<sCeǪMpW뮻.3LмΛHyt5?+3fg<uYsx衇&(*Ҿ袋2^(ZjNi8~a84sGXfn~^8㌜Ǎ78L7Fs]c?exV6O}{> +\ߌ+?Y ;.3Czի&;1׼Vs#(_}ksiC`s ҬkrH??uA?%\2ÉG`AN|wxhw/K\wuV[m/9 |xXzK,-cя~-h?(lR(zNg!{e7 /xABX) sNz^] wwΏ77 z=~wC|f+!p4~0]wqGB=#\|.õ _N?O+{s&|Q,袳Ãn-O7*eVhJ#[H8}acuE!D»7 mA`Aԓszdخ⊳z+"+yBP[n%. .ҌWmNY JMڭmcM'cԛw/X::3ƨ3B}pe8fbYBo!ډQ}UDJ\/4ӴoMn]' r,)]>O#-A S^3e߱s|AT͝J;^s[pm&II{5B|Abc.kHU;110 ,CD=IO)OyJV #<~O} * F=N'^R&ҫ**oB׶‘:RW0b _6{nz0O `CW`"g$\/Yɳ>#hg=bE펅ta2?ðsq'g Bx._Wr9:g?;/>҃_>|Tvw+}_Z]v%^,!p 1,dԕfTs"0 _qa(DRSşh_Sx[.(W xE^y4"{&ƶm}qÍENn~^a>Z0(k}0(V8(-B |҇a,1W]vl^00]XhÒ8BEb Wa\0:*9ﻔ[}22ϩ`jE#cMhs=wLoNFx[}بp1sXJ8 ?W6X?Ŵ͸'6}mQFP`qqahaKb<{ Rx9SNsl̗n:wU}+UJim{Pz3.,M -֡ՋEKb[}v/}e#b,5s}a>Ry b5H(HQްK9'W)+Z {cR+.m &Ak?=+ovbb_FѪێYΙg5DM³6 \4#0+gţѯ(7Pj#c~Ff`W(}C?Y8;œ .&130E뭷^Ƙ,0@}aVuWfƈE ːt<5qօeF UP‰FbE@B@aA0GbًSs !>@|6-mi 0$0`'h XO9Fn.C(c=9pm6@!\$Ǝa(ao7uҿ"b1#p\yCu0X`f#~eS9`PڈG8A!GJKGyRnI1c29uFX#~D׿2(G^P׏T?}~BQeG#̣"y7fi"lvT#Wss1!({?J4oC0!\9G=ؙ(?[lqPoD@}#Ae(q: D/ҫ{(<|J8?㈶#x!42C`9tՈR.b7&ʋ@x^җP402ߕDpW8?@1L(Wʿ2Op@ڂ>a)uwcNgߴ $P2w jSx:Mݨ倿G@OevmZؤ+_,Nz0O `CWHyvȥ)7~n"cmჾoetY<ַה'zH%%P"*W  TNG`F I.B"b…  R|~=,0b\WQZDbaI#!GLW Pwmƅa= PXQ"DsȂ #B%fy6ц`auB8Ga"-păy#^Hۢ̀J]|`<)##ZiSqDKDPI !e-P"Rǿ6} }?m0/A2`O{FN^B8ʀM8bIBh1_31ڎ6}k;``@; _Ѻ\NG"8DX ^/Wh<͓/$˜|hQpUdR/ͮϔډvdnb;1晫!B=Y3IXD'R?>ˠw\Q?;BT{ʇ0!^v[/q!^RΈ#c~].r exy=1NP8Ѿ"t'A׺҄]bx2˯tԦDCO?<@5WF0T}n A8Sߡ#X{<-$2EeDG ++$1-u>gjQ౰}ӫٮ0-haYd0JP}lU`;ʲÁN vQC@`AF] mXS5lCy#tj$Ϣ4bK~H,b 66(h~azadh70D Ef!~ZLy>%ۍxť7VZa#Bxo(K%e auW}ymt3鵥r~ G<^+t#8\9 ķ}i^˂C"вf(GwqumVvJ=KpWBg,0JźW5w9Z!0^_]ۃ5Gn,j%[GfTW3]b;%c57 ǚ(5[czI+IMQbWGc-p$E7q֨+OL/Zy~UWx14lD.277iT+]n# 8B5ҫ"2}׸jѥM*,!+}!$*NF?vBQ0a}H&4? VO𡯐_Pe -(hMI5)7i~ &%I9{ aޫpdΠѧQX*@-i#B ^5˸oڷc,wܲq5 !/$B!Ϫip?s17V7\uKʠNC%a™ΔJk{yuxΚ!`|@a R>O}Tw aKĥ5eAJw7uĺC'X`~ e}l"]Q+D4z3h:fتS}rKQ\eX`&fM37ha9Ed[.zFg!׿=dN1锉`e>,<1Y˸U.Q+\p-|y}T7uV&Ѩ-/,zV0 i1EɻQXްAULcdJ qb;A #I (º B1C/$`p/BZSÒPGr)=Mzx=0:wrGaLe9Tz\Q~-Uګ*$*ʂg dJWgq7[]1ʛ2? o^ ^x0IPJD^xynDxҎ ^EW]ۣ*_Eu]; R (&[qT_(YJRqѻxad^*&7lWױ4DeP\pQa|]/%K? O(*sClSxUxt;J}Fd@]njy$'8M'<):F,whUT@Dˍ&=Er{0,n # &ZIbIX6^y$aZ5jG ^L.'hqURXX`Ы[J;*j) pD,y]y:kˆ@m eiOK+JbAǂzsHN'm%.0X@rO< 3ܴ (]}0?ď Qe^ ,cks"_rU,a-lv!k'Ue*CWYR@Kr92ɷ_ڎ*w2R!zⸯPx9@is&@@'x>Ek潪e{ı38H Gܪ~+ͮ:Wj'Y=v N`-f`N!,Tӈa9?)9_u+R[/WsypAX:F[e>*cTalQ/XPB`g`蓾_xMghJ1#c0&}PcF1īHyj,whUd8 d{߫Wy?>k[X<!+xT}ӟίWO|b4hY, ;? 1J,b]qkJX ]ėu*m:Guӻ~W\a{o`ÌF+r[L^{bV !Xʛ6BPOx Yu,ȂĪ|<CC2.1|!1b` PDVVuZ{ A:Ux `|c;WɷK~[g¯Ⱥ:#錂*EOX},_]ڎ*w%y$~2NA J©UY9g dduniQי^ivm"m!c7![0z7 XBQZ xI"gaueS8 zѝ-(iMac)' 4}U2 喲2V^Uy˸}wen MiT0+/]5DZ1#cat #:Ajd,whY(`vJU.O0ةCXo6'.L84\No|c#>fVL|M3L ㊾ ӄdI:V#mx'@ueHF9$,JF)mY@xĴ5n~5” 7*Xx0b`#VvTUgĭ(R(ko38?3JUh5wvjsOẺ[{IG]IR׿*Z=V.V33JCc5lX0KR<*1!-ZEK9] 1_ uʿ6*$ⶠr u+M}8funPF3XP <]LUp*O]8eb?n#P<_x zD V;d6m\z:Ϟ{  *n 9q7{3sy,` `Ȣ\.>ֆ@Gs <(\靮X+W;'`pHw

3+L,`RB53L'HaJP6h%, Y9Gm \jScTRna2KOڄq҅v9cyKXҤ1i.e\i3-նcj+Jyx!0b`d@]JPẇ:}suX#Pp2b}8 M%LUPhH}1C߄"_HkڋgMѠ|}&#``n"x bw49AO1WN*xX99yVe=0]xßI"+ߖX~C‚ FuG-ڍPnd徧pA[E5wR?s6">1W Ӧ=9L~D)*RX;`^úÚ˼ 9Q\L#,GA $^Ej^(TL/k[($KfOPd*S%q'>k? _<چb29ygE׹IJ_ڎ,Ц3(0XeJ>1ff1^!>b8,0d0PL0O }6`Kp w`׍SUz6ڔQJ<ƒ- uq><ږ NPAA(l];X_uV$R|ˊkkis]Y<(.c:&5ړy 9c͸oӷThG k@t69|@ȷ*{I|/鑎UϪҌ&=buAՙF^X [9^kMuuUΘG6A4pe(s>+*o藬_[ OAぇ0S S2Tae3Lg29QG4@@2E(T8 j;-*B!@.[6P E Ebvi:f,sNG+j3MUܴh{a*T6Mx &M\zQXᶯ׬lGZS2737F'_sk.[LF``g{ {M0& &rdyŢYgRx- ;#0E:j.?ot1pTA z Lug软F#0X&;j>AgG=M3@ J7ƴXij s7$nQdg8.[8d!W-kDG9#`L&s`nmgQPxxƜٴS7Au|}N|6lՙ:uIΊ#`0FtC``'=]Z\"--O(#`0F#`{3=|.ecZlc*0F#`0F;>҄[/맯F#`0F#`=ooIg7;s#`0F#`0㉀hmo{[m[,iuMm]{k0F#`0F&XoR{M3f(V\>ZkUS#`0F#`0 v:#7<-ӻB.0F`!po=:iVF%wQ{n{*^UkIY/)O[0>xi嗟tg⊴j%SJ'xb<")]uUOJ]wԚ7zꫯ z)TU#<2=i-ʬ^pG{,miUW|Z;3_j5V׻F`: 0mx QL)${ct72- cBkM'tRZ|w17ȗ]vYV 5\i}Tλ+ 3Op UTkPm&o+ܧzhbBÒބl;c?vmK76a<7 ң:jRFu]SK2+'"e]*oQ^{8i+LrJVnT׻YL ޅk/!~*l:r9Q![oMw}w~΁w 뮻.m馕>p'܂ .V_} ""9a`&oWk-B -V\q\$?䡇yo _~ybΤ >"3%u\la`G@ݎ>,iVUI}G[wU7t U@@@AWiAjh> ;A)$RCoAzb {;YYdSa>'g A׺ֵmF$ʢFO~^&8Cv[s~/xA/r\dxֽ2DB`+c; w?x5Cwg?{ӟw#jWZa{XwQG桇}c믽E/Kb9}޽ ?΁y-}k_x.z׻vCnpt$X=Y>q{\-w|g( N}9\xC>D}֙Lg:S{k~1 u7ͽh'oX>~^W -o9h\绷-}o%xSbB71u`~_^W6kU-ַu<~~#OGwJ3#{Kk9pì{U%;Zɝ?^W/|aw\x !xfSf>O{ \8 UAg=Y;Y"sx;0I/B/t d|y$dOSTODŽ'?|p{qhzYo\=ka+" -xFCx-`hBmJƎVyyg?^^ #O!CEO<Һ8b x$a\ϘzV73DYן{EyUOye2ƾu"_8S_oQ"yN[ppȥ: 3f mY~{}yv3 5 ڭ|cx1Ǵga\˒9iX*gCw<6s폏ոeQ1f ũcf1>+̖}fT6/B[.sQXC,3vp-ڨ߆x/x0#o|q8`&a-nqd>OPw8 Ϝܐ E *veH / CDqzիUw3q.o\_h,'铱1#KMim!?C=e{!ر *;woGrq~ <&u{߻R Ƕbv+\gҘ}mo{`8yxH6Zx-XDa#2p߷LIx{Q2K& h,RjPڷd+(ox{h$<(ZQ A72,06(- ;C[‹h bEQq,]ju2Syޡ/((v b ! !-AiBr-/q(?k|q'NH?dDaȆbt-](apQ&95ۀ@IDATom U\ @Z:fT4V bLYzH X#ga5_xe̹n/6 # T(n py~  5+$eP=\ kX>aP/"DՏ1޴EadkR%!` MgPi~Q);h ȫMUe7O<)c~_[Qxm1-;vߎ)mқ_l3("$2u):Xk?RƒfQiB&S0Zll2a=Vr y!etfض.gu/NG`(w;I^P] <, ZH,R-^} dP(~y#D9䙰PnNkXxNy{1D̑|*&+WIGh3X,P֌}s \4x#5aA{)bq} _nW,ȢRcuxP޾f%+ke B'Eڲg|)KqGs4Bb$pzV_("Rb[H8h1^2A%~A/Yl[z9'SaiSqCW yvaS3k? dƜ1Ȩǖ ֆP~n8LIE-Ł!$~@Շ!+Q<'si/֬XB7!/1)OAN<#%1b)1c{x])|=?~_2g>_:C6~oݦb  ?>#5 dƙ2GՌQku^ݟu/B`7 |('9͙z|~l(񛥼xyL݆f A#.(,|=i}2B..ZeaV-O~YY-   uP>3.n蓒%x[P^.xǸ5Z륈(iLBhF$5}xj!U+8D<+2~S0,P i-Ѝ5 Z>;;?;ta"L Ł" AYk< +$Ms+i둌|}R8@cCq\oBze(Re_,ڟCyCWʻxzKѶ&Ax?l7P5d(W`8;#3^F[٘k Ч.b 1"cF>&g4P䱈D{ {8u2΢v o?CD1J[N77x8^MnCQQ.wM7_|ƚ7?;-"NbhN2c|dz1e]IFx 9{Z#fc47N*hWqa:a`ysÑԿB`! |N>}qy{>g6ۥctQ(Czf#腰\YYÛJk 0 X(ͬ؄0vf pY{HB[ނ n\ yҴ(^ykC{?~ø'P [BOPfrvlJľ1x[ QY'lkHgPYW8 C)yeIB,g ,!4BA#8z%Ŗ؊AQ&s8̊ `$\oJ^3`L©'Y?|`{K -¨Q҈~{~SAm+qЯ?yh"_^YXeyѰoefa\$BDP?*47|Q|:f2kET͸BSv_FP?fUUN=Xy yi$7aʡwA"+fAʵ% E)̂HB xgxz$d!yL0͊|OOeѼ( ^iG?0S-(UcŸވ:m5&yMl xo~gpo;ϱOUޕxJǐZމXdEq|hS6PH\|l -{cv4`=Da(W€cwqx^q-1Fփ_ψȞN]P<eQh=o *_?.O#SX c~)2oa'ְ)MB#m=1Eᤐ_޲w0/ˆ?dؘu︧!{v4wI6,=~WhZs7)Z>)j {4S>)|~C'/ ZCH^`"ڕ &<;9B b=8}{^B7B/;O\zC:0d1tJLy&8Ikogw#yLrB =vnkc͕v<]F^4/)cs yMFs"!GH5P~Z.V#8+xbd1^+ e]RiVX< }ʝG['"M>!3zNh #^DqS2Ȱ"lwB`!Ox QS'W_Pm.uPeo7UJVcrHF(< /p˖'UxEva:Y,LYtf"qݭ(hZOgP{48!@c?O'!%uD]BC _e3dCrHus !S4D1tI_8i1~vZg)'r?A13@9d,a1=Yr:kQVW3ze1jC[{BQoϜ,N9H 9(Eiƙ(yκiM3H{W yo9Ob)*Ţ-[谰* oxgV!k!z_*$ WJy_UVc_ bSք iMD&b)ODGw\Z_px'P>B{=Cf) )* 1r~u!ʁ~FZi~-Ao'2 ?NQ@Z7EeK~#s](Sgs_9 ;x$""m{(Bě}n3Zh#پh A7n_j=G$ 7cRTZy(-2N-C`}`RHDP{́z(/gp1'U\A`Gۨ5h-2F"}v*ݑG_dOhy"!~}qhb`m>[< o*c|AD"G}cf@?AS6 7 Fa=L)cwqX({;y#N#kX$ۼ6aip9!!RH1(kKG_YkuB](+n\aR4Lqއm-hcy.s>2urk(s'=,#m0 Nh Q7cQS2bPެ{Q,vTTV7Ky(b}{\# x} Xz z^I'P}cNI(UdAUKyg<r|»{_0v_o&Rx ,ʢLk'U?cou㜽'BDŽuʥ-Qv{2PJpn(2`!t3(AG+Fxs$߈bO9f σ 5F0v NL33eTmRQ%ĮŠLU&!~Vc?$?eAڡ<`,R.ʞ)g2/ØR&qq:&gD s^|eq"Osm Pm\FD1aƞinW0H7|sH̢OFL1GϔyL AXe00"8R( Yㄒ$"oα"2:儰<ۇExLceƶD* ^C!9oߍa.8δټh8n[kʉY7ʼ~wB!Cnx0O7;8i4!?|N_^jLk tƖg(AdT9Qu2DY״-o#׬(\l/#Y/€̰&&x-RCkM[yPWCe0Z\2K"2s8Y׬{QV}op[oE}`QY$pPB-h"Br٫~WʀЪkʰE'HP`BX{n @ a'6"-LJ߲l;C)BQI׆YSͼN<8EPgµ۔7cmg6?a(y/r-~qH~CixؙUP}m"ƐyE_o؝Wa:m:C14īaK;)y揿EH?ß9CMi2ԱcL>2V^;-rO?37h(klNG$! aP ݂@)𻥧ثͭ0v1Uخ|G]ur B` m]±gm̡6ʫ adg֬{QvQKJߗWٻ c݌F?+ZD{E_Yg桰ew),tuno(Nh,^'XH͊Î=?9W  "PtI+ ͺPU)&!e 9gwԤItL8G }@Zjwu'ٰ#^W Bc|v]e@!P,)?|ͺ_|5dSbs(K61ʪ^XsR{9Ρlc{[_~ݯʖ lddwQ"]@!P@!P@!P T 2 B( B( B( uDujS!P@!P@!P!P ui5( B( B( B`(~{T@!P@!P@!vv]Z * B( B( BXGJ_^6@!P@!P@!P]V B( B( B(RױWM@!P@!P@!Pk@)kץՠB( B( B( uDujS!P@!P@!P!P ui5( B( B( B`(~{T@!P@!P@!vv]Z * B( B( BXGJ_^6@!P@!P@!P]V B( B( B(RױWM@!P@!P@!Pk@)kץՠB( B( B( uDujS!P@!P@!P!P ui5( B( B( B`(~{T@!P@!P@!vv]Z * B( B( BXGJ_^6@!P@!P@!P]V B( B( B(RױWM@!P@!P@!Pk@)kץՠB( B( B( uDujS!P@!P@!P!P ui5( B( B( B`(~{T@!P@!P@!vv]Z * B( B( BXGJ_^6@!P@!P@!P[׿~u/;߷Nzғv'?ɻۯ( B( B( BX'L{o~m$'9IWa@!P@!P@!P[)?[]OySvg8B( B( B( B`7"e wmG??( B( B( Bl|gsֳu_@!P@!P@!P@)Ow}?}w󟿻E/tA;f?o{_?۲/~q7/ WBw;ien|8>5㛒YzVwkܔW&@!P@!P@!+(~B9Y\wJuGuT_mfpܠ׾֗ɰAwF]gs{߻խnjK?={_?]x.]z( B( B(R'teVvue7rqtg3K^,g9F}e|#pG=?ӽMQ ިus}~@!P@!Ph>}+_)9qMF@):2+Wҕ:-}{{[ߢR^%mG>{^֝^tַ]bm~cTr( f949Mw|3\8|<;]W_[B޿;uo@m u+;NSw;)X9oz_?g旅܍k17 G2n>Ot^7o`;&9l_ߗMnҝԧ^v8)0U_S鹈`$/~F7UVד;+jWU_ _ o7&ӟtw󞷻ֵ5/׎{K].ySfg{5nw'?ͳf4Ovu%~~݌Mۺ?G#Rn[p~W9bn~_ow"hin\~u/x STmnsn_:Z< p~{KpmI>oחw܏); p9yζ~o汏}lwӞ]"j?Ͻ}g?жY < {,Vf tŒt3u: 7-K;3jR><}t%:gL%{6K_S /Rn|E'?I?nj !'5{^_Gۈ& ~2}sNlB"7txynF۶--V16[zd@9 Nbn. \vҗ;cP}qb,~B/?<v߅ڏYAhCU!Pn+ $v_KЅۿKk+y^{x;N]o:oa٧͋YD|8Xf}=q>;׽u{'=I5HTz^z/ -rɁ"d♍qxeiG}t=n@ȇ2X˷-eT<6w <ߝZsկzo?%zo^}Y/ax2lQ.WnWvr9 CQ.!Ye288LDT؉CzGκnZY/iQЇvzի' E*ҔS '=" .vN:4Og !ط7Â=evm:vg{ }V}wfR]2pQVF cED֘PAQ6X38j~E0]MB/ "6?uOX]ζ-SVŸO 3Aȹ<\h!<7)xN_/K{c#"$¨E-C^\ʿX%ZYv| ՋaS_0187̇!mc zV:} bhEi>U[\A=ۺjQxn(yi{6F|kR>|s''^pxТcyreYH;ƾ}xOeCHcsyGH2^YƑ1GF",Գ%:h^E^~wcVD;yaH1.XU x>P^^}W[Sr"=+5~o|SH#?;b.Z/ +,Hrx}y|N{zx9X,2B)8 zYR/CO}S7~cE~ݭϪ%ԭo}>l=M ]%أ%!f) &{ݯ€=OWroDxz=!U2{3c < Ehk B<Eie-n s}{7|V u,L|=cg50(cJ[ŐDGd0ia 2}k?~J l۔ԛd *l~_58+w]YBEhJ`gk9f}C그[67HX?Cs Rhu5ƿ\a m&cq͖L7?~B+i'W2h3 `*ڂwPF2Ym*o7,~n<-PXy藾6I#2{,c8\v,/[nn[T!֫vmXyz.m{|<" #eĀa.fCFߗs1{2O(HAZX:-T:_'~MQrHWØ1n}fcX>qV~c-˔=}E= k2V^[_v,.#,2s<7c~  I4#<3>b. Ǝ话DR"-$Rе/| \}Kg($FS~ Qw9,Q&Y0*B%S0 Ttc|;œқZ["<v|Ԁ`y߳x1MFPV]0u}ɔ0x3 q?9紡x5Y,Є| Ǣmv/|7vH?g-l5J:a4Dy>X)xe JIG0^(o%岔/[+:¤-Zl۔K [$t t}( z?DDLPDP؋xU} u XyQGX{4(xWt"x\e"厵 #sYP’D*^(~C1]sx]M!Z#yl3:F'|:C3׌s}5e]\##sxSouW;xp,;Kcs3͘Oxra0Bޞn׌s83/`a|:B&p:p2 {x'/2׳oy.;˜#OkK9 ZE.:!W1ǔMOh'3hU>+c(+>IRF:WQ0E)?g}ףOyS.-w"K"^y˓ْ-Rll>;.9K'XjPrLh.Jiȓ n10,!ee6'Ye/x zm}ӛ^º Q})Q(mi!$*Qxn #g ̴Ċz (U򔻝m[,+WQ a=P< !1,tzcF =$0;3f⭡$ʇ@ dL\O%~, BygcHVN cpaܘR߾#h aMT 5G=?1܋(y-ڜەx?)?d~n#P2$e4N7k6h,y.xg ќD bhePh gݸcx173iOcn3 H Fy,C1W9}!l~jc1wϘ{ ^^>5'JD?QZf Oqf>o\d]\lu>ez^XkDf!?ediڹK= c/ [~Rt5KzrK p/υ&*+yO!"xxdl yf "4:^_UѠbĴ|3+#[BJ^ѐNv4cB, 1b|xԣߞf#0' z-\}4Ps?k?! ExBlO[f1KsDPXpۭ1Ye!R E!Vx~-ݰm++ - CFa,r)f %긧ukQ}+m›` xQFOq[M`lTJȄ`#VO=lI۳WۈG.\Vg C1 x e<-OoaFC[#vd_n+wm*3"[%\LJ Q;_VwO'́e [Dge 0!^ bYF Gg FcsE6)%/9F-<0x"w() |(~m^^Ƽ$CuO)F+ٸ((يfeECxN;<Sgǵ6͵ Oٶe ` Pz)\YK7f疭8zg+oËs{FW < !;A ( E@ [Eh{V b"|f P9Z_2ca߼&,f,f y;~BABn^G)cy^C3c B=bdsߐ6 JH>-^y }Nh 0|,`E$q̸CEpxżNEʞھ8(NCk9(SET_^Px9nǃ9)MmmihM>oM^̯\/25s^S!cr}#P > >Nj<=C^װb9WGΘg/9 \^S ,esme];g^mgۦjB`EqzyM0^;gC=BY oZɏg]>P \{m++T{<ͫ=U-R-ل [j#"WWmlw^Jڑ0Io)1=ѧĦF#B`+^|Ƣ!O!Z@ js!f999>mRVǫOPh qC#y2I3YyɆ,-z@NcoBs=n`B֟(3s\oQyJ}#{VPct).s%iHu9,82{s}˞0C|OSƲ;6T7~/D9l2cسy~m~|`4þ򫺢Nd2>5KI>5` %>[5s9 ǡghh O(³"/rCeҾ|PܐAXԒyȫdžrvϛ Mc~Filq/-&ssKL} SWF|rl 6jG yԂ[,ZYz׻J AY(oaNTGEL0TE?LC'>Xxy+(cOæԳk/:)x\‹@/cIE&(;i 3"!,({b6eҿ{^DCp1mÅRqζM) DPx&F!AYѡ<'bత8opAVESWF_qpAO_2VPB>cC'>G` a_a 3 z`VJ>oY%@Mb D̪}&PzgcAx)oF0}|WJ@Wj a1S);a/`r#2KcJꔱ,ysQ|xP.r ^ByU鿨Qܔܗ9){Q EJ{! s4'8_izI=|`ܙEl4 W  ˌ0"? Eeʞ>er;lQ1p!C${-;Ke|fhn1i`K^a![8}H> EzsC5@cs)~"L|Һ) xwߨk(~Bmo^&Ȅtz:ex˚Y{s> XX"ij@ -&I)B8ML{w7Ƣ,0,0'Y(qyE81,л>D&tWڜv^Y3l۔I&g"@QBa( !B:afB'bԋ)aHus)OS= !l52Mŀ@iefoA7 t?7Vy9B)*}2,mO/Ɵl)Pw\k#dq-/U{Ʈ~Gٛ]'L*_Ҩ׵0<" 2=;ܱyN;xcj_1,LB!\C &NZϑQwWL7l})ES=%#!d6ެUxv)Ɣ <8NwXS>\9*̼1<_DПׇ>)Sڗ Cx-bl\%C\G1&|_d,eE܌Yd GY^-P~ə"kVWA}Yıe|z`/d,(óƳF,KYs[ݏ@)p3xŲ 730PJM8,+Қ&$Ut1&} 7VyY˿-B&`{K<ʓ6 ni;"0W³#mFs3o @c QL(AA 8&L NEfwZ!N=B Sg?E̘fw`?Kx PnE-{<-|Ci>@s=57eG4V.! n⠯#ڒ~XXcm Dtx21˴HO9<d^)/3"g.~/SSQƲe\#ɼ eE܌o aܡ2d錐c1gy6^᝙zǷ4u~msyA >eS,*f=/9-aדb.GmYVD1 G- /eYRoQ%FEi/f`hYm:Stkmxb(>0^ cGaQ6üqa-" i[ޔyL-\/o>y8Q(K_K*-SN1`3-;OFCkq-E0h.+ˌh˖=}2y6є\2fP 5㩹oQƒ=#پU)dwJln=LB $*DW@!PBF3;u-[o^@!PLE`xۃao[IvR޷ʻ( B( B( B`_ hPY@!P@!P@!P#cV B( B( B(R׮KA@!P@!P@!P@)ثզB( B( B( CjP!P@!P@!P:"P :j( B( B( B`(~T@!P@!P@!Zm* B( B( BX;J_.@!P@!P@!P#cV B( B( B(R׮KA@!P@!P@!P@)ثզB( B( B( CjP!P@!P@!P:"P :j( B( B( B`(~T@!P@!P@!Zm* B( B( BX;J_.@!P@!P@!P#cV B( B( B(R׮KA@!P@!P@!P@)ثզB( B( B( CjP!P@!P@!P:"P :j( B( B( B`(~T@!P@!P@!Zm* B( B( BX;J_.@!P@!P@!P#cV B( B( B(R׮KA@!P@!P@!P@)ثզB( B( B( CjP!P@!P@!P:"P :j( B( B( B`R׿u/tғ;O~E@!P@!P@!P:!e IY=]:y{^-fg?1}_Rp󝯻U7r7vojVyYzVwϢun*ճ@!P@!P!P x ={{݃B2x߸% wuzh}o)> ox4SnlGk3yЃԽjn۽U@!P@!P P vF.Bg>Ӈq3{K^%^獂ՊWVU@9y]}ȇb߾@O[&r]_W#WKQwӞv BXx)Nq5hM5a/~ѝd'1џ*dDAXf_ tNHsKտַO{ԣ&[_Fo~ww|ݿ˿t/zыӝt6Vo3JJFJ̴MybXgF??~w~Cv}_sկ~6m4W̾j;O~7&[[~R6>v8&^ټ/~ʔue]iF{|׿@e.s^ܾ׮}]wt'??kV#P QekMozf_ ueՊ B `^Оͦnfw-Wҽu&{w6 uGc}K]R.w6m4DSTmY$B裏ynwmUßm\O9#/6/Iw' Z@G02-{'>*m><ַ8"g?Y~sD$87vVDݼ64.-P0:H *ovg&p7)zng B~M$B/nonպ;d33 r0gnpLfsx5ܘ46wS @0C9dK%oUm>-aJcMZc+^ {.o"oz;ܡ︔gO?թNՇG.pc̨?|#կ~^a}?^^3>3 VBw-gz׻v%J1u{[E.o)$2׽nw]zo3<1ܶYMmw ~71čnt~Rc"G?ڇ׹ur}@Y1oy[\:i)vQb%Эo}49ͦ^W Gqu)b׾ 90ο}룺T}r.pk+C__:?pn%jNʊlρټqRe%Oe>ݫ^E KaHǔQ;Y $R)cDyYG <֔h<6[!7Rѯ18K(=;U??W 䃜{[>t? 4fo`p _½nٳu2Ġs\ ax:F4GULЯFjl/֍h+$x ՔW:~}J=P펱/2|gZ0'6"7 Y.|-A%E"~KЪmO Űq4Ѯ~ ,kV\cWi!poǝCeƐ{pm1<5fnq[lO%e0[ʉdz# ;ϙ۳xhNExя>9Զ s>"k;cQaYҍ2Ս{֯xs/03L3@D8QFtcgųpPo|}{(ېGEd2˜ETp Huy2|=)k _ƂriIk$nїƗ3^=ȸ7\ [#_f5,ReTwSzxxx6C|9Nm^xg!0@)c̸Y`!vY $cUy $?q+>+X7K9`ŸXB .Jp 0'K\3l^am C} *nCz"&c=m .y"O{Cΐ`B׋ (FC 0CD&Xْ[ sZKUzվw0:.Br/}ioƿE5y#AWW E_W g򳵦%ϊ1>&07E@i#.IQOLM<I}{{a-_ܓ_c`^Ҩ:+ا m{-ɚ #AiGo󃢗Rj;ND[2},<#+o,sRdMOE SS?olzŶh&f#c8b7.(:d܌Q~|%Px(>A*u o>mqE?JkB)-B1!ѡM76 ꔉF6[|BC Z`V!=)O|C~\5h{IpbD`{o]fṠw)mH@}|c'L)a "rFa({j2~XsƚP <(e/UF9YDh[D}_ $ ^iM3Gw^Z}(/ۤEyNn=qe!UPv5n\< Oha}'AVXQ~v0XbΠ82f}1WkĞ?kSsQaq%qaȑ2B61sNˬMRni<2 8?㽜 ѬB B BBPfEcX>u Go.9Ix(зhys6،9?`EyMnj"x@[Q;F]a՟YeƘ=Ͻ}E2.XD5@yY(Aq(̖P^'vdR !'%?ڡhl}3ˍ3j&|͚l~ژҹOIkj1FhO./dz~ѰoGGDOx?F'1 ci_V6MokCC|1^+ ƈȨ0֯?af$ʔqP)'$sS]wԵ>ƹx8#`,2{Ev P ?q{jNF #L(&!Jࡇz!!Ŝe +O%Q#R$@mO% dP@1h^D[ 83<RHw)yVƏ1Nw o< !T¢c4cyu^x< Q)Ƭ!)TDY8{dG Z4A]=9PѦ߅V"P tq,"-*RJ-NrX)_y? CH3+j@w9G?5J{Т a{ ϬeO!wj} w `gOd=ClXoܶY vW,Ccm!dTE{qyͺsN4)9<1=X|x(|F %g|lE(2gmF O-@O6@QjqYcezn*ߏ|YwԵ>X8&DBZmϟ`%S~m 9m}/ R' xBH/" <-oڲBгP5kt#"D7z] JG豹GD0晓A❄8.{[9k?WoWVhy<@J1rCaο,X樆y -Yzx~f~ ㏴>Ƕl)sNVe K׃"c9asSu7=C~nW\2ֵ{Zp D-|mm]lO@u3x vUl BhLoǢ-lxԧn$execd <&e%BO 50t#!2`Xܝ>OiDK8#kşAqY}Mic ۬󼶡\00,B\Q(9F*$"xK#ڃkd>8|[VϖXñ#%a C?W c.2b+DP[ʓcLi|w;P*Q5߽bM+⭡!J?$r G|qiO{_>}>=熈R Ž(OC bp_Vy][ .ߛnJq߷^W[!9,Nn=ϳ Crs(|(+2&@EO=e8Aȑ"5WoNu񈈼ah#ǥ8t #ɾxHy6#;\wdxQLQOB msSQZ$M9Cm* nKYțxvۼ`$7fm^Y pi(8c0rR6'[Wmy!~4Og>f܈r`%2QFbG!.b٠egxt쉤,EՀG,9T#f-yUJhTPFDE3b`Ψ( sp^3;;U_ öbEl"oO2`\?i=esxfORyA6r }:v@9_Uy{=WD^cMn(_D1 $+*94=}2i'H_#$ux`H; 7i"0ly](v"PV#"J^Iq}: K^#{{;=RÔ9`d%p yDMB隆k1ޠ_d:o~1fsw=d|\>:,P]E@~Pi hk2g*?x)XSV^nذܼ` <1C8TsC0ona(D0x e}$t'(`Qƃ&2{YaRQH~<2Ha<£Pcy;eGgP`7t0s5~bOjˍ>% 6%e+֛PO1's_'~IT, Uß:چAg^6h H6FxxyC8,bH:}|8bo i3fb(H zDP$ }y)8 :=  Pʁ?HYX~V\s?9luu"Pn `@C4A/ᥥOk9CG0o;5Mm˹'œ2Rf]Ștc5jҬo06у3J&տP} ckqy8F>bG"R"o,oBŰ}%YF{/F>yc80Cˁ#E8<|\ %Mx4 ;8*@B+roYy+C|;mc10rhHG\-C,m}fܥ|!C[z1zj\roL uWzP2xxbz] |pRC/+ ZQ>TL9(^ɡs1rpCEDs~((}@<$eÝWeCa2l~zG>y}m,a"|0A3n$ /΃i;(Wu\Id?e+H=/?8e t=# Q~F0`arMQC"/1oxHq>^u; <&Fj| p܋&M5a}:/mNLaPmE>m]ϐŜs N?̔!*VGqZi9uG { xjop5?qDU' 1:oGeC'u,'sy.mQ~csBƇE |a crěA^I史c`$!{roem}S%H2/!ei@><i3шlo{7)ܢ<߄4Y>w)ߤ{Ȥ2s }91Ǹmon})OD@~v 5B401#Mnx|Q(9/<Qhǃ9bPlQQ`B/R(qpF4,b{< <r^a ePȋc(A oy=X c.yVn\>7w<-&> r(P2 f;۹^X%*B9dL`"Mޜ) >? j@WE^QkgS˽ +9$r_Emp-½*<8טcU(|Y"y G웖nVgF+%"K}JK>>sϠVpdzlèڧ$$?-kẠ>((GD 'Ʋ[:X q!wqWb}_a'>\{<`;,{f}1%qO{o{dC[|<^w#Ii囕"z*q,GC|%j*&<"}<ĹA0ȩoƓƅ2ƒ$7A a͔ &XnZ>H2P.(Eևr0GC˖9' AB#xnȓ)19fzGڴz80c/>M(P;HQ/>!c\YŶ(/P7u~ z ڬB L szQxQ&IgIi̺o:EԍA.u qs&}I&2qRYڦ/Ц\cKcӯ$69(ypEƫ{K{~pq(!7J@$  H@ΌVw[w{4![ouS$  H@$p'2f6۬kL8S-r]53 H@$  H@8kP_YC;?q2@/nvvu"9WUݱ[2y _.>OuvXsÆ ݍo|V%  H@$  4B@~Άo~eׇ?ᅧ;*y>nm$  H@$PPӟ}+82LwЅg#n=$?ݧ??ɾeonst{z]?^gVE/cm HG~U]R:$  lV*sESwu{㊧;Gyd7;#|9x [B:^s+Xavp+(%oش]^L@#QGUz׻޵.jGuO<hw+]mo[$  H@X)92h#{K\{=y^ǜZ*yた%1ǮGƔc|sۀcpu3Wu>OvwTO+AO:SXFLK{^O|mby =B~xws{ql%  H@X 2nWκ nr!3u(xtmj _.wWv 4Fǣ!!MZp ݯvm]zsI #7i (}9zH-\jZ׺VQΙ}ge>eXh;ϕ2Ń3C9kXF NKkWU1bn>Gmo{[w'MVY0OYEp@Inh; x`Uծv2mF7&Pzh=Onߤ ,X-s';i.qkt-'> ">4ʬGuc~X gx1x$  H@X 90>93H:J4yS}<|fw;ޱg'=iX<,wI'm+^.+w]<ܛxm٦R?DG,|cJ8y:7^} .Q |#ʷ[ Q a̸al+x9y|&!X t10} [Qsl|-{[Z"#nu[G}B"j|;_I#@t})[neo?b\ 0ʰ&C#@dP:bL9;moS _*ִ!чHѿ`KG;V), 駹܋|&}cM]2}<2fXV'qmNܻ( z]8vZ8tݬ$  H@+'?Zˍ'%o` I7F~(|n )^ז̀7+&<^WE&-F#x~_l2c׿6!b! ^Ѭ}}ߩ^.k .ykiD<`TO1X#i(gs1 |P71D[bG} |vz=a! DRV5#<:Pb('yi_W/| O6c;F#a; ܩ#}i&1%,eCRkQ B8x؞\&\A ŝBYI3_6>rApoĈs~'h'*m5OR H@$^T$կ~{_ݡda;BCQ(YEExx#61Noy[[x2k夁G""P§@%_2GQDP~«CPH .Oiah0ؐRa,6>'.?C)OK30d˃|X?#v8q}D>D/0~>kH#s +$  H`/4^?q%Yw<;1.!x ',emxb-;N  (x Q g<%'4? ґ.{D0]F0Ja Ԙc肣:| D! QNa"<@7Dm)onŻ>M?e F6rw(r@G;#˖eOI}E&r v/.C}* f_1 YvQ&1)rlW$  H@ 0<(x€㧥e{(>aA'CrjirqFuh ?'I@$ /ol3O&ܗ9@!( h/PdCY+z#1"v}2C)EPP6^3ǂR sBa9v#OK8<݄g&F 9uP m,F[a9y/<Ǔ`pc9L(T1REܵz'cj>2vB: |1Vs_Z@"i8?sQQ$x-_)R_֧fe@Ҝ# y)pf c H@$T@%9 gs"xbv/-gf75yc'|x1O@yY̢s>aLÈeV/Rه"I.e_TPx'5'|>ylTtb~l}ŀwt~ ?ԩOk(9X9JLK0|<ɣ,m B k C p{$uH5 0wP1z aT{yL9X&$üxH>5ii?Ep 2o9/vvn"E:B0K+ H@!?= dW 1V&̓( Yx'9#6l((ܼ c=ޝyS9BeHg L2QbAasՋs&)pȃB4Q>(R.o"jxK 蘅͈PC޴-BA"OTxoRjX<5xwz(Ce'=#W2|wuwg.~^c"&#m-%&@q<#`C ,%94&yPIi?8I2O'3quU d\?A"C3)I@$ yHy,«SGX/^}(>S8N8xXQ2vmK=P#(G0A_%>>PlOyc8Dz;A# ""_E V/ 8AXp̵gUtwŒ:%2C Vf=<Pp%V_sOr;J G8<aXck}"8,@<0S ? H@$D*sKkX.ϑ#lon'&eV1sxQ ZQڍQ9O?a*-!(~7<^Ss9cI,@H&+k#ׄJN%b*퓾2 QNZ+}iaP tmIkN[»YO$|'5ҴQN: QYozΧJ+г"Bn ?"-7YlsZ5e偲LY.(gCտ9k# P`?$%F?/ i1uMүS0J3Z.u}`tV.)5#LоrM$  K@~^r4 ~xQY`G`\/Dm C' OO`y3|-$  H@Poiآ>`Ўȃh!{9":xc{O>|$T_i^|ڙ(ovD$ 3)|3B(i~(yJ@C< IIqn$  H@V3u`w9A?nIENDB`graphql-ruby-2.5.19/guides/javascript_client/apollo_subscriptions.md000066400000000000000000000164031514115062600260150ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: JavaScript Client title: Apollo Subscriptions desc: GraphQL subscriptions with GraphQL-Ruby and Apollo Client index: 2 --- GraphQL-Ruby's JavaScript client includes several kinds of support for Apollo Client: - Apollo Link (2.x, 3.x): - [Overview](#apollo-link) - [Pusher](#apollo-link--pusher) - [Ably](#apollo-link--ably) - [ActionCable](#apollo-link--actioncable) - Apollo 1.x: - [Overview](#apollo-1) - [Pusher](#apollo-1--pusher) - [ActionCable](#apollo-1--actioncable) ## Apollo Link Apollo Links are used by Apollo client 2.x and 3.x. ## Apollo Link -- Pusher `graphql-ruby-client` includes support for subscriptions with Pusher and ApolloLink. To use it, add `PusherLink` before your `HttpLink`. For example: ```js // Load Apollo stuff import { ApolloClient, HttpLink, ApolloLink, InMemoryCache } from "@apollo/client"; // Load PusherLink from graphql-ruby-client import PusherLink from 'graphql-ruby-client/subscriptions/PusherLink'; // Load Pusher and create a client import Pusher from "pusher-js" var pusherClient = new Pusher("your-app-key", { cluster: "us2" }) // Make the HTTP link which actually sends the queries const httpLink = new HttpLink({ uri: '/graphql', credentials: 'include' }); // Make the Pusher link which will pick up on subscriptions const pusherLink = new PusherLink({pusher: pusherClient}) // Combine the two links to work together const link = ApolloLink.from([pusherLink, httpLink]) // Initialize the client const client = new ApolloClient({ link: link, cache: new InMemoryCache() }); ``` This link will check responses for the `X-Subscription-ID` header, and if it's present, it will use that value to subscribe to Pusher for future updates. If you're using {% internal_link "compressed payloads", "/subscriptions/pusher_implementation#payload-compression" %}, configure a `decompress:` function, too: ```javascript // Add `pako` to the project for gunzipping import pako from "pako" const pusherLink = new PusherLink({ pusher: pusherClient, decompress: function(compressed) { // Decode base64 const data = atob(compressed) .split('') .map(x => x.charCodeAt(0)); // Decompress const payloadString = pako.inflate(new Uint8Array(data), { to: 'string' }); // Parse into an object return JSON.parse(payloadString); } }) ``` ## Apollo Link -- Ably `graphql-ruby-client` includes support for subscriptions with Ably and ApolloLink. To use it, add `AblyLink` before your `HttpLink`. For example: ```js // Load Apollo stuff import { ApolloClient, HttpLink, ApolloLink, InMemoryCache } from '@apollo/client'; // Load Ably subscriptions link import AblyLink from 'graphql-ruby-client/subscriptions/AblyLink' // Load Ably and create a client const Ably = require("ably") const ablyClient = new Ably.Realtime({ key: "your-app-key" }) // Make the HTTP link which actually sends the queries const httpLink = new HttpLink({ uri: '/graphql', credentials: 'include' }); // Make the Ably link which will pick up on subscriptions const ablyLink = new AblyLink({ably: ablyClient}) // Combine the two links to work together const link = ApolloLink.from([ablyLink, httpLink]) // Initialize the client const client = new ApolloClient({ link: link, cache: new InMemoryCache() }); ``` This link will check responses for the `X-Subscription-ID` header, and if it's present, it will use that value to subscribe to Ably for future updates. For your __app key__, make a key with "Subscribe" and "Presence" privileges and use that: {{ "/javascript_client/ably_key.png" | link_to_img:"Ably Subscription Key Privileges" }} ## Apollo Link -- ActionCable `graphql-ruby-client` includes support for subscriptions with ActionCable and ApolloLink. To use it, construct a split link that routes: - subscription queries to an `ActionCableLink`; and - other queries to an `HttpLink` For example: ```js import { ApolloClient, HttpLink, ApolloLink, InMemoryCache } from '@apollo/client'; import { createConsumer } from '@rails/actioncable'; import ActionCableLink from 'graphql-ruby-client/subscriptions/ActionCableLink'; const cable = createConsumer() const httpLink = new HttpLink({ uri: '/graphql', credentials: 'include' }); const hasSubscriptionOperation = ({ query: { definitions } }) => { return definitions.some( ({ kind, operation }) => kind === 'OperationDefinition' && operation === 'subscription' ) } const link = ApolloLink.split( hasSubscriptionOperation, new ActionCableLink({cable}), httpLink ); const client = new ApolloClient({ link: link, cache: new InMemoryCache() }); ``` Note that for Rails 5, the ActionCable client package is `actioncable`, not `@rails/actioncable`. ## Apollo 1 `graphql-ruby-client` includes support for Apollo 1 client subscriptions over {% internal_link "Pusher", "/subscriptions/pusher_implementation" %} or {% internal_link "ActionCable", "/subscriptions/action_cable_implementation" %}. To use it, require `subscriptions/addGraphQLSubscriptions` and call the function with your network interface and transport client (example below). See the {% internal_link "Subscriptions guide", "/subscriptions/overview" %} for information about server-side setup. ### Apollo 1 -- Pusher Pass `{pusher: pusherClient}` to use Pusher: ```js // Load Pusher and create a client var Pusher = require("pusher-js") var pusherClient = new Pusher(appKey, options) // Add subscriptions to the network interface with the `pusher:` options import addGraphQLSubscriptions from "graphql-ruby-client/subscriptions/addGraphQLSubscriptions" addGraphQLSubscriptions(myNetworkInterface, {pusher: pusherClient}) // Optionally, add persisted query support: var OperationStoreClient = require("./OperationStoreClient") RailsNetworkInterface.use([OperationStoreClient.apolloMiddleware]) ``` If you're using {% internal_link "compressed payloads", "/subscriptions/pusher_implementation#payload-compression" %}, configure a `decompress:` function, too: ```javascript // Add `pako` to the project for gunzipping import pako from "pako" addGraphQLSubscriptions(myNetworkInterface, { pusher: pusherClient, decompress: function(compressed) { // Decode base64 const data = btoa(compressed) // Decompress const payloadString = pako.inflate(data, { to: 'string' }) // Parse into an object return JSON.parse(payloadString); } }) ``` ### Apollo 1 -- ActionCable By passing `{cable: cable}`, all `subscription` queries will be routed to ActionCable. For example: ```js // Load ActionCable and create a consumer var ActionCable = require('@rails/actioncable') var cable = ActionCable.createConsumer() window.cable = cable // Load ApolloClient and create a network interface var apollo = require('apollo-client') var RailsNetworkInterface = apollo.createNetworkInterface({ uri: '/graphql', opts: { credentials: 'include', }, headers: { 'X-CSRF-Token': $("meta[name=csrf-token]").attr("content"), } }); // Add subscriptions to the network interface import addGraphQLSubscriptions from "graphql-ruby-client/subscriptions/addGraphQLSubscriptions" addGraphQLSubscriptions(RailsNetworkInterface, {cable: cable}) // Optionally, add persisted query support: var OperationStoreClient = require("./OperationStoreClient") RailsNetworkInterface.use([OperationStoreClient.apolloMiddleware]) ``` graphql-ruby-2.5.19/guides/javascript_client/graphiql_subscriptions.md000066400000000000000000000074311514115062600263370ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: JavaScript Client title: GraphiQL Subscriptions desc: Testing GraphQL subscriptions in the GraphiQL IDE index: 5 --- After setting up your server, you can integrate subscriptions into [GraphiQL](https://github.com/graphql/graphiql/tree/main/packages/graphiql#readme), the in-browser GraphQL IDE. ## Adding GraphiQL to your app To get started, make a page for rendering GraphiQL, for example: ```html

``` Then, install GraphiQL (eg, `yarn add graphiql`) and add JavaScript code to import GraphiQL and render it on your page: ```js import { GraphiQL } from 'graphiql' import React from 'react' import { createRoot } from 'react-dom/client' import 'graphiql/graphiql.css' import { createGraphiQLFetcher } from '@graphiql/toolkit' const fetcher = createGraphiQLFetcher({ url: '/graphql' }) const root = createRoot(document.getElementById('root')) root.render() ``` After that, you should be able to load the page in your app and see the GraphiQL editor. ## Ably To integrate {% internal_link "Ably subscriptions", "subscriptions/ably_implementation" %}, use `createAblyFetcher`, for example: ```js import Ably from "ably" import createAblyFetcher from 'graphql-ruby-client/subscriptions/createAblyFetcher' // Initialize a client // the key must have "subscribe" and "presence" permissions const ably = new Ably.Realtime({ key: "your.application.key" }) // Initialize a new fetcher and pass it to GraphiQL below var fetcher = createAblyFetcher({ ably: ably, url: "/graphql" }) const root = createRoot(document.getElementById('root')) root.render() ``` Under the hood, it will use `window.fetch` to send GraphQL operations to the server, then listen for `X-Subscription-ID` headers in responses. To customize its HTTP requests, you can pass a `fetchOptions:` object or a custom `fetch:` function to `createAblyFetcher({ ... })`. ## Pusher To integrate {% internal_link "Pusher subscriptions", "subscriptions/pusher_implementation" %}, use `createPusherFetcher`, for example: ```js import Pusher from "pusher-js" import createPusherFetcher from 'graphql-ruby-client/subscriptions/createPusherFetcher' // Initialize a client const pusher = new Pusher("your-app-key", { cluster: "your-cluster" }) // Initialize a new fetcher and pass it to GraphiQL below var fetcher = createPusherFetcher({ pusher: pusher, url: "/graphql" }) const root = createRoot(document.getElementById('root')) root.render() ``` Under the hood, it will use `window.fetch` to send GraphQL operations to the server, then listen for `X-Subscription-ID` headers in responses. To customize its HTTP requests, you can pass a `fetchOptions:` object or a custom `fetch:` function to `createPusherFetcher({ ... })`. ## ActionCable To integrate {% internal_link "ActionCable subscriptions", "subscriptions/action_cable_implementation" %}, use `createActionCableFetcher`, for example: ```js import { createConsumer } from "@rails/actioncable" import createActionCableFetcher from 'graphql-ruby-client/subscriptions/createActionCableFetcher'; // Initialize a client const actionCable = createConsumer() // Initialize a new fetcher and pass it to GraphiQL below var fetcher = createActionCableFetcher({ consumer: actionCable, url: "/graphql" }) const root = createRoot(document.getElementById('root')) root.render() ``` Under the hood, it will split traffic: it will send `subscription { ... }` operations via ActionCable and send queries and mutations via HTTP `POST` using `window.fetch`. To customize its HTTP requests, you can pass a `fetchOptions:` object or a custom `fetch:` function to `createActionCableFetcher({ ... })`. graphql-ruby-2.5.19/guides/javascript_client/overview.md000066400000000000000000000020451514115062600234030ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: JavaScript Client title: Overview desc: Getting Started with GraphQL-Ruby's Javascript client, graphql-ruby-client. index: 0 --- There is a JavaScript client for GraphQL-Ruby, `graphql-ruby-client`. You can install it from NPM or Yarn: ```sh yarn add graphql-ruby-client # Or: npm install graphql-ruby-client ``` The source code is [in the graphql-ruby repository](https://github.com/rmosolgo/graphql-ruby/tree/master/javascript_client). See detailed guides for more info about its features: - {% internal_link "sync CLI", "javascript_client/sync" %} for use with [graphql-pro](https://graphql.pro)'s persisted query backend - Subscription support: - {% internal_link "Apollo integration", "/javascript_client/apollo_subscriptions" %} - {% internal_link "Relay integration", "/javascript_client/relay_subscriptions" %} - {% internal_link "urql integration", "/javascript_client/urql_subscriptions" %} - {% internal_link "GraphiQL integration", "/javascript_client/graphiql_subscriptions" %} graphql-ruby-2.5.19/guides/javascript_client/relay_subscriptions.md000066400000000000000000000154721514115062600256500ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: JavaScript Client title: Relay Subscriptions desc: GraphQL subscriptions with GraphQL-Ruby and Relay Modern index: 3 --- `graphql-ruby-client` includes three kinds of support for subscriptions with Relay Modern: - [Pusher](#pusher) - [Ably](#ably) - [ActionCable](#actioncable) To use it, require `graphql-ruby-client/subscriptions/createRelaySubscriptionHandler` and call the function with your client and optionally, your OperationStoreClient. __Note:__ For Relay <11, use `import { createLegacyRelaySubscriptionHandler } from "graphql-ruby-client/subscriptions/createRelaySubscriptionHandler"` instead; the signature changed in Relay 11. See the {% internal_link "Subscriptions guide", "/subscriptions/overview" %} for information about server-side setup. ## Pusher Subscriptions with {% internal_link "Pusher", "/subscriptions/pusher_implementation" %} require two things: - A client from the [`pusher-js` library](https://github.com/pusher/pusher-js) - A [`fetchOperation` function](#fetchoperation-function) for sending the `subscription` operation to the server ### Pusher client Pass `pusher:` to get Subscription updates over Pusher: ```js // Load the helper function import createRelaySubscriptionHandler from "graphql-ruby-client/subscriptions/createRelaySubscriptionHandler" // Prepare a Pusher client var Pusher = require("pusher-js") var pusherClient = new Pusher(appKey, options) // Create a fetchOperation, see below for more details function fetchOperation(operation, variables, cacheConfig) { return fetch(...) } // Create a Relay Modern-compatible handler var subscriptionHandler = createRelaySubscriptionHandler({ pusher: pusherClient, fetchOperation: fetchOperation }) // Create a Relay Modern network with the handler var network = Network.create(fetchQuery, subscriptionHandler) ``` ### Compressed Payloads If you're using {% internal_link "compressed payloads", "/subscriptions/pusher_implementation#payload-compression" %}, configure a `decompress:` function, too: ```javascript // Add `pako` to the project for gunzipping import pako from "pako" var subscriptionHandler = createRelaySubscriptionHandler({ pusher: pusherClient, fetchOperation: fetchOperation, decompress: function(compressed) { // Decode base64 const data = btoa(compressed) // Decompress const payloadString = pako.inflate(data, { to: 'string' }) // Parse into an object return JSON.parse(payloadString); } }) ``` ## Ably Subscriptions with {% internal_link "Ably", "/subscriptions/ably_implementation" %} require two things: - A client from the [`ably-js` library](https://github.com/ably/ably-js) - A [`fetchOperation` function](#fetchoperation-function) for sending the `subscription` operation to the server ### Ably client Pass `ably:` to get Subscription updates over Ably: ```js // Load the helper function import createRelaySubscriptionHandler from "graphql-ruby-client/subscriptions/createRelaySubscriptionHandler" // Load Ably and create a client const Ably = require("ably") const ablyClient = new Ably.Realtime({ key: "your-app-key" }) // create a fetchOperation, see below for more details function fetchOperation(operation, variables, cacheConfig) { return fetch(...) } // Create a Relay Modern-compatible handler var subscriptionHandler = createRelaySubscriptionHandler({ ably: ablyClient, fetchOperation: fetchOperation }) // Create a Relay Modern network with the handler var network = Network.create(fetchQuery, subscriptionHandler) ``` ## ActionCable With this configuration, `subscription` queries will be routed to {% internal_link "ActionCable", "/subscriptions/action_cable_implementation" %}. For example: ```js // Require the helper function import createRelaySubscriptionHandler from "graphql-ruby-client/subscriptions/createRelaySubscriptionHandler") // Optionally, load your OperationStoreClient var OperationStoreClient = require("./OperationStoreClient") // Create a Relay Modern-compatible handler var subscriptionHandler = createRelaySubscriptionHandler({ cable: createConsumer(...), operations: OperationStoreClient, }) // Create a Relay Modern network with the handler var network = Network.create(fetchQuery, subscriptionHandler) ``` ## With Relay Persisted Queries If you're using Relay's built-in [persisted query support](https://relay.dev/docs/guides/persisted-queries/), you can pass `clientName:` to the handler in order to build IDs that work with the {% internal_link "OperationStore", "/operation_store/overview.html" %}. For example: ```js var subscriptionHandler = createRelaySubscriptionHandler({ cable: createConsumer(...), clientName: "web-frontend", // This should match the one you use for `sync` }) // Create a Relay Modern network with the handler var network = Network.create(fetchQuery, subscriptionHandler) ``` Then, the ActionCable handler will use Relay's provided operation IDs to interact with the OperationStore. ## fetchOperation function The `fetchOperation` function can be extracted from your `fetchQuery` function. Its signature is: ```js // Returns a promise from `fetch` function fetchOperation(operation, variables, cacheConfig) { return fetch(...) } ``` - `operation`, `variables`, and `cacheConfig` are the first three arguments to the `fetchQuery` function. - The function should call `fetch` and return the result (a Promise of a `Response`). For example, `Environment.js` may look like: ```js // This function sends a GraphQL query to the server const fetchOperation = function(operation, variables, cacheConfig) { const bodyValues = { variables, operationName: operation.name, } const useStoredOperations = process.env.NODE_ENV === "production" if (useStoredOperations) { // In production, use the stored operation bodyValues.operationId = OperationStoreClient.getOperationId(operation.name) } else { // In development, use the query text bodyValues.query = operation.text } return fetch('http://localhost:3000/graphql', { method: 'POST', opts: { credentials: 'include', }, headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(bodyValues), }) } // `fetchQuery` uses `fetchOperation`, but returns a Promise of JSON const fetchQuery = (operation, variables, cacheConfig, uploadables) => { return fetchOperation(operation, variables, cacheConfig).then(response => { return response.json() }) } // Subscriptions uses the same `fetchOperation` function for initial subscription requests const subscriptionHandler = createRelaySubscriptionHandler({pusher: pusherClient, fetchOperation: fetchOperation}) // Combine them into a `Network` const network = Network.create(fetchQuery, subscriptionHandler) ``` Since `OperationStoreClient` is in the `fetchOperation` function, it will apply to all GraphQL operations. graphql-ruby-2.5.19/guides/javascript_client/sync.md000066400000000000000000000276011514115062600225160ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: JavaScript Client title: OperationStore Sync desc: Javascript tooling for persisted queries with GraphQL-Ruby index: 1 --- JavaScript support for GraphQL projects using [graphql-pro](https://graphql.pro)'s `OperationStore` for persisted queries. - [`sync` CLI](#sync-utility) - [Relay <2 support](#use-with-relay-2) - [Relay 2+ support](#use-with-relay-persisted-output) - [Apollo Client support](#use-with-apollo-client) - [Apollo Link support](#use-with-apollo-link) - [Apollo Codegen Support](#use-with-apollo-codegen) - [Apollo Android support](#use-with-apollo-android) - [Apollo Persisted Queries Support](#use-with-apollo-persisted-queries) - [Plain JS support](#use-with-plain-javascript) - [Authorization](#authorization) See the {% internal_link "OperationStore guide", "/operation_store/overview" %} for server-side setup. ## `sync` utility This package contains a command line utility, `graphql-ruby-client sync`: ``` $ graphql-ruby-client sync # ... Authorizing with HMAC Syncing 4 operations to http://myapp.com/graphql/operations... 3 added 1 not modified 0 failed Generating client module in app/javascript/graphql/OperationStoreClient.js... ✓ Done! ``` `sync` Takes several options: option | description --------|---------- `--url` | {% internal_link "Sync API", "/operation_store/getting_started.html#add-routes" %} url `--path` | Local directory to search for `.graphql` / `.graphql.js` files `--relay-persisted-output` | Path to a `.json` file from `relay-compiler ... --persist-output` `--apollo-codegen-json-output` | Path to a `.json` file from `apollo client:codegen ... --target json` `--apollo-android-operation-output` | Path to an `OperationOutput.json` file from Apollo Android `--client` | Client ID ({% internal_link "created on server", "/operation_store/client_workflow" %}) `--secret` | Client Secret ({% internal_link "created on server", "/operation_store/client_workflow" %}) `--outfile` | Destination for generated code `--outfile-type` | What kind of code to generate (`js` or `json`) `--header={key}:{value}` | Add a header to the outgoing HTTP request (may be repeated) `--add-typename` | Add `__typename` to all selection sets (for use with Apollo Client) `--verbose` | Output some debug information `--changeset-version` | Set a {% internal_link "Changeset Version", "/changesets/installation#controller-setup" %} when syncing these queries. (`context[:changeset_version]` will also be required at runtime, when running these stored operations.) `--dump-payload` | A file to write the HTTP Post payload into, or if no filename is passed, then the payload will be written to stdout. You can see these and a few others with `graphql-ruby-client sync --help`. ## Use with Relay <2 `graphql-ruby-client` can persist queries from `relay-compiler` using the embedded `@relayHash` value. (This was created in Relay before 2.0.0. See below for Relay 2.0+.) To sync your queries with the server, use the `--path` option to point to your `__generated__` directory, for example: ```bash # sync a Relay project $ graphql-ruby-client sync --path=src/__generated__ --outfile=src/OperationStoreClient.js --url=... ``` Then, the generated code may be integrated with Relay's [Network Layer](https://relay.dev/docs/guides/network-layer/): ```js // ... // require the generated module: const OperationStoreClient = require('./OperationStoreClient') // ... function fetchQuery(operation, variables, cacheConfig, uploadables) { const requestParams = { variables, operationName: operation.name, } if (process.env.NODE_ENV === "production") // In production, use the stored operation requestParams.operationId = OperationStoreClient.getOperationId(operation.name) } else { // In development, use the query text requestParams.query = operation.text, } return fetch('/graphql', { method: 'POST', headers: { /*...*/ }, body: JSON.stringify(requestParams), }).then(/* ... */); } // ... ``` (Only Relay Modern is supported. Legacy Relay can't generate static queries.) ## Use With Relay Persisted Output To use Relay's persisted output, add a `"file": ...` to your project's [`persistConfig` object](https://relay.dev/docs/guides/persisted-queries/). For example: ```json "relay": { ... "persistConfig": { "file": "./persisted-queries.json" } }, ``` Then, push Relay's generated queries to your OperationStore server with `--relay-persisted-output`: ``` $ graphql-ruby-client sync --relay-persisted-output=path/to/persisted-queries.json --url=... ``` In this case, `sync` _won't_ generate a JavaScript module because `relay-compiler` has already prepared its queries for persisted use. Instead, update your network layer to include the _client name_ and _operation id_ in the HTTP params: ```js const operationStoreClientName = "MyRelayApp"; function fetchQuery(operation, variables,) { return fetch('/graphql', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ // Pass the client name and the operation ID, joined by `/` documentId: operationStoreClientName + "/" + operation.id, // query: operation.text, // this is now obsolete because text is null variables, }), }).then(response => { return response.json(); }); } ``` (Inspired by https://relay.dev/docs/guides/persisted-queries/#network-layer-changes.) Now, your Relay app will only send operation IDs over the wire to the server. ## Use with Apollo Client Use the `--path` option to point at your `.graphql` files: ``` $ graphql-ruby-client sync --path=src/graphql/ --url=... ``` Then, load the generated module and add its `.apolloMiddleware` to your network interface with `.use([...])`: ```js // load the generated module var OperationStoreClient = require("./OperationStoreClient") // attach it as middleware in production // (in development, send queries to the server as normal) if (process.env.NODE_ENV === "production") { MyNetworkInterface.use([OperationStoreClient.apolloMiddleware]) } ``` Now, the middleware will replace query strings with `operationId`s. ## Use with Apollo Link Use the `--path` option to point at your `.graphql` files: ``` $ graphql-ruby-client sync --path=src/graphql/ --url=... ``` Then, load the generated module and add its `.apolloLink` to your Apollo Link: ```js // load the generated module var OperationStoreClient = require("./OperationStoreClient") // Integrate the link to another link: const link = ApolloLink.from([ authLink, OperationStoreClient.apolloLink, httpLink, ]) // Create a client const client = new ApolloClient({ link: link, cache: new InMemoryCache(), }); ``` __Update the controller__: Apollo Link supports extra parameters _nested_ as `params[:extensions][:operationId]`, so update your controller to add that param to context: ```ruby # app/controllers/graphql_controller.rb context = { # ... # Support Apollo Link: operation_id: params[:extensions][:operationId] } ``` Now, `context[:operation_id]` will be used to fetch a query from the database. ## Use with Apollo Codegen Use `apollo client:codegen ... --target json` to build a JSON artifact containing your app's queries. Then, pass the path to that artifact to `graphql-ruby-client sync --apollo-codegen-json-output path/to/output.json ...`. `sync` will use Apollo-generated `operationId`s to populate the `OperationStore`. Then, to use Apollo-style persisted query IDs, hook up the __Persisted Queries Link__ as described in [Apollo's documentation](https://www.apollographql.com/docs/react/api/link/persisted-queries/) Finally, __update the controller__ to pass the Apollo-style persisted query ID as the operation ID: ```ruby # app/controllers/graphql_controller.rb context = { # ... # Support already-synced Apollo Persisted Queries: operation_id: params[:extensions][:operationId] } ``` Now, Apollo-style persisted query IDs will be used to fetch operations from the server's `OperationStore`. ## Use with Apollo Android Apollo Android's [generateOperationOutput option](https://www.apollographql.com/docs/android/advanced/persisted-queries/#operationoutputjson) builds an `OperationOutput.json` file which works with the OperationStore. To sync those queries, __use the `--apollo-android-operation-output` option__: ```sh graphql-ruby-client sync --apollo-android-operation-output=path/to/OperationOutput.json --url=... ``` That way, the OperationStore will use the query IDs generated by Apollo Android. On the server, you'll have to __update your controller__ to receive the client name and the operation ID. For example: ```ruby # app/controllers/graphql_controller.rb context = { ... } # Check for an incoming operation ID from Apollo Client: apollo_android_operation_id = request.headers["X-APOLLO-OPERATION-ID"] if apollo_android_operation_id.present? # Check the incoming request to confirm that # it's your first-party client with stored operations client_name = # ... if client_name.present? # If we received an incoming operation ID # _and_ identified the client, run a persisted operation. context[:operation_id] = "#{client_name}/#{apollo_android_operation_id}" end end ``` You may also have to __update your app__ to send an identifier, so that the server can determine the "client name" used with the operation store. (Apollo Android sends a query hash, but the operation store expects IDs in the form `#{client_name}/#{query_hash}`.) ## Use with Apollo Persisted Queries Apollo client has a [Persisted Queries Link](https://www.apollographql.com/docs/react/api/link/persisted-queries/). You can use that link with GraphQL-Pro's {% internal_link "OperationStore", "/operation_store/overview" %}. First, create a manifest with [`generate-persisted-query-manifest`](https://www.apollographql.com/docs/react/api/link/persisted-queries/#1-generate-operation-manifests), then, pass the path to that file to `sync`: ```sh $ graphql-ruby-client sync --apollo-persisted-query-manifest=path/to/manifest.json ... ``` Then, configure Apollo Client to [use your persisted query manifest](https://www.apollographql.com/docs/react/api/link/persisted-queries/#persisted-queries-implementation). Finally, update your controller to receive the operation ID and pass it as `context[:operation_id]`: ```ruby client_name = "..." # TODO: send the client name as a query param or header persisted_query_hash = params[:extensions][:persistedQuery][:sha256Hash] context = { # ... operation_id: "#{client_name}/#{persisted_query_hash}" } ``` The `operation_id` will also need your client name. Using Apollo Client, you could send this as a [custom header](https://www.apollographql.com/docs/react/networking/basic-http-networking/#customizing-request-headers) or another way that works for your application (eg, session or user agent). ## Use with plain JavaScript `OperationStoreClient.getOperationId` takes an operation name as input and returns the server-side alias for that operation: ```js var OperationStoreClient = require("./OperationStoreClient") OperationStoreClient.getOperationId("AppHomeQuery") // => "my-frontend-app/7a8078c7555e20744cb1ff5a62e44aa92c6e0f02554868a15b8a1cbf2e776b6f" OperationStoreClient.getOperationId("ProductDetailQuery") // => "my-frontend-app/6726a3b816e99b9971a1d25a1205ca81ecadc6eb1d5dd3a71028c4b01cc254c1" ``` Post the `operationId` in your GraphQL requests: ```js // Lookup the operation name: var operationId = OperationStoreClient.getOperationId(operationName) // Include it in the params: $.post("/graphql", { operationId: operationId, variables: queryVariables, }, function(response) { // ... }) ``` ## Authorization `OperationStore` uses HMAC-SHA256 to {% internal_link "authenticate requests" , "/operation_store/access_control" %}. Pass the key to `graphql-ruby-client sync` as `--secret` to authenticate it: ```bash $ export MY_SECRET_KEY= "abcdefg..." $ graphql-ruby-client sync ... --secret=$MY_SECRET_KEY # ... Authenticating with HMAC # ... ``` graphql-ruby-2.5.19/guides/javascript_client/urql_subscriptions.md000066400000000000000000000027711514115062600255150ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: JavaScript Client title: urql Subscriptions desc: GraphQL subscriptions with GraphQL-Ruby and urql index: 4 --- GraphQL-Ruby currently supports using `urql` with the {% internal_link "ActionCable", "/subscriptions/action_cable_implementation" %} and {% internal_link "Pusher implementation", "/subscriptions/pusher_implementation" %}. ## Pusher ```js import SubscriptionExchange from "graphql-ruby-client/subscriptions/SubscriptionExchange" import Pusher from "pusher" import { Client, defaultExchanges, subscriptionExchange } from 'urql' const pusherClient = new Pusher("your-app-key", { cluster: "us2" }) const forwardToPusher = SubscriptionExchange.create({ pusher: pusherClient }) const client = new Client({ url: '/graphql', exchanges: [ ...defaultExchanges, subscriptionExchange({ forwardSubscription: forwardToPusher }), ], }); ``` ## ActionCable ```js import { createConsumer } from "@rails/actioncable"; import SubscriptionExchange from "graphql-ruby-client/subscriptions/SubscriptionExchange" const actionCable = createConsumer('ws://127.0.0.1:3000/cable'); const forwardToActionCable = SubscriptionExchange.create({ consumer: actionCable }) const client = new Client({ url: '/graphql', exchanges: [ ...defaultExchanges, subscriptionExchange({ forwardSubscription: forwardToActionCable }), ], }); ``` Want to use `urql` with another subscription backend? Please {% open_an_issue "Using urql with ..." %}. graphql-ruby-2.5.19/guides/js/000077500000000000000000000000001514115062600161225ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/js/search.js000066400000000000000000000066071514115062600177360ustar00rootroot00000000000000var client = algoliasearch('8VO8708WUV', '1f3e2b6f6a503fa82efdec331fd9c55e'); var index = client.initIndex('prod_graphql_ruby'); var GraphQLRubySearch = { // Respond to a change event on `el` by: // - Searching the index // - Rendering the results run: function(el) { var searchTerm = el.value var searchResults = document.querySelector("#search-results") if (!searchTerm) { // If there's no search term, clear the results pane searchResults.innerHTML = "" } else { index.search({ query: searchTerm, hitsPerPage: 8, }, function(err, content) { if (err) { console.error(err) } var results = content.hits // Clear the previous results searchResults.innerHTML = "" results.forEach(function(result) { // Create a wrapper hyperlink var container = document.createElement("a") container.className = "search-result" container.href = (result.rubydoc_url || result.url) + (result.anchor ? "#" + result.anchor : "") // This helper will be used to accumulate text into the search-result function createSpan(text, className) { var txt = document.createElement("span") txt.className = className txt.innerHTML = text container.appendChild(txt) } if (result.rubydoc_url) { createSpan("API Doc", "search-category") createSpan(result.title, "search-title") } else { createSpan(result.section, "search-category") var resultHeader = [result.title].concat(result.headings).join(" > ") createSpan(resultHeader, "search-title") var preview = result._snippetResult.content.value createSpan(preview, "search-preview") } searchResults.appendChild(container) }) var seeAll = document.createElement("a") seeAll.href = "/search?query=" + content.query seeAll.className = "search-see-all" seeAll.innerHTML = "See All Results (" + content.nbHits + ")" searchResults.appendChild(seeAll) }) } }, // Return true if we actually highlighted something _moveHighlight: function(diff) { var allResults = document.querySelectorAll(".search-result") var highlightClass = "highlight-search-result" if (!allResults.length) { // No search results to highlight return false } var highlightedResult = document.querySelector("." + highlightClass) var nextHighlightedResult var result for (var i = 0; i < allResults.length; i++) { result = allResults[i] if (result == highlightedResult) { nextHighlightedResult = allResults[i + diff] break } } if (!nextHighlightedResult) { // Either nothing was highlighted yet, // or we were at the end of results and we loop around nextHighlightedResult = allResults[0] } if (highlightedResult) { highlightedResult.classList.remove(highlightClass) } nextHighlightedResult.classList.add(highlightClass) nextHighlightedResult.focus() return true } } document.addEventListener("keydown", function(ev) { var diff = ev.keyCode == 38 ? -1 : (ev.keyCode == 40 ? 1 : 0) if (diff) { var highlighted = GraphQLRubySearch._moveHighlight(diff) if (highlighted) { ev.preventDefault() } } }) graphql-ruby-2.5.19/guides/language_tools/000077500000000000000000000000001514115062600205115ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/language_tools/c_parser.md000066400000000000000000000015651514115062600226400ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Language Tools title: C-based Parser desc: The GraphQL::CParser gem is a drop-in replacement for the built-in parser index: 1 --- GraphQL-Ruby includes a plain-Ruby parser, but a faster parser is available as a C extension. To use it, add the [`graphql-c_parser` gem](https://rubygems.org/gems/graphql-c_parser) to your project, for example: ```ruby bundle add graphql-c_parser ``` When `graphql-c_parser` is `require`d by your app, the C-based parser is installed as the default parser (as {{ "GraphQL.default_parser" | api_doc }}). Bundler requires the library automatically, but you can also require it manually: ```ruby require "graphql/c_parser" ``` This alternative parser is faster and uses less memory. The library also adds `GraphQL.scan_with_c` and `GraphQL.parse_with_c` for calling the C-based parser directly. graphql-ruby-2.5.19/guides/language_tools/visitor.md000066400000000000000000000122041514115062600225310ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Language Tools title: AST Visitor desc: Analyze and modify parsed GraphQL code index: 0 --- GraphQL code is usually contained in a string, for example: ```ruby query_string = "query { user(id: \"1\") { userName } }" ``` You can perform programmatic analysis and modifications to GraphQL code using a three-step process: - __Parse__ the code into an abstract syntax tree - __Analyze/Modify__ the code with a visitor - __Print__ the code back to a string ## Parse {{ "GraphQL.parse" | api_doc }} turns a string into a GraphQL document: ```ruby parsed_doc = GraphQL.parse("{ user(id: \"1\") { userName } }") # => # ``` Also, {{ "GraphQL.parse_file" | api_doc }} parses the contents of the named file and includes a `filename` in the parsed document. #### AST Nodes The parsed document is a tree of nodes, called an _abstract syntax tree_ (AST). This tree is _immutable_: once a document has been parsed, those Ruby objects can't be changed. Modifications are performed by _copying_ existing nodes, applying changes to the copy, then making a new tree to hold the copied node. Where possible, unmodified nodes are retained in the new tree (it's _persistent_). The copy-and-modify workflow is supported by a few methods on the AST nodes: - `.merge(new_attrs)` returns a copy of the node with `new_attrs` applied. This new copy can replace the original node. - `.add_{child}(new_child_attrs)` makes a new node with `new_child_attrs`, adds it to the array specified by `{child}`, and returns a copy whose `{children}` array contains the newly created node. For example, to rename a field and add an argument to it, you could: ```ruby modified_node = field_node # Apply a new name .merge(name: "newName") # Add an argument to this field's arguments .add_argument(name: "newArgument", value: "newValue") ``` Above, `field_node` is unmodified, but `modified_node` reflects the new name and new argument. ## Analyze/Modify To inspect or modify a parsed document, extend {{ "GraphQL::Language::Visitor" | api_doc }} and implement its various hooks. It's an implementation of the [visitor pattern](https://en.wikipedia.org/wiki/Visitor_pattern). In short, each node of the tree will be "visited" by calling a method, and those methods can gather information and perform modifications. In the visitor, each node class has a hook, for example: - {{ "GraphQL::Language::Nodes::Field" | api_doc }}s are routed to `#on_field` - {{ "GraphQL::Language::Nodes::Argument" | api_doc }}s are routed to `#on_argument` See the {{ "GraphQL::Language::Visitor" | api_doc }} API docs for a full list of methods. Each method is called with `(node, parent)`, where: - `node` is the AST node currently visited - `parent` is the AST node above this node in the tree The method has a few options for analyzing or modifying the AST: #### Continue/Halt To continue visiting, the hook should call `super`. This allows the visit to continue to `node`'s children in the tree, for example: ```ruby def on_field(_node, _parent) # Do nothing, this is the default behavior: super end ``` To _halt_ the visit, a method may skip the call to `super`. For example, if the visitor encountered an error, it might want to return early instead of continuing to visit. #### Modify a Node Visitor hooks are expected to return the `(node, parent)` they are called with. If they return a different node, then that node will replace the original `node`. When you call `super(node, parent)`, the `node` is returned. So, to modify a node and continue visiting: - Make a modified copy of `node` - Pass the modified copy to `super(new_node, parent)` For example, to rename an argument: ```ruby def on_argument(node, parent) # make a copy of `node` with a new name modified_node = node.merge(name: "renamed") # continue visiting with the modified node and parent super(modified_node, parent) end ``` #### Delete a Node To delete the currently-visited `node`, don't pass `node` to `super(...)`. Instead, pass a magic constant, `DELETE_NODE`, in place of `node`. For example, to delete a directive: ```ruby def on_directive(node, parent) # Don't pass `node` to `super`, # instead, pass `DELETE_NODE` super(DELETE_NODE, parent) end ``` #### Insert a Node Inserting nodes is similar to modifying nodes. To insert a new child into `node`, call one of its `.add_` helpers. This returns a copied node with a new child added. For example, to add a selection to a field's selection set: ```ruby def on_field(node, parent) node_with_selection = node.add_selection(name: "emailAddress") super(node_with_selection, parent) end ``` This will add `emailAddress` the fields selection on `node`. (These `.add_*` helpers are wrappers around {{ "GraphQL::Language::Nodes::AbstractNode#merge" | api_doc }}.) ## Print The easiest way to turn an AST back into a string of GraphQL is {{ "GraphQL::Language::Nodes::AbstractNode#to_query_string" | api_doc }}, for example: ```ruby parsed_doc.to_query_string # => '{ user(id: "1") { userName } }' ``` You can also create a subclass of {{ "GraphQL::Language::Printer" | api_doc }} to customize how nodes are printed. graphql-ruby-2.5.19/guides/limiters/000077500000000000000000000000001514115062600173365ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/limiters/active_operation_limiter_dashboard.png000066400000000000000000001524651514115062600271500ustar00rootroot00000000000000PNG  IHDR!1P@iCCPICC Profile(c``H,(aa``+) rwRR` `\\TQk .ȬǶoc=,)<zZ qbrAQ c\^Rb"E@G3@t{ a r)@ [' I< ؍MB}-M PZQ *23J!`d`d o7(Ɓ+c`!B,v9> +H,J;KqͽuÁ^d`{Áo^ FVeXIfMM*iD!ASCIIScreenshotߵiTXtXML:com.adobe.xmp 1057 Screenshot 663 g@IDATxx4 ^CcH&@DQ*. *+"@Ŏ&Ez )@}.dSvI<6ݹs׶]KB!&0lnMYY3% aSO}X "HHHHHHHHeA$@$@$@$@$@$@$@K>$@$@$@$@$@$@$@!@Kpf-$@$@$@$@$@$@$ P        ?Y yTBGHHHHHHH? B#R_/Y+rlZ]QRRH|+ )TP i/]'HHHHHH2GJc P%H4}q^LBK/zl٥K\m\W)V; |TBx$@$@$@$@$@$ITBd g?GK' gʦ?7; Rj(R]J=t\L.&\H3__-[@$@$@$@$@$@9cB{ǖ'Z\Z'-H4i(lK#mC7H_>|*=qg       y)I Jhrd&YtamHV$9 ɭ:U+K-egS/-G?|"ZIAED@W6HHHHH "ӼwEJip)V PD\)Ӹ\VSȶ _JblW+_?9it#?*"s+&    9*!|r@I("bp5CkJFue˷?j Jbc%vͤMke#     L"= k""cfϏ-~P+C@ɐh@L%^s'NUS&8C""n0[C$@$@$@$@$ P oqpZDq"B!weE?3<;vB:7VBR:xHHHHHH ]Dgp1qN!-"`7 n~-Aat` a⚔e7'ߒ蹋e#c$1<)` xOJY1e#`b=@qI)ڠ|[SQLұd~O] JxRmP?YE$/a5A!     H?SBT)5kԐIIm69u0We˖&MȬY33TKɒ%Ք%JCdm,xm4KɒRp>xN?Ŀ%n! )VTֿݸ١0 \(.]$K1ȢGvxf@qJ>BHHHHHHI `7Y .$[ٳζw }Sd^z2fiРA<Kƍ'11שee!ݙ0aԭSm1/ʚukd„s&TʂbED-ѵP }&n*Gmԫib.j'Nʩ[eYRG'jƜ[;`EDP+=؁3" j d ;q1| Ruظv)THgWYR K~>^TY2{""ȹs r~ƖkDBrßѣRpԋݘ)S3))V3wHHH B 11rukٲul޴Iu/I-Zj2Ҩa#y٧3{ΏlzB٧չsdҥrA]Ɩҫg/_|g0 +̋jITJ[떪=&~(sl߱=+{YX 6(D9y̻qtCYԊ* JHh !l+פ[9,!r"tҬispbblذ!,6i&JY/[5o<,pr唵SRG!#βqFٷow@6mdk{I)O<CjoӺ7oOI|v<ϮʎWӧe|^,矫c (Gaسgt M&Ekƥ~Xbl޼YNYW.DIFeܹ%~߮]V}{2Z$ @ {)N@kU䥗_?å~}cnݺ :x43fcP?j(\믿!U*W!C{R*Bҋ/I351bw}~i*ѱ%ke娗t5C ^50WED¥-D|Vh32{1%T@AUzF,U4lP6m*'OHZj% 4*e)í/utB^zE[vL4Q)'nWuz-fRV-sXaݶm[źTXQ]aٶm,XYwN%Dʹ")Ճ!`!TD8I|oJ ߪGӕ!H,{%isڗ2`IhԨƸ4 䪮ݤi͠!(SoҸeӧO)wxu6UHl29s.q[NAfVT.RYxV6! Rx7m큙8]:wŊi=G}vUfFjwZ3SwN wHll7o.SN'Eeu-{VH6m<w` rɟQfޔ4$@$@M ەj:zӖ"Ծkf.Y+&.º!IǂЊ o?B7 Tyoe $1r\BYhQc( L];{V2_ݻP{Abᒀ&AaU jMBiN:)˖/s*90"e ҺUZa<ǎVx=up^R<ΕҥHݺueߗkՖk8ǀ㮁Q5h-:!!v|BA;ɯr1ucǏݻ[(lWrYh7,NJ*)9,PfN` + ~OmuKwaW"#(^ѡ̙;eƶlٲX)fϙ̎:^qDDD`x.6mZ+uzg 7X{{{br=rwoO2gs.Jw\;{\}w]L3GYY`Kxf (I]TPI׍$|r%Vex-gʕJyO 2_͚5 p@?S+p[2LPܹS+|:p@]ի, ^s*# NoXmݺUCT|_ઁ ju_qRCK ߪUnS~3gX'(3fv{97nkӑ#Gtkf'|T @!]1#$@pzv^-%ٳG>Cyǵra萡ZdoͷT';g/Fw1`?y1wN(5~;pǟ~{-}zq:;v: D +*U/>RyS. =H`=c O>9ڛj6 [kRWvMSo @TD( 7)ELN8ڄ*QeƪWߑ蹋wzRJ%GYD>H~zC `lh)VD༺J+I v%Գޙ&D(_*3rH[m(0heܵS?wۿOPpQ9Dٳ[+I >FBU߿|JWm@,0=h5i#GA!dX׉A v6@0+ZP|y\h;.!/VDM U   h u`yskJ:VAZ<#{ۦ[.)+'!-;X$ ( &WZu}J7(?Ž j =@swo#l.ځr1 5ʺgb^E V7€]3F}P2`0 bTSօP s 6)) I7ۀA8 ~g׭[`QOx>q߸qc]-[d͚5p˖-rߦVOxgCAew E~fkTuzϟ vj /ڏH_~^N=j0,$@;Jzʺeɒ%{A~5 # :Οsۄscm=Y \nۢ]22bɒNw$ {*%3+%fWZ]f"(#ȳ% ~7[ha{[>Ynm3^Gϛ/k~Cignκ1jVyu,Ei&jT4.TX+yY=s& 5/ 'R ̷mۦP}kz_c =JɄ<7+(W0h"e1Eܡ*ԩ<B&/ (!0b ZV-Fr&hLԶC@Z'ϲUtxwb (J($@$@y@J l7IaA5Яߍ;TKF I6^|ךnҪu+f6m>a@o$Qu,!&漙mM-x*Y|*}ժZîAZ;k ֎>o-9C-$1NSKrB-EVbCcM>G`]]G27'}|`p-̍:+kz쇇9*.k_'-E^P삊Ȃ9Xt@NFs.f!,s$ 2f|1c'8H]Ihx32FwnD #K:9nߚA.qHmȅdE`"P]O^Ų`cd]-v] 3l0łɇJ V;_g%b'RS̅]Zw7PizI+܍>ViěvAe?ǀ ~Z&}^(б߮/\2L`>+AOZRz >^=/o^F<|Y}{}fmO3J@`I0\ByxSl/#ea .j/t"~QtI)Z}¸OUVD;wiKR P\.Z>5B9\@r Ȯ9+"`] U,22R7ݸ0`3^|6,:h^/ $p@: 6 rU`POau*a9͌ R%DXcPZHY79~1k8VT 5K~ZϙX#,`̔? \:Ku~Ѱa#1:jԬc! ((@|<UFp? VO H{X"Ђg+GFVJ;lEpacE (/p7i`tw FQ|Gt &?07m @utGtePղt4s *᥋jҩT+#1[vJJrL`Ռ$jFNQ>p) t@*p' >|_>>cЌk7?#Vv3.LL| >X ttψX JzPJe N.b9A\Xuj^~wgJ{ialP.Pj4o2[8%;+|ѹj`Ծ]+:TȊpǠP:` b{ }2C*XY5K,0r #*0`XqA9{StVobrlB #ȃ!r+uwo.@ͮe Vw wϗF<A3V7F:DEaE <`cՉ?(A)vW; "``@Ql`U~M3^+![?]uUh˜9sL-JƢclMs8s̞###;Y3RU޽v{*6eA)k ]!cp]BJ#Cx1i% ]m&u{qBFTg?/J!Q^kÈHw?Y n)w8QoeoH>#z,YUE˝3ˀ]4ۄa_3 ʬ4`nǍ{[/$:W1fs J*.uYs5Ң3 /lˏJjeow|ݷذ5e*^gdfI+JÑ;,#ԳcX1ޑs GIht7zD4/W|,V-&H?գm;!M䇃=;g][|v,-tb0@wҘf)o(bd.Bi~ӎzVX d;%ie73P@ѓsi )}]fg;X7 %˟LO&7?gLDj1*GuO&M(CDZ g޽1{6A̓ht`"7 d:O0f:|rH~Ӏ2j(i۶NW3QPHTV~6lpyg8_t(ae= LNhS~K'v >3"8xb|g1bV IG=&M=3ޢh}@cEBDڅBuV $$T]"rhG2/J5Ij{YQ[\2is>L==5l*fd#P*U,o2M:J9Έn~g)+xSWNIc^yAj33ˬYeq|]6-&L5RǕinK[93NFxgťWOH[KZa^Tjܷr90}{k<ӭ,0m玬$ SE\;,&ONbϣ7=^ MHC<b{&   j \y9ZNho 1I)˕r.nGB*k-5TI,yrFYz>P.@$`,.ta99M =l y%C셣=biWq:u(LwJ b*"N|UIPFV朧"bލCO׌sOȅT @n%_~s TrZ:3DEK7 j%eCx\"5H|G6l}v.E"\rzϾԲ yy^ 矀\Whzb /_VyH DR*Z!b7 |> W*'m?z]~S.K      wlYGrSYгrhR (*!$@$@$@$@$@$@$+P +n#/HHHHHHB      A$@$@$@$@$@$@OJGl! TBȋ       '@%D#HHHHHHr*!rmE @"[H$@$@$@$@$@$@6"HHHHHHH P -$      \AJ\qy$@$@$@$@$@$@$ ~B  ~ϟgJ\\`B$[!!!R`A)\`B$@$;P ;#HHN8!RD )S6M$Ν3gѣG%<<\HHH u)_HH|C&*TM,H M)[liHH P # F "&C/"6W8/i8x.1W]/&g - F TB3+! ifkiMg{I ضm.]1"rλ6nG6')\zޅ+6g9K- \/Y @N# A!NI!FT@䶻sϞ?p9[J$@$GX#""5* ;&HH sJǏ{|y߷_8w/\-. ýq'ޱcMrw:G6۷o tg\"[%'w2K @8%K{uʷ~dw.O>dr1nݦjJ/Ö"A6@_~YZn-5jmZ\q駟AQꫯ֟oVر̟?8裏ּy˕W^)~Kvڥ8? @v8%?#k֬ӧOL0 IQn$22RJ*#H ҥ|f x|AرcSO~r?7apwARhQ)\o*@:غuW9la206/H@tt/ Vi[  x?)k]QFdÆ J}^yHH,]x[3<#{Aɋ/ר7yar+ˀ2ayͩ\sue˖-9O$; $,2   %ӧO&MK/`ʔ)^] 8>;^eC"X]Oj,?5uTW+. l7,$0k ̄g z:u,;K5syCe KGAxVСu'9rݩ,;:0[Y&pN.Ʀ>OOZ @!٥DY]<'(G,DuR3 ܑ`3c(%O?1ڴi I"1|z޴iSV  X]vby̜9SsuE#Nk=0X;pq4hB سxn>[N2dIPDPZ>bH]VǓ@;ԩ׹q<^b6sO>׀g/)X@-[9 sœyu"z}8`}]7~[F9cǎ?X9R4kL+ܗݻw$=#C뮻ƽwBy?iMa7d]#SųeY2"#ռ۬tMBΝw{>;V27׃Av-z=]Z,>E\\}(i:XG"ӪnnWj^DMUD_yhE{>|z&9z5~ܮ:|q뇬k/#n9:\Jv󳟷֟۷ e,"`pm`~zٳ>!ߒ%KtիW x keƴiӤ|ZɁիWwi&>vJ.SvYx\RN#$ +1x``ѢE:+d_3PB&ۭ5IM`)ټy`0 ٻwo<<\w̑si~ΎxB5-xw!p0\>w.+m?~Zp?Rl9 A~ @%@-%liQ?rv~9ul|{[J:L,'E IXbRh6,5qa[/bErRi=9|i,6RE~+E>N2@P8eP^?XGs{|u맬k/c[5D콇埲~&b6D ̺옍2wJg@р5`SNzEA䭷ҧ1lܸL-4y[4Ys̞SBA ( 0/1{'&N}RfԒ9~Z߸&(u A;!iȑ#e.A`kF#<E0|3_FI =a0 pY1;|⫯Җ<; 0/qy`.t{֜`Ft`?\rf*MUf{  @14zh֖$9s4[_~1[((By@U\BU/^ꥠ;Όj'ry2sdK;ۆA ]L$T\9/ wIAظq-Y>iY@iLԶh *UrIc8g?.'H ` oپCe~%zJͯRlgBgkʏLJ٢Q{uz4R X~Rn|*#YqUFcV1Yj6YB<Ìܪ@̬:qmvF@ I(f`Up` ?grC̸%fk&fnoѩA 58F1ee](Kʔ)Jc%n:C/@pp'p,%1H{VIԂRw ib̽69A)=򲿫Z^#͞y\:\At$*׊ʱd=Jx$^ٲTunڴEbCpbf9}NhW*/zD Ulb t[wP =pƀҘ8x>iF@ Q<`u 1;ԤGr1V#qq=:9?J soKUa9h .yhPn;WPaS^cu 8 hBB3Q/ mJAPʮyYoRNm)׶Q ̅7wEv͋oɩjR8Bv|,0Yv> )ZPXW]nt1&aphR)X[[̚'_"kpɍC̅ΘF!ŋJ< e}zI6U{:^'pSKdX:mH +UB GV|7?_p~ C=Yžo{rf3cf\+Ǯ;ʾ?J1UWRoS>$@#R a]w](3{Vw ]S~XH4e9&Py_:A`¾JbO&q=)psP^}֡L~iEѵժJ:Jr#_۫~F4ME_^q[juz°_ߒwsbF*.Tt)yE+m@) /^)qֈ{$v>dh9 w]Ҙǽ]ny5* u[3>aeSI@(![3 ^믿?F|[nE#%,bįp7 3k; i3f7 xퟙ6魧SΔQ`Xdݦ,4 5 J(ImjBO|l']Nsf$[ RC5"PXZ&k/Rl }{IBy$n/98cZ\uqX\;'VKy}!PJoޡPBE=@lMU Qxqf1N-3g}❁4P>8A})hUr\V1C緦WPQuQ(-8q37M_03q{aB!8J VV Rn Ǭ9\*|R.u+ѓrfNٰMуhkY%u* pMh%ĩR(!-T Uof  -_Q Qq]~b9_ryrl +'3W~;IJwm,=Fۣ!Pr_^ j UVp)8V}sl$`f_B /ٲV`&njM37u Hs8PCalWXXa+p qrXSԘIo"Uisk귦> b~*0~q⡧Ji1i~@Ԉ4v /F\"y`a#b|vs{>sVBMA.߲g_SMJ Aõcp М6Tn+Y)x(7u)QaQKeJk!="P?,7437 HBGŔr"P<`H("`phժK͛kW "ߗ(0B (( ~a͓UF}˗/Վ1"Npp'pAAtܥ1QUŚ5k\fyԃs0Eڼ,S}ȝ`YWrwH|GKsCwtm[D>9,2+茞`šOJP0_PvX4&Hš?=fjw=CZ.3o۵['Yv(_>{ caK k+Ӿl1qxAxA!ab1xSFr]&7MlM=)yI\ [\7z"S]틥r 7N /V\̞/y^@Gir_Oٖ-Rr,f#Qx9UĀBuNol2\iX[Z~,b@OTq*CIO|?N`esrcr|O*"z\3VfUt$e\jK[Dt2#&@&@ٴZF߾}G6`LC)9ؚ>4S&{6V0ȇ)4-Z$oRF(t_]`BN̬}O&??GފqR(J&xf]sy$%:zGf/tm+ou)h#Prdj4Xj Ta(MƤnfaf"/P>` r[-La`}`Wk/!`m?4usV7[F($@%b I]I[V5O)ުQlNrZŐݱORyCɧ~/NlZ?k326h"me e=z+'"=Dw7J.rɡd"n؞ؾ%(gF1.kVW8FG&ꎁX 1NeW^zk_~A464nXQ7ozK.,'۷bЌM0HĖciGuoAq`G`e @(om}Wu>@f͚W0@Y0?u 3c9>`I,!Y"Ƶ_yҹsggl7c n:0Kvx@Cp_` sw V@aÆt6X{fX@6XFҗNY.]=}'\uΗ9Ž?`Or*ҔރC)zU#Ii ,RopYs2RrAvm.=,)/W./ڵ+қ5.W{e˄OuR =V<3Yo5\.<-+GDԫ)>2#C*![y@`G:5۵k'ӧO=('gd22YYaa{}Y]@ M0f0 eȐ!cYo/?x%Hy0 PL8ym:O&KjÒ"9ޤ`5ÁP؀QF/X \fX g8ạ4V (ı1mFY>:қgXKx073 hy;geɒ%駟j ",i~<;ga6MOy@he.KZjL `e倘,) 'eZSI*ezMS]N*Lbҧ=nAoG d3kf ٳڱd׏k,J(7˛9+K@  =P@eU=4(PL~+ ~Gr A{Pi)3cJ׏Ajy5~aفk5ޓ|{ִXz6Yq\XWō@^\H /,yiå6J͇27$?&ύ5ό5ZRF52Mz5Ə?,k_'~yZzy+Ȃ343]ƥD}L-;JB&cV1]|). Fm+:c*`%DDsiBEA-ıT}Pe/Jqe@.ܩ9!Nuqj &Rta #: W&hWf>z,0L*t >}S+ rxzz\)%N$@$@$ bXrEIHrR[KЂkv҅$vSهۑPv2ϒ% d@jx2_z֖`]:Ky秊IHH$f6oFS} )թ e.Jt# ;v:[l eHHOA sjB$@$@Dq:t@j-TB؀䆏XiB$@$@$@$@$@$@Fa{HHjtc@4 $~Μ9X4 @z ;&HH ";HH .,'Na ,H wB$@$s P s[N$@$C0CM$-|;ĘHy_*  PD IHH"" @"ȍB ٭rr<{x%_WzHHH -[Vel۶M"""$44eQ$01  |)$[ 4=Bt/%Ku0( KEEE]WeHHr*ٳg%..NO!-] %b@÷Y: ͺHHHHHHH `L<|y$@$@$@$@$@$@$OTB6"      bIHHHHHH\ P ʃHHHHHHH|DJe$@$@$@$@$@$@$@pO$@$@$@$@$@$@$@>"@%X       WTB'       G`Y, +\t钜H.%%^q:?%'H̡h\q'N|̇qp.3EؼYΝ;'Zk ?yǛiH`:ai>0Z'vM HZե}pf5Ǵ.~ԨQrK&MR$;vTZUn<O̙3˚5kdʔ)rJ.]&L;v %Jhv+Wv9S>d5ǜrl' "8<2ԼE*n17'-E+ xޛl^t$%z6HIL U7Z:2O<,{-eKGy]W %ji]4p'8{S]T F IΝiӦ{sꫯri_pl^A{ũeɊ+2>_~Y (XNӮ%    @"F^ wKqKJפHw外>s᥊K:5u2lkt 9}۬MzS\b2?f$r2{̛ V,]wǪܕXw'=mhhԨQy# .iӦi+ śI0oʁ)5E3]9^D3GI_ H*4/Ǿ+' r"J.4Ck6ȚIןY/)>Ro6ԦV< -XP$s , I+JR&+Zm;$q)TξlzeYqQxy4e8{`"R}+ٿz\nƨ7J& 31R\i}ĮO$^Y@S-A1bh- Jqsdo3%ɕW?jbb}(JV+׋'dR&9RƝ:?6rhu,+1r:]Z 'Ajv|$  oOkĩ߻7&*]ۃتػjTm\ZYJT,KvY,mտ*iDMjg ~1:.XwIe: ipMUe|# ޞ$g];JnЈ²/\YR|9iuٻxI2q؇!!J@D dI* uEvQt ( ₨b 0DɈ ̀$a@r {ovW:szSoJqW÷[v'Xb.W\1/I_ߧ>#&nvqb<\sMzӻb;Nzp%~\}6xlou+_.򖷤w=SNI6tIY$a~s- usjfm#of̘?x0.~'L)ε@>d\oF>0)kSLɆ,}f:/{w6]wմeI @@o^ ;RC5&O:̺7M9䀴kѯ)zR[h Oqўh#\sgmL7ZhBf .ُ͆gA}K/f@i7twT!ąz oEtWe3Do{6E0$.=4yl}\|GgYos=7,Hbv /]GĜp SwYvlM׾.l}-"xI,ӟtOa _M6$E &mʎ7\# [l~_fuoCLM;,o5xnB @@~1/+?*ͽ'myyO7 E= ^s_Zvj}H[zbl̼tg]S7ݖZi4g# o/džoCݑ Ѩ b]㪛l/;Z{VZnu8ip# ,R~nL7u^!@{ >:kf_K؉eEe-Pd1yo>nu1t#G6Aa)ʊ z/j}HCv?O ַ Z( nľw=\DЏX㎬^Dc`ϵYfiCKbJ\%zKl?9: #͖z3_׳^../ @O`+gDࠋE&.q RKd/_=1!'K?>cCv!byv:qա.Cvb-_uE?\ xNcy.ba׼qs4LXxE'1/uW/>lmnGڰX?#鲯nٿEj#l[k76- ψ"_ti'gAj=KQ /xnh9 d1Tme,L~p .cNK a~]<DOK}1gbHDPR=-q162@HK%hTvb8Jb.F6-.ψ"^zi:䓳 Grba!@ @`x*”ZtZ/"}ø k]//>\_/ղHm̥W;bHTKνh҂ 4D^hEk=},xӯcQ_}_f?cW[%m7Kb.zyd/76|䎋gkNI67D}ːk+6יZ0 >>Wne,#pރK `%\ZŪ=믟.l(~~Ae=2ZazOXhM^bXb8J/h?Oy2x]w5ſ3gfu_W>I'@ @ J (,挈>GC^qu k1$/z<[+"%W\>=~nqcs'{oM0xy=V`g<^v_(e.YoDbFeN2pIOz_,E{&gB-(NgOWoW_oS~(ړKbrgj=Mz][oM<"z"L7aH"Ĥ%z$~4cI-_~!C<>c=aX97&ӌc8 pIX$@ @wQ;#̾t5DaDg / "}*l@څ Q{b%/jEjiQ%Y{ڶ*1kO&m{8w]e<7wn7eZ=mZxJqT]LkVHу2aH}gvf~,rL/^Gvpqwo?u &=*4[Qo=gמ1kQk=-/i%9PnQ'}yţG_w}<&Jo7zEF}tqHW}GٿHIxӲFeO'TFW~`^q\Rп˞ kb9&COc у>a~ *;xl(F|c{EeAxnj~s']aZ^dG*1q~y}Cٿ'?E1!/G~X`2CZ_1kE=bԝ"O'~s1ٮWS}xg=iw,țHHwG\7;עB E:<Ǔ8"0hcĄ}/}i@_x1=ЃYY=b(+z4#D"oF{'8Ho @ao9X_9Q-#.]8O\mS2☰G6ʷ>(niW%VZ]C[6C6bZ7ڗhnwwEρɵ Z-/]6i hKtIGEJO @@C"t=F G͡ǰ.5@  @(DpBGw&1!zX @ h6tc˖? @x6ZJ5||W8 @. 1ߛ @ @jBTՒ @wA @TC@ @ @` B&p @ @hg$@ @]@b7 @ @F;% @  @ D5Y-  @ @|M @ @@5!jI @滀 |o@ @! QvVK @ 0!{8 @ P ըfZΜ9u[\s$!@ @m @ @TARF @+ QݶWs @ P D #@ @n۫9 @(U@Tn @ @ BT՜ @* Q* @ @@u!jN @J([a @ @m{5'@ @ Bʭ0 @ P]A궽 @ @R!JV @ Du^  @ @@r+ @TW@m @ @TARF @+ QݶWs @ P D #@ @n۫9 @(U@Tn @ @ BT՜ @* Q* @ @@u!jN @J([a @ @m{5'@ @ Bʭ0 @ P]A궽 @ @R!JV @ Du^  @ @@r+ @TW@m @ @TARF @+ QݶWs @ P D #@ @n۫9 @(U@Tn @ @ BT՜ @* Q* @ @@u!jN @J([a @ @m{5'@ @ Bʭ0 @ P]A궽 @ @R!JV @ Du^  @ @@r+ @TW@m @ @TARF @+ QݶWs @ P D #@ @n۫9 @(U@Tn @ @ BT՜ @* Q* @ @@u!jN @J([a @ @m{5'@ @ Bʭ0 @ P]A궽 @ @R!JV @ Du^  @ @@r+ @TW@m @ @TARF @+ QݶWs @ P D #@ @n۫9 @(U@Tn @ @ BT՜ @* Q* @ @@u!jN @J([a @ @m{5'@ @ Bʭ0 @ P]A궽 @ @R!JV @ DuT/@IDAT^  @ @@r+ @TW@m @ @TARF @+ QݶWs @ P D #@ @n۫9 @(U@Tn @ @ BT՜ @* Q* @ @@u!jN @J([a @ @m{5'@ @ Bʭ0 @ P]A궽 @ @R!JV @ Du^  @ @@r+ @TW@m @ @TARF @+ QݶWs @ P D #@ @n۫9 @(U@Tn @ @ BT՜ @* Q* @ @@u!jN @J([a @ @m{5'@ @ Bʭ0 @ P]A궽 @ @R!JV @ Du^  @ @@r+ @TW@m @ @TARF @+ QݶWs @ P D #@ @n۫9 @(U@Tn @ @ BT՜ @* Q* @ @@u!jN @J([a @ @m{5'@ @ Bʭ0 @ P]A궽 @ @R!JV @ Du^  @ @@r+ @TW@m @ @TARF @+ QݶWs @ P D #@ @n۫9 @(U@Tn @ @ BT՜ @* Q* @ @@u!jN @J([a @ @m{5'@ @ Bʭ0 @ P]A궽 @ @R!JV @ Du^  @ @@r+ @TW@m @ @TARF @+ QݶWs @ P D #@ @n۫9 @(U@Tn @ @ BT՜ @* Q* @ @@u!jN @J([a @ @m{5'@ @ Bʭ0 @ P]A궽 @ @R!JV @ Du^  @ @@r+ @TW@m @ @TARF @+ QݶWs @ P D #@ @n۫9 @(U@Tn @ @ BT՜ @* Q* @ @@u!jN @J([a @ @m{5'@ @ Bʭ0 @ P]A궽 @ @R!JV @ Du^  @ @@r+ @TW@m @ @TARF @+ QݶWs @ P D #@ @n۫9 @(U@Tn @ @ BT՜ @* Q* @ @@u!jN @J([a @ @m{5'@ @ Bʭ0 @ P]A궽 @ @R!JV @ Du^  @ @@r+ @TW@m @ @TARF @+ QݶWs @ P D #@ @n۫9 @(U@Tn @ @ BT՜ @* Q* @ @@u!jN @J([a @ @m{5'@ @ Bʭ0 @ P]A궽 @ @R!JV @ Du^  @ @@r+ @TW@m @ @TARF @+ QݶWs @ P D #@ @n۫9 @(U@Tn @ @ BT՜ @*0Xa\pAkӬY3<^y啮k8nܸ˦u]7M2%]eG @ 0!h>uY{{͋`<ٿ+"z> @ 0"!:lN:)]|yH|_N~{:#; @ @` )+=__WyM @F Df!eht8hu @ @`Blb~.1G @fA OBc“zC*C @(Q@"vӰ.?|?p @ @` B Sˎ?>-iPo_zٳl @eAaj{7~r_tEe]n2 @ @@ Qb+)gqF* @ Aa}I?x:sJ,UQ @ @`dBnmZB @ @#O@6^ƍ kE @ @(mv}챜W_}u ) @LAan@,9β'@ @/ 1mFٳg{,0;˞ @F 01L}a.I @ @`d Bqt @ @`BhxHXFq @ @O`d\]p'#\veGq8 @ Ѝ Dɓ'H5I]w/D  @ @`!rn9R B @F Di2,#%Y~>|ș @ DN+gIv Or%@ @% B䄎;C&{;ޡDr#@ @ \z "GydG)) @FauvT3glRG bJguVz{ȩ1D 0Dk'[  @ @` D14#]pkMfJqƥe]6c8)]Sڑ @F D Gy$]i!0vet!  @ @Hef9= @$hg @&,"AnnG @I@֊a" j @ @(: D@/+ @7Q@D#@ @/ 1 M.!1 $@ @/ 1LM,!1L%@ @/ 1M48!1ز&@ @/0a(?Dԗ$@ @U(gΜ4mٶVi"k4*m빺{C @ Pzʍ @h" j @ @XAb=F @4hc5 @ P Dr#@ @B4 @(V@XO @ @M!XM @+ Q @ @&M`&@ @(Sn @ @@A&0V @ @ B)7 @ @ D  @ @b! @ D@  @ @@zʍ @h" j @ @XAb=F @4hc5 @ P Dr#@ @B4 @(V@XO @ @M!XM @+ Q @ @&M`&@ @(Sn @ @@A&0V @ @ B)7 @ @ D  @ @b! @ D@  @ @@NnU9sf ԟ @ j @ @XAb=F @40 գ[~lW`1=! @=!FJK8 @ 0!x @)#% @cU @A @qA1G @F Hi A @Ƹ o`#@ @#E@b @ @c\@b7 @ @" 1RZq @ @1. 1X @ @H)-8 @ @ z @ @`Bp @ @` BV= @ 0R!FJK8 @ 0!x @)#% @cU @A @qA1G @F Hi A @Ƹ o`#@ @#E@b @ @c\@b7 @ @" 1RZq @ @1. 1X @ @H)-8 @ @ z @ @`Bp @ @` BV= @ 0R!FJK8 @ 0!x @)#% @xTdKT @=!FKK9N @ 0!Fy:| @ 0Z!FKK9N @ 0!Fy:| @ 0Z!FKK9N @ 0!Fy:| @ 0Z!FKK9N @ 0!Fy:| @ 0Z!FKK9N @ 0!Fy:| @ 0Z!FKK9N @ 0!Fy:| @ 0Z!FKK9N @ 0!Fy:| @ 0Zx̯W%@ @^XkJARj @ @`D1"A @ @/ 1X  @ @  @ @m @ @`DBfp @ @` B6VC @ 0"qG#+u]|4gΜ"q]X`a(ulfɲveYeij8O,(6'fq,Y'PlNeq²8ȉgq,Y'PlN_p@=0D߰M6!˶D%W,[teG\m-W,[teG\Mc O? 6 ]|iV,sBH2RIna~; ޲<tep vcUuO{@|uWs`Yg%=\:ꨣtG~iM{,Yg9,Γ%ɹY'K s8O,(6J!=t%s9' o;9"b}m.ݬeٍZ}x8y&d,;Xg9,Γ%)Ϲ{{>sbG)J /W^y%-馛nJ?ă>6xl+*eq ϲ8ȩ&l~ś3 ˜P9 #H9 3Y+Os"eg^RlJ!,)ַv-M:5-~v 6IYp3Z$gM,;kg6MRm:̳CYp$L'L뭷^c&V`G)_W}:`W*_:bG)_W}:`W}q퓌<@8qb}Fc-Xϲ8ȉgq,Y'PlNC=4vai;蘬_LW^ye^`9ϢW,{'/Bꪫ${Dz,{&=a߀YM'fڧ,;j1nt=wW椻+mv u]vlbL,; Xg9ug}C,1Yg9,Γ%ͩs,[te'ZVb8FD_~袋fc7ptygô+ D љe.V.y=~t=gBM,#'ydY@9=7}ooβQ,JKW z뭗}tg*1恈x G?JKi GXT,koYFNy=fsD1^Y j5ޏgcnֲF>,t6Y7ʛe^|*9sO|)r'[,ysSpn,k\YFNIdNewnbHu<'"ݿgٽ]=y6RnŲJxvo7xOEϲ#8ƛ+LK-TZxᅇTx-HӶn>Ϥ &d$"`Y܉8ȩnGZpgM,;kg6MRm:,[KɲB+tn~%X"mFiܸyCƏ_|k[.}LrHZ`JV| NYFNx.2~[l&Iγ PYv&)6@nָ,[te'Zλ➷2ƏLK;Sа[mUDZ8}s˟yR,Yg9,Γ%ͩչ{{g,;je+*q<nAva>:tI[nI+bz׻ޕw_A }rIkXv+xfW]uU:䓳VZ) a}-˺D?Yn?5z{Ͳ7{fwn˲sf{l&|}A=bHG\-sNZo6m,Yg9uyu]y&`,@k 8nb!X,[tO[lVhT9!+z@̜93܅^N;\ieqβ8)g qdO)Nn,kKYFNX`߳ޮў<tewnbHu<'"ݿgٽ]=y6RnROFEuӦMKwuWzZFyS+/]LH=*V:mcٙW< 2U, uμZfJm,!Θ_|1'[EL^:K/fj,; Xg9uye{c(75&,#'ydY@9uznܟesN/6!ұ2xY%_~4iҤ4y\sʹ +d1cFg}6Jc%\X6@aU'SLIx{CcwWŵ-,#'ydY@9urnFۛln *Ww_{*cx=k֬twf)ZjB -=^`Ǘ,{{' /pm]e],{7fٛ_Yu'fڧ,i BsJ1cC=$"01qַ5ޒ`_,{7"ݿgٽ=Y=ͲFoY7xoEϲ{{'"1+ @ @` c޲&@ @ BQxA @ pʛ @"f<~/T-Xh#ˎ&mI^e{)Xʗg>ZӱlV)?#iqo]wݕ7\$]ןetCvd9x]wyOswfYY,2rY'K GydY@JzBtk/8|ivJwqG9H^`Y' ׾y^oЮ,gvgǗ,{G@zx˲AtJ{׽.wyiܸqi[nɷTCX!zˮ{n{ۧ[oa:+ lo7˼R'^4`إ?|[/iʔ)iӼtcVn~,terإ,Qk.ݮ٭X5v ˁrXz ywiO~Ô -Pk_}ճG}&Z`a\Ş /p6os= .`kҞ{6hb @na[lϜ'˜P9́A`Iʲ PYv#`Oߏr5I² LYCg1RK- */==#}㤊(.N<@6(#A̞=;𠈺Z \|9V7ڞeq=@9Xʑkz;Oowڋ<^<SKes3Kig,;:,ԱS#Ϩ睷y;˘}ƌo~sWlp/_]{)[m&}ӟNK/tewg?y62Ct{ҟ_&M>vWXsWOg* y[>3i„ nggm7<=<*esBQz5ߟ} x$_k/~~׻ޕ"ӟeqm<;~,6,mټN?" v,xƤqׄgk8_dw"[c=6#, ̫YO>)˟t7|i4M'[oeu\<^vq7"H/'@70m~lsz뭗xߏkѲkd'ONm݀dΜ9)Pߘgc=m׿5Lm|UwdgLFy'|]JogY\zY9ԗxC5,Zy^wu1`ԩٜ0s5 ^LO>oxQGוXk}nq#2dn&^{k PXmߘ~.>+1O@^ϸ?ѰIܫ%p,N8! BDogi-2%AȀ1dL*jqqWoǤU*ocY\ǗoC2 wPohH \sGb(P $1\/N_aDѵxV04'4˜Pmyܵߥǧ hms㼵4`إӵM7ݔ=~;nDH$qPꫯN[ET*}^Ϗ|#3f 7L?2 }rIk>lODc8P,qg*w̙3ӚkYgV};&W/<ݺ6_Rbz/ſc MTܰe]%0.~{|9W@ܭam{!O!KT,;ds Őߠ>2>y=vu:jn/rZDǘ,iM7ݴi*oheCSRL$7h"07"AX="%+f~_gu ]ٍ;(nL'eY\hEY$#d]c}_+qlijs1>3#O3GIDATX|ŕ8vsbYlw}Y6w=PӅ-qƲp'/ +Wω4pXXziꓲϿgÆ},qAN:ix`gJnƬvlyoX[!:l{,U].{gQ.'%VRݺGwL s=|isxs&ģ.|Fv7$igdOt&33slf2f=|U;b{B|zwx$|LXki,3[\>zx\6αk8}s$uxB7ͬGYO?tZguo~&ԓ9H 5-a+{X4u݄z#/袬 /]xK̮dzgZ f"5kVf2ˤx׿>ӿ*Τ[wIDc~rtXwuqf~>Vq|Տ`CsnDI&eŹs3_,.#K|fj5η3{3Zm?7/>741T oi-1c~q3gfk[7bPp'g~qwcx̣fs]gA=cM|o!qWyWuF[nl.0bȵe@bΉ"H=x[|9>G!K{ǜDY,#p|cmt.[4.Cxt|_/ ]SNILNG?tUWgwKvt/aki.OnkRf,{G..\77ɖ@t#8E_܁':쮉s.g|fa33M [ꟙ333gs3 (lmhk33)l$z]0z򗿜ݜ )cuڲ_zvNƝ . {Cܽ ,> BMDPK<Ķ(ߐ'Z}2nxp.Z+>^}^|yj,qK}Cʂ2T b|q)ӫD5-oyK3f*w?gXa|Ɏ%.]X5 i:3~э|񨪕VZhMj3 Lcnӧ®.]b'͸[cNξF){i|f1|gfc\kiL:x駟Mk,S4&_q7X İN8!Y8UJa83rRgz'mT/q5Aǘ)͝w9{£aSR )k ob#+W@D;CO,5fI,-8S bzK=ʚx\dtE:Yoxe6 Ks:ꨦ_c2{ ̌7ljX_}ƅ̺Ho?"f*^x>77]uU81qGcǏogf >e֦Mbx,qW?.Ç\q1N{jyŗQ7VC& T1Kܔk4| {|ޚ%Do=SsϋkQDK/Y5ه?ϴK:3eʔC>zB΃M.T,,M޿*t͖. abf+~%.:O6&4)✍egf'}f:DEPg<^^}n^{Y^.Ygf|ɮfևU^h}xѹ1z9roPYgY 繋Jb(j|>YO{XL Ƅ;},k VE4w袥I7#@^܌sf]wy6(~33zgfЛL gaSw磗I a?>ĢkW|)oqݽYť0_Kk_l cx%@E938crqq*&F9Ks3;ns|q1R]̌μs?qv>a:frs=7n+c[|fv߇2e ?<ܤYg CDyܤYd}!Q;ż(w7aFc/BG^n7q&o1;?7>CM9“YAݙs;G<#|թ+Mz @ @Pzyp @ @՗ @ @ +`V'8 @h !Z}IK @Flu @ @՗ @ @ +`V'8 @h !Z}IK @Flu @ @՗ @ @ +`V'8 @h !Z}IK @Flu @ @՗ @ @ +`V'8 @h !Z}IK @Flu @ @՗ @ @ +`V'8 @h !Z}IK @Flu @ @՗ @ @ +`V'8 @h !Z}IK @Flu @ @՗ @ @ +`V'8 @h !Z}IK @Flu @ @՗ @ @ +`V'8 @h !Z}IK @Flu @ @՗ @ @ +`V'8 @h !Z}IK @Flu @ @՗ @ @ +`V'8 @h !Z}IK @Flu @ @՗ @ @ +`V'8 @h !Z}IK @Flu @ @՗ @ @ +`V'8 @h !Z}IK @Flu @ @ ;{L7IENDB`graphql-ruby-2.5.19/guides/limiters/active_operations.md000066400000000000000000000100371514115062600233770ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true enterprise: true section: GraphQL Enterprise - Rate Limiters title: Active Operation Limiter desc: Limit the number of concurrent GraphQL operations index: 2 --- `GraphQL::Enterprise::ActiveOperationLimiter` prevents clients from running too many GraphQL operations at the same time. It uses {% internal_link "Redis", "limiters/redis" %} to track currently-running operations. ## Why? Some clients may suddently swamp a server with tons of requests, occupying all available Ruby processes and therefore interrupting service for other clients. This limiter aims to prevent that at the GraphQL level by halting queries when a client already has lots of queries running. That way, server processes will remain available for other clients' requests. ## Setup To use this limiter, update the schema configuration and include `context[:limiter_key]` in your queries. #### Schema Setup To setup the schema, add `use GraphQL::Enterprise::ActiveOperationLimiter` with a default `limit:` value: ```ruby class MySchema < GraphQL::Schema # ... use GraphQL::Enterprise::ActiveOperationLimiter, redis: Redis.new(...), # Or: # connection_pool: ... # redis_cluster: ... limit: 5 end ``` `limit: false` may also be given, which defaults to _no limit_ for this limiter. It also accepts a `stale_request_seconds:` option. The limiter uses that value to clean up request data in case of a crash or other unexpected scenario. Before requests will actually be halted, {% internal_link "soft mode", "/limiters/deployment#soft-limits" %} must be disabled. #### Query Setup In order to limit clients, the limiter needs a client identifier for each GraphQL operation. By default, it checks `context[:limiter_key]` to find it: ```ruby context = { viewer: current_user, # for example: limiter_key: logged_in? ? "user:#{current_user.id}" : "anon-ip:#{request.remote_ip}", # ... } result = MySchema.execute(query_str, context: context) ``` Operations with the same `context[:limiter_key]` will rate limited in the same buckets. A limiter key is required; if a query is run without one, the limiter will raise an error. To provide a client identifier another way, see [Customization](#customization). ## Customization `GraphQL::Enterprise::ActiveOperationLimiter` provides several hooks for customizing its behavior. To use these, make a subclass of the limiter and override methods as described: ```ruby # app/graphql/limiters/active_operations.rb class Limiters::ActiveOperations < GraphQL::Enterprise::ActiveOperationsLimiter # override methods here end ``` The hooks are: - `def limiter_key(query)` should return a string which identifies the current client for `query`. - `def limit_for(key, query)` should return an integer or `nil`. If an integer is returned, that limit is applied for the current query. If `nil` is returned, no limit is applied to the current query. - `def soft_limit?(key, query)` can be implemented to customize the application of "soft mode". By default, it checks a setting in redis. - `def handle_redis_error(err)` is called when the limit rescues an error from Redis. By default, it's passed to `warn` and the query is _not_ halted. ## Instrumentation While the limiter is installed, it adds some information to the query context about its operation. It can be accessed at `context[:active_operation_limiter]`: ```ruby result = MySchema.execute(...) pp result.context[:active_operation_limiter] # {:key=>"user:123", :limit=>2, :soft=>false, :limited=>true} ``` It returns a Hash containing: - `key: [String]`, the limiter key used for this query - `limit: [Integer, nil]`, the limit applied to this query - `soft: [Boolean]`, `true` if the query was run in "soft mode" - `limited: [Boolean]`, `true` if the query exceeded the rate limit (but if `soft:` was also `true`, then the query was _not_ halted) You could use this to add detailed metrics to your application monitoring system, for example: ```ruby MyMetrics.increment("graphql.active_operation_limiter", tags: result.context[:active_operation_limiter]) ``` graphql-ruby-2.5.19/guides/limiters/deployment.md000066400000000000000000000072431514115062600220460ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true enterprise: true section: GraphQL Enterprise - Rate Limiters title: Deploying Rate Limiters desc: Tips for releasing limiters smoothly index: 4 --- Here are a few options for deploying GraphQL-Enterprise's rate limiters: - The [Dashboard](#dashboard) shows some basic metrics about the limiter. - [Soft limits](#soft-limits) start logging over-limit requests to the dashboard but don't actually halt traffic. - [Subscriptions](#subscriptions) need extra consideration ## Dashboard Once installed, your {% internal_link "GraphQL-Pro dashboard", "/pro/dashboard" %} will include a simple metrics view: {{ "/limiters/active_operation_limiter_dashboard.png" | link_to_img:"GraphQL Active Operation Limiter Dashboard" }} To disable dashboard charts, add `use(... dashboard_charts: false)` to your configuration. Also, the dashboard includes a link to enable or disable "soft mode": {{ "/limiters/soft_button.png" | link_to_img:"GraphQL Rate Limiter Soft Mode Button" }} When "soft mode" is enabled, limited requests are _not_ actually halted (although they are _counted_). When "soft mode" is disabled, any over-limit requests are halted. For more detailed metrics, see the "Instrumentation" section of the documentation for each limiter. ## Soft Limits By default, limiters don't actually halt queries; instead, they start out in "soft mode". In this mode: - limited/unlimited requests are counted in the [Dashboard](#dashboard) - but, no requests are actually halted This mode is for assessing the impact of the limiter before it's applied to production traffic. Additionally, if you release the limiter but find that it's affecting production traffic adversely, you can re-enable "soft mode" to stop blocking traffic. To disable "soft mode" and start limiting, use the [Dashboard](#dashboard) or re-implement some of the customization methods of the limiter. You can also disable "soft mode" in Ruby: ```ruby # Turn "soft mode" off for the ActiveOperationLimiter MySchema.enterprise_active_operation_limiter.set_soft_limit(false) # or, for RuntimeLimiter MySchema.enterprise_runtime_limiter.set_soft_limit(false) ``` ## Subscriptions If you're using {% internal_link "PusherSubscriptions", "/subscriptions/pusher_implementation" %} or {% internal_link "AblySubscriptions", "/subscriptions/ably_implementation" %}, then you'll need to accomodate subscriptions that were created _before_ you deployed the rate limiter. Those subscriptions are already stored in Redis and their contexts _don't_ include the required `limiter_key:` value. To address this, you can customize the limiter(s) you're using to provide a default value in this case. For example: ```ruby class CustomRuntimeLimiter < GraphQL::Enterprise::RuntimeLimiter def limiter_key(query) if query.subscription_update? && query.context[:limiter_key].nil? # This subscription was created before limiter_key was required, # so provide a value for it. # If `context` includes enough information to create a # "real" limiter key, you could also do that here. # In this case, we're providing a default flag: "legacy-subscription-update" else super end end def limit_for(key, query) if key == "legacy-subscription-update" nil # no limit in this case else super end end end ``` With methods like that, any subscriptions created _before_ `limiter_key:` was required will not be subject to rate limits. Adjust those methods as needed for your application. Finally, be sure to attach your custom limiter in your schema, for example: ```ruby # Use a custom subclass of GraphQL::Enterprise::RuntimeLimiter: use CustomRuntimeLimiter, ... ``` graphql-ruby-2.5.19/guides/limiters/overview.md000066400000000000000000000037001514115062600215260ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true enterprise: true section: GraphQL Enterprise - Rate Limiters title: Rate Limiters for GraphQL desc: Manage access to your GraphQL API index: 0 --- `GraphQL::Enterprise` includes rate limiters built especially for GraphQL. For REST APIs, rate limiters often count _requests_ and block clients when they exceed their limit over a certain period of time. However, this paradigm doesn't translate well to GraphQL because the cost of serving a request may vary dramatically depending on the GraphQL query contained in the request. Instead, `GraphQL::Enterprise` implements two other kinds of limiters: - An __active operation limiter__ which allows clients to run a certain number of operations _at a time_. For example, if the limit is five concurrent operations, and a client sends six requests simultaneously, then only five of those incoming operations will be executed; the sixth will be returned with an error and it may be retried when one of the five others finishes. - A __runtime limiter__ which limits the amount of processing time a client may consume during a given window. For example, a limit of 120 seconds per minute would allow two concurrent requests on average -- although in practice, it might be spiky: perhaps five concurrent, 20-second-long requests, followed by 40 seconds of no requests. There's some overlap in these limiters; both of them constrain the amount of _time_ a client may force the server to spend in handling requests. The active operation limiter puts an upper bound on how _many_ processes a client may occupy while the runtime limiter puts a bound on total processing time (regardless of the number of concurrent operations at a given moment). To get started, read on: - {% internal_link "Configure Redis", "limiters/redis" %} for the limiters' backend - {% internal_link "Active Operation Limiter", "limiters/active_operations" %} - {% internal_link "Runtime Limiter", "limiters/runtime" %} graphql-ruby-2.5.19/guides/limiters/redis.md000066400000000000000000000036131514115062600207710ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true enterprise: true section: GraphQL Enterprise - Rate Limiters title: Configuring Redis desc: Preparing the rate limiter backend index: 1 --- Rate limiting requires a persistent Redis instance, just like [Sidekiq](https://github.com/mperham/sidekiq/wiki/Using-Redis) or the {% internal_link "Operation Store", "/operation_store/redis_backend" %}. Set `maxmemory-policy noeviction` in `redis.conf` to ensure that Redis doesn't silently drop keys when it reaches its memory limit. ## Memory Usage Estimating memory usage depends on the string used to identify clients, since those are used in the Redis keys. Using 100-character client keys, the runtime limiter uses 400 bytes per client (two keys). Memory usage by the active operation limiter depends on the limit because each concurrent operation uses some memory; a higher limit permits more concurrent operations. With 10 active operations and a 100-character client key, the active operation limiter uses 350 bytes per client. Additionally, the limiters use up to 35kb for dashboards (2 limiters, for each one: 2x 60 per-minute keys, 24 hourly keys, and 30 daily keys @ 72 bytes per key). By those estimates, 1 gigabyte of memory would support both rate limiters for over 1.4 million active clients. ## Connection Pool `ActiveOperationsLimiter` and `RuntimeLimiter` support [ConnectionPool](https://github.com/mperham/connection_pool). To use it, pass `connection_pool:`: ```ruby use GraphQL::Enterprise::RuntimeLimiter, # or ActiveOperationLimiter connection_pool: ConnectionPool.new(...) { ... } # ... ``` ## Redis Cluster `ActiveOperationsLimiter` and `RuntimeLimiter` support [`redis-cluster`](https://github.com/redis/redis-rb/tree/master/cluster). To use it, pass `redis_cluster:`: ```ruby use GraphQL::Enterprise::RuntimeLimiter, # or ActiveOperationLimiter redis_cluster: Redis::Cluster.new(...) # ... ``` graphql-ruby-2.5.19/guides/limiters/runtime.md000066400000000000000000000124211514115062600213430ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true enterprise: true section: GraphQL Enterprise - Rate Limiters title: Runtime Limiter desc: Limit the total runtime of a client's GraphQL Operations index: 3 --- `GraphQL::Enterprise::RuntimeLimiter` applies an upper bound to processing time consumed by a single client. It uses {% internal_link "Redis", "limiters/redis" %} track time with a [token bucket](https://en.wikipedia.org/wiki/Token_bucket) algorithm. ## Why? This limiter prevents a single client from consuming too much processing time, regardless of whether it comes a burst of short-lived queries (which the {% internal_link "Active Operation Limiter", "/limiters/active_operations" %} can prevent) or a small number of long-running queries. Unlike request counters or complexity calculations, the runtime limiter pays no attention to the structure of the incoming request. Instead, it simply measures the time spent on the request _as a whole_ and halts queries when a client consumes more than the limit. ## Setup To use this limiter, update the schema configuration and include `context[:limiter_key]` in your queries. ### Schema Setup To setup the schema, add `use GraphQL::Enterprise::RuntimeLimiter` with a default `limit_ms:` value: ```ruby class MySchema < GraphQL::Schema # ... use GraphQL::Enterprise::RuntimeLimiter, redis: Redis.new(...), # Or: # connection_pool: ... # redis_cluster: ... limit_ms: 90 * 1000 # 90 seconds per minute end ``` `limit_ms: false` may also be given, which defaults to _no limit_ for this limiter. It also accepts a `window_ms:` option, which is the duration over which `limit_ms:` is added to a client's bucket. It defaults to `60_000` (one minute). Before requests will actually be halted, {% internal_link "soft mode", "/limiters/deployment#soft-limits" %} must be disabled. ### Query Setup In order to limit clients, the limiter needs a client identifier for each GraphQL operation. By default, it checks `context[:limiter_key]` to find it: ```ruby context = { viewer: current_user, # for example: limiter_key: logged_in? ? "user:#{current_user.id}" : "anon-ip:#{request.remote_ip}", # ... } result = MySchema.execute(query_str, context: context) ``` Operations with the same `context[:limiter_key]` will rate limited in the same buckets. A limiter key is required; if a query is run without one, the limiter will raise an error. To provide a client identifier another way, see [Customization](#customization). ## Customization `GraphQL::Enterprise::RuntimeLimiter` provides several hooks for customizing its behavior. To use these, make a subclass of the limiter and override methods as described: ```ruby # app/graphql/limiters/runtime.rb class Limiters::Runtime < GraphQL::Enterprise::RuntimeLimiter # override methods here end ``` The hooks are: - `def limiter_key(query)` should return a string which identifies the current client for `query`. - `def limit_for(key, query)` should return an integer or `nil`. If an integer is returned, that limit is applied for the current query. If `nil` is returned, no limit is applied to the current query. - `def soft_limit?(key, query)` can be implemented to customize the application of "soft mode". By default, it checks a setting in redis. - `def handle_redis_error(err)` is called when the limit rescues an error from Redis. By default, it's passed to `warn` and the query is _not_ halted. ## Instrumentation While the limiter is installed, it adds some information to the query context about its operation. It can be accessed at `context[:runtime_limiter]`: ```ruby result = MySchema.execute(...) pp result.context[:runtime_limiter] # {:key=>"custom-key-9", # :limit_ms=>800, # :remaining_ms=>0, # :soft=>true, # :limited=>true, # :window_ms=>60_000} ``` It returns a Hash containing: - `key: [String]`, the limiter key used for this query - `limit_ms: [Integer, nil]`, the limit applied to this query - `remaining_ms: [Integer, nil]`, the amount of time remaining in this client's bucket - `soft: [Boolean]`, `true` if the query was run in "soft mode" - `limited: [Boolean]`, `true` if the query exceeded the rate limit (but if `soft:` was also `true`, then the query was _not_ halted) - `window_ms: [Integer]` the configured `window_ms:` for the limiter You could use this to add detailed metrics to your application monitoring system, for example: ```ruby MyMetrics.increment("graphql.runtime_limiter", tags: result.context[:runtime_limiter]) ``` ## Some Caveats The limiter will not _interrupt_ a long-running field. Instead, it stops executing new fields after a client exceeds its allowed processing time. This is because interrupting arbitrary code may have unintended consequences for I/O operations, see ["Timeout: Ruby's most dangerous API"](https://www.mikeperham.com/2015/05/08/timeout-rubys-most-dangerous-api/). Also, the limiter only checks remaining time at the _start_ of a query and it only decreases the remaining time at the _end_ of a query. This means that simulaneous queries may consume the remainder at the same time. Use the {% internal_link "Active Operation Limiter", "/limiters/active_operations" %} to limit behavior in this regard. This implementation is basically a trade-off: more granular updates would require more communication with Redis which would add overhead to each request. graphql-ruby-2.5.19/guides/limiters/runtime_limiter_dashboard.png000066400000000000000000001476121514115062600252760ustar00rootroot00000000000000PNG  IHDRGMm@iCCPICC Profile(c``H,(aa``+) rwRR` `\\TQk .ȬǶoc=,)<zZ qbrAQ c\^Rb"E@G3@t{ a r)@ [' I< ؍MB}-M PZQ *23J!`d`d o7(Ɓ+c`!B,v9> +H,J;KqͽuÁ^d`{Áo^ FVeXIfMM*iDGASCIIScreenshot@iTXtXML:com.adobe.xmp 1095 Screenshot 655 z@IDATx|E$$!z/ޛH/**͆]+(bAHWtC$g.smK%<<;~mڶN k"}E6-`#HH qfvI@u$A$@$}Ml xMרHHHHHHH Pū6 xMרHHHHHHH Pū6 xMרHHHHHHH FM$*Տ,VE<T )(sUtMj GEb@P4;R".btǂ$07^UuxHHHHH#y"YO`BLlz{ݸ5Ho߫wŤGd#O;f@PIhr4逸J1 ű/JS1b9R*DQ^̀IRuO"hUlڼc3^-%*]neVb,Y4h@4i"'OHq?\6b:æM+fMrVWwD+W"U#*NMA= TS+;vn#U"S[)cٲ.~Oy}r&fuwP!GhK[J:q ;&? ti # yJZ~ 61 JرC #'23S%  $'őz֟fBg(ʣaҾ]8ұCG)-Z;xi)su2FխCHh٤FyǑ^[3ghbŊ>~Zʙ :E-[H~1=x8zqXaYF>`Mn]8bmyqRfM[aݦMźPA1Rm* .to0-HMF#(q $ o/n*GՕ ˞|QZ=9G~:ԃ~P}9q8_v1ucǏݻ[u4JV+!N%!@,wjO"BlN"}c SL\ Q+WT R ըQCSl"# ፇ;wG.~M馛tnW*kyUW9ڛb6zЄV)ڸx)(@$)ݸS8Q=GsdpIXY;rd6cvU)R#M.NNvzEJBA>bc[6ˑ#GhxQٺ*JmSL4@bսΙ&Hj)WO90U"gT@~vک;t('{vvuE\K58={vkXD*.bU .;&5**RwVFjh'(uөC;{NGrL?w\ù0x];-X :W1 th Bݺk&:~_qZ-Td5=W]m(Iya !VuVUV!Bxk{a)Th ?w6vbx%ZwPސ/E!!66f۶휟g ~ biTUޘyAܹC`+}ʣ|7@@'6QÖ꠳_~vЂSfmΟփ=maB#ݡpA=hb%tdcT=" [GG>6:FC{RZ $0tË΢SL}A3O^LBiN(!Mk`WԻ^ vC1YBOb{&\T2"CêlF^|L<*jv. /x@a):YAM׭۶5eK T7+/FWZw8D֠& oږKzD/x1 e f(뜧T0f{MҫWo< ԫ{ӎ>3A;GXVP3d--Q5YФ+ d@G(n`n=Bk] ~Uq3g5jƤA#oVt{GyHFp>s>3:dlvIs1ѱrIHtwÖ4nq YOO5nv^kL} # s~ڶ+E+ MX=!z%?z515^/xR. @)TSs(R$DMKܧ=. ˖[ Dp.l"NF'q`ڨ|K,=p/E8ERR>!~mis"+->@}'I`M4>b5Ly0 ujOu)!JœPIrBM-ʯs +`jup7 19g7xHe{jf" q[ g@\={f !;F$@$@YI e'+sϦ.Qw3^ư đ nzm|:v.}&u7;ߜqt<tЬv^=DMjVly}Yׇ̱%u|1rs 7r_{n]_K3lR+E Q1Hy:M8XZVZu6>/"]+#}D[/{+=ߞ60_|9pmgyُE"/#ya k/ew3j c 5ǺcQvĩZ!Yz̼?b )x-:7 P45jTRvpp-4[|  +ٽpŠ{Aq_w qc/BɷZ>ƣOhQi/(;-^˞'>FN-[ӿ?3zu%Kh9mVw1]MU=ؿ0~v |ٿ_1؂i,bJ**x&3Bu#~`#gUo{;pOTܫXѮ]ɹM$@$@"~QlΝ;tj '^xs'Mt^VNWw9~R$cgr1oYX62/ΝMܻöT`hoF . KԢ VZDY/GHg{A:;3}bL^0a":vXi ի[OWVZutbiDX: ;O' 8lbF["`jjM 5\oĀ^`2=5_z3fHkX50{>""j1шi X! {ƍu]A)[35~g0uØ2+gkcavX,ڍ6Օ3o< /^6mڤaF9c- ݿoJgߓXyD%K-|rVBʂ gߛ4  JyrZtܵ}ӫw2I ;|# ӧ;I^TZj` apw׮];4V<]QC6W|共//V{tʛr1W_}ǺM^IIxxz'_s~Ctlx`z૧JS=TR5-@P .&I@HTQO --[vJhDE,?+X&QbWDxL 'Cò`5L A1`FgIa^>`&Y1ۘ>cb`:ޭvb#bq@ʕ:|+Ք<|#V1 MPCyZi6-[5knxO7Ҿ TsЏG] VX?+V8V@;p!C{=>!`c+1%l '\B ?tX -ZtpСk׮>fX\`gb rSSCZ{@.j ">i5/=R?1qCgܕt2aކ+sςYV['g κغtWmo3ATO */?݇rw{G]?ڵGqu;wg%aV>jfkHhvDDV4jY޽RT&X6p](>LAP1!< 4|%  kӶu]f3G0<( :~L@0~;.ŏJ()Fڶ iltԴz8 Qoq~N^|D8XVAT_'/h?pry #SNuejx528KN[;wfSuܸ%BsuS/,lV1pT\٥,k~{bxH‹4vLE)4ǿ;^M]^Rw'QMZ}RCi0>}Ou ^͹ #HA}̕m%,ٷ`DJ7'xWҪ6c'zr|M]M;Sih]<1,ϸ|2=]VL5NzWsZ(8ʌK)I #FH^RqSFO93~S.Kz$.ߑj/OyLO>3-?p-[6Z x8s9~/g=X6 @.h'|,m,<!4ЁwUq  t"O~HF?9Z7nD: c>}6짩QtHsE!00X &=֞VA:|P F6mڪͪpU=rGпC;SP]cyRO**c؉SfۇYKc;=3|yyo408#u=M^YƛJ;;͛58fIܕ{]>ؗYQY8>K,+r oȳ#ˁQ8pjO6F%q&:F!p s00#_VU:MϑեsɲG?~* gⵗzgSqqRe޺Kv}3S.Ib,T!W~(  sHHjH d@=U̜j:ƌeO+#rt39ӗ#e4= LqNv"~GRG̦̞>\S_yB.}Y5M&TԼyT # !ŋIվWЁYʺ&٨K&,1u0cQ7C@b 9'5A^m Mbx: @^$3H^:\y)`fo'!eK{}\7ASa۶**=fYV?0,ڒe~9dv8CEgQ(ʑ #X=f0@)YLS[G=uܾST U#ꢼHbeʧ_ʻĘKXj ^cMr|3z*IHHHHH2NH}?Z%Pj 9w舴y _KZ?dRI9/j&Yp=%HmJNٗک'@z l%s [\KÊ *bHr|. \>0r緒qőv$@$ `U<V|>|HHH 8W//_ݶe킸vCII1lmVUi|jDK.KWm! G O#wEԩm۶2p@yrZMZM,ZX^~e=jp´89r$c[ … KA7;>'hmn HcgnFuyH $>#I?j$  pO |$%\t0^SnK}:l.%.긜| ቹO# 08v옜:uJC?z &ܹs=^~or|2bYb 0@ׯ'M6u7,j̔O>-g65f4x5>iժ*TZU+ ,I-[ ƍ+_kr:p {nwɳdi53/+چo9W_" bpW_}U3{!CȜ9s䮻1H L^ǿ+?oSm-ޘj{YnYp.˞={<;w.C5KHHm۶ ^wjph145CoD%۷/ULgux<#A+`y^6 HV5*KSv=t!A^;#_ȼ^7?]-$g|OKβX=N$Vo\bwXvWyL{HRxZ+<=Ƶ))CE…APc֭[LwDzشi>˕+'Pbڵ2|ޣsT\IPVvC̙3u֭[벪U,͔24h " s='/.ہo%22Rƍ%;CpPBt{-~X\NʂnsL)_tA3d׫W٢4?< @23c!U*pmY9HÑJ^W}A%ITbkj™Ԓz,IIT~,)9괻C} ta&KoE! Ds.wfHH;Ɗ.6E[Ў ʩUV,p 0)IR$N;(u(Tlǥyp& _|+EHzJ&uYБ- oZ߫f@8e/'YvrQMy

՞Ƕ&f՛Kpb?뉀ϊ#XhqLs9~g2zhyo>QF2iҤt߿yOa^$e˖F=K {b^%Kӧ<|s[o[fgEuYf@=zЬaK/ D @'W @pV4@T(y07\ A.}dAÍ;K[~s,N%ݺ<XIm}wgf B{zd><H (MЧk=ΐJo aʺW^UKq!W`[Iپ3jˢ<'<:̈o/_lF|Zʕ+7`x a3cԩ2o޼*F}Y0L۷o/;w^*[nMVZG >|X^uakȗ_~/$rNYV-#u L 0<r?C`9qlͥoJP wɎʅSgsd'Զ/H|+R|7V .[^|OvLNNEѥZCo#q潸m] K։Ic^"k:krBM5 U۶OA6FK/^RjGo8E gfoΫ5?+n?WUsi\=s]GLKq89d8jV^~껱T%Jmw'_Iνk=)ImiHje{آ7OHɎ-t$PySޮ;>FojkOZ*}Bŝ+2o޼~Zf RK>"OnNmHcy>Խ{w}zޑmbFy]h$@I`SiyrlzYNgc!7osn/*N|]}= Kưϼ%&|[OxU7'_}Wvp\QJU[?Btlt v=)s[Iv P5t!C.{0^@|P^?؎2HZSA2:ŚOjQLAی ĩ@01΂CleY넶6QȚ.HB7Q'^1.u `:1 S({3x+\xX5iJ@IDAT$?ܰYOXRH T1dqG P3Y3-9sT]Bd7?A\ YJvW(Te^8:[ 0PH ԒΗ?]#8ɊgC$j"IL&T,\:5M<>ȼ>%ZFE\KʶnѱīCc$H-u_dq E UJ8p Y#7quwˎWBT(^,i|MdhŊŸ/S1b4x<}9C*'mo|5I{V{}`Oգ_|%uu&ym& -5EY9d9۬h[R9zWj+DJth)]KW ҳ;io#Ͳ&qtOO_2;/StE7 \Ό|M>Zy}ÓGj,[f@ˆ1ta}Wx 8l*U|YnydO$&Pa M jz0OeՃO[oڶƯB+ۓ;K5o"6oXv~XJL)ڢ9_Kw<GpsIf@PY뢚Jô aSI`='jJӁKB01;L%Kb:^ dnn^+ ^,zn !`+W1G7Mj&,KZfL:ĉeɟ$@4 @B9I +-g-S!!l-kȀsKLz &`@6 ~3R35%bt'leG+O/!1vLڬ~ vGYi]ǘ=tQa\ŐҥԣG^+ݛjF_y^$ ;{Y֞Kaߟm̡i+z6kPz 1{ڼm1Ϗ``50O}iРԩ['\D}t3Mύ>2ծOt,~1]ؚ!J(84{K䲝&P#lBIcz4x"Hw7@`bDa?s,{kG ڃb*Pe5*k\oLRg>HH;I̡ÂXOSf'6Yi%|rno1Ĵkr.U!T(ob-`%Z,>ҙ8MX=w).Ǔq0}qRzb6`:V?-ް˩a#cA/9sj̅5j̘1C/e{:>Z.=;ӦMs<2)>Gڵk'{Gf̖΋es~a-_j׮}7sСN.i w_,`OJ:1dVKݾWLT_^"nY6O""uF 'OjmO8O{ckֹe[fGP[{nYlc6tgq DH~ۇ_;_9!O#p_Vgxw}l2璾ZҢȑ#O?u^dž ʔ)S䡇rF7M mE=@?a_ vmtgjㆇ6d8pv"k[#?/7xIꫩ_1چbIoMn9޶aT n&Lcn3>8K/>i/$@C0UA'~,E`jYu]ms7:Í2`LU脛!𬀇Ji` 1JAaDk}2e!z>v~ko>P՜7ϼq skL>xE9ֺWTQsJ4` \gL۲&{ ug%p#b^[^oһջëWO="^~Gr\g_NM9,pކ0iGRb'YW|!v,# o]1ådGG. [񎬾)90g^Ź5n ^vA< 1*P+`ŘBJPazR:@>.+Ś5ud6qݲh)Jؑ%kcG{Ͼ|%A%IʎUq S4mLѣŪ)W.(SjʌΚC Wu$1Kr_0H Vp/J {(η=ĸx9A<a-ClΌCx<KZacx9VKvYZ̳ܴ=e3N+- d=?Q>TrLJ(ǖ"ju\*T5鵛 X K2\EOnRx&=5왠M[w'Fjy^ִE \L_ Jn2 ެ!ˋOM8sV8,!*8~4MzJHrş_y:UG^I5M^:]Zκb*g&O$G}i_#y$@$@$vxS<  Q%; ɡKm )Ru Q Wu   &PF<1"\K#  H4l/=DRSk5}88}(l E UNNmi,HH @ &4c+S%HHHHHHHHPk ?A#]4K.箚6$@$@$#qb*  \@PBr̙\PVHJK|>i$@$@$W PɫW& |H 44TN8[&@&%>4  ȫ(+z @>$}l=L>\rZM> w(xljHHr ŋ)joFyF$py .o%Xz!{ /oƗ B$@$@2em۶NYXX{L$@YGWcSi1#H?z6@.%\ʰ>O9_2Ç'R  C >>^bbb$66VF$0}WcSi7K  Gr3K!      ȥs$^VHHHHHHH gP,HHHHHHH 8K/ E$@$@$@$@$@$@$3( gB$@$@$@$@$@$@$K Pɥ"      Gr3K!      ȥ( j #9Ù RGraHHHHHHHrőRHHHHHHHr)#°Z$@$@$@$@$@$@$@9CHpf)$@$@$@$@$@$@$@ő\zaX-       !@q$g8       \JH.0 @(3d])vʺ̘ @TV-CƓ8 6̍,Y'      6lSl @z PI/1'      )G|r1$@$@$@$@$@$@$@%@q$ĘHHHHHHHPƐ őcz       "@qħ.'C$@$@$@$@$@$@$^GKIHHHHHH|l @z PKJJIRbeo&;/ч{vf6cIdgJ9 !..NΝ;Žv!Ivy9r^;y25Ώ#?HHHHy'hfojYTi2mov.+9v)(Qޗ>yǛLK@2z۴3-ɝp@)]N *bv׬VG%ƍHꫯJ*UoNqӎO?TΜ9#?YFN*\st)ieƍ+He2i$y<כ+00Pz!'N(;vp9 ŋ*Ur,ld5Ǽn֓HHHH+|%*q$㵮جY*j17\G˗_L4$]( ~.Hff:#e+Ȗ Q^f5Ǵ 111ywQ]76h(W^y4iczo[ʖ-p7y|ŪnI &+V"//P\^ @~#z-xѠkݖi~~~)^W:Ev'i )YLJ֮zϙ;ݞMޤqm7`j_*r[z3#'i_۷Ǣn_r1w|{`^s7G$@$@$@$@$;P\سsR,\V}9)gɹ_ @7'm.w@ +UR:z@BչZAL)W›r!a#k;oJXG> I!JنuT>\Ο=;p*UD5:":m;RT }>k+OzN]uY;yZ+o>7‹Hv-e1:/J\fCcS#$l)ĮOƀjv^1^rZ1W@4qoseϳ%!T0}G3wBB`DFF:y 0@S)t7Mpd=vB0퉺Ï8(tF2E[Jͤ7H*dȎh_]MJ<:ѪVC,w4: u9@9zTju(YBe%.:F#k׮Mv ٳe:A`aܹ!Mtt{ cMUA.;Y76yrTk:tS .>^ H1G ֈ);v뮻NgצM7n̙3GQ!1cҽ@XQFukժ/B(5o\~vO1Ud:WjtǞy4     E#i\5ˆ2u#:F豿Rf{7^ (Q#8RjJ8T) >  TmUS/%*o"eHyǕG F]siZTV "xd6=#؇:VhToev\^VPM:[TuuW`ɚUu;4N 8ʪAikѢKF@S,덁.͚9+ }i.ĦWqA"б7^+:T?~\z&L -]<2Sx`۶m2^۳g3B1LT"xz0e\ b! VdB;CB#    \v[R0"viIP鿽 ,Au+ Tq<ʼnUZ4:W9:.mjL;nSWQKo D );J.N݅s*vQUӇ`W_P6>OT7^[ 6<,eBY·Z|)i/>`U^l57$34SV}DW49mϘ'5Fnm0(C1u]7k2<m|1Jry5ZDˑ[oH~1ͣ2ŹvWfɬY~"@c1dmtg#9_df; @ +wʲOŻt[+?1ݬuC ܺt2)C~.:[iޥϲx͟3]$f篷_w.OiՁ.:?g1 KY+PtÙd`md?_Φm<{?e8%/媁irF FT 7?0;J~}w/̺q?hZeixiglV%]]bܑ[@ (G7?c Eeyƃ}bA9O:lƘ sFXN9e N}-4bZCDˑ 'T1i/ZC F:D#_K2Ek#!1L b|;%'K4_ 64Y;T?;p9㲴wH#8yq**r/ "enU~ײ;P,~ @ 01[MtYӹy1 d֝$D!i]y`̑d/5`',7&m,q gmn61w9_wL}3M[YC5R8[Mkfwd ʰ΂3}bYk|= 4o_} <设ٔي^80 q^ry/>-SS"_jϏ +(NS~N#ӟY֊$^r_t鷾t-hC/Gl٬/1cLcj8`txAO?]C3V YD,1}J> -.]U4\zYc 7 Y|4>G7瞑UBLG=^?k?>SJLA38#moEp?a/1NwGo~Y7XAX5+n,1q+9b,T4" G> @ VS8Dk n,y]p:o'@ @dVlm@6& 0R @ # 8c5 @ #Q>% @ # 8c5 @ #Q>% @ # 8c5 @ #Q>% @ # 8c5 @ #Q>% @ # 8c5 @ #Q>% @ # 8c5 @ #Q>% @ # 8c5 @ #Q>% @ # 8c5 @ cs5lA%%@ @*H]kF @ @qf @ @uk( @ 0.#, @ @#u"@ @E@pd\eB @U@p5\ @ @ L @ @ Եf @qa  @ PW֌r @ @" 82.2!@ @* 8RךQ. @ @`\GƅY& @ @@]GZ3E @ȸ0˄ @H]kF @ @qf @ @uk( @ 0.#, @ @#u"@ @E@pd\eB @U@p5\ @ @ L @ @ Եf @qa  @ PW֌r @ @" 82.2!@ @* 8RךQ. @ @`\GƅY& @ @@]GZ3E @ȸ0˄ @H]kF @ @qf @ @uk( @ 0.#, @ @#u"@ @E@pd\eB @U@p5\ @ @ L @ @ Եf @qa  @ PW֌r @ @"0m\ra&}⋣*¨3 @ P-GQJA @T$ 8Rl  @ @zԣ @H@p"x @ @G=( @ PHE%@ @! 8RzP  @ @"eK @C@p @ @@E#˖ @H=A) @ @G*- @ PzԃR @ @ T/[ @ @# @ @*^ @ @@=GQJA @T$ 8Rl  @ @zԣ @H@p"x @ @G=( @ PHE%@ @! 8RzP  @ @"eK @C@p @ @@E#˖ @H=A) @ @G*- @ PzԃR @ @ T/[ @ @# @ @*^ @ @@=GQJA @T$ 8Rl  @ @zԣ @H@p"x @ @G=( @ PHE%@ @! 8RzP  @ @"eK @C@p @ @@E#˖ @H=A) @ @G*- @ PzԃR @ @ T/[ @ @# @ @*^ @ @@=GQJA @T$ 8Rl  @ @zԣ @H@p"x @ @G=( @ PHE%@ @! 8RzP  @ @"eK @C@p @ @@E#˖ @H=A) @ @G*- @ PzԃR @ @ T/[ @ @# @ @*^ @ @@=GQJA @T$ 8Rl  @ @zԣ @H@p"x @ @G=( @ PHE%@ @! 8RzP  @ @"eK @C@p @ @@E#˖ @H=A) @ @G*- @ PzԃR @ @ T/[ @ @# @ @*^ @ @@=GQJA @T$ 8Rl  @ @zԣ @H@p"x @ @G=( @ PHE%@ @! 8RzP  @ @"eK @C@p @ @@E#˖ @H=A) @ @G*- @ PzԃR @ @ T/[ @ @# @ @*^ @ @@=GQJA @T$ 8Rl  @ @zԣ @H@p"x @ @G=( @ PHE%@ @! 8RzP  @ @"eK @C@p @ @@E#˖ @H=A) @ @G*- @ PzԃR @ @ T/[ @ @# @ @*^ @ @@=GQJA @T$ 8Rl  @ @zԣ @H@p"x @ @G=( @[ @IDAT PHE%@ @! 8RzP  @ @"eK @C@p @ @@E#˖ @H=A) @ @G*- @ PzԃR @ @ T/[ @ @# @ @*^ @ @@=GQJA @T$ 8Rl  @ @zԣ @H@p"x @ @G=( @ PHE%@ @! 8RzP  @ @"eK @C@p @ @@E#˖ @H=A) @ @G*- @ PzԃR @ @ T/[ @ @# @ @*^ @ @@=GQJA @T$ 8Rl  @ @zԣ @H@p"x @ @G=( @ PHE%@ @! 8RzP  @ @"eK @C@p @ @@E#˖ @H=A) @ @G*- @ PzԃR @ @ T/[ @ @# @ @*^ @ @@=GQJA @T$ 8Rl  @ @zԣ @H@p"x @ @G=( @ PHE%@ @! 8RzP  @ @"eK @C@p @ @@E#˖ @H=A) @ @G*- @ PzԃR @ @ T/[ @ @# @ @*^ @ @@=GQJA @T$ 8Rl  @ @zԣ @H@p"x @ @G=( @ PHE%@ @! 8RzP  @ @"eK @C@p @ @@E#˖ @H=A) @ @G*- @ PzԃR @ @ T/[ @ @# @ @*^ @ @@=GQJA @T$ 8Rl  @ @zԣ @H@p"x @ @G=( @ PHE%@ @! 8RzP  @ @"eK @C@p @ @@E#˖ @H=A) @ @G*- @ PzԃR @ @ T/[ @ @# @ @*^ @ @@=GQJA @T$ 8Rl  @ @zԣ @H@p"x @ @G=( @ PHE%@ @! 8RzP  @ @"eK @C`Z=])ΝR @ @rɁ @g @Ɂ @g @Ɂ @g @Ɂ @g @Ɂ @g @Ɂ @g @Ɂ @g @Ɂ @g @Ɂ @g @+>/ݴW:}i9);2eJZmofΜv};IA @* 8R3"sn)ᇲ=VZzI=E`eٿ/8x$' @.[M3n&uӵ$%'?ɞ @ нHa_:*w駟.@R. @ @\\+xFʼn~[?  @ @1icT$ @ @`lGr|Hc՜$c: zd̙e@ @}. 8stuX.:C @ @ɩ;ݓe|WtS]{ʍ @H}?e|W}㛡 @ @@ T$g^xf(7 @ g#}V>. @ 0T`зޕ!^{iƌiOw8c @ @%  /tAGK7|Sz ^fkc= @n5lOo-;Hw{gg^ @ @u)&6l4ut' fG~/˴2ˤӧC @ @eVSǸ+.!G]}SI,X0d7 @ @@Z\#Km~sr8 @ @ 2s3c?t礲 @R@pd#0|6-oQ:  @Q@pGvNY`Ox`[% @P@pd|sO9蠃ҍ7XrG @)`5r,0lMifƚ @ @&#%V:묓"0;k>ޚ?U^ @ @ Xgyf @ @`sdԕ @ @c 8:%-e|WOW\n @ osj|e2W[mPn @ @Gr*|ͧe|Wo㛡 @ @@ T2gΜ9ʍ @HNoJ,u|VO>=㓙\ @ @@ ֯KЦt6 @ @eQ#[{6)nk^FƎב  @ @ E aֺ@ @ @ L+h` yJ9ݔCc)cJc1#v` @ 0B@pdI&]ywǦx;X;eʔj7f)&  @(Y`BG.Kf(~ 2+/ӻV @ @c-`̑wi}aϡ1FxG @[@pGj&@"0# @ @@#5@"02 t @ @GF mD`dv'@ @% Z4@"0RC @ @GJ ) a @ @@#%HFJDv( @ PHɠ$#%; @(Y`Zs4׍u~ @ @.ΝvWv6`ǰ @{jX @ @@Gj\9F @ˁ @H+G @ @{c9 @ @5q( @ 0#co, @ @#5E#@ @^@pd@ @X@pƕh @ @  @ @ Ըr @{7 @ PcW @ @c/ 82r @ @j, 8RQ4 @ @`GX @ @@Gj\9F @ˁ @H+G @ @{c9 @ @5q( @ 0K͟?gS^;# @ @= 71 oH?ӌugL-ӱ,gy,Yg2#,yɒeyɹYgLyGgwޛo3P<7{Ln5Q!gqF? &Q_җҎ;8Qr,gy͖_szeٛ[^9va~wzXvontƲ;vY~`'zݛۃg;5[Fo5n ,kM&epOOw^:ҋ^曳1H?c4?yZek^Un~E,0rgk17'>$흊lu^Q*gqrG޴&|+i=TiYϳԥw^o3Ln5QYe]<,sϙ3'&~qcd2Vne25;@gY$<ꐔe.6@R &yߟ.r%t^zj7rϑ&uͤDzvK1k"8:뤙3gfSiͲU<(Kr֬YO}jy*.\z|flo!(Vް]Y[k+(=b#\X@*+a xrx-ve-GZ-ܒuYhQlҖ[n*uX@" .:$e,*gIX*e.CR̲ Iy@I&uˑ8sҧ>tק 70䪫Jyk5M:iǦmf(5Rsb/,_L}.YvU )H,U H]$V1㾽Syg"z#i2Lڈmݖ>謮N;~]v%]s5颋.JguV⋳?O'SYXKʳhzի^M׽.tACI}{ӊ+8{ #K.$jp,)Jy X>K/{2<{k(=b#\nc`7OW\qňں'^{m:`j=µح}KrU,Yc%=s4mڴVd,=xr~i饗{ކe6-g90=fZ]xrS'KAY*g#=x`:O>9=)OI[lE23SNM+bp ꫯ/w]ix5;%fY.0<|S =68=lZ.,s`zXͲ6ln,ݷeާۭ<8韌 L2\ҩS3gx :c.~߶<ދ_S6q-J]|)~?'ّX[lr\GbYn},ϓ%=s<Ϣ,tgbOߗ-GT]tQ7IBe Q9 a2,GbW-Pz\ŲGl2U< |eʳ[-Y|C,diƌio 6HkF69s>3GgYn,ϳ˙3go,H,˭LydY@Grnٍeqߞo2ߦ-<{Q>}=HDye nK7|sVk+rseI1.٫\veW_= d,=xɒeyɹYg7ۻlV݊7}_GUK`qwd*xE]l`j=µ؍e W.g790=fZ.,s`z\ͳGl*=3GjZ1E @@:>r!@ @. 8RR> @ @`LGƔ  @ @ t}7MT6`YDxŭ: ?S2 ,E]`uHʲPYvU )HXf]U|ꡇzdEyOlcZv-zivJK/(w ɲZYgLWy ,K@l:&Qd9JY6ag O½=s\ϱ:#d7ptk6.馛n갇y,dz[ϳ7V{=Y{ u,{@k 68]nb%X,gh97ʁa5³ N6 g?3HSLIgN]w]$i%Jxn7|ϰ<ӳw1]Óx_PeAx*eIX*gA}{IX@" .*J:|k%KE+ /0͙3']ve{MlMjlIV,˩8Q:Y.^&X[7|szҦn[l?)mvi-MX[EtgeY&]~iw֯ٔZjaG!!Qe]6E?|0;^z^{=3meGbY^%,2ijfvm)~$fP E/bx_~{쑾?Y7'/xA\z-O=tUWeqrbYY*gAX@*eAx*l{h9IXgp&8+gb`%.Ķkpu`ڥqN~{ֵ+`I.\WD-:YG,˭ydY@GrnҽQqkŭYD>i T:d뭷N}{~7%Ze/K믟ҫ:뮃xReZdIvHmQk~lٳgU;M\gLIiYf+n:٨hN1x{b,9MųT}.!;l솸F _Bzk_;]bܑgi-hƹxGK/4;L|?1VR$vyKͲNwXv)5NBŷ,n)%NBmٝW]Rk9Ң&bޯnK1m>echnXp*,<0F(vgلQK% >qF,(Hr=>[|+7<5 Fݤ^'̜[ngwsOr* =مg.7Mrmpzij]Z[7je[7욬 ڢ SL:lFF:ς, BLvgCvmpo|(&2cY̯9fѿ9z>h~6 l(e7%d72>W{'˘ `Μ9iv}ωW\uU+H Yf8$ρz(kgo6b|hKSL?)͘1#}=Ig뮻nG>2Ezh-4mڴ!c6~ۓgyu>ܲݑcf=S%m,˭N/yzY>W^BLu]w%5kV6=֕[oM1^~6 dm&s#l…?ӂ [oDgx8ŋy睗K:7,_ekwt7 YJlj<́_XuYi޼y,;X^W+%ZXzٛ[^pt)ͦ=/5Z-\rIc=iZDz Q|j9sҝwޙk>; )EG?Q#IJsg?Yvw}ف<9ٸ#r?iW"P-,E,cec8$#[mU:裳+y]Lg1vN8k)yo^~ߧkvp ^/Xt_r;^ǃe/{Y/@oO7%KR1~70K[nɮR\gq*R Qocx{}vV_jj>jt16Ncƒ/+v.!;C6Pličg n3~EPy}Zx/zыG1$r.]1@|}M;H֍<;!HZAVYx%Ƿ(mviyrk?n׿8h<7RO`EY;Wt7sMdz`<|5G l]`$yKSNMz,b-`r㼵`ڥ<5\>Od_T@I ltK~;ޑ.׾lb|;Y8#b0;e DW~QGe0O$YܪdmS1 stYfu_NNoy:]$F`9d4k<Ȭ+WDkW,m^|}.qs 6ؠa,~ *XX6$7;nbFſhk КoؼgF44{{l袋i6xf/&G6_,g>g]cKb F7HE$[Z,[lZ~>ciM7Ո1]t/rܴ!ZyF7_ ˣƗ"jGڸG?hJ o"qsug31lFo~s 2o6M,˭q=Q[n岟wuWm!w3x_nh,˫8+vq}8ij\}St#mU^nH,˭F mu2ךkYnh,Ǧb UWY5XwF6W׾66G?8W_}u #Lc+ t0S MM]w,M$`YܪH3)7L1J| {*r~Oò3 9ٷG61Bb^gfoyt>Ff#X.5y-j7Z+hw@#bX R7b{lfn-{SUӟ5E c 5 x㍳~gyfzG@Ab %=Θ1#1(7)х^ 8Ly|ιf5ιh)9skeq$7t{534_3c^ 1E%kf{V[_\Dw[NJ=1b}7/Y,#%ۣG|kg!G W_=%|c:o)AXH91` +=uTpYj;ƽe^`$ScX_l53Z4N|0 \?^j<ozӛޔ}chF344v=Ʒ1JWgg297;+q 53KΖq݌V͘ueFxXஙh\3#0H75sRkbLl1O:(b.ap@ u윌g}v6JvO>GMHKVӼĘ-Ŋ%1ޘYiƗE1 Pȅ0 IDAT+x VSK.h?hTE9xp%. o{۲`IܤZF D%h3񌑉DϘnZ Z2 nh<fe h `Ŏ_!=#f-=o! {vH4K|Y$0yA J_}(l(z DPoѢE3g-7c]+btow͌5UdbF>~ߛ$w-xBX_ChwMb,fᕷc1cl6Kk81Nt)k>쓵ZnqLrY 70v㑴;mpobڥ}_"kV1xc)qQ[=\..d/T 5 b*}a*#cLms@\"ۚ#fߤtzPq>Z̘1} ׸fMfv87]7cnp`;gM[]3#p߸fƹ3nxb-$_YkfߩF ! q=EbPXEC̉D GK@̒v}ebDKѸO|lϥD${cVI2kigd^9RPNp6}/~1f).bqMf)%D4^̥7TbGT?YT4zY Ĺ71^F4y㛹hoi4} l6ݎbj/g[Z ĸpk\7cfqg>RɕaxGg3TMiLY 53k~fƹe\3c3Dי87RX򗿜f=s1kZ,qM/Z}R<) l#k41O~򓬏|1;] >,]t1c,KqdSO;e?1}tvŏ)cPF.zAĴ1 {b6۶[zC?kPѾo&?ZCRI}Agq~nVoy[\3 YygF!5s' Ϙ&Bͦ$̂J?پ-jy|qrm;E) 82ҤF4-SǍgH2G񍞵QZ)+vO!1S]3Go8fCϺ f֭FzE> @ @ȟ{m|D @ / 8oc  @ #}P>" @ / 8oc  @ #}P>" @ / 8oc  @ #}P>" @ / 8oc  @ #}P>" @ / 8oc  @ #}P>" @ / 8oc  @ #}P>" @ / 8oc  @ #}P>" @ / 8oc  @ #}P>" @ / 8oc  @ #}P>" @ / 8oc  @ #}P>" @ / 8oc  @ #}P>" @ߎ0 z `CY  @@ #mc!@ @đ'; @hqm, @ @8rd' @ @-  @8GD @ @đ @ @H @86 @ @@@9x  @ @GB @#Ov" @ HX @ @qN$@ @Z@i  @ p <ى @ @@ #mc!@ @đ'; @hqm, @ @|,IENDB`graphql-ruby-2.5.19/guides/limiters/soft_button.png000066400000000000000000000111771514115062600224210ustar00rootroot00000000000000PNG  IHDR@o7) @iCCPICC Profile(c``H,(aa``+) rwRR` `\\TQk .ȬǶoc=,)<zZ qbrAQ c\^Rb"E@G3@t{ a r)@ [' I< ؍MB}-M PZQ *23J!`d`d o7(Ɓ+c`!B,v9> +H,J;KqͽuÁ^d`{Áo^ FVeXIfMM*iDҠ@ASCIIScreenshot%ٺiTXtXML:com.adobe.xmp 210 Screenshot 64 -IIDATx]XG~ HobQXAh,1=^b^sQs%1J4Q{, Aip 첋 {;:/ )JQ_)h~?:VtDDxA15$DD͡PTOhIm 5 9n!.P{~D5A$M%GG@IHtPD҄Y}T{9ڣ:XV\}}k>yX[x V.0mP%Y(+*qK/*ݑ\]U>;=q]Ve~=;ߐĞ>8{5(\ 6F:Zrn:a ǁNHuN*N:j΍BNuexa]>oVWNHou qʟ='IH=w wo!bB2]9 S¬~/$nٍu$RQr*6q4q)Ht<\qLjk4hzYɋcV̐87x$# ыW 9x0d<2?xrWH={ܼܙ8%CYD%Nύ}m}0)+3~31"; vٜp|L3^<~ w(HCQf6#X/{:|47x7~hNY!q N 9qgV'5/*N ~9VA-!޺Wڙ]&S_qiq| eH@9|Xk 59tK|vmWis yCV='ۉtA.OPyz̼dΊ:Z1 }Th՝XK33c@2~I a- c799Ĕce Cf*Sm=2Sϼ}~[?FհDzL۩i3’cA7)U|Cji>v~Va%(n_4?s\z FÛЫ;ǟv0"C//\?s{۷*u3D7q Τc˝n27dr8.{pFDQ6!&fsb*q⟹9DF)xrHo߼?V^]xY)URy׎Z0DT:4MHw]"8cxy?ԍM8roֈ~΢ea=gy);Uo҄/ $"p{xp 朧;DF{GOA <͂3+;9Ir(JIUN"Z,O;H:z2;: >+3S9iIBܣ=zl\6ޜ*f(d(Rh쨒 <}㑺003޹:h^_>$|̵|{S)Sêg7/As\N9>14@vͧt=O'QɟF683~6lS)GdҮ(OL(z>F[$yb]6$mɇ){)AyIQj#|Pb^^yNFi */3K=wpae#Nc/z_Sf\szdt|Sوٸg.c+])t KԐ 8 Gi`%ɪ#wB:n;}L[ .1k~lI ^gGӽ3]u`cRv;C>+dF\G\&T(6ڀ'[UwWk8&E)i0cVŭG K̒C)J~D|.anK}XhP]? D6OQmk3#Txt>.YwF&*_s4-?ps]0?Y婿¶_35[MTıX>uX8ّ#:93a@&fŵ֓G`v_:P 8l20cMjʳ9:UmTթQ#VyTb왗L\Fe_wi_⌿!a7{5l7IYbM `L7*L#!(+y Sf(vQTy"3"3:JRPӜC^$}ĎsD1yXq>xj;/*%i|QYt̺Зc]:2Bv@u /S_./@ѽpdS¼)썰dKC8nҵ סP%(BomӡCM t|^`ȎU\!Gm(5EȱVvkSΧn%M۷2n|_rJ[ " okwc&h^ A"id/H;7g M" s#zA4 g5D4h JVǞojHul^i9G&yN]u@IL;u@@JDSZvD@IJ4.E@Ik^ \JDSZvD@IJ4.E@Ik^ \JDSZvD@IJ4.E@Ik^ \JDSZvD R2cIENDB`graphql-ruby-2.5.19/guides/mutations/000077500000000000000000000000001514115062600175315ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/mutations/mutation_authorization.md000066400000000000000000000124571514115062600247040ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Mutations title: Mutation authorization desc: Checking permissions for mutations index: 3 --- Before running a mutation, you probably want to do a few things: - Make sure the current user has permission to try this mutation - Load some objects from the database, using some `ID` inputs - Check if the user has permission to modify those loaded objects This guide describes how to accomplish that workflow with GraphQL-Ruby. ## Checking conditions before instantiating the mutation ```ruby class UpdateUserMutation < BaseMutation # ... def resolve(update_user_input:, user:) # ... end def self.authorized?(obj, ctx) super && ctx[:viewer].present? end end ``` ## Checking the user permissions Before loading any data from the database, you might want to see if the user has a certain permission level. For example, maybe only `.admin?` users can run `Mutation.promoteEmployee`. This check can be implemented using the `#ready?` method in a mutation: ```ruby class Mutations::PromoteEmployee < Mutations::BaseMutation def ready?(**args) # Called with mutation args. # Use keyword args such as employee_id: or **args to collect them if !context[:current_user].admin? raise GraphQL::ExecutionError, "Only admins can run this mutation" else # Return true to continue the mutation: true end end # ... end ``` Now, when any non-`admin` user tries to run the mutation, it won't run. Instead, they'll get an error in the response. Additionally, `#ready?` may return `false, { ... }` to return {% internal_link "errors as data", "/mutations/mutation_errors" %}: ```ruby def ready? if !context[:current_user].allowed? return false, { errors: ["You don't have permission to do this"]} else true end end ``` ## Loading and authorizing objects Often, mutations take `ID`s as input and use them to load records from the database. GraphQL-Ruby can load IDs for you when you provide a `loads:` option. In short, here's an example: ```ruby class Mutations::PromoteEmployee < Mutations::BaseMutation # `employeeId` is an ID, Types::Employee is an _Object_ type argument :employee_id, ID, loads: Types::Employee # Behind the scenes, `:employee_id` is used to fetch an object from the database, # then the object is authorized with `Employee.authorized?`, then # if all is well, the object is injected here: def resolve(employee:) employee.promote! end end ``` It works like this: if you pass a `loads:` option, it will: - Automatically remove `_id` from the name and pass that name for the `as:` option - Add a prepare hook to fetch an object with the given `ID` (using {{ "Schema.object_from_id" | api_doc }}) - Check that the fetched object's type matches the `loads:` type (using {{ "Schema.resolve_type" | api_doc }}) - Run the fetched object through its type's `.authorized?` hook (see {% internal_link "Authorization", "/authorization/authorization" %}) - Inject it into `#resolve` using the object-style name (`employee:`) In this case, if the argument value is provided by `object_from_id` doesn't return a value, the mutation will fail with an error. Alternatively if your `ID` doesn't specify both class _and_ id, resolvers have a `load_#{argument}` method that can be overridden. ```ruby argument :employee_id, ID, loads: Types::Employee def load_employee(id) ::Employee.find(id) end ``` If you don't want this behavior, don't use it. Instead, create arguments with type `ID` and use them your own way, for example: ```ruby # No special loading behavior: argument :employee_id, ID ``` ## Can _this user_ perform _this action_? Sometimes you need to authorize a specific user-object(s)-action combination. For example, `.admin?` users can't promote _all_ employees! They can only promote employees which they manage. You can add this check by implementing a `#authorized?` method, for example: ```ruby def authorized?(employee:) super && context[:current_user].manager_of?(employee) end ``` When `#authorized?` returns `false` (or something falsey), the mutation will be halted. If it returns `true` (or something truthy), the mutation will continue. #### Adding errors To add errors as data (as described in {% internal_link "Mutation errors", "/mutations/mutation_errors" %}), return a value _along with_ `false`, for example: ```ruby def authorized?(employee:) super && if context[:current_user].manager_of?(employee) true else return false, { errors: ["Can't promote an employee you don't manage"] } end end ``` Alternatively, you can add top-level errors by raising `GraphQL::ExecutionError`, for example: ```ruby def authorized?(employee:) super && if context[:current_user].manager_of?(employee) true else raise GraphQL::ExecutionError, "You can only promote your _own_ employees" end end ``` In either case (returning `[false, data]` or raising an error), the mutation will be halted. ## Finally, doing the work Now that the user has been authorized in general, data has been loaded, and objects have been validated in particular, you can modify the database using `#resolve`: ```ruby def resolve(employee:) if employee.promote { employee: employee, errors: [], } else # See "Mutation Errors" for more: { errors: employee.errors.full_messages } end end ``` graphql-ruby-2.5.19/guides/mutations/mutation_classes.md000066400000000000000000000153031514115062600234320ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Mutations title: Mutation Classes desc: Use mutation classes to implement behavior, then hook them up to your schema. index: 1 redirect_from: - /queries/mutations/ - /relay/mutations/ --- GraphQL _mutations_ are special fields: instead of reading data or performing calculations, they may _modify_ the application state. For example, mutation fields may: - Create, update or destroy records in the database - Establish associations between already-existing records in the database - Increment counters - Create, modify or delete files - Clear caches These actions are called _side effects_. Like all GraphQL fields, mutation fields: - Accept inputs, called _arguments_ - Return values via _fields_ GraphQL-Ruby includes two classes to help you write mutations: - {{ "GraphQL::Schema::Mutation" | api_doc }}, a bare-bones base class - {{ "GraphQL::Schema::RelayClassicMutation" | api_doc }}, a base class with a set of nice conventions that also supports the Relay Classic mutation specification. Besides those, you can also use the plain {% internal_link "field API", "/type_definitions/objects#fields" %} to write mutation fields. ## Example mutation class If you used the {% internal_link "install generator", "/schema/generators#graphqlinstall" %}, a base mutation class will already have been generated for you. If that's not the case, you should add a base class to your application, for example: ```ruby class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation # Add your custom classes if you have them: # This is used for generating payload types object_class Types::BaseObject # This is used for return fields on the mutation's payload field_class Types::BaseField # This is used for generating the `input: { ... }` object type input_object_class Types::BaseInputObject end ``` Then extend it for your mutations: ```ruby class Mutations::CreateComment < Mutations::BaseMutation null true argument :body, String argument :post_id, ID field :comment, Types::Comment field :errors, [String], null: false def resolve(body:, post_id:) post = Post.find(post_id) comment = post.comments.build(body: body, author: context[:current_user]) if comment.save # Successful creation, return the created object with no errors { comment: comment, errors: [], } else # Failed save, return the errors to the client { comment: nil, errors: comment.errors.full_messages } end end end ``` The `#resolve` method should return a hash whose symbols match the `field` names. (See {% internal_link "Mutation Errors", "/mutations/mutation_errors" %} for more information about returning errors.) Also, you can configure `null(false)` in your mutation class to make the generated payload class non-null. ## Hooking up mutations Mutations must be attached to the mutation root using the `mutation:` keyword, for example: ```ruby class Types::Mutation < Types::BaseObject field :create_comment, mutation: Mutations::CreateComment end ``` ## Auto-loading arguments In most cases, a GraphQL mutation will act against a given global relay ID. Loading objects from these global relay IDs can require a lot of boilerplate code in the mutation's resolver. An alternative approach is to use the `loads:` argument when defining the argument: ```ruby class Mutations::AddStar < Mutations::BaseMutation argument :post_id, ID, loads: Types::Post field :post, Types::Post def resolve(post:) post.star { post: post, } end end ``` By specifying that the `post_id` argument loads a `Types::Post` object type, a `Post` object will be loaded via {% internal_link "`Schema#object_from_id`", "/schema/definition.html#object-identification-hooks" %} with the provided `post_id`. All arguments that end in `_id` and use the `loads:` method will have their `_id` suffix removed. For example, the mutation resolver above receives a `post` argument which contains the loaded object, instead of a `post_id` argument. The `loads:` option also works with list of IDs, for example: ```ruby class Mutations::AddStars < Mutations::BaseMutation argument :post_ids, [ID], loads: Types::Post field :posts, [Types::Post] def resolve(posts:) posts.map(&:star) { posts: posts, } end end ``` All arguments that end in `_ids` and use the `loads:` method will have their `_ids` suffix removed and an `s` appended to their name. For example, the mutation resolver above receives a `posts` argument which contains all the loaded objects, instead of a `post_ids` argument. In some cases, you may want to control the resulting argument name. This can be done using the `as:` argument, for example: ```ruby class Mutations::AddStar < Mutations::BaseMutation argument :post_id, ID, loads: Types::Post, as: :something field :post, Types::Post def resolve(something:) something.star { post: something } end end ``` In the above examples, `loads:` is provided a concrete type, but it also supports abstract types (i.e. interfaces and unions). ### Resolving the type of loaded objects When `loads:` gets an object from {{ "Schema.object_from_id" | api_doc }}, it passes that object to {{ "Schema.resolve_type" | api_doc }} to confirm that it resolves to the same type originally configured with `loads:`. ### Handling failed loads If `loads:` fails to find an object or if the loaded object isn't resolved to the specified `loads:` type (using {{ "Schema.resolve_type" | api_doc }}), a {{ "GraphQL::LoadApplicationObjectFailedError" | api_doc }} is raised and returned to the client. You can customize this behavior by implementing `def load_application_object_failed` in your mutation class, for example: ```ruby def load_application_object_failed(error) raise GraphQL::ExecutionError, "Couldn't find an object for ID: `#{error.id}`" end ``` Or, if `load_application_object_failed` returns a new object, that object will be used as the `loads:` result. ### Handling unauthorized loaded objects When an object is _loaded_ but fails its {% internal_link "`.authorized?` check", "/authorization/authorization#object-authorization" %}, a {{ "GraphQL::UnauthorizedError" | api_doc }} is raised. By default, it's passed to {{ "Schema.unauthorized_object" | api_doc }} (see {% internal_link "Handling Unauthorized Objects", "/authorization/authorization.html#handling-unauthorized-objects" %}). You can customize this behavior by implementing `def unauthorized_object(err)` in your mutation, for example: ```ruby def unauthorized_object(error) # Raise a nice user-facing error instead raise GraphQL::ExecutionError, "You don't have permission to modify the loaded #{error.type.graphql_name}." end ``` graphql-ruby-2.5.19/guides/mutations/mutation_errors.md000066400000000000000000000076561514115062600233250ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Mutations title: Mutation errors desc: Tips for handling and returning errors from mutations index: 2 --- How can you handle errors inside mutations? Let's explore a couple of options. ## Raising Errors One way to handle an error is by raising, for example: ```ruby def resolve(id:, attributes:) # Will crash the query if the data is invalid: Post.find(id).update!(attributes.to_h) # ... end ``` Or: ```ruby def resolve(id:, attributes:) if post.update(attributes) { post: post } else raise GraphQL::ExecutionError, post.errors.full_messages.join(", ") end end ``` This kind of error handling _does_ express error state (either via `HTTP 500` or by the top-level `"errors"` key), but it doesn't take advantage of GraphQL's type system and can only express one error at a time. It works, but a stronger solution is to treat errors as data. ## Errors as Data Another way to handle rich error information is to add _error types_ to your schema, for example: ```ruby class Types::UserError < Types::BaseObject description "A user-readable error" field :message, String, null: false, description: "A description of the error" field :path, [String], description: "Which input value this error came from" end ``` Then, add a field to your mutation which uses this error type: ```ruby class Mutations::UpdatePost < Mutations::BaseMutation # ... field :errors, [Types::UserError], null: false end ``` And in the mutation's `resolve` method, be sure to return `errors:` in the hash: ```ruby def resolve(id:, attributes:) post = Post.find(id) if post.update(attributes) { post: post, errors: [], } else # Convert Rails model errors into GraphQL-ready error hashes user_errors = post.errors.map do |error| # This is the GraphQL argument which corresponds to the validation error: path = ["attributes", error.attribute.to_s.camelize(:lower)] { path: path, message: error.message, } end { post: post, errors: user_errors, } end end ``` Now that the field returns `errors` in its payload, it supports `errors` as part of the incoming mutations, for example: ```graphql mutation($postId: ID!, $postAttributes: PostAttributes!) { updatePost(id: $postId, attributes: $postAttributes) { # This will be present in case of success or failure: post { title comments { body } } # In case of failure, there will be errors in this list: errors { path message } } } ``` In case of a failure, you might get a response like: ```ruby { "data" => { "createPost" => { "post" => nil, "errors" => [ { "message" => "Title can't be blank", "path" => ["attributes", "title"] }, { "message" => "Body can't be blank", "path" => ["attributes", "body"] } ] } } } ``` Then, client apps can show the error messages to end users, so they might correct the right fields in a form, for example. ## Nullable Mutation Payload Fields To benefit from "Errors as Data" described above, mutation fields must not have `null: false`. Why? Well, for _non-null_ fields (which have `null: false`), if they return `nil`, then GraphQL aborts the query and removes those fields from the response altogether. In mutations, when errors happen, the other fields may return `nil`. So, if those other fields have `null: false`, but they return `nil`, the GraphQL will panic and remove the whole mutation from the response, _including_ the errors! In order to have the rich error data, even when other fields are `nil`, those fields must have `null: true` (which is the default) so that the type system can be obeyed when errors happen. Here's an example of a nullable field (good!): ```ruby class Mutations::UpdatePost < Mutations::BaseMutation # Use the default `null: true` to support rich errors: field :post, Types::Post # ... end ``` graphql-ruby-2.5.19/guides/mutations/mutation_root.md000066400000000000000000000022161514115062600227570ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Mutations title: Mutation Root desc: The Mutation object is the entry point for mutation operations. index: 0 --- GraphQL mutations all begin with the `mutation` keyword: ```graphql mutation($accountNumber: ID!, $newBalance: Int!) { # ^^^^ here setAccountBalance(accountNumber: $accountNumber, newBalance: $newBalance) { # ... } } ``` Operations that begin with `mutation` get special treatment by the GraphQL runtime: root fields are guaranteed to be executed sequentially. This way, the effect of a series of mutations is predictable. Mutations are executed by a specific GraphQL object, `Mutation`. This object is defined like any other GraphQL object: ```ruby class Types::Mutation < Types::BaseObject # ... end ``` Then, it must be attached to your schema with the `mutation(...)` configuration: ```ruby class Schema < GraphQL::Schema # ... mutation(Types::Mutation) end ``` Now, whenever an incoming request uses the `mutation` keyword, it will go to `Mutation`. See {% internal_link "Mutation Classes", "/mutations/mutation_classes" %} for some helpers to define mutation fields. graphql-ruby-2.5.19/guides/object_cache/000077500000000000000000000000001514115062600200775ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/object_cache/caching.md000066400000000000000000000214721514115062600220230ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true enterprise: true section: GraphQL Enterprise - Object Cache title: Caching Results desc: Configuration options for caching objects and fields index: 2 --- `GraphQL::Enterprise::ObjectCache` supports several different caching configurations for objects and fields. To get started, include the extension in your base object class and base field class and use `cacheable(...)` to set up the default cache behavior: ```ruby # app/graphql/types/base_object.rb class Types::BaseObject < GraphQL::Schema::Object include GraphQL::Enterprise::ObjectCache::ObjectIntegration field_class Types::BaseField cacheable(...) # see below # ... end ``` ```ruby # app/graphql/types/base_field.rb class Types::BaseField < GraphQL::Schema::Field include GraphQL::Enterprise::ObjectCache::FieldIntegration cacheable(...) # see below # ... end ``` Also, make sure your base interface module is using your field class: ```ruby # app/graphql/types/base_interface.md module Types::BaseInterface field_class Types::BaseField end ``` Field caching can be configured per-field, too, for example: ```ruby field :latest_update, Types::Update, null: false, cacheable: { ttl: 60 } field :random_number, Int, null: false, cacheable: false ``` Only _queries_ are cached. `ObjectCache` skips mutations and subscriptions altogether. ## `cacheable(true|false)` `cacheable(true)` means that the configured type or field may be stored in the cache until its cache fingerprint changes. It also defaults to `public: false`, meaning that clients will _not_ share cached responses. See [`public:`](#public) below for more about this option. `cacheable(false)` disables caching for the configured type or field. Any query that includes this type or field will neither check for an already-cached value nor update the cache with its result. ## `public:` `cacheable(public: false)` means that a type or field may be _cached_, but {% internal_link "`Schema.private_context_fingerprint_for(ctx)`", "/object_cache/schema_setup#context-fingerprint" %} should be included in its cache key. In practice, this means that each client can have its own cached responses. Any query that contains a `cacheable(public: false)` type or field will use a private cache key. `cacheable(public: true)` means that cached values from this type or field may be shared by _all_ clients. Use this for public-facing data which is the same for all viewers. Queries that include _only_ `public: true` types and fields will not include `Schema.private_context_fingerprint_for(ctx)` in their cache keys. That way their responses will be shared by all clients who request them. ## `ttl:` `cacheable(ttl: seconds)` expires any cached value after the given number of seconds, regardless of cache fingerprint. `ttl:` shines in a few cases: - Objects that can't reliably generate a fingerprint value (for example, they have no `.updated_at` timestamp). In this case, a conservative `ttl` may be the only option for cache expiration. - Or, root-level fields that should be expired after a certain amount of time. The root-level `Query` often has _no_ backing object, so it won't have a cache fingerprint, either. Adding `cacheable: { ttl: ... }` to root level fields will provide some caching along with a guarantee about when they'll be expired. - Or, list responses that may be difficult to invalidate properly (see below). Under the hood, `ttl:` is implemented with Redis's `EXPIRE`. ## Caching lists and connections Lists and connections require a little extra consideration. By default, each _item_ in a list is registered with the cache, but when new items are created, they are unknown to the cache and therefore don't invalidate the cached result. There are two main approaches to address this. ### `has_many` lists In order to effectively bust the cache, items that belong to the list of "parent" object should __update the parent__ (eg, Rails `.touch`) whenever they're created, destroyed, or updated. For example, if there's a list of players on a team: ```graphql { team { players { totalCount } } } ``` None of the _specific_ `Player`s will be part of the cached response, but the `Team` will be. To properly invalidate the cache, the `Team`'s `updated_at` (or other cache key) should be updated whenever a `Player` is added or removed from the `Team`. If a list may be sorted, then updates to `Player`s should also update the `Team` so that any sorted results in the cache are invalidated, too. Alternatively (or additionally), you could use a `ttl:` to expire cached results after a certain duration, just to be sure that results are eventually expired. With Rails, you can accomplish this with: ```ruby # update the team whenever a player is saved or destroyed: belongs_to :team, touch: true ``` ### Top-level lists For `ActiveRecord::Relation`s _without_ a "parent" object, you can use `GraphQL::Enterprise::ObjectCache::CacheableRelation` to make a synthetic cache entry for the _whole_ relation. To use this class, make a subclass and implement `def items`, for example: ```ruby class AllTeams < GraphQL::Enterprise::ObjectCache::CacheableRelation def items(division: nil) teams = Team.all if division teams = teams.where(division: division) end teams end end ``` Then, in your resolver, use your new class to retrieve the items: ```ruby class Query < GraphQL::Schema::Object field :teams, Team.connection_type do argument :division, Division, required: false end def teams(division: nil) AllTeams.items_for(self, division: division) end end ``` If you're using {{ "GraphQL::Schema::Resolver" | api_doc }}, you'd call `.items_for` slightly differently: ```ruby def resolve(division: nil) # use `context[:current_object]` to get the GraphQL::Schema::Object instance whose field is being resolved AllTeams.items_for(context[:current_object], division: division) end ``` Finally, you'll need to handle `CacheableRelation`s in your object identification methods, for example: ```ruby class MySchema < GraphQL::Schema # ... def self.id_from_object(object, type, ctx) if object.is_a?(GraphQL::Enterprise::ObjectCache::CacheableRelation) object.id else # The rest of your id_from_object logic here... end end def self.object_from_id(id, ctx) if (cacheable_rel = GraphQL::Enterprise::ObjectCache::CacheableRelation.find?(id)) cacheable_rel else # The rest of your object_from_id logic here... end end end ``` In this example, `AllTeams` implements several methods to support caching: - `#id` creates a cache-friendly, stable global ID - `#to_param` creates a cache fingerprint (using Rails's `#cache_key` under the hood) - `.find?` retrieves the list based on its ID This way, if a `Team` is created, the cached result will be invalidated and a fresh result will be created. Alternatively (or additionally), you could use a `ttl:` to expire cached results after a certain duration, just to be sure that results are eventually expired. ### Connections By default, connection-related objects (like `*Connection` and `*Edge` types) "inherit" cacheability from their node types. You can override this in your base classes as long as `GraphQL::Enterprise::ObjectCache::ObjectIntegration` is included in the inheritance chain somewhere. ## Caching Introspection By default, introspection fields are considered _public_ for all queries. This means that they are considered cacheable and their results will be reused for any clients who request them. When {% internal_link "adding the ObjectCache to your schema", "/object_cache/schema_setup#add-the-cache", %}, you can provide some options to customize this behavior: - `cache_introspection: { public: false, ... }` to use [`public: false`](#public) for all introspection fields. Use this if you hide schema members for some clients. - `cache_introspection: false` to completely disable caching on introspection fields. - `cache_introspection: { ttl: ..., ... }` to set a [ttl](#ttl) (in seconds) for introspection fields. ## Object Dependencies By default, the `object` of a GraphQL Object type is used for caching the fields selected on that object. But, you can specify what object (or objects) should be used to check the cache by implementing `def self.cache_dependencies_for(object, context)` in your type definition. For example: ```ruby class Types::Player def self.cache_dependencies_for(player, context) # we update the team's timestamp whenever player details change, # so ignore the `player` for caching purposes player.team end end ``` Use this to: - improve performance when caching lists of children that belong to a parent object - register other objects with the ObjectCache when running a query. (`cacheable_object(obj)` or `def self.object_fingerprint_for` can also be used in this case.) If this method returns an `Array`, each object in the array will be registered with the cache. graphql-ruby-2.5.19/guides/object_cache/memcached.md000066400000000000000000000007541514115062600223350ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true enterprise: true section: GraphQL Enterprise - Object Cache title: Dalli Configuration desc: Setting up the Memcached backend index: 3 --- `GraphQL::Enterprise::ObjectCache` can also run with a Memcached backend via the [Dalli](https://github.com/petergoldstein/dalli) client gem. Set it up by passing a `Dalli::Client` instance as `dalli: ...`, for example: ```ruby use GraphQL::Enterprise::OperationStore, dalli: Dalli::Client.new(...) ``` graphql-ruby-2.5.19/guides/object_cache/overview.md000066400000000000000000000052731514115062600222760ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true enterprise: true section: GraphQL Enterprise - Object Cache title: GraphQL ObjectCache desc: A server-side cache for GraphQL-Ruby index: 0 --- `GraphQL::Enterprise::ObjectCache` is an application-level cache for GraphQL-Ruby servers. It works by storing a {% internal_link "_cache fingerprint_ for each object", "/object_cache/schema_setup#object-fingerprint" %} in a query, then serving a cached response as long as those fingerprints don't change. The cache can also be customized with {% internal_link "TTLs", "/object_cache/caching#ttl" %}. ## Why? `ObjectCache` can greatly reduce GraphQL response times by serving cached responses when the underlying data for a query hasn't changed. Usually, a GraphQL query alternates between data fetching and calling application logic: {{ "/object_cache/query-without-cache.png" | link_to_img:"GraphQL-Ruby profile, without caching" }} But with `ObjectCache`, it checks the cache first, returning a cached response if possible: {{ "/object_cache/query-with-cache.png" | link_to_img:"GraphQL-Ruby profile, with ObjectCache" }} This reduces latency for clients and reduces the load on your database and application server. ## How Before running a query, `ObjectCache` creates a fingerprint for the query using {{ "GraphQL::Query#fingerprint" | api_doc }} and {% internal_link "`Schema.context_fingerprint_for(ctx)`", "/object_cache/schema_setup#context-fingerprint" %}. Then, it checks the backend for a cached response which matches the fingerprint. If a match is found, the `ObjectCache` fetches the objects previously visited by this query. Then, it compares the current fingerprint of each object ot the one in the cache and checks `.authorized?` for that object. If the fingerprints all match and all objects pass authorization checks, then the cached response returned. (Authorization checks can be {% internal_link "disabled", "/object_cache/schema_setup#disabling-reauthorization" %}.) If there is no cached response or if the fingerprints don't match, then the incoming query is re-evaluated. While it's executed, `ObjectCache` gathers the IDs and fingerprints of each object it encounters. When the query is done, the result and the new object fingerprints are written to the cache. ## Setup To get started with the object cache: - {% internal_link "Prepare the schema", "/object_cache/schema_setup" %} - Set up a {% internal_link "Redis backend", "/object_cache/redis" %} or {% internal_link "Memcached backend", "/object_cache/memcached" %} - {% internal_link "Configure types and fields for caching", "/object_cache/caching" %} - Check out the {% internal_link "runtime considerations", "/object_cache/runtime_considerations" %} graphql-ruby-2.5.19/guides/object_cache/query-with-cache.png000066400000000000000000000313641514115062600237730ustar00rootroot00000000000000PNG  IHDR2IDATx^uǦcfbff!ffvlǔqrvfw{sq睔ԪHHg~~<'U E[o'B!B!B!B!B!B!B=޳}E%?޳Bq01) .7B!& h:!xB'B!i:ЉBh*t ! h:!xB'B!i:ЉBh*t ! h:!xB'B!i:ЉBh?MOWתwLýdYտMwV9]=?Vԓ 寑h:!xt;PTSeF+[9]0?KvsH\n]VTUYleN0BM ՞C4_K>mj:UJ"[['˗Ա2W1reS-a+h:!xt[4Զ9YLz++M!Et"_kq1jpa"DE>=ԫ)ōuUZԨMy"GWR=ZԐT;lǿi׮ٲ:z?:f7[mY>R-Yݲ|oS]Vb_4ˍ 6L=Ng}Z&5ʪO-i:vkZ;ԸM=?no8oiU.pj69|q^p1Q4BMDLti:YFh'O5-H !:@Ӂ:?C Ȇ k`*4ψnS:Nmh*, QZO8S0rMHޙ/\=96/77BZQfJTbB2#m!It` SfN?ܥ>MG`} dYCK0>(_TZsxEtB5KHkPI)@2t ` ",hU+[Aɕ-ʒ19}#T-[X:{y/.rET)SX _074KC/Ӂ"rJ}f>`1,>S:*#϶+Bd`dg1  (&yg: *h `fa}U!|~f$Q~!BN ܖLVX[*VumTzŞlX2 Ni:!x选@ң9hu/Ӂ)\38@{$ObO6+Ko@ cդ^"SB:SߧLV` ,c͖9t `Uaٱ.˟;W2,Xsipl|\_g !:DC26`>,&O ]ח@")1)Ǡ ɣ AKUpW`s.2gn|²TL X!57b cb(pM3IyB?v[Cک>$ԢNyOt 0`0mzhW@a DkPA D[z}$B!Cd:0!'m` )xe: òq+=zH.E.rC) sߝ}Xwe 7&I) DUrIɁN`1~rxX aΜa =k{e^ !OӁA雋+mKSK, 2L]/,kW>ZǍ]3u= k i]k*8iWc_^Y-߈ؘ}` XCNXE,=ua r{ d.E^ Xy$h:!xtDn LwmP!M!2B,B&B!d:Bh*t ! h:!xB'B!i:ЉBh*t ! h:!xB'B!i:ЉBh*t ! h:!xB'B!Ψ}E%I=%EQT8Gq61?(*\c"B(*ܢ ġtPntPh:( h:q(4E[48-B MEQM!pBAQTEAC(*ܢ ġtPntPh:( h:q('۪~~=mCS;ݫ7ʓ>|Km^J}zmEEh:q(=S=;tQ)P1ͽ+GNut횵Rҧj5S_ΙVSwzA>DtP1?=LU,[^T/'p 3{*eJ64 rh:q( CF8k>ѳ*yr 2EA9M48Mǯ2g̤ʭk~)#Ɗ)ɗMr@uJ+QI;I -4QPuTKQ+q`pzʗ.ʔ(>ɉe:vڨzwWǞ՛UˆMUE|O&뷩-Z*,}|}Ԩ\UMtٲ[͝8MU)_Q.^RU=򡭿/:I7bX^+tP5/>t`tG,E_]tۧsw)oX XSHmct0H#їA=TҦSn,uLӁ6Ci۴<۹G5

K= N|[ dy%+UB+M1Ncr-.5WX1#[XMs.±atK&tT(SNz Β+]*K̒W\Ѡf󡨤*Bc㢕2.1׶/>iӡ#Z\?oZNk0\R]KLz0(w\T &hI\ }kH/_93UP1gߨH<ΕCԉiӁ%:N^rL`[5=4}DACtӁwu.UJ:WMxϖ%O7O\iLL cYǗ7h,F,ؽjP'#"mULۺ׫R>{TS:05=T{?E%5tP5xO,jZmU&Y<Jj ġk: 7qz;d`TF>v\;rFP}}ߦcᔷdy%f(*@BfLYM &ϔ[sk:/țփM15f'L+[<`\Pj-K0=tjsR% f ǚ am4i_9L w̝(BJ ZƝj(GnZ5,r ,9ſGDKD `20]oEEMás=Laj(i) ?ɳ"ApQ3s1>:wM=v[Və[KdugvS|}A{R_T 9HbZe *J* ġj:0]@$PI_h4#C F/겶LzxG՘20050LM`U JQr59?=ZD~ 1;a½b&ӁQKDq?|Dh:q(08#ʀfīS%x(R}V87`[ȷ@ŋA[C} +^#ɴ~s?E%etP^tPE#B MEQM!pBAQTEAC(*ܢ ġtPntPh:( h:q(4E[48-B MEQM!pBAQTEAC(*ܢ ġtPntBm˵s ]?/Ͼe냢џd~!$Ah:"P4TEA MbtB#EAX4@tP!M!$(h:"P4TEA MbtB#EAX4@tP!M!$(h:"P4TEA MbtB#EAX4@tP!M!$(5//TNW;LW^=k+Ocm{ufRiD[yE:lӽբЮZ?u1[=fw̳Z;fMUǗ/'9wT73upl,[yb*J y S=^Eak٫ԭ.o\eNZ"\?em먎ԩRxBQ?V7Atu*ST)SyrLv@Yʫj/c'ʕ-j\<Ëk>m7Se'NXVd+h:!AhVztpO}[Q/MxoZec&[;7Ȝ1hr޸]FztB^ Bi:,/?2gRc1mo?o@U,>bkӆ^ѵ*/JKF-ZD``6xzا*?Jk/+f\L Fڭ8F̚Ew۲CjHvrnӦUUƎp[ Wԏ>9okЛi\𒹪Rr}.#>TLUbkY5OMWre$RR@~5_/ӻ~uV~{LI,9DrW=DVVM*'|sR۶Rt`׶ֿC5WWwQ zu{A3y=cH!$}5ԚIc>] zj:`8GB#|`8y"֣y]kӁ4?>Hr`9>͜LJvϚ)|&&7oOw=-Wwfk=N\~|Z rl / lh:!Aȑ%m F&c$U"˂1PR^Luut|۾ӁvX~z )1\qbʦղm5^(TaYSe@uoov#t`#:b۾ӁYޮ~1z`e}k[`,;m\ ?Mdž”!$Ӂ s_|fniuf y &ļT(/u1c;RtLa¬c &u㚂9` kmփi>@tb^'a*u- G% jH!\L^M!$(Ba:119|Sq^ t`jZQ|t sE(G[?N\f:+?>WsU^VXM7yϵTE03 _s:8_)CNom:gatܱAFЗyZx^$goʗpwm7PbtB" ukCDi: $$߿=$& ֹx-D`0 8kvl?:gt DHU9LlD0Ed1V`!{sKCW}gNtPyGVڵyo,`^A,.3$w DQrֳ4Nz0)*M E(L~񣊕)rG ~uB2~/8r, Z׭%Ǹ}W9ޱIxrl=O:/>Ӂ "a%DL~0a` ׌]eK_ӁrB0F"+uރJ|񨡲m.SfR)|p' #tlef")`>>8wWr>b{1X`~b~ɱ(7%M!$(Ba: @} ☂2R,D^ !OX݀7FEP\jArYҫ 0Ǐ~Ê,xQ,B+@NLdz],z˓yq*ƱUrnXex/(ǵXM#|?tqOa{PpOP}#Q$B= Ψci#hDjpqfU|v];||pHVt<`{Hvϝ^C@«~

_!b9~TBԆ~bĠi-ZaKޖ1GVN`0!DX9pO{[iS'sӸ0 {02cat= x7 =2Q\ǵC8 f'0~` |}A.T>g "k)vWc# gDn}0| u0b. L!n36WZ8&gq١G_u>)LZF4ti}A )Yǎ+A s03ee Bl:Bn ѫ>9Ba.)M,;<ܗp/r]SX鄷ޚU`g ̼WM!$(a:0+OpzuM!$(h:"P4IFND^ImV{u]EA MGrwW^tB#EAX4@tP!M!$(h:"P4TEA MbtB#EAX4@tP!M!$(h:"P4TEA MbtB#EAX4@tP!M!$(h:"P4TEA!$l<~OE%-~YB!54IS4B")B!MGM!#iBHAӑ4EA!$H qt$MtB8h:h:!D4IS4B")B!MGM!#iBH 5jlZeO6OGV?Zm^WtB85{O_W1TTYKl;Ujs7mҡ ZֶuM!#P1ltrD@:/%'^jk,i:bEA!$xUӡ*W(&m:u'?zÿ!Lǣa+|c*>z|Vh:!De:)Kooq77_TLՠѓ܃4̒-˙;Zu HRUUO:6cS:vW)S Ug:GL)^}>3eVzSw>]%o>/Z^Or#RNl.̫^ q$8qMwY-d>qzZyY7cAtmP|͎wD7*WSK7V[c!cF%k614MI5Ւ;UBET4i+Fu6;jOxl+W4B"0H mӹGT@(þ3yG*Vu-XM*ۈ`ۚl ahҲ&et(]IЫiKz,a|ر݉x)3xW+^H_N"B!GyxWq,U@vRP Vm9Ȓm^5+亰|W`DA!$t|ٰ{jQS0;S?R3`QEITyt0BjuJbu q6L±uda}$=Jȱt98?LaG%bBH+B!MGM!#iBHAӑ4EA!$H qt$MtB8h:h:!D4IS4B")B!MGM!#iBHAӑ4EA!$H qt$MtB8g?K*i,j~V/de*zQ'B_HQaCB!ENtBq..pBsqtS4B# \\4M!h:!8MG8EA!Ĺh:)B!ENtBq..pBsqtS4B# \\ nÆjjBEu'k꾪6ݯ:vn<Ķ/>=i|&wYդfY4B+ӱnn*U*Q*UUuT TdTtԶCڼϘbNQy˶/>y;x,OT$nEA!Ĺ4dɖME/ĕk*k_|mkp=Oʱ}ɗ8t:ynEA!Ĺ4Ϝ|ܴ}ędmrzmOۮl`,ӗxUp}ah:!827nB={mg㗯/r(!A[+7oj۬u=yI"5YUrwP9լKu|ʮǽڛҫݥ?Ru4)$}nM[sd?EA!Ĺ4P:ue-]E)V\ z!69w y!*}4X%d:PѶMNbZjÒXDWpͭwPcNW{R)SБDA!Ĺ0"xy"z@ 5Kk32[bNQnL7}`/[`*"m;wQvzxA\Qoh2^c}=pH \\DA!Ĺh:)B!ENtBq..pBsqtS4B# \\4M!h:!8MG8EA!Ĺh:)B!ENtBq..pBsqtS4B# \Ęz=m'B!B!B!B!B!B!$3 zx]wn&3M&'֧6k V^#Xa99E#uٲpL`n]?\ Y~wMSmu>7_uq|_mu6MMFunmͳf%KZD _`J:>ػ2!mӭ]sp3^ZUC2P6mU:ԩQT:yf ۺ2\7Lh&]TPΚ?>k/g:qC*槗Q/FSlsQBdjV`mV,sRd {:?zlYd˟R T3ʔ6.lfc?cݚUBKfNԵm;v\ɒqSx$W6ZSVt)3kBeFUOi 0X<<]ڳ{PW#52}:ɠh[2-[ôm~9 kDKfJ RǕӈޮE85t| +ϭ is]Vk;'I:OKg4RܵE6g&yQ߿֓QrewRq~ͶX,(Nj4H4R|Po#s 6lnvK!vOP`)ͻ)\Oz4{;q|Vp/Z$װnuW&:Bo>Ԏi^Sy:G3X'W ڷޝu~ -1osj#ϟ.kTtZ4i B=n~4r u2; 8iI -4*P>n:낦ϗ?4gLU6tN&f}-RMPO瓬RȤ!ꟃB.8չm15 xe5XJ̡e5GΡ`@q K1~Ո6-NfJ ֿq[8osB 'b##o4%b0`B{L%pet\$Lr\\ F4K9sZֈs`D^pdQ???B.S LZ,(NzDO[_ Jr%pey,HmBn\f<\W)#^LOl|j;͎LҠvyc|(gYfܾ"}k_`=]߱x|爐ݙڑ/%nT+3,O0XP(,P@W:Ow/p<Ԯ^fiqu2XiJd0=ɍ yyy_:O{'խa6̽,tL~Y.HW (AF4J/L"?̃Z;4MAg1rR t\}w`fU3ۺ [TE0~G ܹܳ ;/N0(" ' `iIܼ)^ͪxG2XqrPҋNS<){LX0^ЫuK^:V^rh2.RɻQ,'K5$ӣ:4J#sn_2 ?RdNG0kiսKmz5lZwE%C2yI0XPH`&кd%NƸiK2dJ 2GzI@kg_*?ViIOYuJQӻ,J0dt#jFT'}h.`@q V&I 4TR `X)/G( 'B0XP`B ,T`@q U,(N`P  *Ta8B* '0XP 0XP`B ,T`@q U,(N_jPaa1B!~PYO[k ndB!2B?m&T)n{]Y B!P& Q`B!I`@1,Be0Xe  !B$ DB!P& Q`B!I`@1,Be0Xe  !B$ DB!P& Q`B!I`@1,Be0XeL˷n2w<_)guh[B(c4X;V6^lvhLo.5=?כBB!+ QƤhNc^#om`B  I`ݱfLh]B!2T,2 63NyMͧ/NX`z_[ O^[l.ri|R ӨAu3Y3g0m[5:ܵnl|g~顙 ˧l+t1'֩b?OsO5 GʹiԱ٩yBBB )%.u9mcf}ǤQVX|ji}͒k꙲eJQIjڸ)S5o7dMwALl"\xCNmh F]|ٰ[^*rfƄfټc_2<B!T`@1)fMj[ӑfM왰Yxj <7VsrãVxm|;oszSnUb_}SwMדq;)֠6XR^!aʕ-m~ʢ/ΚE`9B! A,2&EU %62P ;BYǯüϕ wQvݫ^c5ԢiP=O d J?s l5!B(`@1)dCy{xB9[F&s&J kݼ]}(;K#HP1XD)T?C®vwGfAWGG3XYn:~O1G !B( DSKε;ܯ64jT!{Z˕6ldk?3ф(Q†"uvyWI9^*,wnJʔCŝ 7M7u]AB0XeLc&v4Hzp[_z2>Jɮ=[˔][K*$e#TY}oMd2lzOUb B 5Z&&S[իo˂K!:ƸK:{7ePXd 1ި ,xJZ$ B!ҬγɷzLQYJb`@~ V~fRO;Iwk<۶K%TCҎ!Cwk%îv)̄z_k~B ̩'ײu+H^PFV$ B! I4XUKezP:P]=2u80X&IURBdI 1T;|yIs܋Rb oC!P!P0dB!0Xi  !B$ DB!P& Q`B!I`@1,Be0Xe  !B$ DB!P& Q`B!I`@1,Be0Xe  !B$ DB!P& Q`B!I`@1,Be0Xe  !B$ D4&B(Mz . ֗ 2bB(`!TPa bB(`!TPa bB(`!TPa bB(`!TPa bB(`!TPa bB(`!TPa bB(`!TPa bB(`!TPa bB(`!TPa bB(`!TPa bB(`!TPa bB(`!TPa :ffEQF4[gwCu{/7{5;.5{oOxl_曡uA꫶{<ZfGjUܗc+ ܷnÕWu , D 3MeMlSSR%SvmƍtmX *ճƪBrfPiW}}ӡuAkwˬYu'{իZpJz*4 m۶5 צ vc]jU(v~sm{_kc͕ɌVXі;S80ҝ, D ւIlriFΕ(3(2)<`hLօ;e$.:{-)K,"_`}쳶sia>Q`/l'k޼>{gB [N8iA޽\^PtKmڶf'l~~L>ֻy>P蒛:C渒%MM͟z+^ȏ1e:}vRɨ K0sǏ7bfO2,"_`v8{q!5XԦ/T~,:Vv\M 4rUfM.@ KnKbͳ[uQϞ pYgVUFewoSF syޙgG֮M%mbjVfNloof:;Dm%u=;\7qbB}=z=me[`"d4(7n3wx=:ڴ̕Lo2PK2?h|>s|1?''ݯuOjմ+`=ۖCN{Wezt>u`)IE%W_m)}wKDS_ , D$ ֿ8`35 nd-bd4%{f@׮!l3XbJ]oՖsN֦iF;@˂KǐIR( l?- 5}ɒt2X0XET2XZ`wޱG2X-4n]Wr5-HMqs$:fT%|sshX0ل2-c|50vW$2sZ5X6Kǖף %7 PXUӾU:_ S]gh9YPB-_=rdޞnH3X~dԋpjD߯S~Ka1:ޜC*,E4re`iNa~gl0Xe"dd0rh d*Wf/ KR M?G`POv'L`)&:s_" D$ Fp%g8[J7l2#jr.,Axm4I~] {U\9tn!:,9e1݃ ,T ~/۹ ֖_B4GIʮey6wVM:4-Kfߟ՞hwa Z׼]պ DI-Ԯ(`@tM`Imq W]e&7 o$&rgZ.kz(zCwM7 zAgNS .srr?| y)E^KP,"&k2vrü^ȫ`i?Z>jT'uKpNcps`)PjpTo* D$ k`a)Xܶm6B!JZ\%M0v2^2 4w4%)Mz_[υI,=h?2m  7X2 [ }K0Xe0Xvyҥv9, T>qA׷߶Iz]{4Cn@i KnK1z 6Q,|Le|'Sl$O*wKs ~*ܙ1SnU~K,h%}RC *?J$)`0u:ݿk ֑)Q,P,?J)d/]I.;aNI5Is\fY?Ʌ_i4J/hve8E krwaЅ蒛>ٳ4i6\?*?%AB)kϢ$ȭ2%p#TS*c@/cV̿UP /SBPTTg]Y 4z@[w ݄zϠ L!o47Ljs"aeyi[gQZ蒛 J FA}02IHMڏ7OVzϔL_s{/Yt,V_[2\VKN?P2 սe0X]r `2UJ bfCBJQӷ#riDߏ@( D,2X P_)l7 AB+ݻCH!ާ?.Ba fRR?,*TaRB :0%R-!O,.YdB(#B`@t`!PzB`@t`!PzB`@t`!PzB`@t`!PzB`@t`!PzB`@t`!PzB`@t`!PzB`@t`!PzB`@t`!PzB`@t`!PzB`@' ,}[})z{VI?ؑV پ;|Б/a `e0XL a2V,~0X) +`EE  Vf Da0XQ+c`a2Q, VTXa `e0XL a2V,~0X) +`EE  Vf Da0XQ+c`a2Q, VTXa `e0XL a2V,~0X) +`EE  Vf Da0XQ+c`a2Q, VTXa I`]yB3eւo}Pcѻs,]hY|PyTZ0x3`@Z;zٿdIhdv?rKh]a*Y܁OBף4}s[1ͱX7>=?ZU%k~reZt?h>axvɓͼC六(}N.=eavƱcC噦T oCכ,:{|ݿET,~R1X%;69 e|shdgԄBQ;ʣT VeCJ(a.2 +L%kx l|%œ߽rus4s~h]TzB _nwN6]Ԯ)六(e\}խRżhQhdTzusީ3Ms;u}GAj~_u7жɨ񩧙Vm'# @I`q9rC7R i' Va\{nGwZb&LZ'7F:v$K' Va`lк:fli뼿lYh]~fu𫿄IS9~jh]2`%O !3phBFZW]BJ!#KevZW:1P\<[֭͟mKϱ ۴1W.%;[g-e I՜BMukH=h5ץg{t v}eL]_qkܕ샙tMmy.۰ӮsQJMG4iYSryTG5R֧?%{>穳0X`IUװf-!5v;EI(f \unc&ϰd4WJ]&ڛ"m;wɪå ;5JF깋l]=Pz]]Ͼa z_=кFfQj,u7ߟ0Jݼ6kzT.֭T|jۨ9vP11!r1S#f6]~ 1Rx׉Uû"[YE+ن)ї:2π5(k@\?7yuB_{vu7^~ה+_7fuZ}-;[,]MFMkk#{3o魶 v5d-SI]ch5̡%t,]7ֵ7 Am#oV۞|r|YKۖ/SLl70z][udS }禛B:`36+Vwwm=~fD2,tн@n]^UG}?:jjWlCuT: Ի[?o[+זhZ GkDtoP(CQkβ7|D+7C&,s`2W%K4;v?v*~RO?UwveNlp/h]q=t}a2k5k׵fNm7>'Yau#W`=z TzUe2X* flS%=4PlYí!R\p3lvY Qc852vϤFJ|Y5$T V՘AѡCK!}仇))U%szht[> oV[:?=~=t,eS=hYZx丄zm\9sey^tX[<$mQ V5ͫu`Ȑd4Nd z)`QBA|*UuٯMS|y~ۤjTW>X>}l3Ve4 '?E`i4onҽSƷI`2t</׭e2ҥz^mJŮTGE6XjoTuDmjԨ^yw׽@N,?صQ~hmvY#zTTW_k=S/zl|l ֻ?}O$ d?6XM[ {P'ˌ=,/%^`zUWLKz{@^x+}PG zTo2&jYy/|~QȄnCME,=05=HU3l[C$DyʕVWm~J5:zxpev`=e aV'+`I5kT#3X'?0DÅ@>J@׭N#v㦪T F9ukTezumR5X 6z,?7嗑3]Ub`瘱puΛg:*E`ըX~ﮓES3A#j8qbf]fW.NI*st,eUmߑB k"\ows5j5­oW&I<{+SR!N=mBA{?MQ,~m˭`F`;ɍ`id@N?,{wo2Xz@2=*Jjd4?,5 r Zq*ժz&R5X~]SXÔ=X 1ܲTWZV8-?6sfhNw`LB7aGQ&V'w$e qKf?'e5:teП]zz\F\fs|HLZ$oS*U qyv5攪ҵדdyf|Y]ջ򱌪JQ2XOo5VQ+N3Ouo2$^4-#l0nk0W 1X[gdtQGF׬VqWOK׹G|Y4'ge-OztsysgC$]G( d?4X 2%)Ne uEv>B \ zdTҧ9'XR蟓3Xlՠ)A /rY^ Crұd8Qu_0ZxW$,IiKBb',=\ֱLhŊP])4-+Q˽ZW(K0XO 1&Pq~K&5B?(SF=}t=jO-ku+97cjʃ5Z#=͖ϣe]t,,'h]IG3Xj ,$mƗݹ(F2(K('u!`$gPJ<ٕsyv\;K_ ZJR]XU-}@mMiZmLt,EMhD]pC|iIR;蟏v̽_K(aҾްbS|+h>J:ߥ:(B)0X`NײnZV-7خsay,ŀ^HV*Ρуi^sK<.=kn -\x׺e͓qNs50W=J$t,̓}7J~}o¹ժ&_ѭ}͉2;+0&`iQ6qu!u.Bry,]?QΫ+Acq :ܜBysKԈ\&kN:1im+XS>`)W iKe<>~h7X 5Ö- u[vxdP&.X~Zzwsd,*K\I*R!||5mn5*@sN9%-ۺ]}@{ώE4X2ZNBWR8)lo0Bu4?~ 5/`)9XOYx7'%G]kYmڶg^tS'LWITu<'K)z5 ƭ!Bs5Ņtݬ螤+Im) v@QHuk"\c T ꤇J7Cy^zUr6LJsԻy#.\+L2Nʄ԰uȬQdWXjt,5jܾ˾Jz2\jet> Rf@Wu]juHWԠhbzՀu'?5RjP?WT.%)AGF~I гf(d0Z`x ,+bVg h$^c攪?x]0:]ACb4R -47P:erWؠ:]W2jdu w )Puﰙ cwRiS״Fp}*Os>ݨKBu2.ЅJXjOʔU B|*Sڌ}0W`u8{]hۑ;P7:f.LU`d^;4mjp>Ʊ߾L޽wyZ*Y,Mj#S$"_^CA,}aX BV}72.~ T~ߙjO6a;gR۪\-m/ât?ps}et/ӫDt,mpym|鸚+WLlq{c\ԙ0XbOvUB?H2*划jrm|a̗FDKonͳRxU uRæ,IjӃK)=tjtJuԨz ջjTWߊcdkh)%ne* B..!KﰩYH2iƱcm=k2jY!U R]fiDK?q$5a+Y%k_ f)Bj] zROOh{Q+Ia gUE%2[2Jz`| m(K׻t\}=2rv:sPFPk۪ z,pkǪd QR M4(*DPI(tκ5*5[7\e{ ʘQ[㨪~k_J|9l~(۱2%QB/YbR80:Nduc%񥿇3ER1X}e4RZDsku0iYs95ՕoZuq7 /ddvn*߽N9"^6&aW#W sWak|_ܽPInLBϩz wGv( d?,TtJ`e42qYڜ5XhDi`^sRQ VI!m+{aa*T,~0X,HR+3U̙^u`Cu> `@LE`=7o KRżlYN*`efR4 :4N/6ݩSh}a `@LE`}vv؀mm}`efz%`Deק* VR͓S__`e0X`2SQ3X+35na# V +3`e0X`@La0X( +*`e0X`2S, V& 0X+ d? `a" V +3`e0X`@La0X( +*`e0X`2S, V& 0X+ d? `a" V +3`e0X`@я#?!,}'c z=w~}a{UqO۷Eşo-|Б;/5~|tP#T?"r *a,"Ut`d79 `0XMnr0XE' @v*:a V `N,&Ut`d79 `0XMnrR4X<=a9S's!#.5+7o1~]nA|F3뺅͔טG`|]zE* @vyoe9;y\ݶTRs}o%l۬EK; 0ի^3p0a睦udɒUO=,z뛫ko\.IW/b:vjʼnӦۑy%% @v)_rH:ߛ:u6%;}f5M5VRԩ[TVݚ:ڤwc Y1lipv[g|w6EPpOV\x(/f?Sމ֔xak $im|5+F3 ʬ$ڼֹf Ͻu&4lxLKz} +|PO#J: _` /bx]Y=ܻ@Bӈ ``Ÿ+8nLMk4'!I2IiC|%C7l~l-K`M>îzB_ِF:~mG I`ZlX5[O-WҋeHr!2Xww^Khk]ըUGFNjbu M ?ھ* @vRBZtCB|J~$(zY |/!}C*O`F6aIss>0XMNKRb %8R;y -_0`MH \)etw>&PA?L9h9/Yبqz9ite=)|P /^yк $i=/V݆ҽ tB /2b uyc5J-Z%c^|=n$L :7z~wnӳz:_ͷ6G I`D)El;Jdқ\]}iAQhBj׭kʔ)瓒1Xi-ObWLھk΍I'7qzsdF^ sTVC}6t)S]65/* @vEi~=ܚwׇlfS5u\u{rOB.={ WO mLAw#%å2PG5=`iP n^n.IsR3ԪSf->W,&' ( @v*:a V `N,&Ut`d79 `0XMnr0XE' @v*:a V `N,&Ut`d79 ǃ9* ~@hPۃIENDB`graphql-ruby-2.5.19/guides/object_cache/redis.md000066400000000000000000000047241514115062600215360ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true enterprise: true section: GraphQL Enterprise - Object Cache title: Redis Configuration desc: Setting up the Redis backend index: 3 --- `GraphQL::Enterprise::ObjectCache` requires a Redis connection to store cached responses. Unlike `OperationStore` or rate limiters, this Redis instance should be configured to evict keys as needed. ## Memory Management Memory consumption is hard to estimate since it depends on how many queries the cache receives, how many objects those queries reference, how big the response is for those queries, and how long the fingerprints are for each object and query. To manage memory, configure the Redis instance with a `maxmemory` and `maxmemory-policy` directive, for example: ``` maxmemory 1gb maxmemory-policy allkeys-lfu ``` Additionally, consider conditionally skipping the cache to prioritize your most critical GraphQL traffic. ## Redis Cluster `ObjectCache` also supports Redis Cluster. To use, pass `redis_cluster:`: ```ruby use GraphQL::Enterprise::ObjectCache, redis_cluster: Redis::Cluster.new(...) ``` Under the hood, it uses query fingerprints as [hash tags](https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec/#hash-tags) and each cached result has its own set of object metadata. ## Connection Pool `ObjectCache` also supports [ConnectionPool](https://github.com/mperham/connection_pool). To use it, pass `connection_pool:`: ```ruby use GraphQL::Enterprise::ObjectCache, connection_pool: ConnectionPool.new(...) { ... } ``` ## Data Structure Under the hood, `ObjectCache` stores a mapping of queries and objects. Additionally, there are back-references from objects to queries that reference them. In general, like this: ``` "query1:result" => '{"data":{...}}' "query1:objects" => ["obj1:v1", "obj2:v2"] "query2:result" => '{"data":{...}}' "query2:objects" => ["obj2:v2", "obj3:v1"] "obj1:v1" => { "fingerprint" => "...", "id" => "...", "type_name" => "..." } "obj2:v2" => { "fingerprint" => "...", "id" => "...", "type_name" => "..." } "obj3:v1" => { "fingerprint" => "...", "id" => "...", "type_name" => "..." } "obj1:v1:queries" => ["query1"] "obj2:v2:queries" => ["query1", "query2"] "obj3:v1:queries" => ["query2"] ``` These mappings enable proper clean-up when queries or objects are expired from the cache. Additionally, whenever `ObjectCache` finds incomplete data in storage (for example, a necessary key was evicted), then it invalidates the whole query and re-runs it. graphql-ruby-2.5.19/guides/object_cache/runtime_considerations.md000066400000000000000000000056441514115062600252210ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true enterprise: true section: GraphQL Enterprise - Object Cache title: Runtime Considerations desc: Settings and observability per-query index: 4 --- With caching configured, here are a few more things to keep in mind while queries are running. ## Skipping the cache You can set `skip_object_cache: true` in your query `context: { ... }` to disable `ObjectCache` for a given query. ## Manually adding an object to caching By default, `ObjectCache` gathers the objects "behind" each GraphQL object in the result, then uses their fingerprints as cache keys. To manually register another object with the cache while a query is running, call `Schema::Object.cacheable_object(...)`, passing the object and `context`. For example: ```ruby field :team_member_count, Integer, null: true do argument :name, String, required: true end def team_member_count(name:) team = Team.find_by(name: name) if team # Register this object so that the cached result # will be invalidated when the team is updated: Types::Team.cacheable_object(team, context) team.members.count else nil end end ``` (When the cache is disabled, `cacheable_object(...)` is a no-op.) ## Measuring the cache While the cache is running, it logs some data in a Hash as `context[:object_cache]`. For example: ```ruby result = MySchema.execute(...) pp result.context[:object_cache] { key: "...", # the cache key used for this query write: true, # if this query caused an update to the cache ttl: 15, # the smallest `ttl:` value encountered in this query (used for this query's result) hit: true, # if this query returned a cached result public: false, # true or false, whether this query used a public cache key or a private one messages: ["...", "..."], # status messages about the cache's behavior objects: Set(...), # application objects encountered during the query uncacheable: true, # if ObjectCache found a reason that this query couldn't be cached (see `messages: ...` for reason) reauthorized_cached_objects: true, # if `.authorized?` was checked for cached objects, see "Disabling Reauthorization" } ``` ## Manually refreshing the cache If you need to manually clear the cache for a query, pass `context: { refresh_object_cache: true, ... }`. This will cause the `ObjectCache` to remove the already-cached result (if there was one), reassess the query for cache validity, and return a freshly-executed result. Usually, this shouldn't be necessary; making sure objects update their {% internal_link "cache fingerprints", "/object_cache/schema_setup.html#object-fingerprint" %} will cause entries to expire when they should be re-executed. See also {% internal_link "Schema fingerprint", "/object_cache/schema_setup.html#schema-fingerprint" %} for expiring _all_ results in the cache. graphql-ruby-2.5.19/guides/object_cache/schema_setup.md000066400000000000000000000114151514115062600231030ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true enterprise: true section: GraphQL Enterprise - Object Cache title: Schema Setup desc: Prepare your schema to serve cached responses index: 1 --- To prepare the schema to serve cached responses, you have to add `GraphQL::Enterprise::ObjectCache` and implement a few hooks. ## Add the Cache In your schema, add `use GraphQL::Enterprise::ObjectCache, redis: ...`: ```ruby class MySchema < GraphQL::Schema use GraphQL::Enterprise::ObjectCache, redis: CACHE_REDIS end ``` See the {% internal_link "Redis guide", "/object_cache/redis" %} or {% internal_link "Memcached guide", "/object_cache/memcached" %} for details about configuring cache storage. Additionally, it accepts some options for customizing how introspection is cached, see {% internal_link "Caching Introspection", "/object_cache/caching#caching-introspection" %} ## Context Fingerprint Additionally, you should implement `def self.private_context_fingerprint_for(context)` to return a string identifying the private scope of the given context. This method will be called whenever a query includes a {% internal_link "`public: false` type or field", "/object_cache/caching#public" %}. For example: ```ruby class MySchema < GraphQL::Schema # ... def self.private_context_fingerprint_for(context) viewer = context[:viewer] if viewer.nil? # This should never happen, but just in case: raise("Invariant: No viewer in context! Can't create a private context fingerprint" ) end # include permissions in the fingerprint so that if the viewer's permissions change, the cache will be invalidated permission_fingerprint = viewer.team_memberships.map { |tm| "#{tm.team_id}/#{tm.permission}" }.join(":") "user:#{viewer.id}:#{permission_fingerprint}" end end ``` Whenever queries including `public: false` are cached, the private context fingerprint will be part of the cache key, preventing responses from being shared between different viewers. The returned String should reflect any aspects of `context` that, if changed, should invalidate the cache. For example, if a user's permission level or team memberships change, then any previously-cached responses should be ignored. ## Object Fingerprint In order to determine whether cached results should be returned or invalidated, GraphQL needs a way to determine the "version" of each object in the query. It uses `Schema.object_fingerprint_for(object)` to do this. By default, it checks `.cache_key_with_version` (implemented by Rails), then `.to_param`, then it returns `nil`. Returning `nil` tells the cache not to use the cache _at all_. To customize this behavior, you can implement `def self.object_fingerprint_for(object)` in your schema: ```ruby class MySchema < GraphQL::Schema # ... # For example, if you defined `.custom_cache_key` and `.uncacheable?` # on objects in your application: def self.object_fingerprint_for(object) if object.respond_to?(:custom_cache_key) object.custom_cache_key elsif object.respond_to?(:uncacheable?) && object.uncacheable? nil # don't cache queries containing this object else super end end end ``` The returned strings are used as cache keys in the database -- whenever they change, stale data is left to be {% internal_link "cleaned up by Redis", "/object_cache/redis#memory-management" %}. ## Object Identification `ObjectCache` depends on object identification hooks used elsewhere in GraphQL-Ruby: - `def self.id_from_object(object, type, context)` which returns a globally-unique String id for `object` - `def self.object_from_id(id, context)` which returns the application object for the given globally-unique `id` - `def self.resolve_type(abstract_type, object, context)` which returns a GraphQL object type definition to use for `object` After your schema is setup, you can {% internal_link "configure caching on your types and fields", "/object_cache/caching", %}. ## Schema Fingerprint `ObjectCache` will also call `.fingerprint` on your Schema class. You can implement this method to return a new string if you make breaking changes to your schema, for example: ```ruby class MySchema < GraphQL::Schema def self.fingerprint "v2" # increment this if there are breaking changes to the schema end end ``` By returning a new `MySchema.fingerprint`, _all_ previously-cached results will be expired. ## Disabling Reauthorization By default, `ObjectCache` checks `.authorized?` on each object before returning a cached result. However, if all authorization-related considerations are present in the object's cache fingerprint, then you can disable this check in two ways: - __per-query__, by passing `context: { reauthorize_cached_objects: false }` - __globally__, by configuring `use GraphQL::Enterprise::ObjectCache, ... reauthorize_cached_objects: false` graphql-ruby-2.5.19/guides/operation_store/000077500000000000000000000000001514115062600207225ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/operation_store/access_control.md000066400000000000000000000025311514115062600242460ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: GraphQL Pro - OperationStore title: Access Control desc: Manage authentication & visibility for your OperationStore server. index: 6 pro: true --- The `OperationStore` has a built-in mechanism for authenticating incoming `sync` requests. This way, you can be sure that all registered queries came from legitimate sources. ## Authentication When you [add a client]({{ site.base_url }}/operation_store/client_workflow#add-a-client), you also associate a _secret_ with that client. You can use the default or provide your own and you can update a client secret at any time. By updating a secret, old secrets become invalid. This secret is used to add an authorization header, generated with HMAC-SHA256. With this header, the server can assert: - The request came from an authorized client - The request was not corrupted in transit For more info about HMAC, see [Wikipedia](https://en.wikipedia.org/wiki/Hash-based_message_authentication_code) or Ruby's [OpenSSL::HMAC](https://ruby-doc.org/stdlib-2.4.0/libdoc/openssl/rdoc/OpenSSL/HMAC.html) support. The Authorization header takes the form: ```ruby "GraphQL::Pro #{client_name} #{hmac}" ``` {% internal_link "graphql-ruby-client", "/javascript_client/sync" %} adds this header to outgoing requests by using the provided `--client` and `--secret` values. graphql-ruby-2.5.19/guides/operation_store/active_record_backend.md000066400000000000000000000073621514115062600255340ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: GraphQL Pro - OperationStore title: ActiveRecord Backend desc: Storing persisted queries with ActiveRecord index: 2 pro: true --- GraphQL-Pro's `OperationStore` can use ActiveRecord to store persisted queries. After setting up the database, it will read and write using those tables as needed. ## Database Setup To use ActiveRecord, `GraphQL::Pro::OperationStore` requires some database tables. ### Rails Generator With Rails, you can generate the required migration then run it: ```bash $ rails generate graphql:operation_store:create $ rails db:migrate ``` (You'll have to run that migration on any staging or production servers, too.) Now, `OperationStore` has what it needs to save queries using ActiveRecord! ### Manual Setup You can also create the required migration by manually by generating an empty migration: ```bash $ rails generate migration SetupOperationStore ``` Then open the migration file and add: ```ruby # ... # implement the change method with: def change create_table :graphql_clients, primary_key: :id do |t| t.column :name, :string, null: false t.column :secret, :string, null: false t.timestamps end add_index :graphql_clients, :name, unique: true add_index :graphql_clients, :secret, unique: true create_table :graphql_operations, primary_key: :id do |t| t.column :digest, :string, null: false t.column :body, :text, null: false t.column :name, :string, null: false t.timestamps end add_index :graphql_operations, :digest, unique: true create_table :graphql_client_operations, primary_key: :id do |t| t.references :graphql_client, null: false t.references :graphql_operation, null: false t.column :alias, :string, null: false t.column :last_used_at, :datetime t.column :is_archived, :boolean, default: false t.timestamps end add_index :graphql_client_operations, [:graphql_client_id, :alias], unique: true, name: "graphql_client_operations_pairs" add_index :graphql_client_operations, :is_archived create_table :graphql_index_entries, primary_key: :id do |t| t.column :name, :string, null: false end add_index :graphql_index_entries, :name, unique: true create_table :graphql_index_references, primary_key: :id do |t| t.references :graphql_index_entry, null: false t.references :graphql_operation, null: false end add_index :graphql_index_references, [:graphql_index_entry_id, :graphql_operation_id], unique: true, name: "graphql_index_reference_pairs" end ``` Then run the migration: ``` $ bundle exec rake db:migrate ``` (You'll have to run that migration on any staging or production servers, too.) Now, `OperationStore` has what it needs to save queries using ActiveRecord! ## Database Update GraphQL-Pro 1.15.0 introduced new features for the OperationStore. To enable them, add some columns to your database: ```ruby add_column :graphql_client_operations, :is_archived, :boolean, default: false add_column :graphql_client_operations, :last_used_at, :datetime ``` ## Updating `last_used_at` By default, GraphQL-Pro updates `last_used_at` values in a background thread every 5 seconds. You can customize this by passing a number of seconds to `update_last_used_at_every:` when installing the OperationStore: ```ruby use GraphQL::Pro::OperationStore, update_last_used_at_every: 1 # seconds ``` To update that column inline each time an operation is accessed, pass `0`. **Note:** It is recommended to set this to `0` in test environments, to avoid delayed updates in another thread that can cause intermittent test hangs and failures. For example: ```ruby # Update immediately in Test, wait 5 seconds in other environments: use GraphQL::Pro::OperationStore, update_last_used_at_every: Rails.env.test? ? 0 : 5 ``` graphql-ruby-2.5.19/guides/operation_store/add_a_client.png000066400000000000000000001451251514115062600240260ustar00rootroot00000000000000PNG  IHDR֠/a|iCCPICC Profile(c``*I,(aa``+) rwRR` ` \\À|/JyӦ|6rV%:wJjq2#R d:E%@ [>dd! vV dKIa[)@.ERבI90;@œ r0x00(030X228V:Teg(8C6U9?$HG3/YOGg?Mg;_`܃K})<~km gB_fla810$@$wikiTXtXML:com.adobe.xmp 414 904 1 VQ@IDATx \Ud8L2("<琙٠נuoVn--oV6iYZVfuS#s!QDQ<߻8j~rYkw_~z]FFF1HHHHHHj=ZO!>>999(.!4K$@$@$@$@$P(Tzz:ѧO4nvvv"~ MK&&&}a I     ujj-7Fr > b ̘se5&IHHHHjbq     @{J$@$@$@$@$@5JJAjԫ>c#3f̨;wN;B:5cոlقC!99Yk-Za}78qkye^wɓ'5֭FzU[[4D$@$@$@$@$@9sKU2C%`իWEիL|\rEjսoϞ=Op1dee}vԯ_6mBTT&͕*9HHH@6m*qF̙3GV4m۶Ƽa_  Y]V\$o X awڭۅHIq8s.uMWBFr,Gř䋨SnuK#~% n>+pUYi)*tJ_ʴG3#WӓҥKw=)), Mu{J~s=ooo&RRRkiu{VV_rbk>aj{v^[Y͍y$T5@qcT1I%֯@cxQP&w :ʚ@UhK>S``H.'agU>$ѩ򔪏0Ԕ8[ |Qxz e~y,{v5ipFum(=-xh%V'jopS"c w0QX}C1S/Mdq;a!WkL㵰rh~\2o!L6˽o\@T0wD;ǣ="Z52`O?&o^Чb NJX!݃ͬlv&wUX;p_x?~[/xg H~b} )-^Pg$ngߟ k˶ɻhӹl-^ZwBrՈIUX87AFױbjc$std\'圈 zTl#3r!5'үB}9ڥY@J,Zzr~'^㐞%v!U 3ω`KFʒ>ƛh,IhM|Kn-q蝯͞_Ҷ27#Uޫ3\Sc|؟!gyn_h!Y.f-t@&A1PdocH D 7Q`id ǮVۿTIpC~:b%=)}\'wFRNXVS< IM .#z"%)h,Ή[Jry@$Nc{/+b{:Ib<:G" rv&i,ΦW8Wf<    ~G8{O /TgZh*MU<>>>U=ŪilJ-+5ƩS(O"QE5@<-j!.~3En%M^j1SIT|[t@ "vÔPL]3 b"#}jgM>'Wie[k1~xiw7ScNM+;lp<1~}ySÝr6]u: A֋'ֽŻ3S9PWc)늺͏?Wޚ4ZgWƤTRG|x 6x4#B|:"gb#l<,1m4v1x$V>zDXApKb#ˏMC뇧ogp,yevp@p}2z#g~D=:ؠDl/<-_M`9 &me&͕,err`n w}N?)S0Y{eJ@p wоtvo-$=waўV1j8(.8y mwcp#h#&)qR썙IG6w۱='u GDqLHƑ upB!WJ'=js֘x$`x6D_FԵ#GF.FohVxE"@U6z /L8XeZi,O\a[u0mxe#ĐJJo' ` CMyr5J\f0Cͬ'-lZ4Í-0뷒'+W7HOOÉ b˩84e2w_(,l Ũ~-LUBumf݆cam~n62(/4Ć!1-Pl#t(F\AAFmFOi1rSy;U 3.{RuN∜/f#5I]UCIo-7&hnөoX(#addҥ$YϢ,bm  {_t wD0jD41C. #iӢ^6Sq٬Saft0,ԫ9uD%^[ٷ_PZ!,9a9Ptp܃nr"wD'@#qj?5am"sGGƋ׆ <KbAR~N%Umkp:ܫ'ōH[.@m#WġJ!g7[etvxe!l]3ss)MeP+d@1_;배0Cb@ZfV:W6A$@$@$p(s"3cX'P1CTKMUkM*"hyMroV7|pSTgL~%:s[c%ל+n56wiE:e#%n 6|MPGʅo`,R5[%`˝+:<$  )TD%Ի?c-Jz-q%^kj׮vzaeI-<|0.\PY*wM[2^eQYRuRuN?OZT/+eayʗD(ϕ@2o`H:Kcgf1>}Uz t18ʇY",Ms*L9IdjK F fkrZ?T 7^rK}xHc@w-1~X ],v'h&[]ѧ[zQ%h z6{(,Z )iHI؏%VYhnP>XCH<OCWFk :MܻphjBqbL(?n cܿTg d,=71nA©ٗ>L2V ۯrI,5.mŒW]Pt1?8$ɼ}Q1pMde=zy!?f9>[ صNҺʌ鋫vͬٴz` |lOA}kcLحcZQ?@~[!?#3> D5[>!w-Y^ᭌshmc3tXKJGO`X$FwFS%סο+d*D.ez ^7?TmT2WU \t$rS'Ԅ8TոT@Kj5hZnHݻWzWbcfѲw~\^=e>"\1xx,i8Ʌ;1PLĹ"}JmMWʲ)XTeDAtsxyB| -D-T]C;=Ŵ[{1A "!{x nhh߃qK|f,l!^~wO0{oy'']'7c8C`Lxt9)wkEJIGyCNY ;`tW,vbv=3FΕFD@J L.JtJ?cnū/ժwnpw6duX\Q >$DwFIDzQ-1:ϖlhU 38enU04/FԚ%CP%üU9i[OƘ+K"j=NcpO/_q/')HY}vfyWDK^\^i(p CCeTTU*i c> KT%خZ_.WKFF߫6%xM6K۪% j-FG'.Y"zd$VHH*]3hz6EGcqݸ;U8WmHHH"`Q\Q><=6le],Yjɢe=eTBw%Z-}qFU> ZAAAZqxRyx t,98٤,VlyAnwDU )7KDmQCv鳐a\Mnmddp: s'{g4ti8 s_"BJKCeo: _,8{y($l 4Q>o!fbTΰ[=.:Z۵()mXwq!1}-3BW _(NOۼ__`厵NzՌP/}WSUQMUMx YV<ͩR |+m]㏶miͰ,AVN\ aC_vA׺^k;8'$)U\ HHHFؒV YzwҚjj//-[@ųgjKO',}TKQk* 8PVz7i Hs]wiJ+V1wAr%+Y[JV*+\L熆\ɩ.hذw;WAyuPH:],S,"9  K`7TD)ۢP WbWF\pW.r\B37[2j|u|X:Xג03iMR+{9v,ulmۢ5Wu\u(Y; {Tbd#"F2[ @MԃXmVŃWgPƧ<9"K ՜Xlu%ht(yUB\QZvWm 9)q9C0+O^y*c =22DW5p د7*Z&_ޘFV7t`̸D-0͕L}=?zQ$[/$@$@$@$@$@OҢnę7hܥxQX;y6"xf12qlѷppq8TD$@$@$@$@$Ps(k-- ܏ CШc&CɲӸt4 +8k-M{Ǣwcdˀb T7ww#q= G*L/  &vts[Ջv'g&pyYi8w3/+VvTNIЗ:Y"[W?RnzED[彗Kpml\8|/9z8ȧfo9?е-wXvt$wƭ #2&=rc )b+Fr,?q1H?BZY}Z@{_jH*as#W3ǎDbE78y.O7vF^CSp&9UC2iL|4 5&tν%M]4QCۄbxmaQ'Ȟ¾}n;a=0dY $0mfW=Jd!ylQ缊~;Et W;\tS0'kRxȩp`S54vΛ؝DX2Xd#6cS2L`AgqJE%C> sR,~(8Jan.\Djߨdut $@3ŹM;l4&Pd.Ԣ"䦥Kd/c]{]ok֖wK\G޽Deه8i\lKƮ!9dV8J[X>7gMB-jl,w vQ}]8[۫:p}Ս\=\r`s  ? 2wԚ(eNٷw#aV`un6eh'~7&*op~:u,l=n6$Ka">'>7Yn%= >{=<:_Z/9iZYo-޼-~b.S;wԪV_⑚sO~xKƬ<6eo&߇ys&SUIEZf]$iX;KaSi,;QJ O)_֢F*â<? qZ@jp ~yƏFw+}*I2ho7/D{:LhͰi;06YQt1%Oxna6.EVp{zɷX,|Orsoa|kd|<h?rӵZrtNf#2BnޣA8wBz^-[ /áȽH)~FF^~iD:}%)~rAQ %~a6.ƿ}E:l?p#/8}E8croh(<F/"q3LR^Z>=@gڥs1 # i7oԄx9Fة+ȋqU*c]3NwŒTNѶ^")`X $@$@7!* :"@x_ұC ,MAZܼ[7𶩘>ql $OI-dVmUERW9Z%~&?VX55}+鯘mؕJ90g,5 gu>JcQTNҷ`륙LMH2mxbz?ԇg6eD+5_(ɋQ180- g$̞T 9Vtzy$؀ñpu l,93OճR1874~Jѡ]cL*n>7<]l^AFnWϲ1< ~0n468=Z/.nQ/l400@-WgyT|l'iW,tyģǐ@@t;qA5à^SuO"^iC\Pu.p F YdQazgh,z-'BOxJ9ȼYE9`(㑅hբ) ڞ7{Z-DVEiмUZtMkpJĮ]6Ɉ=.G/j{r-Z(3ͅѠ.2g5@,9\4ojN0*ʮ˲]{bæM 6h |\uam9~ "2Q!i^(/75,.x@R\`啷rLřsyh`Tfl ۲Nmַ+k7m.E[K˒Z& FvUKB'aQ{'N(3M63v7/-fy*͖Sp7GF_\w@naqZO8<Ȳ=-p؊>,ݼ ۶F_^nWu}C 8I@\j.O\+^Y H0oAx8\wb-A> 3ʯcndaLpBҳw[Yja-]y%f@ʽz& Xde1HLNcDW:Br>oY/uU0u;.{mycލdv26mZ CüᎣH=d.G# 1D,^Efc߆PN&cvmƍ;i3:HԆ4)g1;Z$&\`;|~pS`Q "1CP:FrZ8Oo }+Z*hL_3DZP#ѻ] 6pG;3bLfI$@$@fw8F${.:7H3VBmU/hP剷~6f<N<B:Y增~_{JLV$X_אcY\=Y\;J]mf͕\|<ݯ;~huZI[ )N;Q<+sRGoo`o*iԲb9g앱A&EȖCy"CxT%7CΖzOg\~sXXUAw> Uiq4|j9P-7d/uDiG,<.<LE9v9i2wKՑT^V?)9Bԃo3LEUt_v{Hr#{8IšV1JΦs~^X\E:I*.PGNi(œqHZCJ9l5껽]jS u YUf`<4Bdylv  ֮)8r&Bu3)`G6Nfy84mSnցqߌRpS {VfFӶ]${UQqhTݸ$Bjx .`8^2-QVK%%WqUCKctB>mDzX{'+o:>uMUTMr?ei+y(HW,R>2.苰NOj[F~ZxX*9,… G µM**Mm"Ǫ]3`ҾhmE=HTCϮ*`"  *r{+v>p ߼p_Yjeٓtgߎ[}]>+Ken}%͗fc⵱tn,||s-lڑ~J1Wr~z˻__7MMz7+K;㬅@)/9* L?%x+9zFh0?lxa&%7]\t>9G^UT;iУ{򄓛+dA4a"R~%(uUi)/PnNGx$UZ厎}0Wɹxqj>B+C 1[ǰHeơ} x9'ZFn㎲C[[R UWJG&}a$7ˣ|ÛgcҴ #)S‰setqG֤-*Iġ:š6LYg!)f#l _@~wyäثWU..硝]CUf;kP7(P9L$@$@$P12a3X2p Kx;=RE6D3?hIgv~nL4}Z8a-x,=SB"E3OĴ$>O4mg .{k~}Ge5FȻ1(#E;9R?MDH54ׅa+/brt8v(Y@,/lM֙sLR:Ç+дagm?uN-Ѭk̻PMo Gڤ^ޓ6N gg  ; AơH\D.=+K L5yz8\,,j7 jl6m|~nz^~#QKUAoi+Xkql'-з\h h /Sߕ4(>4C./@I֭ \7^|">WrcLG h9Ga'cd_vK$)[p<+s~h'be)aId\cLjT//{ \'-\4\/1ȒS4Y& l3G/,@ͫzqbXk5+5qB]zú@f<   X?6xe~D]}4 hq@IDAT潋%QCMv_a x`>.1SlH:y:w}~tgI9X,kUy˻?ˌs)B PmiF[[Y՞`'[d/7$7F0T"zM_,'d%am=D=u~qgΜу_buJ ~dX:V[|sX{ gcjӘp|!Wiž!❿.GhHFEX?0onII_-dtA3+Q[& RpNz[^٤9:" jd_oh W䁛!LS>VK<!P:O,!Ŀ }-CЦ+NOWuЮ{{KhhܩrvASh wǎh>"7zk IKt 'pbx҂s8u ZAMРA#h$pe욱k}CaZPGD&xX1 C>ּ(ּ7ƛăش?nnR6$/Hlir])/ ΅ǰY7|8 ?W~&c% ײ7@_udOCw}1uy[cDQOك!^A 1W9| eds5 S&Ϫ* d:"2ޅƮfWu+KC=p~quE6h2f[p\ĊJ}[2w셖Bѵ/fGTԖp% GKnmMZJOw6]3M:gJz6ix+*\[ a- VΣ/b|"}2׫WC'=-%dWnvY'Q`?x/|%M#*QYj[yhG\.o9kY]    K@KNTڶ}(̫,^zrVgk* &iX>ujl3đϿ.[     J M]"b     (k#d+$@$@$@$@$@B"l?>:he,DnkLy_݈kg6C ңS@'߼u%y^wLsdџ6!    i Լ@K9+1pǻ P,zkczwACqO)ژ)QǴndffbw2Ou$@$@$@$@$%p}`|}?p0|Tid޵[mmH*k#7=RK-46?׾ZV lTu9oؿehyڛncNUƫq%ƕ+"5F[7Mj urN:3WUHHHHHv.Q#w wߣ숫U[@z #dϟgGw eyw:@H C}Z3r#ge`ۿIdHHHHHKZjq;~~x7jKM#fw~B{_ce)Z/m,< >rxNܥG2")vqWє[Ӯ ׫dMh)cƝxmوf}5qPG Ey>n 6=5@s9عE_5xe_~x! _u\8_>}=p \ivg2 vc惷Ka\UQK^ 6Fk5,R~Z*jh@pU^Plga]ݹz\P6jy89&]:/3JY$Y\}~:)RRTLj:f>=UfHHHHHC DW/'|G'^{R!~ʟn-T'=ְ[o3U¥J^/ cOz0=aVas*+: (/܌â,zә[遚!G„r #IHHHHH rLA,}(GY5]Ad4e+q姃>V\e'KxS9UM(?q^+o/&JhVJه2TEJkmՄnxaN (9 ~f S     8c9bbck\y?f<:}SWUcv@ߛ#Ì_ak5T#}R\ʇk*7Sj:m,88ҍ|u BC+0C9:Ʌl7$tC$@$@$@$@$pQQhS1,퓲Mc~7q𦈟o-D_>Jnvm[_u[_t큖!R5F:KGCRL<ϟ+1Ac뛋-񨧎˙WtoDEoⳍ'"Fw$@$@$@$@$@9 $#Y}& yӸF+ʴ‡fg1bm^M,%\OdliaQQ ܯFa{pŋB#0b`_mU4+[Xf ;8\V7mV݄/CVqK1R2] w 3ʷMM"RX3kmb<ɍ M0br( 8huxy}ܯ mCq= |/bxX ޖ/k(Ͱ= %K*b?h2Ե3I/K[%~Si"s?F[KIWqu    xhH i4|#E}!&E.Txr"z*)NZzs1$    zIIMTs_1$@$@$@$@$@$- f V %     G b3ƘHHHHHlJ$@$@$@$@$@$PA}y @-X)HHHHHr*/c     T+ @#@1cL$@$@$@$@$@B b`P     } Ts_1$@$@$@$@$@$- f V %     G _IfQx!" % 0`퉠pu lW5tu;R=i^p'$@$@$@$@$@$@9D D[65iQP!׬:oóS<|v'OFn_͖Cݾǟ:# 8 \=3Qzٟx"6W_ C&mםƏGWqg_"z,MZO? c6Ԡ ]4&hbi⫨eeaFl{;kJ #UQ(g`$@$@$@$@$@$p~QpS/ǡ#LŇèfK KqS]x/o k?œ1v_7ccL7u]% Pz~;3 C9Q[ 8>X =x@$@$@$@$@$@ cbjr$om7-_[`*GaX' $Y{Y?WFh"WkTƑg\\ nymπQNF }>Cx^N\aq<HHHHH.PgeQ5 ߺ<] _ܿ_ >b]`urP4'dcد8|_B5q(4ZneW 3     `UQQh }c6Z驍ڳ# ᓣgVŖVqBxTl};-[9-|&@ B[t(Ѣ)>$@$@$@$@$@$p!8&kQtx"x̔/N0Ǝrs&~\ދbH(\^EcՂϑxFBHHHHH;9 x]xL{tl?56lSsgʣ[mbj_Vn4* T|u{?o#[G/ی #     8 䈂8eHVN ˦ŬA7vph G>Q.m.\ y^ &_>Ub[9^3WP xR@'B6̐-Cyc:vC     8 $%%Ld6`!Éb6k.~C0 K     @^(N     - 斜bc`kxߝ—zS6q&,@;v 1sW 0}}vLmmP S6EV\Tuxk⿶w8_ىofaf}{PG>sr C\4y:| sѦUґ(tk Ӯ6ø]xH$@$@$@$@$paH9 :!2֕/4ELKȈo Sʲ_w?ʯC=M9]AGtԾR&y+ݍwƐnM$^bkEiR~RuGK}ZaabSnۢ^o+?{*5nR_ 0 ;x W]ˆxUQ`\>Jvv*fo yN Do+}{c꺟ټko/ލ!8b/U%^󯞝C:^Ư>RŌ?ۇs;zx00f;g\{I>_j1Eǿ4tEӱ}| )@ 1ݫ\61R{h^J_ 0 k0潊=5&ϯ;cm;p>,| 1ߎIH yMXf]J^؎g[W0xTń{[V]~oa'eY}3HeNFR`TbnǺɦ_O!KڵO.]H0܃aoj nr|?Θ/hH$(ӵ]1\95,99p]x"45eiyxTWožqD(&4넶ډQ=J?0 f熂xU»_l:|,{K9} M:jѾ*{2tļ*j_j_`(l65g1K]ꦾ&C*uƀq5QM; QPzfߞ?T#  +=R[,$GVMorv29+A2,7'    8 #&ghmh|?VL@.D1/mg-ƞ0NAoP]ׅ 5ڧߓ|Sdї^J9AWd`^AnzXvp/Tv!,Ϭ zHPR9+;~R 5o Dɖ-=DSHHHHHCWAuv#K4hSj,^&zAe|Q؊7~NgNHM9 ͭ[v._ٻ5r}/H0O Mp\չ{0 e &Z 5w?tAjҾ{V9ymK ωG$@$@$@$@$pȵ&*ix͓vC]q`h xm=[/mB=xS_Q"n-Ed6%(m-]p!05qצxPxG#ͼŽDcSxT)ud:9* xHHHH.9 Z)WC.x-;YiG9T "]'.)GuDvJ-RS{k<#pQ{w`Fo|ܺ'&i7H>a:bpDA|u<؀{nwi# F~zF{UMd[ ^onYdlsgOHVw$@$@$@$@$@  2}#} (,ߝQPYгR|K{ [N)+HFhs)}Tk-Qpap3tH:Ecyh4ƷOp/U%΄ITHHHHH ,J-ARQP'%%4~9yqI6{ovrP%:bPE2*0Nn#>6}|㌶t°"5*aods1zŐHHHHHr)\ &cD9NGJvl_4JC1Y98ɰIH T+2µI2%    Y4g1t     84lg#     ;:     ĈQ!  ԑ @2X2b9:T) \Ax<Ia#7      !,)KaYeX"E \@i`&e Pzҿ[+$DF+'H͊R 'r.)=,+,WUCٔ&{8ŃDHHHHHH (Ro)P_Y ٢ }\8v@IQf"A$@$@$@$@$@sJjPQ Wkܓ d-~1kyR ZTsm1$@$@$@$@$@$ f-OJ#     \K b:FHHHHHĬIi$@$@$@$@$@$k 58=ǰc^VGsd;Vl>Voptð3CN>N+zx,0ڵ'Q/gn{*0p:ՌعtdKE?K@Lĉb:VyX9I J4˱2O]\S$?u>J?}ڕ[Cs;UۑaKW_pV5JBKkW( u>~u,V'ci\tC‡kxy/bRŕf{wO<^[6K3!tFW+ׁ[%x}̗8b_V!6s@u%aqS^xf(Mot"rqe#[Yܷ{O o9toշ64bI=v/tÇ`1"pbN\4똰&s]5_`D]Ցy>Rv^¹~ã( r'4ꄶ Jn~1Wbo\?S?~a?Zz,sHHHH.(III!ӛVo ]mv\r3h{>1ۖZ-srV?){1<{ߛ˱UM\zs\gЦv " ~ v3Rwǐ _a5qg#F2 4ja ?{`[ dhiQ0%,*:>Ȩ^~c5[W&!ebehԽ_yc(W k67bd]Odr.Sێ„xj^|I\I I0xS|BD9܊$oi FוXQN}.xﹷ0{>hP䵱hG#Gc]Q)H9w.ye0UMJHQ8A|R•v_rXe߇fˋYS& $yVXuA+ l0ib߲xs8ؠ.2xT򰢤o7Ʒk`l܍^ϡv)OavB2*ޓ0t|Օ2wt=F&e.)sy}@A K,Ú-QbIYHgy=|1PNF*a2$vlI  صnNTU.]#pè{K(-OMQ[**y#VDQN'g-Pɭa4jZdܮŋJݱ 0 T(N<>C%*\+maFʡi(1Ev[F3߫̋T{80q}UQĽ)%ucb4TսUTIzLYz\=rYBe,$DF+@%!n vY{E6<mX'#mЌ[uhB5CuM7U|lʠc;QOnR^{(Z+ƢW&/#Oq\"e -$c!p G2BJ|0ʍF%tkq3 C[Qnr5GD9;&'c(qWgEQTSX ÆvF)eޒ}GJDK.yE9Ua) cR?ڊr6r(mH9}t>oL#    Jst#J<ףqɏJL%u0fj*G`TzDT55r-9qܱn[l_PeQ4'qTq1>rS6"#z3 Cúb*^8(up{??79cթz Z>nV7kpI.#zWY޲_A?pjS\4 {;On^o+.z,7e_fmי:aq]ej7skDB@w$/R#0.  8:ntڸ =|l("aجڋ:%v.Oĩƍ^Xhk(WR—x=v4ym)?kyNٵX"yV2 1UW?}[*&Sk^9*n e{h\+/6ᕿg4(&#զCUWA)ǣJL^%yF@˕zT py# "O—Q\ԛO﷕_Qe/'eN:%R<\ ↊"OV)0ML\w WtժgI('*bWU1(xV$J͑]2nNVzZlGD>Y5['ᴬ^F2lpHAK/ub󜐴L ~"|뻓)1sD>[ҡ}sycⒿ31T+/ZiA)1I݂M"3&[ T4W!-˯h5U8mx? ɇhasf3:TJiW~-?U+r̿`˔ wI,[uZteXE8.&ǖ1^/.<]SUG| |F;!<&    @6 4*يa˱iY3l\wj_e(,y2FL3 @IDATw|#u[9lb9(џmE:Y#ϵiYMr F]mFVH 6jVOk[a-R oQ}R+ 2:FK$?I;ЛNQ/~;EY :\Iݢ\ű˛)=P9+qg' /* E93Jމ?a!)p^qYs;Nv R:p˜0,+X [U̍q$     \I "\4 9p ⿝w1O      ss*;     9Ts=C&     ss*;     9Ts=C&     ss*;     9Ts=C&     ss*;     9Ts=C&     ss*;     9Ts=C&     ss*;     9Ts=C&     ss*;     9Ts=C&     ss*;     9Ts=C&     ss*;     9Ts=C&     s@وu_é 1xzPxug# _ ×@s#NFHHHHHH d/3"oVj7İ-Px[<䜙8m?3O_$@$@$@$@$@$p>DOzķK7:zlAwXuY>zr HHHHH!;AIj$و^~sNOǎB -})Y%vԺ}fj?zmn-T p-ׇދk%w`㉀pyB$@$@$@$@$@v 5v۷vO|H@W6~ɾÇF2a͟F?(z,A xw",:y+V>yAJ]:vAŐ}_>r&-?^\wK)3e݇ZrkDюw$@$@$@$@$@$pA߿+P@'Z-6f(ɷm@D'XxvUz`\xf<{ѯhE,kNjO_ՉÜd<@qϢ?ѿiLjVOWF6tqBxʩ8zsځO{[Qby'4qFH͞哱ɋGImb'EEeYiʍ*p[nF;^y](m{_m`n(d^ d²uQh\y;5g*qpuA}"5;V./nI>(~)elظ;\G94e_\ 'b)'ЩqeX=۱ 5ؒypmQILJWLXx"\F)pD9,W+q%6\cqxh:$C㯺O`)?)?/zrWw"Sय़Y۝x0'\E!     A [q'Mw᧯F_bppV 8Qn/q9e.\ yS;JM2lѢ&aþ#8{%t\bpzqw>䫟o\> -߄v,_:UA+gȇi#}&6=? Ė ^7v~ɇ*R3QX3kyϐ{[wPk:!z8EGTitpO$@$@$@$@$pd*pP/<΢TuORtl"2Gܱ^YUiG\%}M[J=hq\aR8>Otn6mcx T/x?4ϭ򹍅OWtB uuROl~z} Jj^fbIMnis8vżzi6W8qsqgrUK@ѼU)HHHHHH [4D'!2WЗb/Fˈ_9#&˷#9ލ>G˜?e{dcb"ZEVe~6'f=%ߓs.*^?TL dp b5-"!! Q2R"9{'Sh C(p?9_=יq C'x@$@$@$@$@$p ijs.$*z~qTs_1$@$@$@$@$@$- f V %     G b3ƘHHHHH9 &бIᅱw̗5 K!$U})"?qO"M~6E[1Q=E wޞyK # s A+[?}>D_O:u{{JdPw #Kb0pʼn:OChOx) 3ѳcOI*-?e_nIkb\<ۼ><sX+%|`mśpdUzEJg7['E\e+OU}\])i= dw˥(v{zS):{֛"ǼdGfgPW1(vU1sKZN'ޕuA܃n=6fnPې2,6%aTx0i*m;?#JoN2/忛Շ0MݳulwG9dt4oWҔaR=Fʄ# zm,?FXʮex]$e BD!yFH +Ƽ64voIRW:@D|U,! Tz{doP!r# ߿пcǎY3d;0`^?!k`N A]#v;<ֽ͞猏/6~?sQRB9︋KG9֏q٭ѯÝ̍ᅻzjӎG!ݚزkR^1tK'.E kvU2d NNV1Aǣb~fY6oO3ahF6, $ϼuVxPmt|`ԟCKvySʓl~l*39AfΏ~yK:֛*\!HݴD1nu8R<;:vwu2Q7H!o6o )So?T:e*`S:ĆP بuH<]^%) TNikɑ PtGNUӏӁrU%Oyd}`UٹruC! Iw޻kt^ 8/g,i?եea*R\Ôi 1"Y녈륍!vc~f! ;J\a([@鎸ԯdU&L]AG1vu#PϷ0Йԕ깣0vFI];k灺!zvh?NjۡʄmzǬk~Xn q{ǑWw re}v80ly꥖d`*!,L\bh__+Ҥ_I:8R.]B ;7Ґz&2i'ʨ0U@`w%bv=BO4!r5Q!DQ% \N X[t縉 },sĤӘF迄_Gr+~B[s9Ɍ``.fv ONWӏIA&Uo߄0叽 ~< u]扨o)^S#h?-yԼ4:)YgV*R(ٶ/8у;m!,cmcP=_tf-͓(5m.2#˷W̻\f`h{xk wxZ%H! - (~%1k1'[L"r2i=o({!/d0,SM1l3qc];/_JQw*NͭHժRۀm+~ 0T7wL탮K>r!I] R69ˬR31vVL&Ǫ-:;b|$Hȹsn,)R#|4 kxߌ V5ۧS~3 FNZ Qq_ndW9NQcZT,tB_b\ b>bl=,2a:nS6qV+p4PfV|Jtx~zݚgbIکo}\m_aQɂr§RFyqc;\T(u vʷzϦ5?6t#&ֻjK70U\}cGZ ^mjKT]5]齼!%?gKdgqda=UiNyYStUTAvu<Լ o]dۇ/?픹誏aI뀭`.u;Q $@$ Y3#)m" mb&سY͙vH&/Aoy3#K°LZoV.&͸Tlui~dH֊c.ƈQ#֕sgd$9H&;oݦ"fb2dTPnP_d& 3՛cejx?}bL;3n-ޮG"q׍mbE-nG ~Ye~(z;yvuycG_̴ݨg䥆(ߦz4^kHW<`{ö~Gڄ7} z[xwkFfww5km5+`•f/~,]^gPFU' xqw-`UC_[eDm#~ZW3C٧) u9#.Fͤ%􈤎6QeG"n?1zt{Ȋ2Qұ?5 OKt\3Wic.-f@!,&A{WWa/1뮌4}EdeNL7<ggQxwt_*m:?[s8wljYF7^k$̇7;dFmah˄6"mR|tiP?j$i&vʛ,Aζ`Љ;V[Vnu}XRVu13<ÌK&q#}FJjϛʸNl)~K ݓKz$obi&mxkJEv|%~T9z=M7 =oկ뿟G+!'ȭ229gb(;;luj|3`;}-zTxΟ_׫fSs0Ld[Y h/g1aӦ.gL6^6;.QLU;af: [kF_Hy+, J@}k?L f⮴(6#唣*_O+7ZVg/Mb6^Si}?G6]ZZAќ<\./(1vxD,0 Hꩆds]D7TrDI6IH!}R>1Q7i:GWG?F"yٍypx:~N~K#&ot$P2 11;zTCgԐw_MHW#Kͳbg(Mv#|iYbjݴXZ~KF?hdge$ThJU+G5A a8iSaiɨLC~L(+[!,eG+:]dn7ar͝mܮs9aZ!ԵJME% xQى+EV΂U~Xgfn[U2yGDnǫ5"*/kJ"/J[:}: <#J?z[zi~vosVY*Gy!lF3K[r~.M# @F `FJ8Dqpn\sC!Wu =1P3dΥ>*pq-iũֺ0 MToWU:ҙ INn8ȍyGs y? _Fv߲:feT_p컄K'2v40֊-+eٵLҦ9wbD@p݁'d1U~,+)-sƔ>[e4 S_UyN~4˒6[U77jH:--J+;i;;`;k&6ˎSC+º~\׫4uTW}w;[?1i7&EcޒMARBH(׈.N.a^5b,N⒩M(u2^S4.`}ˉ>V(Npov"S^ע/2 '!7/ɀt y0E V,Y=B;u7a19<4` ~J|\xIWV,_>Gk>\qWzTX\c(E{a$E8 j{q&dŠif&<U#dYqE%,X̌NUJ,2弍Y/d-P}1 |+F8*N9A=ߛT6$ v(S7J|V;y>ln-'7M_3۫ŵ=_ZϚzRTb *eBy[-T` G-ie13Lʷz ďlZź~9"M{Ykm߲D^Y׭~rT]"M A.Syu7|RMUDWmpu'eg3A)p-"v[_"-"5"vt2zf* j<(㑴{)AH];>RPn<ں\ <&, |bsTp":ܓ @pkiZneSsQo~eӎTe%!! ɛkW$;4oP(z΅qF!Ԝ5O>ad-FLfn: r0]q`}b25bPt6K6sP3 Y(yKz鲺}5kǣiFrS|eiYڼM%ʝZb)H\s0O?J;B'?JB/"ʡu!_wГUUR??^9#) 7(͟m&wβ;&]ՕLs OxހHڽfSDWU39I?uH烮o]pwpA~&I͂ݻ@qэ(dػ5F:hZT! gfYEwltG&Av:0*bY4HiToa.f!VvzV.ʂ/6PFQʠR'B,rM攽&_wVW[ZfHTeNxhg;{InVxiVJTy;81`ecˍyG;s Jd,Z$EJA)Ƣˌɣ5#FATeYcؼ!c_ X,&IFvQ9 na gr7yx2m ~b "~U@{ȯ_Fa ($zS%C+)AVɴ+ 6TfL7~*"ԙ$ayƼYݙ V|w"+P\fʖnyѾc&sM:$`-/seMyWV洏yR^LJhk"N$@q|DWU.3KW GER>;mEGATo ]<=oLRYٍ[^lG+͟bcv]w)FR_"-JqHq=u25qe ]Sϝ%R; E|4vB/(&<噱xLY /?T @—sR>Q|:Z" I7zrsrc:*of#ƫ:gn:ʳZȠOcݤkaWFˍ:n'VC;iݓnVoBVWYEo,s>Q΋;&i'[J%k^>v~(#H˜7t~hiG_J2fkSZ$Cw4o Zz?"!MX[TT&CQ*YXTxYWN6[At3i Z7jq /390:3X> /IV 7 H]pwuP^CsF 2_ϕ>( U`M'p4gebsHЛb9 S@kɧ8YŶP8Z s^rNw̤Io{<jP7ED+J`:5,Tύ=1%i^20*~}&ȅi5U:"/G4 SXs 6vRj:H"3來י A) @'Y0ؽ[)t{ԉ<̭@lG/X`ĉ7PbQ^@G3V$g܄ăL~J ;hH|cB09Yn2O Hd "S#a|Ybŋϋe J!$JG)U<}24&#ާGl1Y> 'a&K 3LTOҨ%T_mIr㓹tEgK9d'V%Fz=3f|~a&s6Qz#LG(-b"35+n NM'r̼Ei˔;iOʀWtD9ҩ`F[OuEX#nDic_Y+@=sJh1?( .ǏWNJ ^wn(*iL$@$@$@$@$@Rs| +Z     8w PAyGi @@pѽsC@@@Gr   ^{熞!   1@@@D =C@@ :Mc   {z  8*@@t@@@ ;7 @@pT(7!   wn   QnC@@+@@t3@@@Q4  W޹g   DGi @@z|OLbHA@@jZ . k7ar@@@NC5   ap*Z m@@á=1ԍ  \@U{@@@*Pв|>_5LLL&P    *V5Fݠ@@@1$    @@Yf   @(  D1f1"  !C@    heƈ   @@ "   @4a#   1$    @@Yf   @(  D7<1UPk0áef?WP-6@@j@Xbmà   @4 y;   $@@ `@@fb4>cG@@A"  ,@@g   @1]@@@ <@@+*HR[G V8əˌ } ΚOu7jRǝcݫ7^~[unyDcqV@@@?>4%+utߴZ=SW:)_5}`XcGȅ!\Nf@@"CF/1Q+d%nV\:(>7h4c4M?R]Z)h*sp0щ>!  P jv$@vc?tVUWgtƤv/Okהtv)t52Q}z& +օs~oJӰ O7,{Q)^vy:V:+_Q; /O^}c}s;_vԳU]3[_UZO{TY7Rۇ^/ݞn2e*~tIk2 Jzhpj?Kx@@@@LVc&ۺt2V7MHC?n:D_C`Guz7f(wugmǾ=ujf R_;sQ@?oѱ"7ӝˍ<Ό9 ^zTxjK, Pi~޼˄zJQ!6@@Xj=դPmRy.,YS?Cg%ViDl{R.^ڲ?϶k}*]Y]4wKj`uZkģ#1ɬ(f(<'2-2<ѬHLnA@@h6lWM+MNܭ]K^U31ޭ=2u[^2C;,bu~)iBfѮoY =l^Q`¥}:SΨגZwDY+m3ջ睹ZZUZ[d} ղڳ]ktxWS67ߡE˷Wjxy"iyuS;9:n݋_?Y?;VtN(_7Bzfʠ{ ټWEpW$V?VmtQ{v֛ e(R@@j@ &=Rs=Hut>#h-5-F9ꞔ߿Vܛj^<4pY *\CD&VL %m'$gjB?]!:vqh`IDATuÔչM8ls-U/lR;ۏQjxhUW'YlWѬp=μ#-bqRtH;֍;9Qtkdw#cԩ)c^IgCy/a+ָyyUJqz݅5Ɗ72)ӱ,;Ŵ褑O?d뉩fS`1OIڳ.S7//S^Ԙ}.@@@v xrrr*jH UC: @gJJejS e; cc+Rzv~sbUL|O@@@(,,rdaĴ@@@DWN B@@ :oN   +:  8/@@tޜ@@@W ]9-t @@p^9-"   rZ   ysZD@@\)@@t)@@@y洈  RiS   Di@@pѕB@@@Λ"   J+N!   7E@@@DWN B@@ :oN   +*,, >   @ EP@@@ /1@@@ Q4 @@r:C@@HEP@@@ /1@@@ Q4 @@''@IENDB`graphql-ruby-2.5.19/guides/operation_store/client_workflow.md000066400000000000000000000057251514115062600244650ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: GraphQL Pro - OperationStore title: Client Workflow desc: Add clients to the system, then sync their operations with the database. index: 4 pro: true --- To use persisted queries with your client application, you must: - Set up `OperationStore`, as described in {% internal_link "Getting Started","/operation_store/getting_started" %} - [Add the client](#add-a-client) to the system - [Sync operations](#syncing) from the client to the server - [Send `params[:operationId]`](#client-usage) from the client app This documentation also touches on {% internal_link "graphql-ruby-client sync", "/javascript_client/sync" %}, a JavaScript client library for using `OperationStore`. ## Add a Client Clients are registered via {% internal_link "the dashboard","/operation_store/getting_started#add-routes" %}: {{ "/operation_store/add_a_client.png" | link_to_img:"Add a Client for Persisted Queries" }} A default `secret` is provided for you, but you can also enter your own. The `secret` is used for {% internal_link "HMAC authentication", "/operation_store/access_control" %}. (Are you interested in a Ruby API for this? Please {% open_an_issue "OperationStore Ruby API" %} or email `support@graphql.pro`.) ## Syncing Once a client is registered, it can push queries to the server via {% internal_link "the Sync API","/operation_store/getting_started#add-routes" %}. The easiest way to sync is with `graphql-ruby-client sync`, a command-line tool written in JavaScript ({% internal_link "Sync Guide", "/javascript_client/sync" %}) In short, it: - Finds GraphQL queries from `.graphql` files or `relay-compiler` output in the provided `--path` - Adds an {% internal_link "Authentication header","/operation_store/access_control" %} based on the provided `--client` and `--secret` - Sends the operations to the provided `--url` - Generates a JavaScript module into the provided `--outfile` For example: {{ "/operation_store/sync_example.png" | link_to_img:"OperationStore client sync" }} For help syncing in another language, you can take inspiration from the [JavaScript implementation](https://github.com/rmosolgo/graphql-ruby/tree/master/javascript_client), {% open_an_issue "Implementing operation sync in another language" %}, or email `support@graphql.pro`. ## Client Usage See the {% internal_link "Sync Guide", "/javascript_client/sync" %} for using OperationStore with Relay Modern, Apollo 1.x, Apollo Link, or plain JavaScript. To run stored operations from another client, send a param called `operationId` which is composed of: ```ruby { # ... operationId: "my-relay-app/ce79aa2784fc..." # ^ client id / ^ operation id } ``` The server will use those values to fetch an operation from the database. ### Next Steps Learn more about `OperationStore`'s {% internal_link "authentication", "/operation_store/access_control" %} or read some tips for {% internal_link "server management","/operation_store/server_management" %}. graphql-ruby-2.5.19/guides/operation_store/getting_started.md000066400000000000000000000115541514115062600244410ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: GraphQL Pro - OperationStore title: Getting Started desc: Add GraphQL::Pro::OperationStore to your app index: 1 pro: true --- To use `GraphQL::Pro::OperationStore` with your app, follow these steps: - [Check the dependencies](#dependencies) to make sure `OperationStore` is supported - [Prepare the database](#prepare-the-database) for `OperationStore`'s data - [Add `OperationStore`](#add-operationstore) to your GraphQL schema - [Add routes](#add-routes) for the Dashboard and sync API - [Update your controller](#update-the-controller) to support persisted queries - {% internal_link "Add a client","/operation_store/client_workflow" %} to start syncing queries ## Dependencies `OperationStore` requires two gems in your application environment: - {% internal_link "ActiveRecord", "/operation_store/active_record_backend" %} or {% internal_link "Redis", "/operation_store/redis_backend" %} for persistence. (Using another ORM or backend? Please {% open_an_issue "Backend support request for OperationStore" %} to request support!) - `Rack`: to serve the Dashboard and Sync API. (In Rails, this is provided by `config/routes.rb`.) These are bundled with Rails by default. ## Prepare the Database If you're going to store data with ActiveRecord, {% internal_link "migrate the database", "/operation_store/active_record_backend" %} to prepare tables for it. ## Add `OperationStore` To hook up the storage to your schema, add the plugin: ```ruby class MySchema < GraphQL::Schema # Add it _after_ other tracing-related features, for example: # use GraphQL::Tracing::DataDogTracing # ... use GraphQL::Pro::OperationStore end ``` Make sure to add this feature _after_ other {% internal_link "Tracing", "/queries/tracing" %}-based features so that those other features will have access to the loaded query string. Otherwise, you may get `"No query string was present"` errors. By default, it uses `ActiveRecord`. It also accepts: - `redis:`, for using a {% internal_link "Redis backend", "/operation_store/redis_backend" %}; OR - `backend_class:`, for implementing custom persistence. Also, you can disable updates to "last used at" with `default_touch_last_used_at: false`. (This can also be configured per-query with `context[:operation_store_touch_last_used_at] = true|false`.) ## Add Routes To use `OperationStore`, add two routes to your app: ```ruby # config/routes.rb # Include GraphQL::Pro's routing extensions: using GraphQL::Pro::Routes Rails.application.routes.draw do # ... # Add the Dashboard # TODO: authorize, see the dashboard guide mount MySchema.dashboard, at: "/graphql/dashboard" # Add the Sync API (authorization built-in) mount MySchema.operation_store_sync, at: "/graphql/sync" end ``` `MySchema.operation_store_sync` receives pushes from clients. See {% internal_link "Client Workflow","/operation_store/client_workflow" %} for more info on how this endpoint is used. `MySchema.dashboard` includes a web view to the `OperationStore`, visible at `/graphql/dashboard`. See the {% internal_link "Dashboard guide", "/pro/dashboard" %} for more details, including authorization. {{ "/operation_store/graphql_ui.png" | link_to_img:"GraphQL Persisted Operations Dashboard" }} `operation_store_sync` and `dashboard` are both Rack apps, so you can mount them in Rails, Sinatra, or any other Rack app. __Alternatively__, you can configure the routes to load your schema lazily, during the first request: ```ruby # Provide the fully-qualified class name of your schema: lazy_routes = GraphQL::Pro::Routes::Lazy.new("MySchema") mount lazy_routes.dashboard, at: "/graphql/dashboard" mount lazy_routes.operation_store_sync, at: "/graphql/sync" ``` ### With Visibility Profiles You can apply a {% internal_link "visibility profile", "/authorization/visibility#visibility-profiles" %} to incoming operations by passing the profile name to `operation_store_sync`, for example: ```ruby mount MySchema.operation_store_sync(visibility_profile: :public_api), at: "/graphql/sync" # or: mount lazy_routes.operation_store_sync(visibility_profile: :public_api), at: "/graphql/sync" ``` This will apply that profile to all newly-synced operations. (It doesn't affect operations that were already synced.) ## Update the Controller Add `operation_id:` to your GraphQL context: ```ruby # app/controllers/graphql_controller.rb context = { # Relay / Apollo 1.x: operation_id: params[:operationId] # Or, Apollo Link: # operation_id: params[:extensions][:operationId] } MySchema.execute( # ... context: context, ) ``` `OperationStore` will use `operation_id` to fetch the operation from the database. See {% internal_link "Server Management","/operation_store/server_management" %} for details about rejecting GraphQL from `params[:query]`. ## Next Steps Sync your operations with the {% internal_link "Client Workflow","/operation_store/client_workflow" %}. graphql-ruby-2.5.19/guides/operation_store/graphql_ui.png000066400000000000000000001423071514115062600235720ustar00rootroot00000000000000PNG  IHDRE"@iCCPICC Profile(c``H,(aa``+) rwRR```\\TQk .Ȭg~~|AKoZ0գd  Jl_( {a `5!A@ [ 9#1~d$!#I)JR+J@s~AeQfzF#0RD``.߇S{}Ep0~c)N3630Ns8˚ {100b`8 `pBiTXtXML:com.adobe.xmp 1018 207 v@IDATx|E_I C* U `W b{Q;I!|Lcos-#y۝ݾu25lPH J{]KRǁ(HGocK6#   (2ʉe# @!@E?|(۾YK.%G")Kf?wH$@$@$@$@$@c;^,Y);!{@)U$s` ʕSn.gPUw& pq폒E-^0L22N+HHHHH2,jH'sAX#z&UZj(^̑cu/:KpɔHHHHHR&9O!Kr|fY;${co54wɭ;$mi=߂OSHHHHHH H~ 羢QR9_VOl&$PjPȥ3g=|@P8|9KSwɕ8HHHHH3D);CL2]D9sF.o>*XW?@:u(ǏO1WyɓG2+%5_ѣGeذe 7WYn| %uū&ZJR;&SN_`{]1rf~9zRi=rRt:-wLt =m_PRqï|d\fqIHHHHH 9|V[h|('NݻwI@@f)YԭSWg2sO-Z*ޞIrʕ(EJ{xx8xV)"6lL4Q1(QBie2ԪUKe&QQQN#̙SYz)٣GZ@~"lUUa ɩyKΉ ;rVQ̯|^9u2~W q :% eTD0.\\GLL[Nf'#'X͜qY.'--ԿO{2HPP^t5kVwS@HHH CI 7^S+L: Ztzf[vTw:H/o~xVrG(ޟ}tE&O,ׯwlQ /_nݻ&>o޼_i^L`xH! $ޢsOܫ^UpóPBTyCTN:B(0>}q9r t k֬]vɝw8˗͛ɒŧG. ΞNz97nkN-XDž#߆uI|BQ߲e߿߱ΗjUk͒ #|韜6*VRFp6_wֲbr9z쨣˭ZKDaYfcHHHH|r>@?!v%֡{vC#'?b)ޱ֯SQ&WG L6'ݶd+ ?>Ӊ't:H>}qGL"HH-cşUU&7J>;i$ݻ/-[Ԝ&OEd" (7@z $wܲ\)ӧO˖ e^0,qW7Cv-7mLo3 u"($@$@$@$p3 vY>QF=>TB JiOHe5ҠAC`VM>'CwFѣ0ҿO>UxJ tRڪe+fۡj DDD8[K3sFmxxc _tbȑ#dOc0~ͬq P=ua>B'X)2(U+I[`-ߌ֞ҽʮqSTfݺuZ)ѣ#^|(?f6m#f?*Wl_ǵ c!*_o>m)ŋQQnu/`p&M3xbA-ZʣJiG3gȪ+ xx *˔wB~i+ٻGEԑo>~> [n0"@!uAFެY3ߵ[mkתرtupCYzW ֹܱPpΈ'֭[҆ԃ6Hٲekn#!nE۵} kYZ~_dHJٿp>v[4}u6#hA}vAz mHmFฐ;loIuZJΝ;ehѢWtϞ=h";7V7kLfϙ夘H#կSN5zB7VRUVhk`!+TQìYZvQFMtzYXh}ھ}:uJE 8 7#* k:ӬX\_e˔URY}ծUGUm۶Jn njJ9w>H|z0ǍڲuJا 8`c^fS1HHH2<}SQjߞ JQ6PWQAyHX2J5xlYp Y}8^x`xlIyIL%7lh5Vj<3$Ay!TRh/7[lC:)fRCJ( [P(RF *6m/" !߸q2B R)P?oV 9s0 `?Pw_JP sN`{??Hd# b FҥKk … Qm: %⊋/###$ZM$FE`^^*J*9y?   Iѷ{fvx):( >y_Gp+vB¤ɓ9.'w(fBR~<e̘J_dZ92 mRQ]:>(>BAZ7 (DԈ2h F߾ݴFv<@BqCjU`/RᇂO RǕbY9y*4e{I_ !?^))XB*o<=4`kz2^?Qa6(FWe ^kA(xq} ]w3܏8!0^6@tBGFE8qʩicUD[x%l6ݦu[]K>m'W,DB aB$@$@$@ JM&%K:6թ]GvBvոQci/P,Сh2.̝;ݱw^퍄 xb\ <P@ I2}xjowi]יx8Dpիͷp6XWV]o6Ʀy]Khe(. xl'ʷ]>CU.V^GFMi@gdlà LυݮHT|''OUmFSouxž33 )THWZ3m):wtXxq%GU7$z@fE J`.Ӯ]VpFIqjAٷ9L͗R9xOUdstU}5cnH@̛7OE(f3ZL];5k^.o`_NB1/Upv?pk7 ϼULմi^O8׏T:GNly|_YĪ.U(# <4)K(;z mq7 쭫c{:~<ɱR-*F}{*H?0{0+w[~cqHHH IяR9h ]ɛP9ȨF^Nŋ@h9t8aEc33Ͼ| tZk1FH󱮃B|r9Pp/%Քˢ (^ RBJHX8857eu-1 qQS"r3f = @#弐 4_!vXB{L)V!nh0+qUI7}}E (P<ʥ1 "r*xsu|vIiW\S׊&c=E qjf(+QԎ0/ 8xlgkV|*rAȁi^m:&ڸdX/~N)O \}v0< w{0n.؎HHH Iʃb:0C!=MQ1xdwb]ܳGO{l]ٽi5ou[b׬]}< c?árRUgWۓpP(oͶﻨ 5iDڕS,*5wR $/; A}rn!==_^]%Dy|޵W8L#/u!>T r1$;#LJ><~5kj —1λx:?Pӧ'PlG;w~ ~2m"I0AA87(O{+' c`b#!?jx4^Z߆*`JUQ8(ˆ@GaݻW"a|>TϝzDakq&#{}v0!5a;|׏7G ow%ƈkb$*㜠̣=% aiu`?vkN}?5P+8V/8"WMxq`XZ@!   ;B0=~ߴA%Liŗ_8(`߈ao7~Gw] S!oV,+rzmC#>7J$J8|MqgVF,ٳPRD+P?:l9D>= Hǎo 3dCq6;<#ٕ+WK.Vb <-[sRaPWX!˗/>8Nk-CR #(c0cj=!c`3}׭[ש.ItL뇙 `s.qY/<."3,y:aeQ+WЊru3=6!VB? -ZvkurL">.=!_N:˰dϚ՞E>}ʀbv"Lh3;vtѯ>TޠJNB"Jfd9d޻z})qt=PIxxiklǃotx@Ng$K?_JRݲtn}beJL O> ~j@8$Th~"rCU:o +^xGnfeåȎ5 ߵtxG>qu;(Vz:32W2K &R%nTQa*SRy5H '6l Gw\j#E=Mߦ/r@9tp_zxᱴ"y2dSHHHH HVO6=g+崰RޣT=׍(y^])J?rʹ?,GI53{ej œ$JMwJyp*/1gsAyW+~lTWԄ޺@_bH mS·HiXHHHB M(t}RN}nCTI;55+9%JYU}D["vVʽ C3S.LJ/ظ;)Yrd~o}HHHHH:4u<~$[X]9?O2%H ^Yʦ/wvqIHHHHH U*>.~mګy _S i'A$@$@$@$@$@7I?{^'o]F6M?wL$@$@$@$@$@$ϝq_$@$@$@$@$@$@$@7˗ _ P+nHHHHHHn,*7/G'      W XTo,_N$@$@$@$@$@$@~%@E߯3      2EDD\$@$@$@$@$@$@$@"@Hs?$@$@$@$@$@$@$T       / }?@.HHHHHHH_4C$@$@$@$@$@$@~ @E       *" PdHHHHHHEHs?$@$@$@$@$@$@$T       / }?@.HHHHHHH_4C$@$@$@$@$@$@~ @E       ,C$@$@$@$@$@$@e/b 5C"_zB wyS  '%/_KH*7~gC!pb&YQ}J$@$@$@$@$ O%?ʉ??\򠢟xA$@$@$@$@$@$@THHHHHHH '{ @%#{ .x<K.ylg߈~gIHHHHHH Iꫯq_'uE>|4lPJ.-jՒ%J-"ƍ+W$OUծ][kҤO粟3gΔrI۶m<}p; V>MWX1}yu:'NHNdRdIy@r!0a 4H%K:wtYo.P^ Kʼy矗'jCݼ1b={V6l s̑6mڸi$@$@$@$@$@$@$\׾=]w%˻ᆱ~X^~e6<32m4f|ZWs~ر2j(8pvOo^pBY]VAE1n#     Ho| wuCG^sRO?Tr)SŋzWJ|ldȐ!zԩS]?CoѡuȀ "mIJČo^:uF$U6qiz&U^|E.q:+$Iu|;ʼ jm8lVÌqU}K1n+/66iH/O!   'z+R/lvRĞ:jt'Eǎ:Ν߼y^~G B}$sr뭷(G}$!!!2x`A uIc+e CTb?+=gr;+RIYL#m6͕f*dݏ$`MjR(t[+#H]C= \kxWZwZPSR׮];Rr.qx:BZxWx衇?"8V{[nn y$@$@$@c] NrP9 q1gMK9P5թ.pqeYr!ꘔ{Tzv%oU?$sHzH<˞=e/Nm&G-;o.>)Nxrxx͎uf5'E}/ʑcP ^ۣG(-[U'>[2c q5u(֊~"rɉU%1y۳_j}=TrU*'K2%g]Psb^Hr'CCi3C@nOJ6@ glYf>)Z-Xhأxt;ugkއ }myo.TUO0{Ow 2*>uB}z;C5j(/(תU+0`;SKr1] rxwtToY&FKݝԫǞ<%PrzV)zmyٲJ1\S.|~^396(UBto/5,Su{GU*HgH-3c&DDGG01Ί+t'&-Ǐ)B0 Cf!~=_IHHH(98_)ѳnvj9xVYu0I\%qv?(.|~R ~2i$&Y9xȆԩSMݻ;) fCʕu-[*kR8:'b;vq^@,޼A~ e[לKke+^, ˊnwR汾gϲo{sA$AÿFIX沪 9rn I%:(e JfusKE6Q4\P[>x .78OH_H 4o uG*!di6o5 Νߐ2Oeo;9_65LJ WO-ꪏ> eK)(}zj]  mڵg- =2zE144TZlpX2fJѣx(PNb fb=ʀ1xL?져Ca=>c< Rm * Cpm/^\ 0XގWZp~qwkDG(,0 N1M8!0NRkLJ4Ha4h9nHHHC^kȖOGƛ_am(kPevY֨u#֊oɅ#Jeѽ%ןg־llS?( |=39r>b5gٝ *s:ޫHG;`8i^}VE1CBj$[= CּXI/` _ju# iB{NĒwv\hwN XЂ ceqߧ FhNH+ڸUm{pͷ${@PU5w/xo%:#G_|QzJ Ppas}N-0M땋Y)ft%qgI`W$0$oIlC֪*sd J.hGH^BZŷYE=!ĒS鵧ŪZ~ agJ_S{cr;OJyh@)ҭ^PgT|V751\Q win,LY,'?s/2(={͑ڂ(g:2ۼye|.4ع[.]tژsBJEժU(. 3yEQbEFL/zF_- ?3&_IHHHuNag)eP![6Vy5"irx#BX=gtP3gQ:oG^$ yDv|6El偮sm.w y*9QtC*}CϚ]}3u ªAo{CS'7em" fֲH.av%rVq[b|'7W!Ů7a6Ke$N.WJ>aJ1LQ Gev{d_(w;",Lۤ$#~nCMsilzp[_}|'N&y̘1.1ʑRTYOMb'};9G $VmixOn Жy0 @ɇm֏qaU.D} uhԥmt|Pb&u "&'M然06dU:A(mP/L>N[B9 @i+hHXc|Xg?(8|DؕTcЍR? XC<k?'F|X:5 Ezf7c#n,ab"G$@$@$@>@8drm{\i@jZ?hcCe,TP!a-0xߍUjyQ;  u ǯEyn8E:ܪCu ͓U+HzG%11MRSzUp> F|(s^ܩ Rnmo/)>ʄaMh^^lxH٤t㖈el+ncy6x`B(+ox݉oxƍ=P$\ <xVwy5)ġ={3NJwe=*"A[wʵ+|.F[+KU>%UTQYvW7̶Z Gɖw>(uc<*Zs-ex}5F{`XcU%;z:NF#̟0]bKWFEW:   0BJgV9#r =< *Uȕؖu7Rnׯ4 ׭y]9PWs^M釼s(F2eVX!_׵^$S@2ou>fSλzGGT̑髜U7M+(ʒ/"%?qiz7xQQ*PRѶ % P8ٳihP|(D!=D7߸P4;w: eX/(0H0Xgn7fZEfX)uIHHH %˦J߻7PcVN!}ߟ\*P7t*@NB3pllȻ7]S+*~aw8^sz[0$l]<дAOo_o5r:?KPE!0/>u>y+ {T=F) {}j?#pSĤ.myMG;}wCpYx۷oF> !2OoGjCX>cQeӔSAp ^{ੳ+] %U1j $OTTs Z8,ޫn*SU_3ES]YFo:+,Uq Jτģ(ƗJJnCszvY<Պ` */?\Y^=jTVzP/ٺX=TX TE6cs(&HksXڿuTXrJ!/sSؾjL({DW{ah ByTAѣuwT7('G%yTG$ mmXv#ջm4' ء~ o#hs j> *蟉:vfWFb%/D= Fpln:d?/Lj ;@C Hƻny(܇׽SjtTǏQԳ= \Mod/<|<{PE]U.Hmnן\'C@0qgUUT2yk^'dY]')w+"v:R oۃTx)ܮ^w本{CP{S3Vgc[g&q〚{όO)5fpc.x1&|`ڱ'x"AU~<^}(* ?dD `j-ڵ6L`UQӏ8gc= 2Ic< bUPoӛ%HXs{YgMʨ 7ےt+yO)||I(go{ /z{\!(_S[)rgN"4o$P} dɕüub3/hա/ennzv^}NJX@EuWP@1@~3GY>gϞLe=LevPqB`ݚ@_Dՠ^_|Ao޽`۷VM#='9PM}mqV0<°"ɷ/91U^Qf:,゘Z>?#\S˝Uͥ :$ʙ A{A<{D+APHr MwCpؠfzPԧx#bQ7ʲu0cs* 3pΜ9b{;?_Ӵb\-q C>O\:   ieSzpNeG&vgp]<#e3+U-[#ԵbOmN{%)ih^x4Z! :VP7oy;7}%+šr^EF[T{)|T1t4@D:*WQ=:Z]"{ UʹNZVoݒN}R{36`L)Ȧұo<CȽ{$2LJ?"}R9 d\eUmAM?#sf]zy5¦Ι2i06hɰ<:      H*$@$@$@$@$@$@$ POݟHHHHHHE~p1 nTSã#      D(\lL$@$@$@$@$@$@HHHHHHH Qxk~zoMHHHHHHH^O{G'LlD$@$@$@$@$@$@i9(IHHHHHH'T}F$@$@$@$@$@$@$6POHHHHHH|"@E'LlD$@$@$@$@$@$@iתi4x$@$@$@$@$@$@ًO%4:Y斜u\7wF$@$@$@$@$@$濎U<5KN$wEiA @ O~zPi+Y2rH8q}5ߓ@#Knĉ mӦMO8S{. &uAɚ5kmnj#xH:]v.+}z|b;R3\3fV $8TX=5kٲev\A$@$p-߹[}_Ǚ9sI\\tաr 8={/ň0vm'uիرc}v {7~$ $K7'ɓ'["\qvk[.,\P+&tkݶQ)RĺJ/r&\A$@i@b[SS{83y}VJC8$coײnO_K~֬YҹsgiԨ ڀB)I 9?\ rEW̺dWرl޼Y!ߝ argF?,ժU]믿JXXz]fʹ{ڒWvmcU`o?cgϞp= hxkӦ4o\`ڳg*U-\gWXQYneɖ-tAC4u;֭+=}97 @!wʕ+H3A׮]]DDnwm޼yw5*E5Mėq- ׯc6mTn*7h@8t萼;?rOZF" Nw}'b-e<C<=S̛7O Bٳf*~mp;K.9r = O\?.Æ cgɒEB3"/܌ո-B1itȗSְ=)"w (ًۓ4x*l~``<Ɇ ܞ҈#8=*S}i(믽ކgyFKի|KM0z)=ƻᆱ?D?HP?~pC'ٶm9rD79|Xϟ/Gv ~w`uK! HVN#?f͚駟:)<_xF'b ]30YJq~HB~> 0;ޤ% gԩ#L7nFu81F@@N7Yx(P@ׯeS#SLZ釣} j:~t 9k>g$אַ$56H?7ycҥ͎eoH%&|!-YD-Zp4Ǘ?AqȐ{NnUJ?x`ĻKK@!W)kpzsV̙3:Z~ezޠ=z8_\x 4תnHH QzWTIBCCoAO^Μ9~{5M^uC3{ p@8- S ҁz.P[ti2IgTQ&bdɒҿ3EHH3<\4 x.^nF܀(\ > ;DO?zjӦM z02"gL-@ԁ1M`_SX綒|iRo8zJ2%ּTy Χ 9kT4P2e5/ 'TۥH@`9_i( ͯF̘''M}+ȚȩۥPR${xf  _Yz=h%E+9֩t(l *8Bp*8_{u X VhA¿֮]kVy|J>KC4YUla?VM4DPHH >|Z ~ر#;1%}=|/C|G7o"p\  4(DA9"bG3E9q aD ܃0pAg^TȮ]F2~W6D]C5c5c@^=:I-rfnwIz{юe7c_sȋRGe#g/2#~}J?!1G$[Pr9N`\0vy*'e=J5WCⱗLC<{4fu=]rdR)u_V^)=vg K^VH`REy%[P!m@$͖Sq" ʸX|w/(cܴM+ß]rm_$ @ Lߕn McxTF=T㇒Qk}B#h6XG!y HO|nC>b \*xGb|7**JG"qĉګhM!HЀ+H jS)bwpA5Rw%ܠl#^t믿tm,$=!{;LQiF.YJ 8R_1WDcxN޽sQB!G;̵bg 8)Io?a0:ꂓ (KHc;6afBn"h0C*-"^ylX 0&~Y= C%c9)G-'N'}lj me݀1"{Vֱ2S@|>c*Ju?<7ÛV=OR%M}M}sG2kX>nr܄r)!9puB񣢿3@›XQ!MUH`# 1E<:?p'P yiW H݊V]&CARO(/mR\9 Df *#ėqP||Y2g3q'/u\Θg`[pM^2_Dag O:U?8xE<3 Y'qg X8F܃&]^H@: o({TEDEˈ}NPpi3x2yq&>W iBs,&V9P%zF۶-y'7lwPX~Ow9T| RQÀ%sp6B,?7Q<]`Mj<갊#LӅULx4,vpF{3ڙ~ S}/Cif2jv/p]k ۬Wvwb6  ?>@Z xztcG$@i@b[ egȐ!<\DB;ҝ xnn 1϶ sM/_у_o"n݆e< lFBd"r}BvfarHbDʾxw_)l30h.룏>Say#x6tBZ \s?H(0 H׮]q>G̈s:32"&Njʔ):U݁5aD=zc6%w>84e>r󭂜O"?z]U׷W}IY ׬j觗m1nfuAHh::\Pf9[|Tׯ퇲z[ڻ_u^w3_3쩝/`%ʓ𥟯m067c`ۑ@r _}Lj}k_Ʊ2 un|gS5Ex^Ft#tc:I8}y=%֬OkܹIE`{qU?"Evkdy{]UL8{ד@r!̾$@rx o7'/x$c=IJ>SAmܧZXG!E Kw Sx~j;A d$T3ҧs%     H'H$@$@$@$@$@$PH6ϕHHHHHH ?b Yk7p8ůU}r @"2K%oo [[5QNIgǞ$@$@$@$@$@$@$POu HHHHHHN~ٱ' :TSG"      tvI$@$@$@$@$@$@@wVonniEDE.B.QE@)|,{9g/0sfv<9R*O,t8>, v:|{8;u8d᠚gɔ9dΖ-]ݹSvm"9JWiپj(ZX+̖LYHd| K,[-|K3[xR7J<$K^=ٵqd˟^]6d$ϔ{dז-@ 6Μ#GmRʋb櫽vɞm;5_Sq21F`sFɖ7dd߇oo̙3?Om &s/_>ɒ%K|Qk 6HBhol1^f˕+Wxz@(PXQ3U-ƶm$Ŭs`xҁ ɥC`ʕҫW/;wn,^zŊ .c9K;OjU۟ʏ%xLxپ| _}_y3YC{dF͢xo?JZͅuFSgJ:-e_2J3|;7͙/6=O&?zwa36;'nCqcq-dT7eإy %KNÐ&i9cʏIK{6ɜ#[ ҳT=S\t5nT]嗪M"}07w:۟\?G^=0#G'1YO^O_5_4~ϐ?ȻnYvy<>`?"Y1/ɒ= p8USن?cbN%_| ~iV9Z׬)ϽvfӦ+nyiC.l,\;!~e]~[™/}F,]O(m Ӊ/,1X|rWǤOJ*%{Y;ulNu/[L.R)\{Z?e{9GsdI،R׺u(+WN|dk;wT2 aei~_oɬYAOO:uHɒ%xҨQ#9rh_=;ըQCn馘{ǎEq߳)UF!mԡ6}η?o\GGƎc1]D C 'x2w}zj\|۷7zygyƻSz|**^|U*1=rb3ڤcv^W$'} ?ZƟvƚb/G zE73.U_qg@'ż"bf&".?' 'gvZ0mN)z6Ⱦ2oy3i~M|W5=Aq?cjX/g5eJ*SN9E+oIvy^zN'k4`?C9m' y5/"UT}?,UVSO=U¯{ҬY3eNa=n8Z1J]\s 6lT\YxfQk׮1y4X,詛/^,vfrWFy8or 7(;&u2m4)[l*c|X/֭s9ÔX֬]VƏ/gSZ֗^zI۾XeKm~gekiK@/BO?TyI"EԞ2m'5kg;?s2gꫯ4}ժUo=ܓf]b;Rn]͛0>`*nGAiA+[|+PTZS p%BPb6[K*\FNCYiSR%~9g_`__y:ڐ9[V)uVˤϳ\[=`2XWFȝ/LJIּyB֗i2g]=d|M)]sP W޷`Syyʞo-o1e&}H@Ms.) :>B 3kEc \l_Z\t4MZ&$S;+c5w./fԶ׸l@QVfZj1={2RpzKyQag Qcv. 'E]> ڵH{77Z}0X$"4\wuʸ`91xZ5I&I'oCԊP|a|^yWucy/1CZwqRfMfQ߈#ªUq2?VH۷ :mWRG}]}4YR]ۖ.np|cɺyta<Q̅1M7Oh0Ŝ:Nd"'Si>tx>8h f0#[*P3Qz ֲeK`/ ߮%,hR:R^>H_}{?0D0.u?,f͕ObRޛ5]z/S-fC.AΙ8Pr(svi˝~WVmKR֮ϛ"_{Žo=L)$KF0Yysep Xg!^A=`Pfy 3}&?jY; X5} NsNi~ru2 |*w\Fo~,"a^pBs52|K[ҢwW)HN`>:yr߿zaq! Oʏkl{}i!_L BX4.9_t7{?W Vï#8/D`_J!cQuK[fZo1dLKjc@9 de&_|vS7eʔW4h20h=Xۄ06m*cƌ?^aO0P9DEӏR&hA}>4ɟoFt颂`P)̀ 7K +vΑ{XGkz _{LvZ"hWe L5͟юcm`η)l ?oeA$h`QPB жm2>=U]~Ee]&3fP!;-:>nQNG<tQ04h0i&,v?tQAmͺj8ev`UOJ}1G``DcOkG?y](қ|I֌M5]?uFԯ^O׼r}*8B0,&z.@0{#:خ5}ًV`]C\vDQ jLO|4w1Ύ{>evo3"я= d2tQ*4R\#T;J_}FktxT~kzzRekA3D>~2?G*2 'g9v/PV>=w)>_6^\]7ǜy0~`@8Rt !\c|`4}/>A 9Yny#/AaڿR DGaw)!F-A/ޢ cn'i}3oZ%N;EL \ ;A/_a'_K<#W*$揍a[ argkoy?0' x1D+RHJ>xuk?0?ВL >p]6;foQ[yqJh!͎L=>9s~,cy ʵ^w$#㈛/nʂD?' uӏ+1 }00'r/ocTRW\z]ha3{ $ןמM ba![t&~Kٲe|* l^w<8hFM?R6p{d MhYxh_4Oh?pah4hb`vsUm~O-y)z?}VaiY,Yh4@ۖԶM1 `}aYQx@IDAT?仆y_ AV,Z|\*\[vg ~*~jS`*,cϢܞs-07U8f?oۘ\[{D8?{_z^j뛌*X2,&CnD:{va0#Q۷Zhp5Byч0|o[w:uH~0 Ad&P|̳])/eI>4$4-sj$72 ?QniMKĜ['~X @L2գ'a-a X9 7a; ڬG0hXF!"K2jѢ.xѦԍ-pcJ4[(m;Kޕ ps@.ǘHv=P&,MKoB^O4L>9r9ᡇR聸Tpuc%xtJZk(=F" kcXIv|V /VO. E |ڴisB$  DGs )jf?.Ѧ1I#վF~Le1G%$ `8څ?-941 B',YW8Ň} II ph5\IA!m ABhVǶ9OP&~h1}.E{FA'@2ϳ??m3!>-/_cl=W̙3Fxܷ~0X7Ks.'a̭=y3Ļ{=&{ů~ѢE tKF 1w`aTLo߾gz96$Dg1{b[*^-1:{oi~8g3G֣`,,u4H;ץ]k\a_է䢅ϝ=25Z2a32^/M#.'+ gɠ??PWnO?`.+Z,E#$ܞ3O}6~l#6ϘQ?7~И~A[4e~CL 獑|)U︞lJڀP {_'zxMO:Nc0b9 ƣuZu & mFZ4 Ll"¼<@~{;O< o@'z^X`CN4A:sR#, Jm/1GQUSv/^f+Ns 9d(`r1 6?k۹~c&Z=c"/'=θ.;mE ^ T"(avKzAon덥X@?#&Kᅢ_6 ٷn?| e¶}Al:G[K.R7?-\P}us  ?"6ծ]ip(f#, 9}˽DTB݁qҰr/v?8 fa/ܷqU9ie&O#í0!j Dep b a#\rI'.ԺdzxwfޭbMW->o2hV֌&~P¢ Zuzu 9ŧes45ӐYPAh1[ǔ.\@& |`WHN`ljB.Dy&f/l.*,XzL~1 m_?wo\`j>D5eX8fc<@~h_klui`>;_˞&1n05m?["d[k?/d\j$6u5[frz?^bztό]sۣ9A3Y\4\#/,+Trl9ł?=ߕ;Ly_-ұ..)0("%Vk(M@v/JaS'~,-x& )Ͻ>M}GBToiAHxst7y*^w7ZLm|zsojp0Bڅ?B  ,LFNҟF^IדP[|sl-SgWfd @nx@12{p 9c&߼j a7͞ߋpϫ{n{= 0i#P췡z;MK3,OXZzıƲ_obݠ꼁=&[,&(Qq bBef8\l bOt{5-W{%.!MCw‚kF@k9QA$#\4va, 0QXO!%(Bp ċ˚F؃[ fwb0X0cA@<VEcua ckaGl4aZiԾO!,@駟ʦM ׅV+Qh>l xb5cǔF1W_pd{[E YL}Q ոZ?.N{x>0ŻuMp:჈OP2 z1\O+20'B9 I2B(h`BU4X^齏M@8oc$#20q1gz#s€!'FX훪XZEi7뜘N40I:D40ϬbS7&b m2eR+Oi +[nLȭi/gMkG`h`/b}2kޞ0O"ό='>lg[_P펾xF*#_v;ۚE-N/j,F! ">\Fo 菉2F 9W*3 lGH>-\v|נqv%.sT_D7_V^ `G*%O{cF""PBK^~{TjI#~6 1R7g4=ˎ=fV(i)g,SvʋonjE?@t2ygvxw3Pjςd#`rqm 6A/"|0cexK o'k\"x". ~JTogT\w*^0h!Ɲ92?S&? :kw FDQ 3׌C\{owp+%@z6xY~@<IEeeK4x#CY VOU3?["N ^a[Bgx& )^0H~-FSwun%QQbЬs" fvD5`oM]l~hy[O:Q,a@Ol=e5,1j]¤%[ѣFƶ#K߾}'~i g1Z)/օ6W0 ??,a^0͏GhɈOf n6Y8o#^a%䏥[A)# v'[7J0F吝=σ6-Bpx… ǻٙNHF0<9g2ؔD/f}HzCĢ@H~)X?f3'<zg)l f<1qD~qh݇G įfh۾b2a&:G9 llF?R5 sD݇ hgwY/ًT nzއ}*"*D럽p;6n6x\5/ߖa$þ<[7,(Rx%=%5,8?k|,h[99-ygYZx ;1?>譹`<{5j-|{3I5 -n?oLqfڟ?u^u~#x-h Wg aֿ-S_[z!Ͼ_ϹF6͝*T0<d-ʨ曵؆HVIׂU^x1mL%!+'O9?MAeΜb^nz MdQMS$؏D3AXMZ: *q2/U6e&M`nzM;ڏ=΃̅3?(wIz/dO^zVG'OKʹ+ٲeS&g2ڵgØg͚Uxŋ$J]k׮/X"Ehwql߾=DvZ;/^ȴRuUϼ4{2sLV<`/w}','sZjɰaEӜiީlٲrWٳ/4j({[1j{%/AF:gĥw=S3RV{Sf}ι˧;1nθ4e$|5k6o0e)GM\GMi3SV]gÍj6X>eZ5mȱx#Ja<k&NIГJyW;RnVe﯌eaaƌ!;MaNt '{zF},)SNղC (uޠ`㣏>t\+sa0\p)^ziO/BH'w5ר A10s `f842Z /?q#2͟ t϶ck&\2_;KO&~q| 'ˇ%Jb:_0r-SK.Ybv-o4Ȗ/ݱ#MMїR )B_m{l_F1GcɢfY$覹 d7_7iG?]M?/U cVd|VRf±fST˓Y-e~l_z|Eb:%06@={\#wݫY%7hY3M[Om2g*[,\ SX18Rmu#`>b~SใS s{D,*_FJ *V'd/T0Qq2|mv؀'l.}J6͋ԑ #bbԸ ՗kQGK'YA'9bO`>?"]P_;A˻;Jðu]% S,?Һu܅;ThQ=&{RfM1Z㘢||1Y=p-ȗ_~)O>d=E|#G38_D2sLLzRJA5J SPy.C bKBF=0%BUc7`Y0X=@>]wmZf>%_l !դW:Iϑ 4L`=48 339N0NH޳IG ꎘ&W 8k,u h}F~y#xL*Ӻwu'× a+_~ '\eSOy<0=rϑ#.Ay7&Bc;aYfb4ʈpׯ__𻇂9q`*~vZy+HQ tEv]Y+ b[o%0~"!tb,=Z }e:Zl)e_czBUTQ%cY!#F`!p8 \4Iow<)O>_h\:s)SRk+~RCBԺ&ٮxeh1Ϙ' „iן\tE1 1@xhd=㫮ƴ`%`U/n[= C}YMBh_kHoi+W `^w M\'X3p hLeҤIzp1!{Q7ިyޑ<Td7gaEF5e!Xq6mhCaz7fB0nIʗ/oC!p$@ ɩ|TMGӁ$QW=Q1ck[?<4k7`[vhwxVIz?3&-%oqbDr^Ζ}jlS3V' 5ǒ똢9nh [4{FsH`/0>0RМD1Q'Ae`P$ٲyK0*h1!ۛSm%wKfmXG ׮00] 4n'WnGuj;'kr%ژ򗜣x0&>u-YpL~{Q3Օ ]!ڂ BTB0O`,:iUoJ~=*F GgE'\P<؏2/FNα=1n| 悱z*_Nqa)o| u1Q!L?n3%Fˇ~( _X11+wtҪi2˗/Wfm6̟(u:T#%'R|2AG+^=+;w-[0~jQ>QÞWB|Z%-z}]}'{8r!GફJ|'$q}_&Xaf3#C!po^Zecʼd_d+WL / 5 d ;O,L-O|Ok0aZz9{sܦq?g c@.wߤL~~&ߖE@$I4mGˏ:0}m > 'Tj%=?+!>0^}J6fC0EXe W=Kcwjth sliXUɦS0izS!/:J*7<\U3O >6fɨ8 m i1ll]L5:1ovy= \ЗqLBhǟy晘7bduc޼yseo9Nl')^]h֭B 1=x/m۶z p[ sjl6=h3BH?ύI$.wB2{a@ C`}K~&km5ޱnCHuY\9&\l^iuF+, `$\5Rj,m&@{^zQYԞ gd B揅1A኿ s }^_ۃ8AXha]㎶vKC*b%_Bwγ7cz5b[!0cE\a̸bFh!X/Of1i30H1V8&/  c%O};֮5 JgE Ko "~fDhM:N4 mNڠQ%B}A?&NIp5 hlF(DOlxҠ4 e.2qJ֧0BVb4bOķ"!Vdj%UXa<>Zx #A0Gnjϴ~a/lqimA\-%wwyml1=Zwb `"_Uß.9FPZ% #p@`x{N:aD(c#@` wXѷe&T [%NB޽"jb>_lkC!pd"MY:ѩai|/Ps.aAŠL eJv!WEsIxˇ2@[t0 a-f[MCC5̙0cm!%0)DCG[4Ky X`~]7s0•_5>&F>HgI6%rÕ5G $4CkT1AS%bD|cjl502|i|˔P/;T3A*ҰP2{I4Abv/Z|~BvZ!H-Z@{h?ŞGڡ`y. xX@`^wM5~Ho.C!88`FLmnUc&(#BM<(CERmᰛЅ=7n(Qy(Qha ǖr .T3vLmDu7z0ho65+Q"?BCˎa駟w'=kwܡ?5Ҡx] >;~uEy eAz٘_}պMwRoN>@CM* (#CF (D>t,jZJ1׶Fa$^h\Ld՚h6)BTHYw/*nj߹=~ ~,~DMf fb[&ea3$gȖh# dI¬"{9[;%uňzO^LaBX =!Lͷ->P62T4kw+DNk{a `:0`YBs G:q=z>ލ׺ƘD7y4{=4q" []9cbѝeg5{m __t sVzb aP ؾ@oav~ȋ ;ȘRLWF駭K+z߳Q(bIcn >3Uh1nd)Z|qga V}'{l7c`mc)্@^Zk9CG(?u12T5+b臟q^4,Z~7>41ƘSqߺmSbAvذB(sye^ѬP(2Ovs?ѯ=p~8" ㏭0s9F``NsIG `w~ٳm9& u_}?unҵj:7&{ʀ„E\`v팠tW _[bDfFZ-f{c<|Fx)gq5CDi,;Hm\f) fp&X09U,w5ۚʇb)ɶ3n]mr"<40a~&@92Xl7oqa0nb7Ļ~+V0A|g9ׇ߬:tqG~zh!HwQ-Չ:ة 6tIDATW\O f'pu8G_Bя !p8|NfB~ κ=QO JY!p8nt{{C!p8C!8觵K=€pp8C!p8 M!p8C!p8xw{AC!p8C!8p]C!p8CG1G|t8C!p8 M!p8C!p8xw{AC!p8C!8?_IENDB`graphql-ruby-2.5.19/guides/operation_store/operation_index.png000066400000000000000000002147551514115062600246350ustar00rootroot00000000000000PNG  IHDR/9@iCCPICC Profile(c``H,(aa``+) rwRR` Š\\TQk .,+v*^13;f2zZ qbrAQ c\^Rb"E@G3@t{ a r)@ [' I< ؍MB}-M PZQ *23J!`d`d o7(Ɓ+c`!B,v9> +H,J;KqͽuÁ^d`{Áop]~liTXtXML:com.adobe.xmp 1016 422 J@IDATx_: @@iiPPݭXOT)A:$4mk Hk/=cy z\kKJض-[b֭V~MUSfX|yÀDD@D@D@D@D@D )pCvw#Vl3o^1ibֲ|lKG4ߐt:+" " " " " 9rsكƶ[>n^kkV]s@#At{_ЮW};Kڟ?@ j/8k.'H!+X-m =ke+׾ wJmFH~ls8O\`;B@ rwym7;|iJg/Z͂[{zm^)x𰭜<(l/" " " " "CH!bF@ߺ)(μZ~ קp^AI`{ 6@ǡlsﳵYLJ,$" " " " "^wNr#o <4xVǮ^|A= v%pQa?h6y& H˿}X~ēn~/Pxc74E>[}m\kNF@@個yz<θ| _"{y  B?}Z|g?[am<>xeKLSKPe浭7YprV1-kӪ~MvH]u?mΜ99YʕQFֳgWrm2eؼysmʔ)nݺ}vq)"EX 2ڸq͟??\=y ]_%CƤ' W.om{M[lD@D@D@D@D@D`")ζ; .ڵkO>ϻ}np~0ƣ:ږ/_aCIyJjX'> _z!sydwӧەW]Y /J`@/f7[?~-8qb6neP֊4=J5om}ӆatU?Ă/ Λa*u:´XߺuXD4/ng6-_gH6lUVYr"{as0",Q c\mZ@hѢΨv]EĪUf+W^{EW ]jM<)v]oyhKPkP~lksαI&و#Pد~оr*6|0aB7lx')7?kڷoE?d_~U]P˯TXi֫gEJT׿_][P BNI8P#Jذw28/{ </?ht_;k[7lTd8XA(~XmQIuԩS ;!ݯ_?Ղ>;%ձfrBSV^e'CZ;צT#q++dYэ"> <\)~$x+VhW\qEQ+W Ͽ!OSOj7x}{}Y#e]B8#m_Fy7yӃԃh`,qW:e+]%Ӧ.X?c֤q <3{5]RliQ!W+μZv{9ϘQ 7mg^=vؠ/NUkv|\ gt;%KttW_<ׄuQPiڎ?ݓxiCGqDͷ~I.^y/1,XزF?}/##}N Rγq )Ryp|w߹)I2҆ćG& †P|Ɗnw |_8 prwH!rx\8G7$k=t3V3jMQ'ڶ޿r]b7<$0W<֡}>O+^/Wlc۸ְVFm]Ά x~:w:yK*| KZKޮUX!S}gC q9M7 ƕw^`4S397f͚igLw}UP 3jH?U掽brø)`Kʄh= mx`ܦVDD@D@D@<~!;9?&mFdx 񡈣ugo{X]3)q1߹t? ɧGIyt~x';"kԨy(K$TkGr9{^~Xoc?΍XpÍN~ݷ]H*o]w@1 ~OuJ>} %+׮yDw={(iOqi:8dW5ʡ '܇,"z"w傒O?EΎ(ٸqc׆ȍ^#^zu\z(`6 >>PhŘbԦ0oWɓ]!H"1@/hhg0H]@h/n?խ[׍+X1h>5o(|71{:̂HER R,Jk-߭3g#(ӄ#D{<1P~.]b{:FAЌ@{w]>2_ 6fϲ9x($KM(3q2s}3)˖-wƾ8UHѠD 3FwDSyfCʕ|fW0^p^SN /A." " " @J|ҚE+/>'^ŰKo{ëlP eM2C ?E0Mzr:҉'G}n/(a 8߻!s^%K;%p;h+VԳLruqvFQλ?b\YH] 䨑njo5o-Ӯ﩯yilx۸4Bm+.\?P['?.n(.l?'mOXe->z/Xd=*w1XDP8bؾ%/\O `bAYsn3b6k׮jʭ^#[q衇F^`yc%=׌H/Gv/w&ӧODkҤIDi𓦃=Ɔc=֭k֬I=g4r)5k: m2b睯x`?wy{q 0 :v꺁"5kr>}SgC1#HDn^w|ٌf{Fh~[1mEfa?&wͱ(}|Oq;o3DSE@D@D@n)*y3PSO>a=J4{?p_'4;ʽFy<1B.!{#^0yl޲7%0H |߁L.?-$ֺpoD._`Sx}Wr4 0Q@=QwpɻR xՓؘkH;AUlN { pVAo\1Va ||Zj9CNP iӦs,Y.:62(Gmҩ-_fSo֬y{U+# 6+dg+n\!?!J+4U΃/^5{m(PoB 0|gm<^/LvǤ{}_LrɧCn#KiOsnt!y ^~**'a؆tʚ򰍿]v;z _Vl#~^ne^тGy[6;ǏVD T^b^O_v!ƌA(}1+\'~{ttSd^ʹxʡRFRAA}kɨh@u~*&4O<\p?²94bx}xyun u??d(MneF%"r4hh Ӯ[֭ˬw=@J >HorQc|^xsዬGq!lqT%hz{$!e]HVADkY&Q,v:ߎ(^)[:}᩼Am]-/;f&C!~â.T6l˔Ilܼvm^%:܆;N!ho%&|xgA0 m>"ȷ;Ԙ ooeX#s I5O{(0Hq< ij!L?R$ljUWw=u=&a(^(.,'R>PБ7:zǻO \3T^Ul*|]pɖc}G2X^eϜ p&oZ," " "{)O! "B_Ҿ C=v-7WA}<ʧҸ#2{wfqwEPNn+޿v(O2 gVيשm?KS}H}18%?xOɿΚJ7o\3m9.~!h}| EW)GP9>㽏V`L!E;^9wޮ EPQkǐl}xgM:c}@' s>0z>ᴝXc~ܽk=ub:<E/e^`{BEEo(b+ ӱaZCQF:ns-)B.E;/C33x:~j%򃈎Ν)N2)lk dchCI96m3x#ȒXǦ@e{P ]hA:Df -s dQ(]f])yy2L5=J'|ytUX^vn;5jTwUyX _uϿ;BSO="Zbe\)~8D 'ګ)xȄ2&PL("u27+>[vw:#M!svPſrPڇ+'#KV/^#`Wc`E,;BK5o5;ʵmf/zo{oZoߴaAGy?zl8M"J+ J6JttTob⭏͉':Tq(61u.Ogyf+E @A-(a:|Na#m .8&$ 駟=]P6Н&A<h =B1&Z|>ӆ]Á?/[4L2TWP;qDӍWXH?_l?z^|b:6~ݨQ#"ƨB" " "  W\G(ax(yEъ'jjbC/W4z}0>G ǚ6?(8xՕW"=Ҽne6־Lq#I)AX= *a8Ϊ(n l&H7҅}۰^vt~ƹO_!p=%~F3V~ck}z.O̞}G*6~,{{g93 ǯ"<+$b#M.T=@u?Mzx Cقsxkլ)F%w9}>fX}[BO3v63vyx{f{YGo1Wkmxn'(^ʵkn5?J5i`4oě 2we0ް3ߡ6'l1;./8fZn݂H.YvgE-:10%DD@D@D@r='AA4yRꄟ'{`Sϟg}b+&9}Gxg (xzB,ষE W.o>c7"Co (l[(z5 %?"~>J<VOICE\45@ FD@D@D@a?N{aÇ痝'-HO(cjQu&^V#ȭgۊSlC/~C#AqGg =<|Ն''BrA"R+ȉī1B W( O:YS Zkg[>vS櫟uU޶]gslnx6,Z90Ȇ aOӮvJ>ƥqM" " " " " " ?0^K񝂐>~xlm5UӏqjQû9*kcx1pxk[ǴpS-C`1t"YB Y9"ZlVRyuv%<m^wu" " " "@6|'^Sth95|6[W"" " " " " {*){a5wۈ`3^x_]A`)++h" " " " " " "C HϡFZj+" " " " " " 9za4, )" " " " " " "C HϡFZj+" " " " " " 9@ nˡcӰD@D@D@D@D@D@D@R$ ~LD@D@D@D@D@D@r2)9hl" " " " " " ""))R3䫣@JD@D@D@D@D@D@D '&" " " " " " )"(5L@ ~N:H@ ~LD@D@D@D@D@D@r2)9hl" " " " " " ""))R3䫣@JD@D@D@D@D@D@D '&" " " " " " )ȟb;5@sՉ@N"hѢ4\7)i" " " " " "wX5lq탶~eU+ZH[-?!m]Pu{J\ww !Gw)6G9 h"~oWaTo+" " " " " " YH@ ~TW" " " " " " "H]u\B;oڴϟok֬¡ZV\}_ۼys Z#" " " " " "k ~֮];w}CZjYF졇˗4/=ܝ'7vwYͳcLjҰaC{56\F e[ok֖.]jW^yw}v':jmڴq^9ك?c 5kXbg&wƍիUC *C4h}gvG;o#[n1~idD@D@D@D@D@D@D@v <#FpA+̟?wy֩S'_o۶-ZdׯO̶nj .4SR֮]js۲e-^8n{۰aC #YG(>٨;#MMׁ|cQkE@D@D@D@D@ظdmY޾yZ۰hIVw|`<`<*T(fΜ կ_ߪWr{ク&L`gu֠A~؜9s?PcS5kZVcD+SNu'*Dʕrv@_(O>7@ժUN8L}*V7޽{v:iҤxHv믿'lUT8Q˖->pX0={v}8 .wkqfL|Oo lnOR j׮ֱ޿XnoꫯN0uֵW^y%j$" " " " "9WWg=Kb}j^ZZmn([.[C%}`ߊ-aP(1‚bڱcG nN=z!t6f駟 |HzYfv9غuGƍ^z7废znuJ,ڿo> \.]3ϴ)۷#GZ5"ܷn}># RK.̂qg(0<\ʥE|s=K/bŊ9?p?k?ܫVj'N-<1 q3\_|9D…]TIdE6-` C)%" " " " " YO`ۂ`2Vcmÿm=K-kY?rzܺ)))vaֿ|N,_|3'9(^Pz۶mA-ZM}}/^<S%2e[n21xȐ!.:e袋 Ay%%1 ~tJ-D=.{gE6o; 5F pڠMCFhAi>(xww wIӦM/vQx ;G];iw |g,YO~}DbH]nJD/w'|<-G xG/PuKsռ\z96Wb+#'sH&TMMg}(|H1skG&;1^wy/q${FIGa~<EG>C?RK/]w_tG |TRvg;/36 ~-^j#|~loٲCڼy +l'jVwV@/;05j G'jwߵ"E;sʽ7`PȬpӧ`Gq,.Dq (I` ?ؓpW׌h/@FEJ'iU`X+ӢtC+ZGakX;O^|?5Ͷ_{ndC;c'A4\{oovӢ͙oÏ$omχ72Ďΰc +RR@M=a(5(8(4P|pXÇ;#Ax; btߎy;vuٍϭ G~=a^1 mry,!7PKsc+C#sڇOfŧDG+,矼9&n!#<2j9!v>70 |HQDR|y;\Bٲe3=o0t8jP~ß1Si rڵkZbTd$Pv>6/lW?يkOYwʧ6oҭ۲c#hRyΘmokx6Vl#kavQ׈rz\gu7;Bw8`h!mYb 7ѩ c'ѺT@}LLDP;(VNۼzMd%ժ_6=ʉS-_"n{َZ~8/(ݳ4{vk?{k"M+Vٶ@ˆKlâ"룫J~VIҞȧ'ëRZN6;kQSCV  `5MuHrUWL19pwER+0>xAKӰ}x 3 \fJpsM7fw`t#v`E>Eu{3U^t?<+1.u=r1^qz7pPM6uԞB?c51p޾⋑nn~J-^N }n6l?췺9"ys!uOg9(29}kkWakI-bܤ`܋>]paw_"mf%cDhmCC(W 8}.PDP\/[GDd <$=2Dtl ';gB ɶ7 YdsV_"; &dJ}wt[//臶mzVwv'xM8|M;vG{~4ԭ[׮j;C>3gδO?*T`?[סCk_~_ԩSYfvM7{\pad9r*Tڴig}GD@D@bSƍm/_>{w]c<\{q駟nŊIko?l,h®Jo\v#/TT)ر.]=o=z_|axv%X˖-fo5˗7=묳,S9D 'sB*U3ϴN87w\\EOѢE/J(a7px}F_;#'Ry>|>:ay)4$RPy$੧.S@& [oՅqON>aH>쳶_o/vC#!R?31)8"E6na ?&Olܱh@|xE=J3>ay-~1~[oYʕ]d;0`,.P=o Goܿko믿nxfF#|ΈL; |u=҇D`O TN;Ǝ=X#c-Ƴ'|k^zS1±kgd_*z&Mr#M4q:=Ϗ8ɠA ^я~G:dcvQY8蠃gy}= B:{1QԨQ#@N"A$w-?&4SOuJ|Æ o߾p^&pnadխÇq!t M~,}5׸|nR!v0Vh  ҥKf3) Rǫߩc9ƅ1ѓBuX!#B#8;H2e|wν\rDq0w@"" @)4OCǃ (DȗǛk;?yd-$" " #;D#?a2c/,FV^(@+.#}.,RQz5A$.5+0=.\ޗ_~v2o{>{rGF6%Ry)o7EZ9%"O~.(" 9@3#׉\|TF=s$#˿Cl==uQb{eվ{r<aFmy&A&ϕBlgvr{# q:H#xGb6B/RB?{&=6En%=}8f̘)?{l)0d_6y3Ʉ{1&τT8VtgR5-'ƠS?6/fҧz" "dT¹VJ77BxgȗC@"mV[}L?˃Ot޴3nj(Dx~)\s!# R a|ٟP^"B8 /b}Dxg*1_ 5"K³VŽA;Q/Sg&?rLE8> / |7g=Rw_am3p6:3Pܑd_*z܌M`@ &{DF+UEA_̙>R=D6v*DX$.ϏϝgaxfPwg;Ⱀp0g ˄!R1L*iYD@D?x}N+Ǐ6,H>੉6d z& †ÒoF#Pa@q`_XoEggߩs*U*hC Fxmbg%J7(%/ٳһVb.Uc @D@D@D`G $<ހA|f͚hOD@D@D@rd ~FE; HD@D@b8ꨣW^v9'uִi;iC/NKD@Ȑ֪U4hSD@D@D`/"̃//2TE@D@D@D@D@D@r/d ~O{Qi" " " " " " "{ HϽN#)ZK@ ~vDHЂ^RsE@D@D@D@D@D@D B@ ~D@D@D@D@D@D@D ȳpm_hDMD@D@D@D@D@D@D`Xvm£ȃ6@ ?w\'RD@D@D@D@D@D@6@ ?w\'RD@D@D@D@D@D@6@ ?w\'RD@D@D@D@D@D@6@ ?w\'RD@D@D@+l֭Yڧ,;л@FF߶]`۲Hʚ5.-keMgED@Pcƌg}{1ۼys85k$U\֯_o/%]r;N~^{֮]K@,|'w_6mZ;.^|7ƍ @2Yl6W>s[*>yzɒ%IhYD`#S ;5H]`h۶;nf*T!| 6؝wiŋ5jX%믏k,d۷7^ wzYޏRlY;c ?73KN Jr mu[J20x7ow֠At}?Q;$7O<)Æ 3N:{GoٲU^o0x0`]uUv7JW8/2yS+C *Ǎg-J:VCA`_83GP`D<#*g"P|va9;vDiŒ^zѣoZZ'"k 씂P,n^%4}>:Ŭ?uy t )>K(ώ,'v 7<,c:tۃڗk%^yJ<VCq0^qvi\p~_Ê0Š :#G߾}3OfX" @Z,e@V.l8Mlpӭemȴ'^|lVBYna{{/+j=`钶qr~EvWg{8(2&3-qU6ض[x:Tiڏ"8=1'FhCVb oLlw=Rm3;GH'۟h;^|ߍlg}ܮ&|>/s=ׅ9ǽ{k)A(ۗ_~y!ϝ;ׅ_wunLFr~.e/xmٲŊ-jM4"ϗ/<^"ZKA?J& "O$=JX}77i$ڠE蠱cZV"?Vǎ'+/Rٽiƽ)@}$мyڳ`nu*R91|Yg^팓DJev]w9;#Gyap 39{]{O{@ - >V,|{w[7!@LͫV;n6׬7YVm]z+(Ӌ6kzN۰`Q?kfϳV|lWvm |!l?eí> sF}lqX}+Yn/YBmOڨW[A_>Vokk|= /9ׯn͖ i.<ʴllfcϿ Wh vZHH3h J{dcJ; 6^C9̙s?3!(#E]^ƍ;6xnQOomrk:cX_B x!5jˆQ&>s;s\;[񞱝a^7-?dg <<B} ^>X?ƭh7އ{]nڭ_ t>$?܎~?T o/8q?N?l&LnͭNU*ߞ="JkE$)HW=άU|y{ѹsg2dk};^٦ug$Pv<L&Vq}+R->鷬GZ㏟_RysM,mQS.(s?, oZ-0-G7/ HJ@_`?% u] /} ?BH " 9@=@h} KxٯNqڵkGc_̏ܰ0Br|'R/%KtEv%<@1JJXPy&2z+#W 'Qp.(G+D W&*&M@Bg%#5,R1@S\kYv~+c"NK /N:>(s]t7T:P *{0Zbmwlck+|㙎m1TPI#9DqR@OC~< 9˗/재oy1X!&\C> DsߐCJ IxX2Wx2>/ކa’V!ܯ_r';cCǸDмbA ?v I0>~/-/ƹyh 9%W]D@*̞8 9xj8D>^('T_v2L?((1"V(_(9PX3 :PQÕIs~yfIDSL1>o򵙡޺v|!',7m4Bwީ/KLq jQX3ZRe9DMe\P)JW*G'4?TJ4յ\{7kL]?jvW_}s /|-L$qs/ļ$$M 3>}e5ۅEСCunsxu릯 I5Gymڴqw*C$R d (')jM{d7w1*\_v(ZPߋDzt^@hZ26!u9[ Xs~w Ix+_Z~B?M9:f v.fMwQ^~u";:m{bWQAV"DiRY~KCe]Թ~bdH!Ox$Lv.fo^m x +'sw6ē;0bBᦏ_Xs#^~}~6 >q;e<+(C_Pd X*sjN (2pU - @pBQIX\,'`3ǜJ4b_AC7VpPk pHBh<0xo$4u0y~xk LxADS0I "O 3qRBm~o=ه/U,p} C zX UtMZ>YnH׏ W;׭wu~왵ߥ$IC3wRTa <\Fa5{PeRٮeʝ2zHTE]Lg$& ܭn~Lk\78sL¨h>mȳ趧D@t'Vї6{bt>ûe$@@PP׎bT:2$ FkQDXqQ8HXΝ+U9 `S$&}TKtc5aK3hI$@Mq$@$K `' AQE".Pr" T@a 6)΢ۀp$@$@$@$@$@$@$ P*HHHHHH#@y$@$@$@$@$@$@qHHHHHH;TL$@$@$@$@$@$ P*HHHHHH#@y$@$@$@$@$@$@qHHHHHH; ûɓWIHHHHHH@PPhIHHHHHH v;'HHHHHH @ @?v|O% x%@+$      A ~8J      J W<'R$~ vr . S4|$Nʥ;$@$+/Jʔ)%Arm6l:uJ+&/r< W\੯Ӓ1cFnܸH8tMs^ԩSXN$@$@$@>^29^״EeiVx56UԬVYlؘlOlOtyKd.e! @QڵVӦM+ &zɱcǼ:j(wE7o|2gGUVI>[lʊ+c.mѣu_|!*TWvR;$@$@$@$QR] jJ] ڿ_dvZ1$MbRal?IWlD%jcHwo`S޺-!︔qH# ^ǏXw!W^Vwz<СCNl٬:cǎO_v*Xww,ޤH" ŋV-Jaub*YXʍFV}D5}(nQ$F3Wk\2T5~ T}H/GVivw~Mo{왥duwoWw헵;vamO[Tϭ5XdHƛ`m% +AGN7eF imݾcew#tD.e[ɉׂ$E|RvגFE]s Ց"_/F!ӌj$@ɓ'̙3p KFCz@D֗^zI/ZH{z;xSue˖+W.}͛eْ;wnٽ{.;rry=1uidÆ rKOBkx'u;I>]Ć]F7o\_'=N8ϟ:เ~x6`%C o>{IHHH "mcʢ$1}`u@?M1P u MZVūqčCNUT2L)˱)sd3mqL s$;z ə˥O_K?ojS:4ןU!f,WeOexz\Xcs7POKݥDOʎ2O ˩ًu~K?K<9dՋ]x *=ġR>zcafb0qk{ą՘6'KJ\J}J^:ғ$& cZ=9$7]\\H .\پGw Yd`͛7Ž0\\ϟoNן=SLzРA:&i^#ma!ѕar\$@$@$@$9ˇIҥdwU,\)TBZNYJ ӓ +ooMXl` Ч%Ou-84m>Qn<+gʲb\o;8^_Os$Q0:RNuhm(& ~Drs/]WP$VP~Ʌ5$CJr:ɲgqafKGyv5VdoH+ww1iZŜJ}%Q.@96]tq\39>eH^pH"`;/J}X}m߿>|X*ڷ{vƏzP}]`ç3˝ ŋ-77jeab?< `l +`B((CBԩ"I&R [ʐ!C= ^,/BXHH 6nw'yU<#{]*R𣶒+v`8q)SYu¸mY}]pXvkt}_\G~ \Н|ᲤQY,ͳR[yLabn=Gk#Tb0DR !?=Ya|8ϊ#CsO!`u{U_-baw'Pd#lnem@F SkCrtҹ !9ڞCgg= -&*U M2jpܹұcG 7U<5> @t H|M'O0AΞ=שž`+L Q^۶mtm&@0I.s]xCY@W D($@$@/qȚD)KqM;txʢ\-SsXvϞ?1p@ÿm6rs疯$@@Yct3v.o٩c"WNl)Rf TD!SSc)>|S+h]%"؝ $֓.}ABD?6 @! Tv9D32n;vX6 n]=ܥCd]"Px%̛7Oۯkya/NPO^ EYwIHHH <Vam~!ȿҙ vkֽ K<2csKɳ$=E9rz$7NKwmuHpLo~!{>;cKuXJorxY+R900d#pI;+ʶ|oK~V;ڵկ/ۺЧ` vȨgƎ_hfXe`Zu)Pԑ8׉UBֶXH~orm @'G}${Gk׮Bhs^)֐3i^P|wڥ[v{oK.pd9P'NX#+XqHgn$Eذa}L2ոqtرc2#;/x 8΋y> G R.ҤY>ĠCQ;Nrí>APwo,Ki?גb>y۾,WO S}B9̪_AedCf^k98jOLjM-%uQ/d=U~W}ڨ2mH/qԒX]#Vj3F |.ItQAD)ƌA=x*(GR"j@IDATl@|#y4S 2* }CjT+S}4IƀJų]W=`|cUTzBńI jdiu5|_~ ;/. @%еkWCutΞ4$W{@U_5;<n$~hǏ/SL1Jbt|OwcF{pxzi?ŝ ##/"?) XJFɜ9sPC\9XEB$@$@$@!O*΃C">đ6!,AGNH i%}a}ׂq }?ʬڤ֯O ?^j\/W,0d2eϿ7dY3Jt); ڌlpG{xy(͟|IxUkSv{mvΉ[($~O>%2|^~}UV &1{ >Xnz2nݺRzwq?vG΃.]x xCbW$@$@$@0H?͡ ďS7n}$NXzVXQjԨq|<@$@$@$@>2˿k^ [t5忂u%VYlX}npYM=-`߾z-Zg$@q=zH,Y$G*U*iܸ?~oZ]EI^z~t\n 2k;vÇڵk֋`4aG}$Kŋ޽{yA$@$@$%d5a~U]2AL.o!K=%/kJ#K*&mECB{vrn7hi MF%'O(׮]~Z|?9rT^]O 2'NK@S>1^XIjժRpaoբu_ 0aB[ʕ+}6B$@$@$l Sy+UbRsLMg|冈rwn!^<:>yCH}hCzb x$@5k׮SOivʼb <./V VJÆ ={HZ$I$nsBO!wc}9]DIڵu(i+"ڌٲe .D%   0%?QꔒI}z[Z34wd?ʡʭW%IRg'Y} o'-;?'7q%EuiR bW7|]-,]eGѺ֕rz| }GR/(Ň|!˕QK;q~j'F(>$ɔ^تReKۮrIՇVNJ$˞E_ټSv$VF'͖I Yhxzrll\ټKK-_m"$/^)w_r|f7%[-\`9$ V޼ﵲ_ްM6T" U+j'v A$X8uꔎGE.PǾi&V>wL&\z@JÇeԩSeɓ'^xA``~ \/]$Vh#+Wu۬Yd˖-rKOB5kԇ0p@y뭷t}e.ƍǟy~ЯKȓ'uʡC5sy Kt"n @ DڂGe~$4`}~| 7ҿ$כ/iEs E2}e}wtw\UJ]n<#n+eRlPw4o$ϓ]Vm)y[v`)7 wX5`y h&Bt{oɒ%`m   DS,;7ݞ}~IPQ}`\FR.*rZ!Y'`$sТ@U.,['yU9^{^3?Ey꜔6\R*K7_zvdD}=VOD)C31K+9AiCYTg"IgMe ]pQQkJSu@ KZa sM$@Nv ӂOO(㏃H92r~o`.N}L /w(H|gmۦy(-w:mbb ;Ga,?SHH"D ;Aq޶aj8uV_ Tn$ϻ2munҬaɩJfw{*K'r%?,UҼq粺QkZuRdzu񊤭ZZiA >MSH~91yua=OL {B?}Nʒٝq=`5ၸ|H&@b % :⃘~xNg' KVgob$Sldӧky%ڝ!]iӺ 9̙3ˁ\gʔI6lؠ|,~@~>s &+ X>cưжS@!&'#z}QTX~<<0Ye?R]*W1I ]'K"{ًvCڒ@ nM,y(VB?4t%_R' ~PwRuoB][Z65QrX#an <RqFP #;EA|$ۼy,YR]J+ =&"C?m43uq}]Dy-[ ӵ$&Oqn/Kw|9uHHhI''\ ue.VtdG\y${$"\Xܫ%u ?;gK,@VnS/\۹m>d`WNwЭE D t~(HNwf\X^wx`c-յ%ɐNA~Ⱦ6{J}-__:, x| 1Pjd]#f`uoܸG K:&?ʯj/6^ -1E""L0yaWNPza.#S;pHHHADZ~aSo,ݎ VMg̠̟6 8:^|]ӷuHpusƉӲ09p֎=$XY踩:^  K+=/GFOS:K=HBS-:%aMսoj5NN .*${Y_|J υ| HY-wy&$Ѫ\޸C:O$xe  s3pAE{wIXm>P|ݫ{9ŋk%|Ĉs\_zy>GVLM$Eرz L4IgGpĉ!2#;/>8C   W&Tkêl,81*\_[YٱV=aX!Xn+>26!u(5ww^P_8Uj ok%ʞrtj 3Fɦ;2{~]dG޲cO}mcH/QTV<=+7y^vԹ~ i}A=\bo_.Oɧִ߬ꙕ@ uFH"1_*R|hCY^-T%x\ˑQU׬A$@@ΝqG9\(q>>M}iW{1l-ħ;!3> &X`?q^w&R̜9:pzY:OwcF{Uwha )SGL?2/XcI<ּ[VƂx;׉r D@[R|,P̍b9ֹ~$Θ-0˙UM *|69&xDJA//kZQ+ |X*W,3f̐sylfٲe(Q"i޼DEXc=tDŽy$d޽qOw_D4=ҽ{(ԯO:9(u 6۶mʸAQM֭[;n8W\jI{ͫɺz|o;ζ'Oz +t]6eԨQ qiYf!VK/䲏gw-]ه˗G? +_* ʥ aGy%SV&X\ɟ"E"SܹsF!~tw DSJfOɹ{n_ddǗdπ嫒4K)'筑0C6wF~'8M*A 3%=wPxe>s X4AsUYf9>uܾ#Jr#J㨿-ޒ4s}|Jv%C@%m!YrAՇdYQʏNrd6ng_SvǞ,{f)3i]v`ؖ$K*;ڈ;q4^Ky[NSU*-U7em;\_]j  +Rzo tΜ9*]{  ڟ7oK5kViӦ}+%:*U׭['UV~"pEWVy뭷aYF OږK#vp;O/`oxؽpva(d|ᇂ{XDF0d޴3W_}=0LŰޣ>H - >.-erh}Hwyn iJ({"髕 WVIٵ|z/JW񋇎IeIwW) l M ə˥O_KyՄ(jSj ,\)Y_#$~IJ񽞲Rw ƅ%SݪZo I3R J9"pwէuICQ.SVP\}"Qa>ܱcV`Pd p(ٲe"^ x=LˇŤDYJEy.HrtM6Ynĺ >bk)$]G/L4/"{QOy(` \rȻwmߛ. p?iqJuM.#3_'7P'}BQ/pu{SWR*ƄB'@DP[~nD'&b01V_-:tp0I$3MO.ܽEK:U'髖4e_܎J Ie$+o' |Euvj)i{E} ÿNVɳR}XI,:+m:G /Ӻu /=#VדN;&rX{@7VCjhb}{4);ow;0H~ޟ I$oLv?fO7Ue ,qC4EF$LV"fm!Ija< *D¥#kx2XBXB$hT.ӲxKV0mG;k,ӗlM4gHn.=*^PqS{H4$Rp_ceq_oKb5)ŰhšMV%KGtqahvس7m$)& &$@$U눶 'Úwb(&iϞeP+fOXiWTrpv+Suw{(F A K͚5ǭVlBl/6O.I  O$3I+xۦ(O(yk*^+sHi_f׶io߾+}rA+3upPz ΄sWfr~V7}\yU8]j+π)LUcٍvHB$@QIN 2C0Q`mCPO…CL9ad501ـ XP /Lt'P]]@D t!+AaXx6;$P(yXKۘ %x\.r qNp}P1+=GDr}E}̘1j >uP-ɇpo0++v vQ # !5>ϻ^ȵ+3'pLJ`yO<{z<{,G!xDQI#۫f:Kt붨dvu#qjOx(K7rI?OQm2F+dbޟzV:ᵍ}rcRnN)UW] ߤ7`qF:{sm>/%VЀ qN/l5Gp{,qǸ{ 'ؽ%tÁB$~acK?3{YX\X*KǹkYꞢ_c ]}~ɥ;Uv6z:$@ze7?5Xݹ~C/N_L҉RdW`:wzm0[-rv_$@L>2Ûկ/ۺN"g}mp'X* 2S^y۾,k_,H.$w)$@$&MbŋHPK9 ~߾}2qD aD;`/"ׅ+dڊo A0N!<Ydі=Iv,$9цj2~WA roPaF2=oXM4щm(MDŽ&0!f^EzX17 Ҿt%f"ݴO V]F{{ Bm/3 2g}e5ۅB* x6/O^$ b'yjAHam= >!+BuQ z1Xڡ|B6XnE2;u^jI^}(WO S}B9̪_A6ZF!Ažr{ y?Gw҃Tq]qQep+^⴩%FV1J.KCI2WqA<R#FMɿIk~~9?|CM`,%OzlȒoZ>XM, ?;F6UI$@"KxwNk96C ֗EXoWjwwG=bRxA`zFp;e^v/Xno6! b~am۶z2Hg\\X1abo[5E!cwXr $7.cOo#xVUXk*䧟~/s K!Wٱz Xp%A=L4I7j]z-xppܸq#z>u3wɒ%K",'rd˖Ms;SAAAt>09d~Kږܗk!$$DΟ?m2M${ DJ٥,,ḓ٩Kko΍hd[G4' pCɓҴiSI:ȑCd"?/_.7oKǎoFFi=uɐ!C~I&[/~70an棏>ҥKŋEɊXy~"p%w*^x2e-[V ,hbެY3)WUߵv[nmqn8^fn3cƌ)S&xwom۶I͚5%mڴz!O<2b{믿 ᾄ 6C-`RZj_/WV\7oʧ~*)R\rIʔ)w8YsN}޳g0ۘĽb/X ٳôx8p,$x"(׏SRwu ![aGqCY($@$@'ꫯj+̙3eңGBpBoذA?Ci/j*Ν;BVq?_Nyꩧܹs\X+U$9s۷ѣG[n& ,ZHڷo/@5 oi/mY'ѫW/ٴi={km_Xr Nzĉ2zhqdƌ; >R@0p1Lt֪UKO 5JW7kܹseO( @o7͓O*a?}xaRbС^u_Xr ~{ /= ^s0 6LO)&40I$ǂ ´p# C add!yH26 c*[L.fa#1fvS֋ uH񟾒iRzG![w˅%qԒ} ћ/9rMXy8VmzIv`,^PJS?^'ȭKW’5n5)6HHq [Bjժ. о}\;LgȐA!t˖-RLmɛӧOk{D|4X` \X ( Ɂh s='ߟs-Aە+W.!Y(8%J} wZp ɓ'^_.p?uVF:lnSNn \_%]t&ؐt@Ʉ[~K/V$PZV8ۆ FmPBS`ƍ7ߗ]0WtL+7x^źOᾅ_ri~$y}a509 ^~e{S.?wE"2&L'05j0.XQB$h<,{{K?3w?".kmg])eH xܾzMv|[V7h%WO'GFM Icήة\-KȿlU1e'ޏ<3g$ɜ^ . SȲ/H} %IZ&ھ,jU  x N>vobaCv;VC %xH3gV^ ^` moBAlرCwE<2+B cB(pq!OęBOڵkڭJ,ǏHT11 XC,Y2+^m,thRE0YLyBio3:k/|ӭOwm#v)p68v1WJ]v1f\m +8&q {]ܣn*;wž{AN@&~m ;sRќ@IDAT.5V{:ŠVX!=D[}ȏ?2Ab8={Zy5"ʴ횺$$"&_^b~i=Ia{*`o! F 7>G!xtDڂK |RNZa KJI>qoҎ$~W/(Bh6 =zm&Tpυ^U G@(=ޓe+$mZ:|Jo:GFLp)΍ڡQ(mj|X}X*K_o,,I˙U.ܠɭ~A$@3jLx@|i-^>Y38dQq\իWX6HD2?1I=XD ~ˈP585cz`F`)W<8`ecP3QI?ݿ.&y1{z~I$LO]TV|w/>%O|#d$Szm:)y7R~R&vt [ڸqcmł71vcE\*nwkPxMblmA =2C/ׁM@@. 28I ?Mb m$u&uHNek8b7Hxm#fNI(~g8nϮDcYquWiޗ}{޼yzיh'V]] $drD7DsMIwwއ~=0O^]c !%1ffj+}wF_\A!^AKȽuݝke}-Хſ,w;$@$ ڵKgEṷjDq>,BF2ĸe.x71l!VE{,ec` }UĄ0aWL9|CQEJ4y{z`swS;3<FA'} m&X >NA x8'CëO>I9rk[1gΜ3#y![<񇽺/lnH{X:ys/;\-j/vҥu"VgH7H@*{KJ z *~K|lkYۤ<}.rk\?vRn]*{{Kjn; xݳaÆ:K6CTLaBIǃ~Dh!GVځ%<}k`//v<G}L`l&u >2Cс ˑAy2k׮X0.pzΰɌrs)$$EsV|9a&!1OPK* Ht[}u0|xf͚! L⏕(:t` 﫯9~y{[fBЗ0N{We,(3W3>}e5 ^<(`fX1nݺkB>spOצMw2,jD!L$\#|T)ZH?q"2ꕟl|#9$ΐV-7Tr{DiTQ$@ C'MClԐg/BO8^ﭶ}dQK$@(Pr. YR0 }Jž={öyF[8Ǹڝ ,J?AІv>c-fxWnF`ǒx #.ĈU}̈́Bv ]nI!`.&ΐ ޞS}:+9Vxw^PV!.Pﯕu$CwcGL2h\A-o>ylϹ߯S7~xkA=)X^DpO 3qL{rD%o|z_iOzkH@YN$@qbR,P#*H:w(&60yK8dus#S2@& 6fPz`D,~$Y=s}D ~cP aTm~O]%" 5ޑ^Ko*"*ةDT (? қ{キ$@Hr&βeCnofwy/w3Y__B@b$_/o28{pb={y$@7@DDΒL;* $;P ,.]ZMv5jranmJ }SbX<ʀ"y$ [[HH6#5k֨gDfM`D1۾V,!  /_9w      ۆ@ ?Qoj $3T 7TQa $3T 7TQa $3T 7TQa $3T 7!2yK $҂ IHHHHHH y<'HHHHHH IHHHHHH y<'HHHHHH IHHHHHH y<'HHHHHH IHHHHHH y<'HHHJ… TZXb}H$@$m\qgxr7Ơӕt\HLG2nРA^˙I$ps $HzD<*M;jCcx%&Z ϵװѷuu&}KNR'1'%/M3H͚5ery `?|ݻy%_|q8tPyW|_={H l2JVٰ87mT)6lM6{׼bwwܡgJ߾}k׮Ν+[o%yTO?(Yf'{kɓ'v|'2fرc2n88q א%=4o\J.e+V/7V ~bڴij*]x$HBcL+'wxΡSUʯ97ϙ㫎Ye^ddyW=^bLt:AW~@cTC,In.ɔ)硇{ܹs7 v6y˖-Sq2 %sp1fkʛ0a9r 3M6 yO<ӧOWm?깕e pd%Ĉ诺Vn>ȖݫW/ORlywyĩ`0om,EcS\9{9s:7%V*'˱)sqiM>l*$|!Nk#MI,y% A ~%ƶǦ͓|YFG(u& G6.l)-!3HÅclβ2x}9>s;TjFN/Y-}lcSɑfJKGJH2rYUMv헝SI.qv2Mj=iTב 3lc9b9ly )c̥RjP'f-T Y*-&|+?y[tvzlQUޅM;$9uֈpr5IbڭʭcbR>Ra`Ou2t0G/>WZ,9t,:=q/'x, En|󍄄ZhHE}YSGc ן1cX^!CݻÛl׶p?\:$M4EݺuU"a?~ypG*Q:* \ԩSutrrؾ`MPkժefEGG)+O0q -Aٳ3xb%Ν;Y'$@7@P2YIC)ݰl2{d,U6TKc% )g*^Rr֭CL&ʻc?ifd|Wl d}%ƸqLZO5S$]:}dr_\Wm)BRIms%e4\}$$>yr~+˽LU眵J*%CJ~C`jB>Yh'eO.vĂA 0/ICzr=*1WJM3D_:0f}YBO<<v .^->\9vJN-Y.ܻtl [ӊvk!pC.S_ݓ~we-je+9?^Y' śl[/1*UrLd(\":uX!Fe9[NYA=u/J6\]W߯/t > C?u`r TdPְ/GO*y7Pn\\ظ] <XH)-EUx{\QI.j*0{Ể+7XJA–]NlY7jc:"Ml{  @=ZEG9W_I.]tV#`~bضmjL_spqx-`…^iE{<+`e`>`8x{5nnoSE' E (.RUPs6J=2uLRCr┧˛+N^|3"Op. Wc'yIol?LrnF9D#`2o\PK6 k|R̮ zOJ[[2@{)l>ewZ4Y;U- N¶~_{'V-ڲHHH ~fϞ[nmkJޟWl :^fM}ZEY F$Wd[Q;E;Gfus2 qk,k;9 *WZmU@@sYf%K4imۀ€2gٶk*OP>xTZU{1D$pkӪ?Wi~RM3GJ^UF’xEcIhB=[eM!ĬE. Ict~Ve !@;[VnwշhB_Vsjyۚx=G,|?Ӎ`; D$TrHۘwh\Wp?{}UkihYE*7(x"tu Q2Y|{/>&K[uPT/=Qc@1(xh4sDdd+: jP }?m|c}o~z:pňI){jxR1wjAz埉HHH xG 5J>/ҠAS)w#hXLoJ[#<iџl(؛x]"kN;t蠬p͇ $"h!0j(ਣŻc[J:zek#a| -+7sʽjlEŖ-[nQ_[/`<}U('NT D;ʖ-k"|#H#ǸiLk_y߳{>/?RsRY;gR*Գ;5NN/[;_5[.HW7pϢ̮Ҕr mkLQ3f+3Y?33Ug/{6Ygv̾w{YsX֏V>&Hƥ^U=Ë@qup`SFrߋ p>h}inf]_RߕkY [01\0ord$@6b1޶}ZׯC`ȶ#<<\`ׯȳܗ,oc! "]U T2%cƌ%EH7$S e$@$@$@$@!x SvB=ܱݰ| e PeHHHHHH5Ȟa& @0 P&M"      $"@?[      &*IY$@$@$@$@$@$@$D'xvK$@$@$@$@$@$@$@?4)HHHHHH$nIHHHHHH &e @D- @H aၪHHHHHHH Ђ_'      ` A$@$@$@$@$@$@IL ~HHHHHHA ~0(R $1*I{      *H$@$@$@$@$@$@$'IHHHHHH "e @_'   pISv BIH6!p(1W#%:mՓgl}zȀ/k93In.)C؟3P"h<9 KU?T F ,Z{덒#"H<(5iӦ)QrO;Y*ʭ3H޼yLᅲ;~퓻&?3*zH)˗/({ri5jH…e˖-r!ݻϟ/:u_]j^Uߍ,}ԩS~kafNzر2j(ٳg2ydX~,I&RT)kQsx8@yǘqIÆ ȑ#U]qd c̙f9{rf \ RPwsl,r䄮o諎|[7K~|;l=q7LeKIH ZJ)Q?% M%?~;O촶C>pR1@y(clٲySI9h PP\9T[X&N}PC6#V2Q᧟~uk,8"eɓJ[Y?X:t98믥Cc)pi߾mCoBuV#V X"Ӄs΍#oqA$tgD?"'Ϸb_J*wp,kLVQf"kE ],LT^#|kXdS>2+ouaܭ.PSm XR\bwO G˶wνe]B-9#zض8{-9hm<  `x7'_US6e <gϖ+WJ& 2,y(pwxC9{N6^n:Y*ؠAYh4v+'_թ][Yr?/_VXDVZڢ,X#'f+Ǐv-zRԴiSپ}Zh!~MyHn ! Fhɢ`>/e3JR}*#dqG$SRu0&[ +=+uW]ں[+֑3 k6 `9yoi{l[jVo5${ѨC$@$|vv옵:2O+K.<իWRN>e?W\"E :7n,K.@n0+H $XKyEYRrԫ.; pח %MШ?$etRcO"U*U힊Orvj^;6SH R_I4iT^خ,˩v!ҩŃk$UgxCϥl\[o=;)nOJꭙ,)Ӧ9WkJo~s5{?.HDL$p@хuk?lٲf<9s,ϕ*UKYxqk&ZxƻU~ws&m fb{E>}ta_A|왇SJHG~,7#XSbԂ>OxX|7jZ$@IGݯ=2YJr4GvK*kq6kyVMMe _ܰTT-{6GJrVO!baXZSGF)%ߣ m9 7Bʸ9d|cGwqv ۱O.mޡHHH\3foWw~w\H%g~l s='pɭlDZGp8(iӦ5eZ-:[.ҏqACv,iIPhPܲ|)Wx@)DE$OW^QGiFʕU,^h9!a.:VT][!`Gx C ( >Sdin^5qmWSJ(8O=+f5 ɔQ{ eXtP8[VUqLZd[72oy3IZLw?8a 8>#[ ,ط mJ>I(hHnd1B{=AvD eޙ`MF .XB}kt,z\a֯;r$YdQAJ?|tr )Wˇz1>Ƨ]u>"kVv a̅, =b ڵ˶C# @jT{a/>KwcY͓zyCΜ*ԙc`Ȉ336uy_NX ~$35Bٲ}˾v'   MQV;nsK>u7n?p?sW ٿtMJ5A]`*V%\G}e<6nMemg=5keXh֬`;5ŇU9Xs,jJWY `[uCCkKx/[ݻ+Ǻ[] |AS1GG%D[fo -Zf?5kə[x5,QaJ1 O]"jVV?2؜ӳ+O|E,1yB$@$@$@@"o\h]Hhk1",YRe/uo[bc ^vD.>`/x*UL >\{*Mt+ɔ)S^~k֭[Bbr*pGy"]e˖fsCea$V^-D$pH!RKG?L;)gKs{YH'zt≊mKz_CEnngj|7*BXGNVrq*C$@$@$@P4oqZT)א@WXQp{ݶm[O k8Ƃ *w{iF :<`ǫ:;wl_~*d˖&Ks# p_p)z(nYUիWkKm&CU4?mxz愠zaaarA^xr^LAPR @P$Ȃ%xA_)1SI ^ ȺgޔCkn2od4^:ʸQZI= 7v4ٲHníꩳ2Lc>7eaE-QD佮*(`b͡ᚿ2DCs_~BOa'1Y FFŬ H XD$<h7K."#[dcVma0`H6U#G#>L)ױcG*}C \59 3 ?;w ZPu@A]}Vn`a^QL;v SB/T^]Y}߷7k?nY`s G Veצ}KGv/aO:D>uREe0 s@Sg$UcyloT) "A`FwJ}X*mGxx@WY{K^Wa! 1/Vї7Oz'Hn%wT,$    DN);c.^(p}wZu9$@$p++; $Hj=cb5       dG$@$@$@$@$@$@IA ~RPg$@$@$@$@$@$@$dT HHHHHHH )PO HHHHHHL ~R $*IA} @ P2P#      @UXN$@$@$@$@$@$@$"""@ _<,$      A ~8J      K _<,$      A ~8J      K _<,$      A ~8J      K _<,$      A ~8J      K _<,$   ۓ $&&&Q&eJ$@ F)oyM_'.؊׶B^ sιn=~xݻ 0uĮxŀ]9sF\^B+H%>;wnBŲ= ě~Yf;wƻmdGDDHxx}`q P}I|u+Ww3<^X2־xN$| $HнL-R[ĸxK$.!Qao7oӻdsoŷYLQI"w`6yg9:iybB^A0S ѣGԩSK%s_2|YflRzaօW_}eݬ˗v7h Kh#GʨQ_W.gϖ۷'T4 ?^RHSx8tƎ+ժU2eʘeP;ͼEyٮ];)C 3 ?ؚmܸQҦM#Æ ~?(} u6l(s+%TSMܲr3k?W^7xCRL)9sTRI|.lٲEݳwaҥK ͅ]oʔ)qd  >D$_=uPtʡqSP7 h<9s-2bEy牎狱3 @6m Ü9sСC˫}b )TRQee+PbƽZ1|BBBE)S&YpaeS{>?˗/C+b8p@j֬)'OӧO{ *daqnڴR7l 6mR~`;Pٳgo߾ҵkWbNv|z-x͛~i]EY28ڵkɓ~kafN>3for17nL8%{9i޼.])v (w77n[U_â4m4Yj.V< K $m5P =M <}[/|om<2Ї 9a.7㷽Ē? ${[n%K,W6,$aTYP<(QBYxܺKU|y૭:u$`/["P̘H  (`eVIDATaQGUP4i"5RJ /`ևRE58eO0A)bN-l=#|XaGz{U<`4h o.\ZeĉVҥK?#FK/䷅VndȐ!jQ'TE=[N-~|G ܫݦ"E qǂ7|#]t1~>k,@cf'1XxN$p $XZ_UM+nSV[6}d°^mZW4Xdˢ\ڱWί*49Jמ:˚ʡS$UעÊKo98fDGHֻJQ$Kg9d\>zBԼeR~9ԂUiΒITSRXJ*;?A"ϜWu(ezvR ϩ/L9:esiaH඿K&HHIVrIݺumE͚5]vL͛WeAmRXX:[^V- ӧ+ v;5 /ƅ'-_j<׌{/d׫WO)4zL_~@ɯ\@.˖-s{q۶m/Ա͐!bN↹akʕ+޽ۚs 3fZB-`ꩧ6ŋjUm>.xcP+ho5jիWKڵM^zұcG]-#SneF@Ʌ5[EYr3kG:fKXqzQܽ{w=z[e_l{|AׯMD$pkHtqP(l9TMz3Gڛ3=B3b(u& .^$?.M7P.l){#yח ٕdÛ˾cc<;T)_!}SjOa?6u˗K|_)}/+? [)isfWJ/گ-ZPJtyFr7nYB߲8Kv!ҦŮ*gJYV.9>klzg/OWNYޮq͝;+ 7,`H{=_:wl[&y$@7@| ~ Ž4,޹֔MJo$Kd%UpXe=g*2P-ZRr֭d 'iҨE߈I5%[bdHXXSZ!ML1ܪN|eϷJޯSfJJCa{U\u I=rLV,EuӴ[YJ<| _jHi&!2*V|Z< ϩ˟Mmr5!OƗ ć8!L<#KKa0+XNdɢ_xpbk <,KxŃ&#Πu*y ,΄S%$uv(F)ߘ7,Q$}S n..\R-ξh6YF}Ȓ5SVm  @ 4,VNг*@ R(Z.Z+{<ՊkX,jMڍW*r"-X@P ' ܣc/Ə= 4{fV7Xg9IFQpN';mj]%`x ڲe˔2(:HX¢!toș:V<-ylGx8ضm;wJ5\]GHC?PE"#_K-tb /Rɒ%U4}D= [:p_3jޘkq?e"u[s1^(si%K[w\ k}Z^>vR=G ŖMbp&G6̄kX'(3nQc~kCӋW,dm<6k\l5TF3)eE2X1_]WUGp߆ݹZx#aѥh %lx To۹c #4?{fP zxna8p!Oh<}.+ƫRgښ+zTyԟ;Fr;ri>G5Wu qu'S  pR(_[-[+ xmKm﮵q$blmu漢^r bO+|;$%#""Y"~8 ]# x֭m͡*;B/MĚ&X>v1[8p/5nք>ȡ[:a u1u#Kt~m z;]|XU>k,`;ӤIl9?;_/[;x  x(1H i f*MO(RTdŋKh:d"e!| eeb 3௞<#5nT+vKHWq}T#@B[}1$+e52] 鍸(3HvW7{VP7a!6{D5_.x5a I !^ =Vk#ƈaQc^P~9bX"]|Bi'fe k/kX:MH6ÕAVLas{@[ӭ[7mr+95/x!![-+7spƽm"V.5;\/k#x80 zRt9NxB+VӰJ7^wNֽ,ju<-[([~^W3Jt q= F|Xws?|a2tR__㧪}m%CP½SmvݱQkf &w eT|{nu}}HMp7E x Exr+9^1Kb ġ`'42{aч1a=X(uXڠ`4.X(qn;F\'t~l{m(:%^'ߵVu#hXL'`-D@K8 {ӱO}/ vک#^k3\{k2A KF\حYm[D{#y3~nYU. Wx,`7?z}U(X"<p v[F4`˦< H~꬙ZS{2Mj5Jԫ;GY֦aK;ʼ߈iE%d 3Sie|7+|jXtך)mї32|HjN!/xk=Yv7^߇S!` 6`dMhoevGGνkU*r?: Oޖ]U{WL1s l⣣RJ~GX *)j+#9]<j:| m$ڽyxPPqmS<,5M6U >"y[]_xذa&Pv1yX5RH tCj m gx!2v˗7G+>ނƹVbkJ"0lM$@7Ù d{oe˖dUzpo!SE$@$@$@$7Ђ$$@$@$@$@$@$@$ 5~2! mI mrR$@$@$@$@$@$@5Tk8K$@$@$@$@$@$p[[~ 7 ܖߖ_+'E$@$@$@$@$@$_#@s$@$@$@$@$@$@%*I pxxx*,'      Hb'IHHHHHH "e @_'      `? Ѹ\ IENDB`graphql-ruby-2.5.19/guides/operation_store/overview.md000066400000000000000000000120431514115062600231120ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: GraphQL Pro - OperationStore title: Overview desc: Learn how persisted queries work and how OperationStore implements them. index: 0 pro: true --- `GraphQL::Pro::OperationStore` uses `Rack` and a storage backend ({% internal_link "ActiveRecord", "/operation_store/active_record_backend" %} or {% internal_link "Redis", "/operation_store/redis_backend" %}) to maintain a normalized, deduplicated database of _persisted queries_ for your GraphQL system. In this guide, you'll find: - [Description of persisted queries](#what-are-persisted-queries) - [The rationale](#why-persisted-queries) behind them - [How `OperationStore` works](#how-it-works), in brief In other guides, you can read more about: - {% internal_link "Getting Started","/operation_store/getting_started" %} installing `OperationStore` in your app - {% internal_link "Workflow","/operation_store/client_workflow" %} and usage for client apps - {% internal_link "Authentication","/operation_store/access_control" %} for the sync API - {% internal_link "Server Management","/operation_store/server_management" %} after your system is running Also, you can find a [demo app on GitHub](https://github.com/rmosolgo/graphql-pro-operation-store-example). ## What are Persisted Queries? _Persisted queries_ are GraphQL queries (`query`, `mutation`, or `subscription`) that are saved on the server and invoked by clients by _reference_. In this arrangement, clients don't send GraphQL queries over the network. Instead, clients send: - __Client name__, to identify the client who is making the request - __Query alias__, to specify which stored operation to run - __Query variables__, to provide values for the stored operation Then, the server uses the identifier to fetch the full GraphQL document from the database. Without persisted queries, clients send the whole document: ```ruby # Before, without persisted queries query_string = "query GetUserDetails($userId: ID!) { ... }" MyGraphQLEndpoint.post({ query: query_string, operationName: "GetUserDetails", variables: { userId: "100" }, }) ``` But with persisted queries, the full document isn't sent because the server already has a copy of it: ```ruby # After, with persisted queries: MyGraphQLEndpoint.post({ operationId: { "relay-app-v1/fc84dbba3623383fdc", # client name / query alias (eg, @relayHash) variables: { userId: "100" }, }) ``` ## Why Persisted Queries? Using persisted queries improves the _security_, _efficiency_ and _visibility_ of your GraphQL system. ### Security Persisted queries improve security because you can reject arbitrary GraphQL queries, removing an attack vector from your system. The query database serves a whitelist, so you can be sure that no unexpected queries will hit your system. For example, after all clients have migrated to persisted queries, you can reject arbitrary GraphQL in production: ```ruby # app/controllers/graphql_controller.rb if Rails.env.production? && params[:query].present? # Reject arbitrary GraphQL in production: render json: { errors: [{ message: "Raw GraphQL is not accepted" }]} else # ... end ``` ### Efficiency Persisted queries improve the _efficiency_ of your system by reducing HTTP traffic. Instead of repeatedly sending GraphQL over the wire, queries are fetched from the database, so your requests require less bandwidth. For example, _before_ using persisted queries, the entire query is sent to the server: {{ "/operation_store/request_before.png" | link_to_img:"GraphQL request without persisted queries" }} But _after_ using persisted queries, only the query identification info is sent to the server: {{ "/operation_store/request_after.png" | link_to_img:"GraphQL request with persisted queries" }} ### Visibility Persisted queries improve _visibility_ because you can track GraphQL usage from a single location. `OperationStore` maintains an index of type, field and argument usage so that you can analyze your traffic. {{ "/operation_store/operation_index.png" | link_to_img:"Index of GraphQL usage with persisted queries" }} ## How it Works `OperationStore` uses tables in your database to store normalized, deduplicated GraphQL strings. The database is immutable: new operations may be added, but operations are never modified or removed. When clients {% internal_link "sync their operations","/operation_store/client_workflow" %}, requests are {% internal_link "authenticated","/operation_store/access_control" %}, then the incoming GraphQL is validated, normalized, and added to the database if needed. Also, the incoming client name is associated with all operations in the payload. Then, at runtime, clients send an _operation ID_ to run a persisted query. It looks like this in `params`: ```ruby params[:operationId] # => "relay-app-v1/810c97f6631001..." ``` `OperationStore` uses this to fetch the matching operation from the database. From there, the query is evaluated normally. ## Getting Started See the {% internal_link "getting started guide","/operation_store/getting_started" %} to add `OperationStore` to your app. graphql-ruby-2.5.19/guides/operation_store/redis_backend.md000066400000000000000000000011071514115062600240200ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: GraphQL Pro - OperationStore title: Redis Backend desc: Storing persisted queries with Redis index: 3 pro: true --- `OperationStore` can use Redis to store persisted queries. Pass a `redis:` option when adding the plugin: ```ruby class MySchema < GraphQL::Schema use GraphQL::Pro::OperationStore, redis: Redis.new end ``` (You can initialize `Redis` with any options you need.) __Note:__ Be sure that this Redis instance is configured as a _persistent database_, not as a cache. You don't want it to throw away old keys! graphql-ruby-2.5.19/guides/operation_store/request_after.png000066400000000000000000000702361514115062600243110ustar00rootroot00000000000000PNG  IHDRb$|iCCPICC Profile(c``*I,(aa``+) rwRR` ` \\À|/JyӦ|6rV%:wJjq2#R d:E%@ [>dd! vV dKIa[)@.ERבI90;@œ r0x00(030X228V:Teg(8C6U9?$HG3/YOGg?Mg;_`܃K})<~km gB_fla810$@$wikiTXtXML:com.adobe.xmp 98 951 1 ç9@IDATx@GG(`ĊbkbCD{E{cbXbWL,`GR`#w^L7o~SܷvO&P' p' p' pGL#֝ p' p' p' q' p' p' pGOߴ ddL9 SS])pF|8+^G#Z7vΥR^лq\ q}h?-mXHyW0"+VU%at3 O<`d^^' p' p' G/35ȀKK0j6k׮úǑGm0^Ƈҡ@A )]<0ZQ0o3sӖ۬8N8N8N l;/F```L,GΑVSP0RWL˸ _Ͽ-_Ģ`8}hnc{ ދK~ Ea ie1pTyM0שh "zaƮ|6~)Բ^x/+NO'0{fE'҉84n;l92 *~:,EDU|\LƩ>VbV֨u,cQݡ ~eٶA_G]@70@զ1o#1C-XSһ T\EnRT(.]eUp4\H?!MXy( UIi<'lFl b3Ԉ ӔyxY%3c0AۤGؽdΆQ ?C5='Vm0(6jEtfu#O7m0Zuw0W =|n&dhyIؽ z bL$' p' p'ȶsóE8f(dD'x$ٌ;a=?ne{4+YpߗfhݶprJp}QZ(Biģx?H ~Jzޡ*o Gє]*($x}]/}ߒKF>Z^Zιu1*߽-)cL&B^hlWMDܕ1ƱLAPsx#BZ1O-9X =Qrԫm?._CPEh'Yo%D8@=' %@06mx<_fU)cHSǜ' p' p@ qҹ3^/E-,,ĸt5)dҟ1@=w.:H@AGKS8~`BE p]x~y&h]\唌r5UVS-QeOz'zZvDhҬ4Μ ]Mw}]5FV{#3\\3( Ѱ+1r#U{?Q*=!UI1<`\0V p`3j*DnumyM2?8N8N8D qkie;b.i'K;|u]1Kk&ѡhL{bWnYӖcZ2nߺZ:SIO%#UqʠgiM/,4ȰK@9KЦWq;"K'A#?vn;V$dRUZnr,q,K9ul\<$÷l+wx5nU/zBE:%Yq*(qq_P O"~ 1z-J4FS2UF/jzP|+>Cڱ-a%݇;(M:mRHHwGM4%3K1baz4Im1{ű4% aCt|:`)H;`m (Ow:s4^HS{*Qt"XYFest_֬5Qb(UdݼwasRZ ˖;(_Xdޤ)kDƊޠmz2=+ަd]߻ [v>n)Xm*ڢZN#Ӱr*Z4RՀ%=G,S=UuB*$ͿP>{**.}By*F;Ԃ,86 tѰwD$^ >|Ǚp*`2 íZ7-޽&/3+׃5.duX,'–#7n5Qܐek8t2Mٛf^W,k֜DBV:#atC["zK#Vc+:f8N8N85n u&{T.*an FU:cw,ދFT.߭I/Vڍ;NhQz+m3?T u= Đر,s 60bSV_2gH5[SE% uڢUzt.^X;^f2kC+_PZזdG7tC=41oL+id˞ǜdRcd1[BR2 )d1{hJ2l@ϼ j 0bghPlC1M,VbI.I#I{YAlߦQSp#z y]íkux]N@=k\+-g'a+p^+YXms>2apW~'ʨg:YRcoihG' p' p'  B8$#?̢2+Mt,"`Ӱq-]OU?ӓ<BQ[Xe}d%ɔAشd2l`Y$):GkŅ:evޒ}VbrOTji!SOi fqњ#^̥jk.~E|xgvM [{PAԧ;yԑi>9hgαMz 7Gy~ p' p' p;Z7-b ;{tkT PlWR+e,(P\T_| ϔ0>#im}2lY՚#ќ39AѮu7tAnI{zBuQ]AnAyVq7?8N8N8wNeKL&Ƕ;v>B '<' p' p' pʸq9N8N8N><9.4q' p' p' pm>x2' p' p' pO~q 9N8N8N!|dN8N8N87n?>r' p' p' Cɜ' p' p' p>׸P@~*8kޝ^x8_?N*q' |>H6xHʍV"V ( ) 3#}|V_"Z3|r(j{$G#?R#AOޠ/JC:FʦpX-*^eN̫VO7Woyonm7hM9mm)O?"3NZ[V4raΐϳp' p@A|ƭvcI^ ۂu\Dp)R]vVmKgO_O ~2v|c~w1Q4d f伈 otj\=1Y%ʰ 13Gw,;BV鎉r_4Zztnl4w]q;8D/z & lZv.Ի:`ۑe!Xqry2Ӌۤ^RPͩ/Kr' p@[[l  qqBzQd?7\p(3Nx,Ƥ =J Kg¥XyD,=TU6AYK" ףUiIB<.\LT%\$Dz~'gPJV9T{gBYBh0j&FuJ RAFQ Bmj. q"-.s|y qÅ ̖B'Ÿg¥;RziQm}bRJܲRd J}& r(+Y|^K%ag"E:g  >pF ńSΫ2K1i x *J8땹&=n\ bn܉(kg v%X([Z|Hv]3+2"palUc eO7T?.4@V0![}8_|~莕T/{K6өqTgA*8ך``Ne] 49(kΐT| lboY׵"?CkD^cǫP !GcdYZʲuoem]z ;;yuz^ԬLD^:j+h % AgJdQXBs<5#U1jnW W lV(-:_h6Y#Ս @=Ptnl GP4M a{'=ۚ3 $ݺE뷣 et3ް}.:~w:s=+R>j$}$USz!dc+܅~j.ߋ+ 6{&9IFɚ?Ǣ}.lkǼV趕iR2GYx!֩'} O%*gTY@35ˤW[4Y+nj 9-sQH1Pmf/GJX:-<KVˆuc6P6^/4T@^? /FN8 G4҅JMDM>Anq GWs?|(6EI#ynyF J׬窼f%_a8;\ |:2`fmc; VG' p' ĩ;J*%<":ːms7Fq70lJEv ao mp'hG@=2ğB&_ iwH࿏RBd/_![ٶ樦h n=xpeZڡc`ؐ~6$3.\C$C lpJ(e?n5GɩA3@dUtgX &䚕qd-Ns~~dG~uUJ#1tetjeT0/U |#~zegyѕ#r!Fu*'x~v%^ِh8l#WaEdOAN˱O|;W_kJ [sv dz8W)mqNJ7=/9PmW ymoY;VG!_9j6oAߜ&ݜbʨKXnW{u\r WkSt'PFr=2 %Ʋ~)"Y;VvsukTϿʨش5dݾ@waK/D2CAڤG8r1yHcOQ,z.̩ v 1͈$ Ck%|._I[ +|q /'CϰI:qXpDžFlmE@ft`?#g>gkӻ._߇ :bBAg8N  u&VS5H c)Zr}뗚1ַ^_G !QGTCte/R (.Dz8w}=yR=YM/h#9㻆R6U/ %Yrr&.hr3XLQ;3OU>oƢC] LPn!dwljlg Pt }nER(Ky1dp`zrj8xjʌ6_7DjK˕1UQ/tK?$(3x|-cFzq VӁ Y*}y.WH/h𵬾V-{31K[JBVW{t k3K_u8(c!\l)Yofz 4VN,茶*|r؏jʹ)9v,,DYa[sz̹^ڢvIb.;,⛧MrUvF5r[3F5,SD(\TfEONm#bj]aogI!nZrXpmUT& iu~ukjTᵒƿYo|׏nTjLEr]}hFuyMT+Cj쏝hrOt3(Ბ?ZMA=ct3\' pGF@ƞ9t7I{᚟vYDw ]A!,]:LNDe+]_S]6&BJf~L5lXƈm+ӛv4ⓩ~:J "CB&Ezijez:~7CvՁWP6ζ:i<Ɗ:YMj}W`/MʀUB7:7UVꭴ{s5KL9j-kC%%ݛ*7Y mb;lkƿ6/ynk}XQ||R`mV7ݧurΧ p' p!AQ>FQ 3=m]ə蹽 ZCiӻf|Rߜ' p'qx N (cNk J?U8N8N(n p' p' p'(ૅ?p8N8N8Nq' p' p' p?n~}[ p' p' p7nw|5 A~S5.IXer;_|Z+pu_ȃ7Irę5%>AOCx' p' pEo?/c$%QK12 ,/_MfM0!$ibA |ձ=~O6}ǖ2dg2UmG9AT3>t )J$E:O7axk!6yYOh_@/O3(Ql9ЮV#8N8N8nܾN@TpIIw|]i珅I8b!Pۊ5,>S w5q+뾎Hئj\WP6cP:,JFlYo6"7BWX{ Fg`,8N8N8C&s2^jsx~Zi!ѡ5G1]UP /0t()66m_$_C ˸ JVd=D f n(7>F11۹`ܺ/Pr&’^!0`3**&>vO QNޙ k"/3cA9 b66*tEeoB){:Nr|-wZ-HWWw <vv|,3F.@ QF?y="}f*  >d*7pb֮4lF+!W~'D>4l`d#aS-GYPt iD3Q^6`C՛QDe9~(q8j' p' p'$*I nd G!". ߆2r` WPsDi#Ȏ!K( dǞ4,&fLfH4FE1(L)aiL*#{ܐMnڐ"/-JS9ŕ'Ɉc[fdR9֨.Ȁs, WɕnGikQE70nMZUA^0o? щ04%NeUG90miTg$;>S'"`t-CM~rTW[-k,AG`8MqAQc(7}Db(u-`p+gF9۽5Bs^JCDRmLlh4~s!Xx@9ep)hO3e6hW v(6" Owoy ?2L <}d2Ft/?9ϜvFl&(մ3_qVYdQTer&m3rd| J=j׳NW@m*q' p' pEqKeB^ L0s/C!5 ۜIc;O"H%t)L XF:.po8֭83E:Sɐ4׉(CJIof*a-a"ˀ"R)S \ q~XR̭Y}vzZG2#v)17+ib_ d9K!C<хML%d2lhǚUpcg: Nz޵?P%y0BľΈ>Jn] A: `Ag,HҧPFz>N Ӝv*-óۑI!W4FgV\?KHZ BEc,ȒMEf:X&Jg 3N8N8N |n(N•#~bC}Pўn|'~D*VI9tY R|I[[dDG^:EH}a#;yb(B7e3Z@v(^FI.ZQz{KѠdu?DM},Nj]'2%ڢ,XA]G[$3y >sE5[Ć!>gi^Ea4R (l7Z ~+;¹3m {w#Rt~4ڭ5-x-R"#j|tFKi p$2U/"mrRKԾŶU j.D r SYJm;xZWnV ̥gpYdfh٫*,0k/Ъܪ J; !G!߬?Њ+^'Ro M` {iSKLPȄIPw XWgDzB)+4>U [mL6X3VN%'\Gh8Qb!NH>fE$wdk2_7dmFT%@6h;3;qyLҴZvmO/Q2jFru##h9jvք)QjJvn~IU}OiSB^pd"-U7ɫSFѧ;Dg_BERCGxϢ Ul%}TA {O cɽ7ǪBOdV+M?VO=Fk/=]$5+nKh0*|+ʙSrBzANdQY jϓ%Q{4Gqԧ(j9Y9~( vJ?'yL]mSLڝ{t{I^.L>ѹll9 ' p' p'&z/,p-7Θ,<8N8N8Nqg~.oP:78N8N8N@?OV[y,' p' p' p)|_Dn,N8N8N8#ܸ?LN8N8Nq_ ^ p' |x8Nxq&; >FO qg>Ԑ`$'wDΨh%!>ާ}3!FCؘ DPgPO[]m =b2T7ߔl]9z9$G#?R&?-Fozs41@F5y5aZWi ֥W1z5/[Ή}x3:[OrU[ˡCx p3;RXhRzeNNڿUP I~).P]b%ap%dTM{)^N՟vcSukR% pf:~8/MO7Em)[Z(6Ǐ I)*D]B[SMS|{A$eS\$8/Nv,拘>RH.>:ZO jM=M& SG#s4e5j kSzU}޿Ӕ)bw$>Ej~l  6rBPzSBm4.!yQMbad+QB@6٩zWhJnq<8N-vn1m4HIַR[ -ICױ}f}cg!?腆r"ֱly [Y.X{ &}pm1ߘ۸nwv,f`Q7_@n]cZM#12d=]r7G-0NM$|}i7_EʕrvƌgPtSh5֘[NX=8 >~Y:Va?v}}.D{ M{j=GS_zwp/F-Kj7x~!߉qfR1mc#q.M/v21۹pi7 !IMj|g,Ge uF qUx qw}wÝul-5I^x#w^ݣS[oȊqO- ]S0V傋7g1gY9s!J 9sܖ{M'Zl<6FvE>PJZ+gj֥WszK?&9UWǸprJJ#F#O4Yy=Zﭝ Iq' qdee8yxM+X SM Ŵ?Tf.0_SfXq,]I?pWr <&nȃS'?89R3EA !M`VabR;B6?T.p:L\9r ^׺-'FOm =9aZ:Q2Zc6 n,IDATLr!yBc&շp2 S;ޚxuJ8zW6Hi%vz1dS&0 ^ۦ&1Ui+WNk7`wep^p,Je82yɋqhS˚m6:isk1d7F]]kY6VrXWY&哛!y((1:14ڰ ^` };ej XhגfTWiTv6 M nHZ Cg-:/; EĘyCaG8zb!9]ggaՀP)PQcTPU}05¬ͣ!ꔫ iP@H~惼oĝW5ئT$<7B֍KӪ5`ħy 'P|Zɬ+A/ZGcڷ 4=d1&V CH,c{w{H լz/.v60k1pJ)af,X|dَèF%{DzR$ף4QM۩3oilV#ܓU{-ԯT^⍦!|ԁzb>!x/zr;I#Аyq$&Pރip7V ݏE͍5&ED5"uհ洘L>έƎ.z  \.{k挺(z|ckYԉz!{")@%)ZIY{5bUx=@I3Bq3k'Ѱ+1 m׎=kz_uEj ֎=Iif;x`s)P}=Z|V1?8Nx[n9`!?,ܼy3kKf ǟqjlp'--U}|Bt=8^HLUx.fOyvS%_fy GBK•' !fac/V;r0iGe^ ׄԴXOsX.)Mq\nm\,$=>/̠:73'O _]Tyܙv.'$RR Bvm"!Zx / T[S$N6HtHv WB 3m^uuAAqmKA 5.N$wSڸWUH},$)n_mqenv .݉PǠ;NNat ! T! !\׍10fP\e ׸Jt9>C g䒪v3_h_)r{nv\bveo徝toqڝ|Mp'}+,Q9Hw/Zı&>2kG*A =ZCcCqsV9N:yq ^Z$85 똩%D05 vdD1W3TvKkhBGGvl*2.Q{F;G_E n ԃh=k* |a}ddH[k LI-fܘfKO(P1oȐU fEPh"e$#S}Lm,G5:ƖRQK-Pش4,?beB gJ⤎`ߊ(D_7@Np"P(UOYl(a2&1sB 2f+UEs<1ng02B 'ͭ~Li$BX?~^TDMi*R*ayf%klx)3-e#rw9nrDy`WT9׫=o 'tl큧Spyq B**P~9n"74cT0|mMЯ ^NX0v@'6@T܆Yߍнh.ԧ솅Ca譡H;0Bz)DOON?vD{|/&j}A;P׊\?"pb-C 5fѸvPKx@s7Û`0څ}6T-H_}m'y!7!ef~Cjn'H5a,n>03F(طz(DIoTS>9cÛ: ʣj&έhF b>Fbh,lݗ"ShYIÍFa~+":aqӖf`BL;qZ- ൻOQmdmuJ;1!uua-P-+gNmUjk"Ɔ.; G'}F{ZjY^=}VǒmgVvHַ3?u_eN?OԍfWZ7xt2J^tչ 0&1nײeKDFDawlo;6Xd(ɾ9W:Nk0^~2a}TaQo^<Zv=<67ǼFQ&T$GL| f"OdnutX:#ƷAu/]A==dM6ng2Y.uV()ēwYN{;;oFE!}yJt@8|g|+>xg')>D0ڱhDKlj?ZqT{f|_1BgYo,C-a5/Ӧ˜H{ E0 o%W  [٘"/ס]ԨVۈ u28zoyhG<2Îb*W\ E] Fq-<^ݕy;Oҽ{ t\Ѝ:~|`gFW4;0(&R[AIf}L:*M[I!ҁk# ySnpdvi ]ˤ"=QCΏP|qF@ֈv|dt"‹]ܳPMR]Fb$ʠM m\0`dHs<)"M2 a^Cw%w1L܍M1 O3d2"eGc)oN àX@R?R/k)cWϕ;]{yg,Jt~ƠǛcUZQQl?: ǔ*VH?x }G\Mĵ2ZI.cR|_z7?;Zf)7S<ԯ9ʿ*~!Uꣃ"_x!*`L +*^ {rEb]oպT:*sw32I,**@;ve%$^NG#(᣹V_UZ'־Muҏ9QUIҁFT~U-T"-n*Y)vX #%ͩZf^hcqbv<>U!*y/*VAm nVE't#w !WԎʄr /^Je%(G]1YQx},*Qr1qw-WyYʹV:;0^sB_Oe*S*>bZ9e9_a'O-sr}\6٬C ~fZsĨAaevݵ(CϰCrauZ?Βޒ~\Qov㑢4K"ѧT>le͈rvo? BE}FQ=ƣ$jFCb8@WHy< 0&`L ؤqkzM_SNʼn' ҫ}hpGdc0{dlm\Ywkf6(>`2 n7'J2=0z.2ZUʳIu18py L_46_^(J+Ri{ S'`poV[iE/%y83.)QAl0ftX?`*->$ ?Ż?#⓸t*;1l:4w"?qaBxdvΞ=e?1 ;u_Ӡ2 hcKGI->J;G=x2eMFys;㵸؟Ph\yzV)l?0 ߍ~JbLsжC[q#7X*#+Ju%y'` }s}=Dە<Ōǐz*N%oCv($JD$jIzl޼/*jWWbȳr`L 0&aL/ֶjA/pTN D;)mi_fu bYbBD{I8Uoj\f}eP .莦N?k̨@1 sO5v`k($~OuP!5DG!tFTX(2`KyJݻk}T}x/`5TA \G p(JAøP_osOxK%(W=<{Ó^A0=/  }4rucR.RY.du؝݆I/5NUUTIQQ~ v.\|-Bz5~/Hi#MhW_vnf5 o-i蜡IcR?Z۫ ,,n).T(EY0\C-UD/^sE#xXgsϞԞO;(C;9g(4Bw| uNL 0&`LOۇzaaa DjڐPE~n-{Dw"$2d[kJɪ2I2ʩ:RBiuRJ.Njmjy2GS;\xaL9[v8;ӱ? })Y>.@γQX7LGkWJm6:ߏ"2x<U oZ?Ǘ3b0blS f~mB2Kr͌poQR*nN8j:nW -LwUW;ԆNx~sBR%bi4dͲDMrXTh|UJ,bcGtj\ *7$zѵ*N2֢#2&`L 0$Pb5!wA޽-uD)bbp5/w2eXDM%z?@(# %Qooѻ)` 磨H-}\K SpK0깸879r_ΥB߬+sɋ i$]kYTCW :6|ѯ!鐕 Ϟ4z1> *fR us]2Jh.i~3,3Tv&AxW!ˊƖTY~ٟ=:\2M-GQOE,0Ш,S "!&VfHdQX8HF8zȅ+9zVaJP7b u#?f/.Z ,QOUJVZQc^.dL 0&`er@-[DdD$6V= mfpU*x;z;C3sre `󋐼Ix Lf#0 ybͭӓXH?"^"CA+bwe8YF~)ڤݷ_fȆݍAdQNwW~@FQ1zpbWLMK`Qg.V}t01K^]u;Ώ+t\InJ2в>0MJ .g,%>郻cQBö|;я|ۀ>k7"jE[ >7>55MkiluC,+JŏNñGL7@Tv"WfǺ>l4F 8Y]Z((638>Gb5" ʳ9WaL 0&`6K@d!tNn{`E#M2=]5և;6EMxWG޿r1/ o)ۍϺEg #YB/ybc]Ԕϝ*sW-PGe CwQ?T%.ǨҕN^ Aȫ+FuXf c0O/w`L 0&'q{?1r_L$U!f<.ϔ7ź;)܊ 0&`L 06n`LG#x5pL 0&`LJۿ̱L 0&`L 0&`$`%&`L 0&`V` H\ 0&`L 0&l=?`L 0&`L XA[+ q&`L 0&mlvL 0&`L 0&`6nU`L 0&`L qk1&`L 0&ظWaL 0&`L 0&ƭmk`L 0&`V` H\ 0&`L 0&l=?`L 0&`L XA[+ q&`L 0&mlvL 0&`L 0&`6nU`L 0&`L ? ]IENDB`graphql-ruby-2.5.19/guides/operation_store/request_before.png000066400000000000000000000551461514115062600244550ustar00rootroot00000000000000PNG  IHDR5]HF|iCCPICC Profile(c``*I,(aa``+) rwRR` ` \\À|/JyӦ|6rV%:wJjq2#R d:E%@ [>dd! vV dKIa[)@.ERבI90;@œ r0x00(030X228V:Teg(8C6U9?$HG3/YOGg?Mg;_`܃K})<~km gB_fla810$@$wikiTXtXML:com.adobe.xmp 93 821 1 dC@IDATx\y*(*޸W*iNͲ2#GVL-]4W+E \w hhý=9sss*4d 0&`L 0&0{Af`L 0&`Vj"0&`L 0&B`.> 0&`L 0Vj0&`L 0&B(*C6MŴ]aii%*胉cDiC}cl.kX^,oĻ_Ooᵉkis)؏&[Ȳ]fG/83G0eg.'`L 0&_#Pd+5I]Yc){F "7yg Z~[zF$˝[k %!{(&9#0 J"X. Ukh[i&q3&`L 0\Yp!"Wƍ͊~'(,TrS_\w~ pIqn& yuf/ލ$J=$WGKfT=bJ9~K~̦9X{4)is#DqH~آd%;FLʀP'yѢ$#o^<N'޳x{+6]#ע4mKi 蚰sD~1 Svo$OD~Հ-SEtXbKT^.QQ2Pnⷯ@9{@\ 4w}?h (4°q=~;Z{liY߼;K[gNFdгRvnae8v-, W?w9 sۊ(Y$6Z`֬/Ø<~e Դ ETO0Cd+3)hg>(u  *186IQaL 0&x~L z{: ;fʨo_G;s';=J&&۾o".> ~8 .!<>FNP#C$&B-B*PF|-"}+)HH@ȶips y|D# +Qi \'nG͌#߈!04n-{1bLl#rJ8:wv_@M=tjSqjՏ)瀔DOi'Mj-}݃/fqq%?"CT| 5 _s)1m9$&e/S1T @ObnUǡi>/cߑ\4= A ܙ"!LE?p"&  .BMG}uo1mpp!Oj"R V6˼#)'ꄛ8>DͨGO sv\D졟}Fࣁmo/f`L 0&#+5\Ѯ];?~BFni.n[RFJwG[z f?n<7=Hslu5"7#b%Ց2E8rӪQDĨsoRw/D{o<KJc#xTi鴞RyLUʑvw ǚ=st-Okj8x`w΀A܏"%]Ӗ>erhJCT~\Ke'>= ezXV!}6nFjo$>gBv`sVNGm%:_łFŵ xw [__{t fG24x roΣRoh+GVB5URhD̪z(  uvm輓Ƽ F28cJf w`;n%Ny—ʱ w &`L 0gD _z–-ʋvz1.f` ĆECm? ngpKO•gp3 \^4E]Y9J@OSVhJY}?JbGGM6Ti5d`EJ%?'oa=*{`۾t86vnc~HOCbU$yx7m/ưɈ?mwb]^vSMs3T_IK :Vp4NCAdPQp/?#H&٥XL]8]B{(;<_1&`L <{-d;wK@#ht$6X2]ū6cۖ z0@55k6,v&tзN ]V.d4߿~{mϋWc]H/KdllWj-') m@yX|cB0 Rd'yYᲤI%պ5-V+;aZY ;,FH.sBDXI{n kՔOp`r>(H6umgeV=[ҞSHs\m؃Xw,e낻ۥ ,&++MUѱn']ٺ .=j6CRM(3Xŗ% Gul+nP\+'߭'%QWZ68\yY"N[!jPR[:g=K`L 0&!`H`tۋkZb{{r9cP>WoŦTƻ Ƣ|žjIضe`^%?eJ}Gak/'"o:X!DZy1ܧs/mGD20櫞p"> ^y!pnm=FBgJ#0K850h@}U!Zu&7 kgt_Ҕ}:eWvvm&hBqr6\MΘ;*tF)ӗ/MH}:-"ʕV>e$y|N;ɊZ^Kg`8F0tZB89%R~5Q"zvի~KTǔ}vԴiٗp|6M)HYE[ľ3p3Ly~VǗOoAKv:;`;-7&9o [8 cׂI,9ul|`L 0& (4du㑔nE:|VǮFw1zJ>n9~<4aSëO+&vdG0kؕ1$W$k:)CVJԲncךpzzS!ikieIeJhZ䲗|i_(/ZH(^P6<*S[e|P?JA:54kiGrk" JDtsL 0&`ώsYt,a^ͬHIHHX';'dG=M VJQt@dͣp$_ge<~~ HZ߼rPr;^o3Z*QDҎ䌘|Ұ eޖ5"]̨dL 0&xJMarA2Ihb5)0e~`L 0&@q$PlebL 0&`L}bI1&`L 0&!JM>p؉ 0&`L 0O_F,!`L 0&``&8`L 0&@'JM/# 0&`L 0|RvbL 0&`Lx1 *տ#sp&`L 0& b\2FW"G5T\ yX8 `L 0&`ŏ@1&)#M!x' ſH=Ww|c/ 88(`L 0&`ŇQ+ .DPP0øq㌺ KUB#"Pj 6'0&`L 0&2xf=ffyv͔Q Ze)J|đ0&`L 0&PL2`\Ѯ]< ;ۨpgf=DmgOۺLů7PDI`L 0&(*LzŵkT0pg*EԠJLq߽}5yɳ `L 0&xz~`=xyNɾ׫V֠GpI'w[;^?FƳ3!wĶڑ:YëE}JQ(ƲuspW;0C8Q8;wybe &`L X)MI&buD;N¼0x4/L/ӏ^3Py4WX+}hjOɄ.h4Bz zƎdi ӷQ"l]g#tY@mۂҵ"1i#؛P8[ ֕)WhLg&`L ,^h%WKl[diXU-z̓ )Y-QZo}ҷbp3)l y5;ԟ9 ohQ:OH=jJ(K efԔxU&}xꍎ_{h-,`IWi4ӿ%u[/|һ>B¦^q_ >/TÇ  :潉"B27E > c%[T! PA&B QEh9JW8~<4;R\6F΀:>+ 4db>ԟ Nڐ;F,&"hFlG=aG|g_>Cѝ(n(Bwof? M TwAVy(8ՒY-+9k jjLӮYp/Ka20J簔b~4v"ÍhQd Pnd=Ld S).S.݅;|tKZ2duGK;Ѭ(qlӢT)tɆ 0&`/=d.(!0!u ތ`ZHRʌu-Ix9OIYý灶3@ npƻ_+ }J¨7r+m7?AٙȚc_.*)4"}.IԖrj8R &+NlZx'K@jb\Y$R@q Hhj  hdو3df4TiȚp͛ЪĆVE[k8*%2ћdO&AAkɳQV2cq? m=zI["j/*0}K)HC=Q݆gCEsK 5UV 2Tʖr@D%)-J*."E75RNoT[I 7s8UCc2֙`L 0&_ r(5TR{&{bVVD%&wfPD kXX.m6ʂ.h Nˏ uJ E!.-l F_&-TRY ;X(Pe#26/My}Mź 1d)tX[p@d'◍ZZz$?maI,I 9ہV_] 1aεA *hy2K76Ek"@p.AmmX:>:k0uA?5 (*Gs>he)=w 1iɊϡT 6TGo]c5M ;m`W5g``L 0(c,hxnT>~HXfCtM'Y N4$#ԱLҍ׈ԛ|na0 akr(LmN3i[ O uHCdi@guԫdN13"zJBHKgʴm %l${eN-Aʢ:WlB1Xy2c+bҤm`) H3*%)Y H D G89"zDJ J8.)I[)LA+kdtۇ=X!R:FڅsPW֥;*8|v)x#^)+55hl')R[HlMIHq!{Wl&\mvEl" #QE4#JKXb`L 0& $+5UnH,s?(l3CNyp.\;5t5e6a0ܳwMvsx9{h`;y\OM/)PI:t)A4}rIvBfWKOyy24Y"?o3/!xP=Z.߻!絻eN+/"6\u~]V?Z/%6AZ^&R=QGܧ6ňMɁ~916j3c G(ҊG~?"Qx"{:o֞)u403`/GDy*Ȏ~Qvy=Q}4-(g#0O'#餉IqO -K]rL 0&ox#ete 5Pi&܈bL 0&`L&+5ȸtg`L 0&q/Rc-2_7#zsKSu'˲ q)qL @׫ b6QgW?u;lݧPjy\_n/^ a#@Wo_5Pz̷.c37\{lYUnׯ-;J܂w`Ba!-O N0UdUg@g&>AYGpu%b}iS! 77­m&UYw}T5יL܀[ua~=saMcG. :PuHnpj Tͣ=T㐺d5UQޤhYI?"sל/}8uuO3Rbp6Zߊύ qlP=]r8jP檧|^G1_r͚䵿4; 9郝0TD`+n[cP/7~+St-q}n$' w?䚥 4Q^=?i[([&@ETlnGʼۦPmczs1b.~? ܀aM ۍ"m[[C%RnZK\R+4 /Ĝ~:_H ځi=7ԴM٬1Fn 1zB,H Ch'ft62(=MaD~Eĩ7.Fgv*lT} hw_P&Ǚ#- w%F7+oC׾rYؕ#\kaWZPz9NnM K(S'W'VI@9;\X al-'wS\LI$g2YUX; +mx W# RʏˏhSDjkS\6ҷj Z^Rw>C$gS#2AIS$湟Den'ݻޟ;]}̯,E{w fFx3i(5 R?G#{H~iV.#5 uC2,=x7$>+M =5 |hgсܼw{Wh&ZMڨyN85;~%!l?>2.H<>JH吾0gCm0u>#j'!zn͆"b%9}jIΕS`55#PWUm RۙFYnPVm4$?p^J7wE *TnHy#cQeOAkoye>j ܎wIh@3b.2"iiU)X,(NDW yG)OE܋G ٺYߓF"(OR,_huLOZ;Û;chPTTY_-_^hYF@QN*T]; G*UP2G hM2 c28)-OėHZE[Ԍe([nNVԌTԞ/qh8]LD[ pTYX|VuXn íWWWOZFGzpJ@CH@Vt mafh]Z*^}y3O]Gi2KQ'(9V~ ,%{VK(M{"kC3diw,^f"κE]ڂPk(supw<\Zj^U{%짆!mD@9;S\|S=Pô˱~@L N޺#w3XVVuiǭ R6E+r$[a5})ov?嶲Υ(mk,;b`vT^ie8y>;T7T= 쩙H݅nÒĽ?`)ofK Nv+ n?CZϜ[h- YNyqg, i}i\~@ZJ_c'nf(oz3T3K *V6Mq-ʛ'J7 iFY׼}M\+:@qH,s ڶ5B3.hƤ DIT[nJO@Mއ3I,% (7IR +'5c罟L{t+p v~_tFT@+E 9ޏ >f)i*O[iDd1VɍXvIWy5lř՘ZJ9d#f4#zb4R<#Ve@b}Λ!:zQ]ؖJX>ך{[(EoQ.Ƒu rQn7QtLY4C& Sx0#Havj"W q4o"nX]vL:n8nKayTC"GxPcuRgҩ84f(,>K G >yOk%y;C]lsniV-w7\?VP9T63v܅V lCA)H>ԣO@@12/LhA[d>"1T"$M#nR)6T4x0}6.toQYF񧫄2+!%ʾU̓VXA[ގ,T|tZjb\YD-:<i-DpвA]Jl.][fu|CF<UZ: >RNԕ܍}$WW&|CJʠSfHJJjCR\5]`ѸR (QsoRh Ꝍ^L˺JeoQ3q?rvU2czIH gʧ-o721*1K"~ KdTʮƒkǎ]gP?Bii,Jv6f|%A?=[A3-q!&W1ƕf[k8*K+)gdݫ'GgjcLqu*B<鈉pGUK|g ?) c ]ְ=&ֳ:(bO#Toɲ~Xӽa-Wq3b}zC76FL Q;Ci5zmmU_8dݣi|auzfTͪf T#i2uTmsq O 5SX+2[Aj[Jv0wmߔ||1z$ y˗CfކƳJi#FUjԖ2xD kXT-ʣ4PG Vbݏ[м]o %HctbSB4 'NlBh@h \maҌR([RǢJ C8? 9)}. nV yh$نjk*D8ka`P\0R~gjAdhh21vb׵2nlUޡ9 ^ e | \h0V{7S[W3TFLٲ6:/"s n~ Uh9ز[vw Ώ p&`"Ď2K76Ej-\+Ppql֦hMMCu*GC&?Ym*DL%Ԥkhl|YHkCS&rQTҕa&zut 34@MV4Σ6CXP?8ՓK&hU_ܝN#NCz9Mah7his{PC&^}p(e5-Ƕ\}-n.ja% 4R;r'ŅJ4#ΟģpM[R&)ڪrQ3U=X܂vT &`dla)~ޑɫ&XKQ;`g[#*KaF1"Fk:FAcvkp>%C£mPh5WR~/]H+ 5¬8Dh@̷{,Qn+u`SVLxHѦ͠zi<ʓAKR? |Mԕ RD*40-`lb1cHNSһp@Χ.KqZ8Ef'dJ1EOCޘbD[lQј }|)tPfE9]XԀ-mYhgM g46wc,\vi4PS vR݂O˼WhUvIH*+SY%#e>G$0ŕܑu6R N^AY /[{.'S{"QVM/Q-njRYuST _ hk8 jϫ<3@km1ԮPۢЮtKغ8FR QgvJXREB4s9>SmIϠʛ6OHΆ ؁SݩM.R?/<}~<+ F.9_&rW.ՠ^UR6=} u]P^i|0&W7G @! ,E.;`5JV㊲q4qD}>me8Fsj^TiC\Ee ;WT=xbp@=,jr4 ,\f+;4{]C bn!?&H/Wà:p7Xy1 Ȣ!Zcs+m\JRX/̅$ W@u \~ ,?\^n-~B݌fj!Yah蘈wHrNoՕθ>:Jr7j]QmDth4nD&e"ޖ5mߤG <pvM~9 LߊqF lQ0sc0)ŭu,=Ҿ0ӌ& zMIDATiYb9=w{ɲ[7'WA+ply~RGrHD/h "49Nj hC_$RvR:bA~\典jAJPW4X5+ 9FtiVHWIw;]*SԾ"`T#Vu%-vk.=]K0c Gr2]Wt,bAQV>6,!i8.Dn(D(^lPt8B0m8DԦ&8hRR鍬IF__Y!Ȕv9KUL0 IO;cΊ=>ַ泽AUWߏm^(Ya֞/euGL@5^ _ӏڕ`f+-qqB,{pOR*wo_J=(;<ץBhlRmy 8}+%4)469]ۧ ViM$=f^vӉMr]yLV}-\S!ԮhKVeȑoh1t-,#B4GEre-7GV-dR Wa{ ~U,+NRNg6or&&d! R!ܺNVӃh>_|j=#jfX~g.WY_6ח*Џ1o~P@'}H?M _8}ԝOۂWh&OP}juYil8/ eĽh'D:o_}hQ&4e e7E+\f'nzzێSTOы iew6C;ZeG\OY=F#]rM(I\b(F"{P5vOP|)*ڢK+/ ,))?t!_זyi^/_;(v;!h*6֖*ʶ8OE5| {<*z搚H=e ɜZȳ_MQM\i!r|@SpQ(?h}1q_䐠Š,@B˥蘨a':&MB#ۓ|G8kFS&;S4)$ia0`P lŀ݋mv4BeO BK?݄8gH li3ܚA۰'Nz घ G %"@.,p o# fG)Oy‰ie}+NғbKn ^nDOu/1d^9ئ( J =j}TU泊dܣe#,Y$78(zRRt<,9E|;`FЌV[POj=Y4⇭`LH ܎'u\sҙ7wf*9M%Uj66 0&`L 0B ٷ"`L 0&( Vj C0&`L 0&Pl RSl :}&`/Vj-O5"CqCt %738sgI'2ӶK ]*A[;iVo^BDBFg p#Aӊh# sj0n\_!&|3P֍-cD0ʫw0qzx;20s9YH ;]˖ܕ}LH>(yܚd$%#}~ }?˛RU`(!굥|㳰#$':y!c3 ΔK>+!z k<\6AloAӚ+3pmfJJ3X:5ŪT\vFՄDmQ=Jn1U1(+6』0Wk*{cEXKxkBv76B"Bp|@(lk BƯ!Q%5,x[-Q9U9>s[aWHȘ3?zW!?R[Ʃ-i>J8+Xy])aHI.hܬ1'|؁?`8jC) k,[n_jMHMFf<vF9 á %ދԦثcǎsѺLů7P<3L 0&(F tm|oDUQ;|n,Ij4ijdꂷVRkF%,]aG[r0_y#ҋۣ9в4Q }ff4כ$D1`$v\5,FyJE2qbyry*cS'}HصVsPY8َ9ݾ,W||jӾF廽ޣ;Z;oֽq o (bmO p(?IIBDЬ Q -yoPBUgUh Ty7h}%- ](,"V5vIޥz]tFbfH|AV:~#rg}>Tpp0(D\)롉lP _m;ʢcjVoMt㫨gZTgcQA#'([PXRxte[Il^{“ZPYJL1߽hƆ 0&`/FE^йsg]`ݽNTtE\nȳb{فN98Z7\4('i ƚBSYXb\LVVJh2eO%J+ )Rv7t,>lzWpN ~tpk]zFZC^UPI{${{o'Šn_1O黆BȌț23: B韶`sk#3{%(ګ_"%s@M*+*^k~ÏKA%V||JC" " J(wRoO$ńh`%< m,3 ^^H1?Ɯ\CV&ה"[!E=+ҺG1 YJF8Wq l3e'8Q_&`L  yb!VBС~tըB#o֣UޝQqSRlIin]Bbkm`GbGL?h4Q"i&@+X5F[Є,(,VR]a,vnR,{^>3ǁ9M30Aiэ]"w#j>1u׬^@dfW7>oNyH~8u!s^ k`Y.RTQM-2jtJjutFuu#j䝳aܞ ?& 奎oߊV8Uƍ[o`gW]8 (tR1ˍpotN [J!W`O9µ|v? w.LW .Gx{rE؛1wką}r(v;]šNcX;vR*ቴFgrˆq6-_^?3oY1*0\ Cg#t-Be7O3~ } ';9baL'َ(ïB c`q.@ Q $p3A.< A (kA58 ΀ 7}X}0 $-1EG F$IFR Dy )G Wrt!wy|B1z:uC}p4fh]nBh=zD0EbEa)X:&`XVaM=_Ǻ#N8<_ķx=ކ_{A+F%X<$BaPAK8N8>;" ]wL"%$n#"!E&yHlRtttG@V$A\L '"_#?#+(*x(D)p(VأФpUOaJ1xQ(YŔM:9EEE#EwɊ|E+^TQHUZQSb*>j . F3h)*Z ,]N)TPR^KeeSe_EG*(UTP2JWuPRU]_s5ZWmnjtnLsK{}DusP,2j44fkTjf` 3F(#qqi8qq+Ս6&KYyH'-VVZڸdY۵ixptJuEutctm ۬wVo@_J߀nm7Xopӗlc wv2zhL1v3N7^oj|"o➉^F^l]^LT>>l*,c7K?{?q[Ҁ@-2jBCֆ քk džo a!hN n҃HHAdC Z0<:?ѓ+'?qs!;#v8q- Sj'$'v'Ot%Y;ܘBJIHٛ24%pʆ)}SL5|ikOϙ~r Q*PZhִA?g#]yyҽӟgxeɬ_edz/{$'1P.975@M-h?s̮<뼒| p^"&j,Pǜv'qOwaeY V->jΊ9ϊ~mg8ow-@-h]hp¾ESg/ؾ%MK.ZSO%J%’}Dsҧ c ң}В١%Dv_"~&_@ 446 1150 1 Y@IDATx \Dd(zA%hc$%1fƤj,Wk>IڼIM߾6K㋶&ڪF%.u`(n!l^f޹s/s/333gy<9Ϲgȑw 9ϟϟ7NrD"@ D"@ -OK%D"@ D"@PF ?8Q,"@ D"@ D8diqMF"@ D"@ D@2(D D"@ D"5 L D"@ De"D"@ D"@@#@d$0 D"@ D"@ Ï2N"@ D"@ -~Z\D"@ D"@PF ?8Q," ;ǚ)D"@ D"@@'2 ?~/`ѭ"9joiK'a FT"l;k$6!D"@ D"ФtE'`Bl~\|Ei"RpV46^*pъ<;Cb$oG`BMe%.\g^[>K D"@ D 4{O1{0P_w=&@<$.jXH,|*о=,%A-a"@ D"@ 4^s`bP rv=b;2qVX`+1jf԰/l}cR߿8T D"@ DU?5Bi?3ժ#o5o F vy'ͺ@わM`W D"@ D", 4_ϽHXqx!n.c?pU%p5w)_wwѣSO7O}y\*%yoE D"@h9.~pײ;6gaEށ?v6|Qq?PW5]/a?~휓xޡ :) $&u7;  [VOgf%gp|(&Mfle\_~i~:y6ڠ'7c癩xd c{td!-<{ڈC;Y}ïנ,[>j)wcL0{o=˦v#s) `$3P~/aIYO>Nkr7!ub{Xx+\0 ops= gk45X: c`fgNzSxb2L3鵭Lr,xq09brn '=xO"b+˛c2^Jl=<ºk}1~H'<̟5ͧ2|Jo]0tꁘ^ dm}[ [/v+G}hQ1aiFx.}"@ D&# oh2qmqzd7O}.l'8/Y4FSso3O4 3<=|u=G6esW8m!+B}V@ڙm˝v 0ai0([$dQ.>֕ز9 7`UmacF^盅y@#fMU?(Ő%(2ςyZjt3b1OnϤ1k@3Fļ_`8z/1<1^Eɕԉ}/H _G(}5}poJџ:4E`Ӈ] '3Ji;WY>(9_G_o,>uEc/΂;erť3"@ D"@Ѽ⪋Kٲ!ײBtV^å>}xh6\v0._O: <2Yd؆8}!]Ơ>|rCS;{ 0#\[NFoρh/F.]D<0;-bBt^9I,XE2~HѝC~.r3|G7i\v,l^d~Mxp1O#[ctl˳6M8\ч.qzOZ b"]u5FΫ4Myo̼Wzч}:rznL D"@@0ӏ]0}L{tZ%O 8[qѻ{w>Ts R=|w਋-[8tWYqiߣ {ip5./7ZQ!= ,3vۣ\lf#vR_mroۖj~č\\/d'ˡj>Oxnvʟ{cMǚ=<_<ؾn 3wfݿ}M)NYهS9Κ 榆=*ŪS~.\^Vtx5^^^+ uAZV~8) bG`exq@E"kiꗂq;ˏΆ(i&D"@ D蕦) #Uڇ%{pKgCiRr JyͽCgU;4~V3WY7"V?6bpNEEqe1ɱgT!*fu<7931XO:"4w0R J3vLD?0p'/g0w gK€"Kzq3N{]%RzvDp3+>{$ퟡgh2QSMS30"*<=X7_0ioJÐ!85cIC}~Q2WV7,\`־l7@9IJqhĉ9m{xanQ1mxW}x7|\Y4苈Cu.w$\9n_*G"@ D"@%)DlB3ԗ¾uul@.A#*msxǶЁu%]:.lr/.V}| 1MKPig)P[ťA"zŜDT"ܖ .me&zd'oꈭWzY /;3-1sn|]C2y>|q\pܾ^^@Wįo۝Lx(?9ﶋ`N"V%[r6[6gnS^_&^6 m©B? hFqoa%;b~~ r λ'747^u+YA5b=[Ruvm5JO`Y IH]{TD [UĊi)?KC\$%= `8F#h\.d9lſ>_? W]ȺlσnŃfя[Uy Vu&ңZ֖qٽz1ؔ) `F{t>#Vm97*}qN5pE~U}THQ D"@7J?#8q)qEGlfߋ׳p)m| ΫDNȝ^@Ip#fUVb75t[B6mB[ĹflÝRV *K ݾp_k#F1?@I, n bYG6 ! [;h'WVv [8:{98=re|D \_d+/q}RdFt"@ D$Kz̡L wY߼jTCNa'bpEcKQjA7m]ًťYNf+f|b/g{XWrЗ8^]_c(I=?ĴTRGjKO6L:B}JPR*AuvDZA<>a.̤k\Qq-h|䷘1)E0]f_0]xqz3Xp#o<ryi%Ն5 @!4[?:KΡ2jjkѦ]tꖄF_+J0-'y :X3Is!vNmVg6LJʕ8mY58il|񢒟ŎAB3R_#}Z?uS}lNbWȼ8wŠgb D΂qo񬩩]:óo}Kp5 xm#z྄>8ak/JŞBKhjSlblxWU#0څ=|q>iZJ4D"@ DCft| )s q&цK<^.\ ]ށ9D>@͍3%d۸^sA }SĪ5$)?Dž¬rߘմujC'6 a[y>Kc{q`I|O\㘰MPwS_;˽T U ̈xٌYU[ ?4cGkѧ lyYvﳥUGO]DQ yMv )ױ]N݅/=g~N[X%hKLϏYElμѧt.^^yUL%@eyLbFԩRVq^r./:0 Fʫؾ%e7?ʩ5X)U (=C3f >Wc[l;+MV/ܜo<6}1kM{S=zD3o/^b  D"@ D9gȑw̟??7n#)3||^lYW>20A%P|ɹ[_ge"OޥnsY i:0^&w[ 8C"Ye(/e& IԦ{[E]:jsn-VODgf a3 }Yߙl%tnd|0 x Ӗ?DZX,3$8.6>OgYG0Oeb;zl\*.uۚmEc[D0wb܅MZ|Q/Ae]SF$Kgmt-7e@ D"@"MfSU[/b'qooy=U_[xi1].hd" D"@@I}h%!&?e`}bk>'޲K }iG"@ D ÏRD@)) Abȿ| =x'EG+Y棴$V:"@ D Ïw8S)D-F`DiYi0HZ,[җL$$ D"@h, |S!mJ({KOr61iw2%Sfn_|x݋Fq=Wg3sV)xlxLE;%1咑;%%]PgeʐV-           hQ:@3~ؗ/%0bl$SfJ4- t8Wr +_~'DW7cȮ¥z-Nֿ6 ~̪OOkT֔ D"@ D܅lߘګ8#zvAvPPpvI?f8bzv邲wW#@v]r .r-V 4{!Ĵx x *w0mZҌL 8}WŊ5U2˙#zar4́\YPNi9.\?(WS$.9I _M zӽj9Qu8})_̷8D(s-϶K/0bFujlY2U_~"@ D"@^V),Mٱ0唧e1w;8M[΅Z}PձeӖI-cn|H&՝;sV iIK|d3jL&9Ӗeebrfک\ey~2ɿ 3 dd6M9l΅K׀IKU嬽4~nRW7_QvNA\ !6w_f&] ȗC~wxer-#vYn>˜=Wp:6g:mw`_lsNĄ|;5()cWټJŒ%=WWU*/x@!-3ErJ޾ ;]4ڋ\ey~_lqWJSv? o]y_C8-X5CkT m2$Қ]5\4V8ehct=3o ?mAeش+l)̀t"@ D"@PH@{_AF:ʘ~f)kK.ҟ$.܊w&Eq ~~A0uDdxR|=bn7.;*)h:l#$q ĨiejANo>~QDc³3I$.zͤ:mT@i!Vǫ -wP.Z}'=~%.܌u 3,IǝغT(& D"@ D 1`T/sFQ wOêĦ(Z3*2|qshu3Tݐ‡Kc3iX9-Z~f&9(oGSJB NX<-=&Z۔`d| &!޳i=?T(+cձE0[* l ~*j`Ⱦe꫊p* nZw¸Wq?"@ D"@caN]E,~U2'qH}Ԗ`Hvw W;)~}Nl/B||>3E㕝bԛ;8/bxXZx-,lȴ ~b w +|nYM b%mPeT_ ҟM#/~;tA6#3 Ver8A.d*Ttg>+{ߠ]N.yX:8 ('"@ D"@h} 3`œؒ+DZMa*/Ls:w+:`{ʱ׎~Wv;:) )r[b? PK80DZR.*5oH"-7^QSrp+WۥKS{%e?H|¿:^z6b)0!fX5m,͟ʼnql)كi)F^VĈβ/ @ D"@ qFz<ͯrcv>.*>M^1j{MMxvnܪϦުߚ~&c_Z&ƶe"*wg~ߋ,?Yp9o&c1[~gX.nL=.3(ښzR!Fgդa%mV=fq㦹1yyv<pA$ZD~<$5 D"@ D02nylxmE5_- c$beNJx.4RG;͈ˮ7W Ki9+{}N.aS o8,21 ę@b .p.˕Ч4s_s=i'o?Mws&"Y/Xs@1Ŗ_B6K*pgL1 D"@ D%`6 wN>ͶY=Wٝ/.ܰ[{v!?w]2LRvDklʟot5XUX|gPrzqĸ\3zR#0m}y`=. m6cn7g8!j'dP9MP"ȑB{)#j U'ġKD"@ D"@~o/#wggY_ };νggIxk"|īp) 6rmyȱc~iW [7%^jr% &ѹS?E[ٛ?h0IbT10mcŕٲdxOH[7-;w,[Z(` 򙎆([4OV,'-vZjm{ll&,K?ضޭMM D"@ - ?oGlߧmnվy"xqob`g cSH}犙OKt쇑o@Ue-.^e`GV5Sb\##.'\:*?7iWo8lFJ<R+^->+;rvx1 JKnJoJ<"Z=lk~]6JfhSk:kH}W]sm{ȵN GLzS:n8eDlq/MB_^]#rm»+W)Pn~Yr3hZlX8ߠs"@ D"@ D/m|:y$Ԕ"]&6R "@ D"@ DY ?V "@ D"@ DExͫ~$  D"@ D"@Zdk*N D"@ Dvdi-L#D"@ D"@Zdk*N D"@ Dvdi-L#D"@ D"@Zdk*N D"@ Dvdi-L#D"@ D"@Z3D‹a(] *NZݲ&iij1l*lJχf$PLbTI߂<75$g,ӑ&.2@>3c5Qn^zqI.k?x~R!oɖڏd]p Spo73v!doY|}>~~ѮMӔo|}=!mgFhu8 E$znrnt|PSzY9>B:*PgM;^l&L~(aC'r}3w HCy)=S_KӴ㒶W\sdwόn1oVV֏x+.6MBLemu5Q_] _ 1{KH&x:#CR@0 -NIž0Ψ8{ч{0$P_| ?S[Tj̛ulyXgnOcs[oa AMAF/wj_c Ycyg{71>l9Ehx?"@ D;?ؒ dDBg-QZvD4/~By4\,B k[QStzG_ }q3i4~2 Ռ׺+cZi5:qZ{cF;i""'k(\B{hT@J8?+a8+=WgB^ߗ𔈭F_t1."_rPoJju|=5~˗shI\e.z3S(<}6 M{TJYNUjAm Dİ^VՖJ]/AU_^XXousc=Zsp2Qa^0Clud0j13s鮗mx' LqC0!89k⢶"O\֕_[y;lAq̑?FG#}4IJvgɶ-5.y}5JM`GCBB4Z~y8zƌ FNA;9O䝀ף.] ΈfEfϫs9kzmƔ'oJļyc }sGj /?N-|)%Uova`m(QAh"ERWb6~ 3VJSk:^b8Sv9tve&EEi{۞\flg`g1J PthX&'a,WRoa/8qaWSELZmah}>h +3#{sj{j~k-Uquy-㮵K֨_z+\TTs|DIÄb䞽qD% ^`|UU4ö~X3f=N0UTȏYRY>sf S0}3D<72S$9*idSQpmw0 q$'#'vcFZ\8X?L}hYCѧ`vKO0جn}tsg坐ver4*Se;8| "1| f$bmKCU$`hS_-+7ΞOJۏvqN%SX>86[*0̌IqZ[m >lw@~+3Vs%jR,# ݅YL[ ߁~}QE(؋"Nx_oFZ''Vk:`.`sZwO9KQ=l4 AH~G{ٻG%/OQ"UE'q'6c/XRW/aP8< #ǹh܍rsk4 ip+.sfv C}}lO~)3mtx} ^]x +h Kddoŏ3QB:ru,;l/h. KэJA<|AM;Em;lY? ,51wb^}2?>sFyw7'ˮܹ[v(>|4f~; 8b '6'>XLj3Nk3Ff\^}QE)ï]xm)h?x]xqJc}ps@IDAT =O*yZjM')樵2/-Pt\%kiXX2=3}I OWMO U.5(~]p%ynj?u[Ԍ6Ex06>yp$ě B38 IDŽi1a&"&2Vo45e$tL3\8j KI}E(*C [Re;LCư9}l.>%p7i%I;~E!'PJ2 padwhp9̜j=,8ܖvؚuX`PKq} /&.f[wǫ.8߄z&Z!qxaS~QM]XK&fӚEM5b֪gۏm~8o1~jxJԚ樵4^J ̥hN|KDɿoTy_sy~c{s~ښ^k+3ޗԣ/6.Zq+ .6UV~3$<ҔΘ~+3^ujz6r t=Wp.dzhG nVj.#Kԍ CsN!4#,ž}ysZ] @F` SZV$s7a?j6ޭ ?VrߣRʌW_QCN&[Dۼam'mft\g.̽cXbD#QQ ?}Py;4RH㷵C2ج~vKlG #ˮ^i>M5B^c^ `K|"R …;a4q@d~?T\?zWI56f\z VxUԴ"G|pT0G讟JZHݫ@I%#eQHN߼Ԥ~.8`Sň̟TgƼ/)G_xZqd m6{h$<ҖΈ~t-Ns- =OsE[Ex6e8eW/Vj,?9_3{e!֗Y*K O`$RBt- í˛j9'2m1l[sۨ\^l}SL l1QiG/r7ZBPkUhf5:f_~L$<0bݼX#0hN5苦*7hZ\TךCm";KT{9yU8jwC?iҘz۽Y' o6K֛߉좓O" 5U54A,f}Z:=%"mku'M6y)]K-Ns͖[iDS\ak9wG5_-T-AN~l.+G+z~ -)?:8z^GYExveOJýloG4ˤ*o\tiR7(ʰ(6Gڡ09'ƛ1>Vc>.wΙGI%Md:ژ~ kfQQnn* c18.߄#kP!'7~zr6N̐8|ݧaa̱0"\0w"Vކ:"0練xL0+pnV !Vrȋ99jTaX4l~g"7+kd pI1/ӛ;qCѡȫrf~mF>s4;3J>Vl fy0i.^CzW:װ"߲? cP`!9=W[ckxZjKQP\"76S?<[Mu:㘙gFps Rurp.QwK y>HZ_li? z%cE3OW*SPfۖ^ɩv0ߪQD?w5ExbpS p=_-^{Maa:4ìO{S'`Ygug;fp`.9^OXS331c P^5/H$OF'sSSR6U;eX 1a_v.s`!3uE.D24v"_ FyFa#¯̹CW;|>k~Om9g.Uq#{U`ʆ= C"dnηε_r"8z0Jf'5<̃ 0+%|"SoA08q.kf ct Yeh@}qd9Sou׬3[W#==]n0LwTAu:BUG#FGW$%v,=*;Rsy:C|4j}ˢi3ߗˆ4-7b >`"XYH>md4@a2!}kpw0}PNER{?l߼$SS_z烐`a,O!mfPYCWyI띛\aSdƆ0^ť fIZOM2X}' 5G` 1qqnS|9b-na.ȶ'c}S^7>2CuT9?^(DE-31]+X9ѧو+@ q1J_doN'j񚫻LT[?eo_Uܷ?n_cѧǿuEޑqI9 r:o=#G#gq㤗ՇG'Qzd3 B0Gg?2Oyشڢc̸b̌<vWLsTL^~-Pvq b1 3]+.6^+dFh`$ϙ6X4 5g˺2C#3d'E(`k=ףHiB8thJ^WEd37u_ƥ粲 /mFft /kJaxԎE6*=mP2^u =s.S鹚i%yx<#5簑:&l {s] #dZI Z]F陫%zHϝ4{Ͱ6zĺlXʀqlt?W+.C{W=jfE(1K@8Ee5udh}HM*[q튆\]'ʟDN%$Q67(N#jm>o}81r~QlS_<"~Q.3xMQ^.-jiZ 3iyjZ맕t155Q!X]Q6V.n3usSkyz^f|P/nв18oɿ/{<wQf[P}5@e;hT?rjʪ'o>YG74 ?1l̐_:6tcf28r쳀kD@8ÉwPSب(t }[Z- '{ea/VTN+%@aZD" ?.ÿ'0% A(*.A[64;wa2jۋ=aMtS^ 4jVf/"xgyYA)C^h/N㧱pԚNfL[Ÿ^RpEd eٜEcjN5}V1zƍR"@thkHy@oDݜL])c s|غg0 ÜXIlʰU&cz7eE<[vxsT!-Âs? }gfIKp#@6k,ԚNGłٮ&>21ʢ3:}E:k^@_H-ԬimL:RR"p7h!χ4s jV\`j]U]a>QrQ2LnU!/*SUH Hz;JtPGu`;Zֻy?"kH_< qD=VZimlknt顥Rz__tU#5೶1Gj`GIL=ZaSlO+l D"@ D"@"@35"I"@ D"@ D O3k"@ D"@ F ÏQ$)"@ D"@ D43dif B"@ D"@ D(d1$C D"@ DfF ?ͬAH"@ D"@ DE ?F| D"@ D"53yH"@ D"2D‹O$ؗHߑ2d&) w5h$$uD[??X,UY<DL,nwfeU  e2(h)s0K Sz{l?^;2׌ EbO [F? 3SbWoҷ(Nnw]^~Fo2c}ft@Hcs*-G?G)Fޏ.V9GơerwΝ4N˩eroҼO)G\>Ab^7' Gblwdx Ep~Y1X&}-*J-LK&;9p,:> Kt_):q1uq':c%XWNfa~Ͽs'>~b:G }Eճo*T,F~no<1w#)fmUUwڌ)OO{kx"-&/>Tу0qX/\ $ .׏=lVm}qv:Q6@~$U]l*~7.%8o'1|ۣ#W~1H_|1kG1#^zw t9aq&ijpŗsucۏc$$c8>mma'qᐏČ$xeEWpdFdW?u4M,Q=DIFLQOaLb7`$C쏬YCk٠ '#uP ǞՑ]vу-c[. FN|~DݒUȒIhUL."dmq6~"+EGjl,Ydl<}ތLD7F_"ܬ 6hY8fPDX47ƾ5Rۮvh#CRԩ/`@da_Ew[$`6ރ ?ZөSͣ"AQ]BE[PDP A1c88L9OƛcYܙzN;jʹukj*7299usy1R1/#18EA[D`޻w ^Yg=YpEnOY.dlebЇGݷ|(%ؼkk ;^(or("s`ƂZ~paߧ9,V Sh(IKstd$[=;CuػDy:K"y/\ҘcD[c^aI&H6a+ǏvY" %MN8cK<Ȃ<f4~V*i8 Um }nO—jP8֜mÁa& 6ůtIÕoA.=9 bNH0)w U $ea[yk]Q }C AX d/gWFl(PϊBYkOZq q;~Ϩ6zB7PZ'bEuZۑV>9Q1-=%BP+ xGv1őf0Wl>E"V׉gf\ >OL.==~¨'&3;*&>ӔȄD}WϜАx`A'N: 3vhŖ7B>-AX;k @@nFv`{% l%Q݉#HxXMv0b!~ڶNz1l/ƾt 阎 $ܑV׏~m9Hw/n͒YYVe *onjnR*l  9PZ s/iXPMjV-9$L1ֿ',i=iYt<l.ArN7%㟄d=6lr obljsXwKuƃc- rF2/J[j[3"FY0]uOpFmfWg#ߎj9؎}yp*Fg yszujJ4 _߿! P%dP-c3 ~Q 4ԯVOW jyA|DΒex $ڑ0ě4)($#@y>$pXxEB \q;; Pq(8Ά }bg*ᡜ8y yS2gdSR'A(sc__1?0tOF]Uq"?ga\B4=%1ґ,,7. DWB!7D([S/ڋz7Hh%$J^b?˟vМg}r*4,{vd네5NSuDٰ¤PW;~JX\n8/dW;2>W':SKHZ9 lF,;ϖ|9$͛[dw0y+^ G:O6 5qNN%6"Co<%z2UN7jj&x"g,qAOPI򜓩fڃy큋.iNCjdܒY%~2.>\SgTWU QU[Y)4&yN6IZ#26ϧlMLm0r|aCWL 9 ǿ|P/bƇzz6 뜅/DFDv(KԃΦIp4"o( e)Yw#,́t 'tgȹZµd;4E.}ͤZEXo;t>GAGKQgoQH :gNY0'(щ?Qݎ~>/? A/=$0b P2sԐ尿L7-?b+oDnYw_EAj?\F>y zUȷQ,q SH$GQV[/.)[gѨd<0J* VB eKu [ҍ~qqa^6YL?aHqIxޯH :zKV]"1 [D_i_A^Vh֬w|x,bQPM;=DGgԋKqZZ(5D6CMx3^?9&*PQ Gw%nˀ'bU'd?#`e[E|^:Ҽ~70$ƢEc؏IVƢDy$DIj-HFExQ27c&ld@6΄@y5&=DJ* —?`Й}7 Yleؿ,=OxƣrxFtrCa4j4ȧ[6zI F6 sdDøu$yt*?*H!7^@%Xp?)ս Wb߮T2x2 ]gC৊ w}wSݴ"3+f`%#a1FMߠ$]~# AoFCӫ)P֍ i$ԗ Ə}ܻۃ'#dӒ3E$ܲg^`~WʗJA MG$?>LFށ!)kwGz{የ{k3u,?ǏwB_Զ#GՐ+D&#G~m6VN!~5 "0&\, ,/M~7=R^@Br}7/ lǿ0I J(LG Zb wtth. I9{1~;~tBO EkgiӼ~>2J>N۝!5 lzs>?8 ^_DouVa$d_Uť7IP3G̷N;30ʜ$G~$=x}/L`щiRu$ dEGq'+~bЉƕ=#{{z;̖&4I6xq,}ƍEg7)LJB˥{[Gv`ciŹ4GDZLg .ʄeLTb#Ynݞy~޾{+,(ټ%+R !mNNBrsOJMtEB}6Kez ~ļhcM%#O[mvp)SMBq^k&qutՑ ñTBBo`௥()q T舌#!k6Nj X_S{SŀC a򏡇AiR$(xjRB}8"iBbZ׍&D'$8A(([C ^1T:m؋JNiV?$n\Kf2dLR@UH|7\O l6Q2@ y/cKN( NHB ZЂ؁o8BZ`'9b_]Q&v6tgZi衮db0-@m ~Jo30O{q-6Szʁ˓/!æ=NH6d8!*'7R5qniV?9gw\n@ 7!sDʾ 1 p 5%7ge 9R+M~ يށJE I_Ab*W.#W>HHch> &8% D{?qs, JVowJp$g*@<_ϝ8 =_t8Ҽ-jc^6mp&`FKy6^Bȩz1KBB 6 R> 1m`<ǖ>ò`h\罼 ξ'6NNvZ9zR ^2!)&W+?֜Ĭ$ѣ/7q͡.s&?Cɠeߐ:)U9i {%%Ы-bhN}OFK (G?< /VRjG>') %:Nj8X6;1FQn6E_($vӱϊz)|(D \mq!M$DE(%X+T w@1yG] bpO}WAOs0IKXg]ո4rzOGsL 'R;_Uhy(KvN^FEqF?F\9QӐ'n>ǬZkAo=pGP0*Օkn-NnW; ՐOdו s*L!LĘO1U2a3kJЇ1m8 Qe":8Zڭ>g>B_- ^*cl2)Dp,ߎm4L$uf>\.Y#Ybvc"Y6؆Bx]$TV")`&ȏx?tY]9wAhdž]PzD<IQu'OCoتN[5CxۈY")f$(y>CsG|UFN~(ςl60en$41N4?GvԽ_`5` '>|M]-hvd>A^WEZbHBILӎġCx5?ڀ؎D4)FqE{m;L h3r*j̛}t-s4rϴ 4{8 m#5-L_ݎ=(%m&K=עSzBF4f:OF>^;^ h4{q|Ђ ގi3'z=5OS9qcZ%t??#/V& Ɗ`wAW5i#t:vQTʂl el=_F92AZoBQ=~g- 6{:pa,pŰFE)o]NI4gip6咺:;5Έ )6jHHL DF'ŁTp}?mU dg'B,~Ywn1nwOqѐevCvdf_\i+?iL],&y6 تʐO&"ijujˇ;OBS96HS@qDbO|ʇ%͛Ӈі-d̥ZЧqKǹHW0Lnҍ_>P0i,߄يa-hr%7 BrEKb;ZenD 9LNsYvJn}lF4oMGȀ,IC7}q K`4[k%k`= Gn5Єu)>ݎtf|W3c>TX(6aq+ UvbFvR-Lm(Ub ~NC;JÏ(.f,` U|S7=yoy( 52RuËQCZ̓n^.*LB~R??$~Y2YTq Ɍ&z ՛֣0mX 8) ^I@#BdҜs[_FޖέY{P/gގI%-[b?DF:R(]q"ۃY9PNrs0Gz`*Hj*.f/V;2炞tXOBs9 9WgBHمn^+ģq*S1Ao?蓱h5`cg-:]i2yQxg5G)*8aG '1Hz^*mxj\p-<\Ֆ؅X 7 %pWȱOG`vCݵ~Aȗ8%n8n҂!' O"b:jg ܃s҅<,x;oY̠!=_O@)x&ugτAr:~ PMuRo ㋋!=+ڟ/p s̚7PD6@3s7ZO=wkrMЂ5d)xS%a.a}cA;G.\d| jc_Nb.b H# q7)k =|.KfkVNm(6}4'"֜Eh> #%5MX넬zf$s@IDATEkG M^|ޔ~~'˖,Hϐ"qs ņi+]o]Qxx;uA466Oy³faL_ZUヌ+•o6Pr]ww۷7ףf{Sx۫E :7^ ЇhN!;b-8;v#[ߵHljs\j~M2gsns r[pboX7vDF(]uZ~tvFI]ޕb,յ>?1^QU2S}>/n7n_B8*KP[{S[m7zS|pTc $<`? f0l:A+=< b;뒵+_)N~S!Z5O[훂⩟OTFg-sFTV ?y:Qm7ݗ.h8OkHبɭ8u[H;! mّ7͐w[Qۺ h/ity8I(\3BU55kʳT$x=kQ!7pfȼ.,WW3vrz6 ԳzS"L9r&ԎgU|ތ~~" (ܹrZ5Tj?Ӟˁ3,)M2>kEnuT'Jtgfdʍٞka/6χe\Ɲ$ҳ[ԏ7leWނcm0:dSX+vIVmxyw1͇̐¢*ێ}(Xڂxqȓk&6K(/$j3X@s%^G:ǍEKPƳ:F(Ц׆A+|CrJ4.,,A} c,qf]<~9[ȹ,MܵZxeճxYճ[Μy Ʒx`p3I;&ba2Gqak'ێšv>\xϛ~K?PF!,7]iіh+*PH<yO)=Mmص 6:LK~ 'iUzGd=0hN0ru)fÉ8m/! r%4>xHTA&'M`%nK8v:88D( YZSM}ű]41M_d=U$ {sM%i^(L^ ^\k uf|[Wէ;qG {jiJjG=ghɒI V?uBgvF>]Kj"+ MrtW m BNԥ='IW'mc9dw!'0`y/(}vVJ*Cw>76`EQ!·^DIgO *O~^Y?i^ #=lYN$a*=Nݏzz w>zwvitez9I*2ۑJ _wLa~r@b*ɰ^0D-~VNQ !&Rxc6k5pwz۳"c8h-+; 6DZ_:d1IY]A]ɩ>p+n'6-wOan1Gzmu5_'Lxia:w :W^ZEn|gkuU4+EatTFt">:԰>ab;r6 Gf:>-F ىU#3~QAlx^x!φDIHkaV }x6Fft(x ቍgV;_6E%Ӂ\}ϴsȓQ\*?c폎ytܱ'8bL;a14ar(*ĬVx]STq|Ri }-^ϥhlY(̙x!a<Ou  ੸nkЌܡ;J崗I5UVxk/.E޼ ٚ$*i>bV顤@߽q+#g>/όχgzBߍXhc&qQ ~>yW5`L+lNQOqPU}&s*g"O7RlGqc%0 -`fbV'N7ICŊ!9| 62rǟnM8hHD_2%g iz+n Cwıf^_2GM ͢N08I9Lbal$b @ˑ.kĞ |ItRKWNr@ `<0Z̗͓xb0ɞ}5L/#tbJz&͡c{0w h8%1acah+n¼!<&-XKꐫ ]f`0<8x" {6ѥ9ab{G-W>DZVY5勐1`L9g4b(+ê4o52Xe0@fU-/B6ݺh˱R>2B~s8:>S?$oUzΜ_Bt>2hm z'C! ΰoJgEdX9&i3ٌ3lC!PBQB@=b*cA^a9לa!<+;]S؟yqa:ujϟKjX܌ GF!Uq#9f9`0"4~LFMo[Y~ ]ICSMl*]puB0 ?Td0 py8!`0 C!`0 C@&Qy3 C!`0 C!0t/A?C!`0 C!`0 G`0 C!`0 C`#?ӽ C!`0 C!`0``7C!`0 C!`0 Ld3 C!`0 C!P@ ~wqExER ޟbLZ6Q `0 @a9x-v*@p>m<W,m؋dWmDG쓹l|XEU"|r0撟xj3uxʑ<ڋO|y5rj>x0s I>7>E+?[)6Ydzkhnԛڷ>hvnD,$S4Nu~8;m4yQQȘ6_ #n^n-߬v;=9L.,uC!`A ~&džm! 3!&KJC6z":1}r:IJ\8LDz Ϙg{h7e񦥞MD&f"H718#!)^ka2-;JJHHom7ŷKNsq6U?"HQg o?q$3IlyE.YӢoȯv; -w6 h!`0&1k#/[8Kč_\ ?s}LcR)!3WP|.nCXML=a*kPfKx5S?f^F3\6~͒vEyv\DbUlVpӍ3p)PR ͻ&i=Ow͘nB=\q!bAFz6r.ĜEqQ$9u> %~(\׾µ{"hF<a[s/NnڱGC!`LǔDiWϜPEق#\pu8:eo|̄jg&$HRcgM!IGGOFH6V՝8iGlڪ [BGC4\t5*I#D%<hXqMlěEشM9uKP&%X!`0fل2ɡ8KDŽo'MrW*?{u)y(.7>bn͇q %Do;HGj0o\ L@UOƼ^*?B #:6G: &^. wѱ`x-{y9?ޑ!:!,7@{ԗ4gRj6{1P;jGt|s2yփ@,b.>6{萯A&I .!D)XGZhsYy'Y-V!|a ~P(Ώ)Nx͊z jꙐWiyփb R4I{/)[gxBAI?F+?fgx(m:{0L+ŔVR-4'%QeBF,cNc} Ag{k.Hp—eQA_q w}wE[K*221Z҄\BIE)փnަp?/Fv]-5^,OJM=B)mSXleؿ,r_ PWhmVl Dƽw?DvJX!EY~65]xJ02 wG+ R2mzIMm8&Յ$YQˏ+i(:}<8 khuozn}sQOsً݉x,ŭhGz;唻YM4=>;?x8|:"rbbW#;X$, Iii4zx!jvq#Q37H[lXV@D#r2k8BƎḢ-U@1y=L}]Hq?C`t ׀V?'*Ҁ⳦1! k8p9A]=ہr'F0>ԍߓ'F oMx1,qahVN=E. RB3p1f4)F[]Gfn8x0j t"ᩒYU, AAE0Չ/#tS xq"rj; yob%t-D) ~=QQ?fp4ԵuPW_]ncî}(~GN]ԏlk)w44I7Nm} OS}_`D`"xe4 C!>Vc^]SիZ_ ]lm6jpti=x<%Px?<*.LFn\-7ʧ 6wҸ"_ωC焴Bݽ2`G@4wn8(pn=dT֗""!SS rܱoxꮟ9j D]$&IXQ &7&фg1Q{~^1’;9=LʻxCH0_;qc.Kʻ֎@.)a,$)| ~ <~_ r7wz\LjrIL1/}r@7/^Y C! `\w`jGG(^8t+f,Wj [k$_z%Z/+&n.(ݰR;㏕ Is8lB-ߎĵs A_xp_'Z^dD\WI4oGahL+\}=p Dž| A=F ψ^p'&\:N9*Ry r6/95څw'JjD4-mlK:#5b6,lWK/.R4t&gܚi@:tW k3ZLG MORJ1'ꋡGNTԞ >j(*i>Rxt3yp䎺r.YϜGgt=:u1JUDSE9 i^e!`0SZPbXMNɰ>AȡaVKlzve ?d5zV2qK$1> j@_E.Obqe ~FP'8ƗT',zzTf6Vc3X%[b7kmNSʟ޴3a%jID,uxP_` k":4j0Nw'=(L\1 C!0]`Ao?n2> XtTi[aVݵ~[a$rbuu•Ɨ5Ǔj=oP^¾̣tJ`$4PԮ],G{q-'3ғoCӤn&pWFWE^*Gz4q#Ưz_$9#aS9I9E7x/.~t_>!5~a,_d{sPCXrDŖG˻0&y1$ODoʋ?rIȟzNMQSЀXΟ <.wu)6;ڼt-C!`00.[vFc>ƅR8y.J'gFepuyYVFwzF y*ż a2z9e&6kx-y)9K baLK*$WZo Φbuصw A}WO님I@?>mQ(Gh[l%xE<<҄-2嫱HrG$|IF}s1$ۜl6Au-f$!9o]!_81丫OyJ+Gu%R۳O85+hK%GYl@}#J'_'b^i .HX#&"|:*|DX3+חz`PZڑ!';|l\wc` ȰGM ^!3*e%(_2QxHcO:g@E!t}tQWYMfwldXg uf(G[tpi{Q\K6\!DFv ŒD߀]AY^Z =yVɢa/*阊{Q[4i/f)Ǐ."OFq1xj0+ni٠]W!D޵PZUY3I;n x]j{*[n$(>ZhH)ߝ >}B#{q) =%$%cc*Ӳ宔+A~ 3;٪+ʃ-s&^ w;\!v< {Z]cich[bvbx^!`0 LO(pOôMrNTujK4N(dѬzhY0']Nzx?/f੅Yy\P֬޷6.ntȪEϳz4hHD?]WuZqW2P\t;Y0}2?k-w}Ei!`0 C7l(oiMXw[ ˶Yn߉muFbkts/Ba0 `!e C!Ə*AMod#?U寯~;[8z|=zkۄvTb=6 7N(K G9 0%{][9ķ`V1'p'8%/:aFw {a.uPNZwwP9# ~~I]Oj!mP:w>%˰4 ї2 C!`0 C`B$ . 0lͣxz[xpZqiH4Q/xc^?+>PZ#LܵC<ۑ%İ%@>F|'% BU>{FxWsC ; Nl'}a_ZW[aiWC!`0 C!0QLgKs3򰶡OA\*-& x5~/NO*b)w#`[ I[/ cuT"g>?mT3{A!`0 C!`L0wBn{l̯X5± #`0d}r&WlLxl?^4߆ }$T#C!`0 C!kl]A{t g/wKf"sX2fXNOqQtI0O/N[_G}y>xpF<(t&+¶9b`,hlh~BkulEk֠/i!&qÿ#Ϟ<`1g΀㻻m=_5a뢙D׾ąkx!߭<.ZAHE0;!=HEts4f̞L,]5٧?V*/rcp^ ]1?oq sn]BVp޾'$H}|X ?];; cøsq dG&e0`<-O=H>wE ~ֿ +_"Գh傮SA̘v$Fd^Y7?6z| cbnÅ$th/Z>Ã#xa C!`0 C`20U굗P[w~0o/$ƪ%3~MK bWa\w.~y:jmg}x-Q}}V8%>: q_#W!e,cZ[9$m i4,䆺}COH͠z)r.(5c2b2b}҇\h宻3yHLǪ}Pjvyb>wd4%kyp!oZ  6TϜ_H1?[R-07DHC!`0 C!+ ~^X=| }_Ҝ3xkۺ@,Q0|Qᰎ¿>2t7b Kfd%[?O.gЇOԯpfwVԯ[*KLMlB,J_^//Ȟ:,UZW 4H&'<ʁQ`}o'd1/2|RZl\ XkN!t~ʗ'Kwp6JHsIt]b 47.]u,P* mGZQ ?c.yFMLcx}w.UTO"  بSF !RS =}#UlPMJ( cR M@p)@~ٗ{Nf_r03kYߵְzVPȴ>}P>ƑK Iy&P Kd$ qX C/IWݕ>kj4(R:lSQgGp !]K-* w:ĺC;qY5&^mw[{@G~⏯Om8X~!q[KN]j(SǔgP*M11jh癫&      xaxU@STaފd5)>B\4^lLq,rwHpWuM*DvZ/kήNCQxS[ 4tmܢK.~HtCxYVchEsx*+`殘0<#JE={SbD~ѝ碛펗<^|ͱRnhʠ]8YJ=x$:jK7SYeç< d YG-skh;eE3h8Se?Dl]W86S7[G;8?]f[OT)X`pbTeيK~K*  ijݲ%EU]W{( yVj-޸ѣA$@$@$@$@$@O2cRHYX8Bpm ܓ,6 ez:tFg5VFZ.ζ s6tzr4CMeFZ/-c5ĈQC>OWuKjxjF >ҎfgvHۉϳb|kGw43:I$@$@$@$@$@-D >Gd_{/58vUM 3ȾuSַ$Tiȟ( OJF3yycSW3bӲ{Khسm^-eC:.Hv;+~=EڧϋstO a1-_2<nG]! zݲp텕ۦ*zd$uu*=Z!3ys҆f4v%+(@A{x]>_g^l-D4ԘG$@$@$@$@$@'O<9U=3/6|YE~blwߍX0MŠ SRʓ֗om/yP3YYi`ZOĐ f9{ *Ҿz"KPIc 0Yv;RvgsOKNjTTZ{dhylx#7}Yq=f,Gʄ +~au("q$6KZ+Vyi*QЪ}_4]FFuX{RbqUȱԥ3B:wE0E✷kt6JwE 5,Gdu'1o6^,@.NBfV{h4 `G6Ogf]~0ÅQqcƛ%3peːlߣ̣ D LD:i]5ʬ_}nϓ&.8W5떸xQdlB*Epa#e^74)x/QV{{f!U˵Oe]iѳx>'<+i'uN`YA@G\Z}.u7unnsB CmyFg7ULߖ] އبEyNUt爾h mFcFX!E Y*[ho ˶&JC$@$@$@$@$@$p%{וm!=ɯ.ENf>|B˪mZ*}HHHHHHM6 A"Х{-)Q^V8-Yn;(W҂װ@ rHHHHHHHTY?8e.зNXb ^\Lk= T$m@I~6:6B;t3'b䯣O2C$@$@$@$@$@$B $7e䑃HHHHHH:wno=FyIHHHHHHH$*~Lb2       hoio=FyIHHHHHHH$*~Lb2       hoio=FyIHHHHHHH$*~Lb2       hoio=FyIHHHHHHH$*~LrNԴ4;G6HHHHHZ@3(~1yNV+r -]|eJ/Aԩxp Z #@|r2M 4+q|66PrBl[pVڕ(;yukpt $%ߛ[b^/c[-ͦ[k8>=x}o?N9 pM܉Mb78spiTق3=שi|I2hqc%{-<|fi^mg ӑ H 8mJH0BCo+q۾"л{&rX/ĉ"}]L1qfwT|$흷f!7{5g?by_w%:y_# trȰ !^[o=|%V~1?Xs-ww~g55(="A~.QKno㳥̸NM :v {ߏ/@Ing?.;;vw b/t#sϠvպ5j?3\HP#F菏?آ\ [XȱaRKfF%>>`IM`ĒdVJeY2B H-6X6l0>6mdYyBc66siS,ؕ7>+x>s-D gK,bu`+ryۦXxGVn| Kb9;tmMSrn_gS~ˌtKɩs:K-x'-NeTN=yL[aqX O>ilM?\Z}QF%y@ƀ^飜,XI{Q]EtN@,TGBu>e y63/ mi˶Sܔ&|M,L/㳥Nj5Ut z(t.Xe#16⹉Z:_Scnl_k>VXr p 1 ? ye}1ڐOpҖ$@@v X+wʜ3W:SFrBa[8w"wݪƟ`R>{wů$vkGEQIH @GkϾibUIv%%M;ɉDS+iyS3[z4vA TlBMCZTwcߩagJД3>CDpND1!m"9c2 Ż3EXg8$b^{EًLxdL~*u֢ts2Peԇ HNNGi:4)>!gHz"@1v%Mݷ e{/upRp Lg=BooNśYe @bb_ =kv9GvlۓД&}n.px7|jj$ME_sF0|GF% :zYT,_<۽qV HIaȭ=P}ٲk>xɟ7tc*g8=]Vߢnr|%}\C#pKd82Kʾقuk>@^cP|k)~Ѥ XwY6,`}9 s$G {/рtݸ3_vHƧ|1ѷ8{ H>o퍐Zڷ ƛ|L0o烒.J/ľ=ѳy<ɫqOz>INƓ G PKrV|ZئL~,K'Ы%d^j,ӂ4K"-LP ƦZr׸Q)Gǒ5v_2iYڽm69~Wgf2rbWҊBm=KwP YK{6^ ϰ/W/c5jYf%(mK,s7|$L#m8ji~ə ,7XgS$3c+r1?fK%!Y+>+?vaWoAX.춤̵Tyڲjne;{ RN{hea/lR=Үs-gf^푉dY_F'5Z]Sw,j.%]trI_Rx,.4djm.<3>q8ͲWTS,[ڻr`|~G1-)-u7`Bc9ϔ\Y)UO2f>h /Ʃ?c[ly5R4cTOOy'\Ň|}~j<ɉccZU[͚"1~JJkbuPX^+c dK1ǨJՖ JLEGV,ž]PY]I|SS?YX< Cåo߈CG}O9EȞC5?:UUU*Tשk2})gBf.gE\#ɯ\}.W&e0K.%X0+ʹ{|,JETX+=彇{*E OGQgI/Y/X=^e{yyB)! ǿ9ִH#7rm#6?@D\Km=9T,)12lPk;J+q']`'IeGj۵vn %@lC 16N[jS>t u|>//yfI%~d}Q!*EUM:ut!˥<}⋘y.wBNI,s^BÅsu/`DLv:kQox~<ˬ!sx؝G>ɪM0XR[ *X9~(8dž߅lH}z+YMe5•sźGۿ9fq/NS> mTm\G>3 YV #׶KbV|m@|I[⛋[pq3Ty;_lڀΆDd?{O] <% @`,~4M:YЬ[`In'yb%ǓՏI ش’f#>RV,FdYi_,EAaH%jmiŐ\grZĤ]YUH;N6K4$#JGeic+X&'7ne.E Ga79EaɛliYcYBVdə츏9ڸ7솕";\"gxqzև[6XV,wd!6#>V/6PՕBKE1k%vm(N{ٞƂo߄\87Rmc >r1-KAnkheӬc?R`rhħX]n44a˦ī,N[_}'.tewβ6;]s;D vK[""7KCR">>_gXʔvXéXMMm+/U։݆lBt`N&Z&as<`Z]ˑo)鳷:Lϧw.s9wȩm1@P^3E1_4ڧU6KzIp,Bv˖UӜ-dQqمFdr8TodUuxXl2|~e?xz^i.:v.c?矴/Nu-k~&b ,鰞St!+#'+o Ge`{[kcc"]">P~v OL4L"wꝅw1T<;O_tqʕ 1bmTaY`)㢣c#3łGpr+Ku.uZ.c]%,[nRprz鳗)wAo薹dtZypٸpDa琹HGEڣ ,4Li?zҳb}V\߃1S(FeѫY^Ԕg繍%+ƔǓ02) Ir|O_[zَ$a"~˶w?x=n]ɇ֥yH<ҴpS\{ܫǧ/CyŚnv~k%=*sB:!((OqJz\!7 =řuA0:+OSX=Q>.==3#2_Ÿ {$ֻ'3;$=+xFik>Ec_BI)l_N.ymIif2ˣG;{M,CsgQ NVK{@J786pʩm(04 rq-%y=>u괰 ௰sAo݀j|-Mp zHM}kY}S\ 5OͣXb{#S`}J}M PwÀ1/kjʅc`>qQQ\?)矈|oYd_f𹑰}}>^.\9`g ]mi T[{J+P LMk"-֝Aak .yg+gۗa{%qFG5{>‼`Tc啜iQJ?ez~A6ǷdgN¤IS)>1&qR ~1˓^t5$JM][]g-gϵzC0ȠΊW8w VH,λUL>w5NaÖ9FE!^,p>_UJ;Xe85Xj=ޛ&8˞jL?x5E/P LD\LNDHf %/8B>bE:Gui42;c^y~ܗNQ/QvēȘiC}5;2&aaHiWf'/37?55t+)mEQ֔]loբX&hs.G?[=`t3REkp5רGs/a^m5]p,+9%.U=֥pT-ۉr([dK3;;`"d7yq߻7V|ĩj|惯3sAD ;oN{*Cs.CȞ[5`./I??M 2% +@?b̛2fbWbI!qJljKR4Tn,wb1clbzȞ/ 7Sr?PH; $b!gx6wG(gU!KѝIU@{xN~Zeԕm F!^$fBLkwO.NBZ|N1Eɲ>f &鬀EMWWE֫ei#/ǵu~5*Hz  WH6P <pGhyBCyDpK&:pp q̶`c%2Gg;D|ݱ_uBaGvs`Oqkenԧ\ 7O9ů)DD!j|뾀qqBb|)<m*n]YO5.OHH*$L+eHKTBh?Gd\wT_"Â4ۍ<2L Y-<84~%5Q\R蛻iu*ΫxŖ-VG!=o-z{Fb9ԓz\pqj8C6e] @*.γkj䨮Xͽ:}L.ˆS-^k%Vi H I+B`7pSx^l_˽ o:$8\,9}8XڽC%!y6\4ߥg.1f@8Rg/_c. JV Zwg_nzoj}fBusi78e)5=YI}w@惯3/C j,Λ}yP|)%`w_7 i\1cIH м+ÜIb}vR7r?t+6qSԖbR.k}9ejRu]L}^cU{T̠웺;l[ nuec=}g2b"fe4cp#hmD6'ʜ%i+9oEb˓0g L{(C7~/^nSgNTzc8pDž!7Y='q>%-O|9wCYSp)筽~Zrayֲ\,?}N+Ś#1~fL=6ve)N>=?M΄$@$pEhŏB+5;,)Û۩s^Wn+Y+Ћe;p}ϐQ^dl餚R WLLLjQ:.J{/"EK~wy.মkQf9v'ܫ̨n]iV\zP[6ro HwJw"`t~\7g?o@Yq>^aRk?ੜ 4e&Xa?6{mǿ*4|'0ytL3W-Y> p:M v%Q)n09n?f[J! j%-˰􃋁tW ⥿}='t?06B:4 ŢTfg gV:-ٛAR(G-aQy֒\T?G^ۙwo7~;[p#X2[Q(_b@zY cބݬ*-[`+12J8· ([ N [uN!7ls;JOƒ"g)*jv0%XY9e  Oݏbw{UFʧY觾3wӢcSbrVg4Vhݗ-n9ԈFRq+0.Y˰xXm]RKK2_/Mlk\L6H`&lWB aQ<;UR&bfLkB>|gSxs>{LXQ0RCdxzܞ78&d7iawI1G]ƅ@?\o7^3KtXtC DĘ!}m ;;r8;ƾoB0Μv c\?-sbE惣4)zeXoyԮ~7^[w}y~z(1!i.$=$un|NH'O|2'78#~2RɾJ>`S+wiB#"&%V%(UA:u̟ E!6&g[۝c>_\Y:Q^FJT:=5jd䮞u19&~QF"̊n2ssM>^$k]y+bw\hc+bHr3OȘ%NW :gsŧʥTJccxAИWn!5#͍["e~KK0!ٓU*Gi ƪRB=7-ΥQ,{°cbTO&Θrk>䰮+vc1NI"/3gb~r\(g;ġ+}@Z${&hPʉsbv7{r0F/Ɗ.7 s3r?CN1{\:q8㷈 V.wiDv{"3i͝%%eqvMwJq߭; 󸖒̎i lȝ_Vi3Ub7ѡ(a3O~\P{ᯛ>^ |]֘}zU9̬¥y[!Oü76\{vZc 8Zd\l¤a|߃]s-]Ɛ ԯgs9?McnjGŁPZVCpc"HuNNw,YƼw:l+v :<=b"7(|G|S#dQ.Zh`͚iCQ 2%`xѧPUy@/Z;)v{\*>ԥf)׳5mqL|E MIX=EʒEƞlQ!5wX= wI{+0{繤J 3~ OMFա#8z8^EqW Dvy -d5x4 &^2M(T㹂xlmC{A7^vVxϮP$‹pq]:x,eC9ѣYb+ywvEG>3vǷFg3*6רW6&y:Ui>z?|?>{I ):i|_?'PZq E>q>8U̩.Kc{d矂a /b?? ֽ11B>Zɽju+D݃2]]2t>yI$@>֧\ireaȮ$qX4 ƋdFt˄,P%KN<:.KT˚hCtt\Q>Tt*ѕ"<<\=Bh[.9gy+WMSwf#M2Ed\ըV,pD!$fVOCmֿ%+ 5%,+xIYoꔄŮgsiA @tL bU"ւ"<ʙr "c04110.Z>8Y ウCnPII GiHUPSNŔezfɃϤb<\)C1O Ej (}Pkx~h"C+ZZu ~$ v)J=țe:DI\4+ R CmGUsF}kS\w?3pkk aw+}XD؀ qH\qB9-6j%d^ĻY-^x8ZUt-;]n%7BxKqjSbL8t{wٲfQ?+}ZYBBLl;bKJ)}$̕PmYM-}cU_%{mp'Q߄;Xܑϱ`hH_ǵbf|Jg+w tb݇~GE9^]o\b>QFM'b]ww֭l)"9kbhx䉢!3MU+˺gS% _;n*{sߏO@B\7٫@R;}puczGkBtޝhmұ /7䟐cdRd?0cMWXW썦 M*<˰]ܛ̶09mS%XVXYGؿXmwݺHd]:Ғ(=oy$R3@<9zs6WlRqT%(xު^QNu'Wm2|7|=c_bًczd)sc!2v.olAk>}'N'*>·7//"A&,h n#GOoO6ɉV7]5h&l$p~ EZ؉uJ[SڽMOGDLs$%܅>?@XN\ayz%ϑLkwɏ;.70͛/PH{K7ذ/Xydo=5_ěa6鉋^&X FY;n[ Y|uR,&̚RrLzWQrtFo 9G|-~O^bٲeeKa%o6Vg5TV[|$m|lXjkܫݭśrǀXB%s KѱG޷[a[|e:;U'foT%qϼ?FcZlA$>O([C!hK1ե,{B"VQ/kV@/k    cE%^]80ܵ^ܔUa1lAt{b-7 H bÓ5: h`q*c_x ʩt!S%e N︳?4(-x LrY7vY/"Y  ]{ ]W~9L~G],O[bsYZyW'L]D㺆zb#.df   8>_9ch}u(߸#O VD\db Q=:qي%xqUf#`!IJ@BM; ' UCo NOǀx'sͿ\SS/9hɫ<%Vm?De;p _ H~t$@$@@ Ν?>uF+NewQz1Z7"~Zبgĉl9j4Q#n:gPR[) ]d8*9md$@Fh7ih?^/f_J^[0i3LןM'֣ K+o2       ?W`IHHHHHH\T\}˖ \O$@$@$@$@$@$@$pnM ~rB+l{'2/sW$@$@$@$@$@$@$@>h(,[k ʼnJ ڕ3QaPF xA*~GQ6UE yi0a E#mۇƤ$@$@$@$@$@$@$@$`@;U\^k% m/0Y?\HHHHHHH4Թ%cw6jl$       @;ibdR1YaX|u,3;)NpVkI~ vw@XT)_'`]$m+u >g`!      h)W(˼$\%ay d7քyp=#;GO\^Yo~/Zʲ '\ HHHHHHH+Ol;~b"5o6(QX]K2Q,Ջ+#evQBXޔՍFSEJPJ$_-Y_+ &a`X$Eʖ?bqEk}ΥHHHHHHH?=D"iH@IDAT %|V-~L.kr47])}ͿtNGQa`3Aq<=E2IU?ʬ/|KU*g?׳꧷,#@z>֧$@$@$@$@$@$@$@mMg^E10gg V2OD R.WoBhQIqU(}|w=jU"9eAQATv;_       6HPDjK%4)> SS{<Qܨ(vdcRO>r3W?z鬌|O+IHHHHHH(C;6*Xi,^L9%کí~}'ӫMo@k%lB(Ȫ |ϠF \#5 P?e,jźZl7Y+-OKZav4Ƚ92*a$      h+ڱG^kg(|KiU4Fl86|6#M|ZHHHHHHH>;Q 9O^xgSDRm)H\nieOR1|24Fք*`G)ske&DLB$@$@$@$@$@$@$zVPM#Y&j2 ZUGV"˿[w>YLql7O$zɺLMol3/;ҭPb&,HHHHHHHZ@TYɈoYF(_,irw:B~\~|OV[%- <v$fDS>J V-۵ho2WӕS       6GAis5*`)[WF`[@,R6(LV)[K7(z3Q,_C"H\jqzJV:,wtsE~7DSQ. >rXEI]"a곕O       6G}Z8FyD4'<7DQԈBc[butz϶]M'O@aQ>HKjk>)OH$_)EJ;(VA)~<ԧg       hs1bUc6}tdԨQmN6!Y7߈d(̆G懢9( |ͧ Jt(^~QPnn3,$      h~.kv_ YKX-[|ͧ% ʌ$@$@$@$@$@$@$5M@ƣ +z+B%u#(},.5#fh K0l?z6P9KYIϢ0Y2#E!%|:%>|0ŧHHHHHH$N? XF6ItLMc $wm;&>9 oC_w5)ULH:(G a iMM@l WR6dEN]pc>5״DS4|ILI$@$@$@$@$@$p0|m; ܳmץyM11ȟUym9HIH ACōW(bgoai SS4T!oXdJ{}MIHHHHHHS:<ΝII c S ۚ1/Irk&.)Ƴ^`|ڞ9&>Z}MP$@$@$@$@$@$@W +rW~0*H6$xܬ.ŗ[ڳJd{HHHHHH&N-~ﳛzt4Ojn( JJJ <8^>ksД?ɸ/7:Ha8Ӳ:ڶ3%К4g/M (|r, ?틍OƐ;#&3Z?ox͙a2k)#c򃸥GWZnU\\HHHHHHoMV[PY敐7SZWQMHw餇H̍^lj^գ s?*)u m\XmAx }Y&D]&?>YK0;&R⽈l?MĬ_>KmYH?X]:ۓթtٞMU9 Py3De+(! ]";X-[Rimȑ3i.CڜUOuu-Cs2fQ4T.4Aa,[hiuz>>oBf."YwEٷ8Q#t߀=(S Epr+k?S I%H2.6NJO-V* 9w=5]0姚ҪzNJuPL"Ezʮ WyM$@$@$@$@$@$ph׻z sM"      +G1ت-mfh5_C llYZ4ڟi^q6z311Q󏤏n/ť{!7^3_\`      B?񸭗ew=-.++"Cca#C40Qlq3.b}o9Y⤆&2<n(cSaOO0-4 &$     vODVFG_!TcB3+Y$dJ?xA~hHd495#ͭ\5C }:&L@^l CJG|Sgv:M &+?إ1$۵݄YxuC1p.i-K!     hLiik}RSbSU(۳{jv?~Do y-IWf xh%oۍcTT` s12J{RE"Uc[/ ^ix(rc f H AXTxNԅ 2BSUy`y?s˽Bc5?Q[a"c`>{`p ; gAYvnn7E!}trԨQ6x \gOaߎϰ(U|cе:uEB+$E;,T[Ufi %      6O*~?TQ( x[lذ'z qIX008MYlOج]8WsOA@ xReD Д֨Yf;3V4:8orIn^&I-L-HQoڗ>Wp]g={l}vnx;UsGI&`L 0&`Lv"L ?}ԯFCt*'Ad ġGWZfBqgp;QGFIOގ o'2&`L 0&` EIHK=Fh;df[VIK=z_\[V'EqIO`L 0&`L 0&",=~Yty2 ǰ. 1Z"֔bh*jPоuue5R8Z@֦Mʘ^vy7NtjœQp6: w@| .xdg`L 0&`L hPt s^|gCіFy9‡z Ş"SU :tbeuyxkvIQxb|M-QOBF}+æ6w9cDK[ E~gL 0&`L 0&e+DSi/j_6cDi3jC.FzQdDR^uy}6ƯzwJUR *owСڀ O2xݠ\ٶ-|=[ F=FQ0(1 2ĩѾD.wቧ=֚d1toJ%WC_[e׵ۜL 0&`L 0&exTWˤı ȁ~!mZRKw2M5C ||$S]U-h2-[֠^-] QSv6mf53<9 46)_Gibk#eO ~|~k[8<76>BlR]y'>5W~uY8)`x"& Tp 0&`L 0&pg/"*\.25"q0:kOչC:`]!gw+vIFI7;  Ř{{UjKUCz1-eTM:~qLGl6ξ?fe'}Lqo|/WaL 0&`L 0&p{pzMi`< ^37b߆f"tT{ƫYWFcsP} 7I߮h+jBN:\!fDqG;bNdfw&`L 0&`Li4?2H*/v]㩡żsr`#R~誖r7jߐNb'trhPGy'z=d!YKy9<[(20T ~8lV3`L 0&`L 0&& ?*hNoֈ!zܸ36,Pl&][YB|P,X|FA车R/ _TS&|`L 0&`L"ĦX hK(aQ#8nSAW!Zӈtkh'\,6E5!C[3[ՋTvi[>:p.`L 0&`L 07 4Δfhpa՟1[JY^t?y۟/z5GMtwN\Rb0۳zII}u1QW1Ws 0&`L 0&p fhtQs44L!ꮜGiŸV%Y|:ve| {+[_t᣸ BC5w^]-/i`Z}ȋ̮r`L 0&`L 0&~~,7X7p6eܑyU.c`~thY+WiݚpPHRơzofa33x-8{7hqZ;_ay ޻;=GcҠ@˥B:BE+W@}6\ҳ%.mGlՐ25Ϗm?C |x.t_(A-<۾;t 1Hws ><% AY' rl'^@i> 9`L 0&`L @މUU 7~_zbz V}Z ܣk+bx>7ן=h]W'yʹlE;5p􂯯AkkH xz;?|=v 5v٧]:hG|͗ҧd+/`h!ׂ˨e>aL 0&`L 0&%bȑ:u#sOǎξ+tl7P]Z_~؉ֶj8]Qq4=ܰN)wEѮ_.)TlC!/K.`c㐍j| 0&`L 0&hDtH1lBc{s4}4sH1kcNonn^s`L 0&`L 0whKA`L 0&`L 0&@3&fX+^"6etg ~e^ND6oX9/M˿ؓv3k4v FhD?aI*1xJOV˙_A!< 7/u;xz{Ã^eO7eQOqNSmqL_RC?]7ަj) SEh'ȶ*J{@zCYN9I } mB4y\e\M D.ϱ,IG\9l鉴ٮiBok*];/ .ּpC!x0>,p>r:ɤx{X7 ߶~TaǹtvƧ"9䙜}{G ^faM;blJGH;3fna"v{kyDG뢣ct gꖮLjK] kgvNHѭX[<-$& t9tF_$uhNQ}Ҷ+4tx\C@6cQINNZ>M1[?_t7*O { Z~k+#èN3';.%`mqYKO1W_l1KX:B߼b]S;?VJ,sk+Ǯtcϭ`Ԝy*9ou17>ֳgܘ=XE1PY}X?-}dafUhCDs&  mxӜG{Eo:|:Omqu%O#KW5A¯E1w&1yhPl*!UhZ+{ 1z@o^il0$-1CA_E݊LtcƠOnƵ8]\.Z.TCQԾ$I m[<Fc0h|Kh|.Gb3+**Prei,v=s<3QFE099.`}*ưit_0~z{x䙸?;@3r HN QhaEQcaT=QcthGgSYl|wthڒ 8q$s8w"/~Q@ܹȖ2;}<:};yofF m0ֳ1 I]Uڸ%fbcыmcv2e˝>4s{dF'Y<8Z[ щIbP.B J.O{vOߑdo몹)ƽ}û5X)4Ȫň'LZTl=p/tZ N䟦Z#<} imZVJsU6.F=1}z].̶0“s%ϐR*:(3ݐ|MJ.WC_X~r1Z_ _nԲ&-ҝ=uyݐh.䱐*= c|RnM|.+XQ':Tdd8gB}Ҡg|GM_;xB,m4fGxe{HGoSI1~r -:]yn.1:?/^נкs,ʏTטM9S5Ԭ<]QȾd,E}5(X5ud%5j]^I5A5맠kO˧~5Kp/jFV ?8\Vp^C,\kL!ո|V鿔ې4<ڨGё]C|qXqq;pS_rLƵѯ/KȡC(*؇> U3*ˑbx?K8^T*gҍH}&~DGa#8^X q x(V= .A&(yk{F }W_Ο?EQR-5.w/ee"Fn7+lqC8^Py `.1)l} Rt&l?R@}i'Gyڭg̓Q8ʐ 8s"hZJdrѓbuJ /)DvQoOō޵Q!Sc^܀ok06H(r I6-BQiVTd-WM 2.%GJ%k;8 ^cҚ4/Lԟ`Z{S OU/GWy.!on޽G pZzxZ ))gnTT;;y2!=]u_0lzRUJL˨0(5b Ŧ}_/E.T_]spѿKk]-J.ǥWQUge̎ų[_i>HM; ;>Wqq7ԙ^ʒN|r_y()^o7'y^a'7p1 ȇ $o*M_ij߾,zw%*bD+r\\{O>75Z;3 xfN!,jˡ 2Hߟ5}ʲ۫gIÕG'yrK愾;>#Źyw/<8.,tK ?w]x}$Fhܨ@)0u.Rܩ cH~?fG{$6 8rxQL1|?4vQ-?A/NǰG1e,Q?i1&xISg`֌8S+U^/EEi)J7 \2WUHz"wcQMŃxmK\.Ҍᢑ`=iX" sN t,␱`,qFϤg;CssngNԓ}zftFţms֢/vAvHĨIpِ[-K'ϾC-?טUԁZK1;^bF|h$7;b&gsYwIT9!jыEҴS' hhC:'~CrqwXti Ahds:V= yugw =HuC^4Ϋ&%?iR[aGKv~pY~QhC[ Ӿ})r 5u7p  Sk7P[s W7QG+ʏ}I-бK cVf='5ਾ(}S#$E/ZOFd2(cإ>"Cp14gkG[ի:l<ųQ %@>qtV¸/q}V-/X ~cѸ7FT|$XՠRYbIAv aҀi2c],eغ!ӌsI=k\/ZI^gJgspHO}Ϳ͛/5UMQ:MX0?Ex7U)Sxd:z뙾gޝw:3+]?vYwvd$aͼJ hF_}dOiQE_\Ǹ ʧjEO(e'zq2zO{(?AP%qg/]',"Ĺ Р+Ē0Lzs E4ʑY}՟*?UJ mUnC⫙JB| py%?lڣOK-_)!|u5Tq}<q_7a?]nh1H FwK|`^s\gv̓j[i:be^1v OE~lPɬNkw0mcܭgjH`%E3{:vQ9`gS]){_\ʖ}EuXé>h=ˣ^7w`ec1pן/Gݝ{Fg(=-VJ xL QrJ}@jSUGߧ {_/<3x]m5T}1Jen ׵YR2y—b(34m-3B;`v;?nZJyrΟݛ KK5Ld4 ճ ^Cav8u$Qy_AoVԗ_Y>}$U)Y?UU3fC XCO.F\Xsu2pPM˱炦{5P,Hu~F $m@cLl6 5!mk<ޛʬQctb^E:> -fTn3;y&4'Íԇ:gv΃ׂpן߇׍ܭ$-pw>B:#%,_\!nx?u+ܯgk8yo זp=w"wUf(%h_T_Ō?F zruR6NT7K0; i1}|A#2$=Apy8E})gxyG#;ncQ<%IDAT澳ζ N/pb7+ņL KlV5Vi׳9ÕyEՏxa}QFlOQ%CX)%&\Ÿ]mg> ~w$ye5s$ƽQ?.P*z$Z^~u02*F}11Nmz͖;9Cqad@UTՒYxz!,y/zt{Qny}[9N/#äamZ)&B4SKҎ=\cQ};>#wo䮘Q%KF( # cP|{6jꉳ ?E/חkVxN!wtl[>[.@nL!FQ^o TsegypkjevE6ۇa\n!|]LD|kEݤg j8'6E]e.'R3 :}i6x).V|0tרT$*1=Jd"S!0K7|{is78Zy+n_LZkj_9žN ѳKr!va)Ϭeh霾Ƚ:ʳ,X \b625߉ ~GdOIȶvSFh,o noc9 ?B8!'7)‡"ѯ[ZS3ZZUJ1e/O ©5Me򞊋a͡G6q7J׵ܪg^[{6tZ D|KjL=sw̻Ȟb+Y[5VuvKi#;~|J;ЦiY.yX=paG;z@]VUL"gZ2I,~O30?u8W!u|rF _4ݙBrC⨘&гZ7Ңd3?1/557t/5?;//K^FT۝wS‚1vp@调g;oU᪃MhK=5q4TQ^'.tQI2;;Nqy=Ғ;KwM>>GlGJ_(}qӁZ2ϽR6iov=4ܹ@%?ٌO"L#ۜ㧤e=<46aW1W}iM\q/.W 7(b%D'czlƑ'e=9huqK3 h͕J o)pT O\A=6,y q.x-0&ɉ U ?GJs09RmPT,wR,~V ( P*0ȘΟuٕ󐗵 m<}//Ϥ(6ϓ/ w Ťm}XECzf&ky<zRĵbܣya$_6R9hh\B QN6h43qã'ũg-"3g2{)z?̴tާfYtk:nceɝ簞ee{߇F3@vM›#M7$1K3܂c*(_ 2M[gJ|&}e|M/H\iӲRy _w75LT=8EbuMsƬV=3M/ؔ#4 &bOg>O$]>|:S8ٲOG9, L} _GOأȱrBW'.E&QXo31f(8SZK_ d~r9&C>oK瘞75RwulUGD%uwT7PQ/oRP~|s -U&3MA Ɛ'CǹKո>rW_bb?b&S=/lVfwqh\*3qg) c;twccKXa`1PH|*QrQp:!pb &O}Ɨ_> fzz{'c Ӟh:˜v"6~aTnҗmzX9mbkw3q'V>CAN,-'imڣ3A_NXgI#.(? fQ=+<ݩgj]/\Ă/x8޾ =D#bJJ?ճϐ{1 A,ǦQ'Q!$ȋ^gnѼ[їwFrZOuNg^]21\~2C7;3bD½(2(fUUmڳ"v1>~rn:P+nӚYf׉\[K.2* bvy4FHpoqWBɥK(.z/ VCǧ4d zҽ8"*=9(EUm =!1L29LVᛷ&cT1Ia;E-H6ɽoZl|dzU$Ay8IKa=[9s+xص)Q7 -8s1jN;/F:ӨN/0:Rx曷sBQo^w*N">kZG"62ҳ;ҧd ;yfӯoO o8}:tBΝкYZpڨ+{O]h}dK>e,+ۙ< aBQu?t =^i7߁v SW\8{ǏÙ+g ~[ݘ-=˔ y|$zŸKX)f_E F7o*y>Gb]w rRϢ83>[6|;y7noܸs}Byz]<::0٬<]AQ.5&XɬnC02C[Z0mƃHv -Cb`p !O 6[!K:ןdhV*3H 7S_Z/ƬUh_S)yxX(SO6_X $}Qt o[.; 4lsWPZ >ؕf" mzirȤTt&  NBw5?_okq qfg&`}14f1%f۸/Te3_E\ʻ8.0(6F b9f֯f=S1u(>XϣPSq_/mPg֥yzmKλT9j9myE 9ZXfwF}ꯟ8,BJWmTUZ~*FUdE=E ђ?;Pn 琉Ѧgr\HyKZf8~O+JG0HxKOt #qwSv;,4F+f{ggG"lמR1Wde,{S-ZFiM?hY2b>{z#W8FZk9gkJѧ>]: )ߓC+OŨGy5bD5TpQuiK?dӽz!x01SMt(.yp8gyKз$|%I?r0=4`E@4ȑ#ujΝ+;Vm:z fa]K-*_An:[k. cѫ[G NcVX8 wŕXQMFc#{ѯ5r !٘titdĄwDYXaRzmH;6J-JD`G[F\ҙ\u&OOCESҙn\(DΪ Λ5=SRZꧥkywo)i"<.aL,YiI$~y.2iL;pW|/(ʳ(#W'_phg=ϓ9&ܭ:ZGͰ(=\,6`gEc1^jOœ5;|F˺<9DMY{Cq }%]UxrHӳ.ݻɲ!Uz;}3A\έxeY(R++/S)%}kgElo=j䱖gZ&gj@.Y#ӏ.//FP;>Ea'1WWϟ= wys6rkغq$_ҙ_Sq?~e?b7lޭ^.A^',6 S~"a˦m>+vGʉ9!y0]8uN2ϖsA-v>6hz jFÏe!8aܭ۳gnÂ1N$Gxǎk kV*C8yjƘ kmLbMFmQj};&If䩖z^zp6Բt?g>̄܎:/޻v|>fiɉV<-m  hf | 0&pRQouM[>sqؼrE?.g$/ zL 0&p[VO y`Gm054{AAq,rC`0'b3 {'/@3%`q8ZOCӆ"|t9t%Qx T+@2W/0H)p8&k("ҠN]A @0q %[ք71&xqv\ 0 [: ?6 }; KRh Xq5^]>j.fL@n,lBg+m8ZJsZԿ#ze\Ge{7|VM/ws؆ ]2n-Sd_pS%k^Jv9ؼF2&D7Qܹ69rXp҃I5 J7le7& "hw^ Ν;l3s[<M3=Nqp:N]CA;x\g ;nH:G*_~o,˸|nFLӦ!h^) L D& 7†7@.`L 0&`L 0&8sSP>`L 0&`L 0&lqd 0&`L 0&`MA ?MAdL 0&`L 0& &`L 0&`L 464u 0&`L 0&`n 7@.`L 0&`L 0&ԹO&`L 0&`L ~`L 0&`L 0&@SlN xI3:p$8 z{Hv!0&`L 0&`My~ e Щj_W"x2v ǩ22Ki 0&`L 0&`64XvP3oG kŵ@ ^/|iq1f)`L 0&`L 0&Xo,m,3}HgY@9<摇L26`zLvMoj6ҳL 0&`L 0&@pspgQ}[8{DZ%:Ɛ]ka Z|0&`L 0&`L@zO>zO_(VkDtr3׿ 5`$N9H5ZOI(kAhѪVPi@V26lHVjcYLm~!jh| 0&`L 0&p;~^tUJiӍ8vݖ M4au#g隟xH/cH} %d5dyR%h9aj}M^bdZJoH&c#RUwF*#/Nzdz1j-`L 0&`L 0ۖ<~%<[:n9?iw2ɦ޳6d#/dyZiCKp7c^"_Dc2xgs ]'xPϩ`Lc5"!SҮEkt}to L Aީ#2&`L 0&`L (δźlx9pxQO!#CI1D/VuTFNaG8ڪo)?vSL H26Y;V]dQٔ|&`L 0&`L V @KKJL&;(@ )֎OeÔzBǒ*_p/29 Y0|`bsB6"& 0&`L 0&`hܥ^da_i;PZ:%꒺%hc6 ?Ų!F‰"/V B֌W.䡶^hmok9g1&`L 0&`8X ϞɰR,Ķ*/]vd#,o)"r)A_ + ArӵRjζ-ǙL 0&`L 0&0p'b,X6AA]><}lt޸C-# /HX̡w! giI2 S*$L 0&`L 0&NyO[~C[=A xB a'8&ˆAAEsfA [}> 񓼭a@ r8 1#`L 0&`L 0&ppR/ v|Aӛ .eooi4h'1q u{ۈ~vkza`:<2y7.;&`L 0&`L ?$a ;ZՐܼ͟0 &8[_ *!J!u<"=wH6A1he$#~-V+I6LIL 0&`L 0&J^BӟqCma!#_z{4~!c y 42 0ɘ )U#Po-/ Do F&u~A;Qۏ'02#ل#vBwS?qZIgL 0&`L 0&B5|j/dpDUK]%=_>D;h' 0$(f'x9z6N/.g޴m3/d>Em~UK*􁊔8`L 0&`L 0Z9ReUΝ+;v 63VdBnf*fj*BcvFh? '͇4'%x;?!}fXET?~S0&,˲ѧ(;SX"gk&V*Y`L 0&`L 0&(Zgx)fi2sWZ@=\-<dr)7]j!ϜlRO@GeW,ml1HG9ǔ+^ڃ -z.fdq,`L 0&`L 0\je=-EK_i?OD]y?ʭWjLl) h` fëfК1&q hZr?/Z"i.`L 0&` ~nB4SCpߎ@ C+6R0 ?ށm(,!K~—z0vmDL!wR[]I KDߦ_7CX⸇&qZ Z nd Lj:6!<6$OyE]Ws\Mr0.Ĉ$\0>2"}zHo6^ UtGDP0ZN@흙{*C`1KBpN( ȇf&#g0&`L 0&0%(qT [ۢE Zl!ݲEKt½}bڄp<؟".dL!ɟ^ ]r jQUNgeK09{NhW/F'G2;I ח"}8SF܏$lBAULZ \Oۍc26`a%q4'|#AmHGёo9 @aV1a#ʯm(z1Zwr|6FSA 3ӥ+SGTĕ#OXۏUO79!ӊMHP;Qs?C1;`rl$:Ǻ |ɨژ0o;SWѦMtzC;/ `L 0&`L 0k\X#h;[O7?DQ{`G)rRW]`!ZxVࡤx`&ҙ*Wa2(G.\V- &,~B"kdj箛''!A6KAwg>?U*hK ʑYEg%>12[_GNa%=1zRU /{́vIE(ʼnHɫAgJ|z{|p:RW)уp~ݻW IABljrll$OEXVH^m#nF <3ڄA1q6s"McJAbmc:[ Gvb\ 0&`L 0&nec {@:OݎwM˾k#7 /:`bޑ߲h;w{'C)Ws_"wo,r!JaGbV: ?GS,)ߐ] yCRعmZ1Kߙ`L 0&`L@#~qA}rATPco/,ȿ(~#6.쮳B֣䊴@2PY;r qŚIu9Zg)_1Yk+PZJTZjÝy^~m#ai2@W_i9 R5N2]2Fd:üldk+rOJNA*s1?))8~^ b'b=lbUu9`L 0&`L[zkpDJss'*Uc ~7P]11LݡCQ skqS ;CMa\(}+hKxidW(t硪\\=rGvMTϳ.kERY Iqb HjL 0&`L 0&ĝCtDkZނ~p LOTb/͍$u3*])Wv oHߣ@^ڕ㧤%D=0TSRmn W*q#/+WF@}g]x6j| ]5~,JBܰIx7xw1Qq߈J;<(Wl4$'ZlP,©h疒k7hr?@:0 !~jx"s=_?͌(@ P(@ PRI9dx `tJl cuL. GžOa" ; ٛ|F.Z'#25!`F>MŰY=#܍twg2&uy$ln(;ӥB\R+zq]?sc`Yf=&7Zˆ%#7o;"o!}7sK2Ϗ<>D P(@ PK@~ c|n6x.%|o*<:1_WI76V`mGXD8>*, 0Jy>{',zz D4$C+,Co] j߅}`1S^_]&=c w:Y3q>C510VU4ϸg +rҺ7cNVhU^Ю}`4bH=g+Ŏ_fv~a:oELR#LW?OP?v,3 P(@ PnXݿ/X=TZqE8{gn୏ӧayU5)e x KbKS[a"j,,Yk؏f!^ !(3~!6lGAVC+Σ5pXyg (ΙYa,6=hHf|=Eb9|Zj"`9X{`զ۸Z;hA~õbs L*(C |ijQ:KW\>GD?)R(`y9 [ JN$< (@ P(@ +$i ,p@~ cl<hooAS% Cy]i-:Gw@2oH 2-Mu8:\ǥihtޘEଭUԵ Cpp ð"1TGMo|b<(@ P(@~'+/Wԅ9}ۏ[5Zڤd:(@ P(@ P-Dzu?t~/OJ P(@ P(nUP(@ P(@ A~.Q(@ P(@w0EA P(@ P?}Ka(@ P(@ P;q"(@ P(@ P}P? M_@9IENDB`graphql-ruby-2.5.19/guides/pagination/000077500000000000000000000000001514115062600176375ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/pagination/connection_concepts.md000066400000000000000000000073471514115062600242310ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Pagination title: Connection Concepts desc: Introduction to Connections index: 1 --- __Connections__ are a pagination solution which started with [Relay JS](https://facebook.github.io/relay), but now it's used for almost any GraphQL API. Connections are composed of a few kinds of objects: - `Connection` types are generics that expose pagination-related metadata and access to the items - `Edge` types are also generics. They represent the relationship between the parent and child (eg, a `PostEdge` represents the link from `Blog` to `Post`). - _nodes_ are actual list items. In a `PostsConnection`, each node is a `Post`. Connections have some advantages over offset-based pagination: - First-class support for relationship metadata - Cursor implementations can support efficient, stable pagination ## Connections, Edges and Nodes Connection pagination has three core objects: connections, edges, and nodes. ### Nodes Nodes are items in a list. A `node` is usually an object in your schema. For example, a `node` in a `posts` connection is a `Post`: ```ruby { posts(first: 5) { edges { node { # This is a `Post` object: title body publishedAt } } } } ``` ### Connections Connections are objects that _represent_ a one-to-many relation. They contain _metadata_ about the list of items and _access to the items_. Connections are often generated from object types. Their list items, called _nodes_, are members of that object type. Connections can also be generated from union types and interface types. ##### Connection metadata Connections can tell you about the list in general. For example, if you {% internal_link "add a total count field", "type_definitions/extensions#customizing-connections" %}, they can tell you the count: ```ruby { posts { # This is a PostsConnection totalCount } } ``` ##### Connection items The list items in a connection are called _nodes_. They can generally be accessed two ways: - via edges: `posts { edges { node { ... } } }` - via nodes: `posts { nodes { ... } }` The differences is that `edges { node { ... } }` has more room for relationship metadata. For example, when listing the members of a team, you might include _when_ someone joined the team as edge metadata: ```ruby team { members(first: 10) { edges { # when did this person join the team? joinedAt # information about the person: node { name } } } } ``` Alternatively, `nodes` provides easier access to the items, but can't expose relationship metadata: ```ruby team { members(first: 10) { nodes { # shows the team members' names name } } } ``` There's no way to show `joinedAt` above without using `edges { ... }`. ### Edges Edges are _like_ join tables in that they can expose relationship metadata between a parent object and its children. For example, let's say that someone may be the member of _several_ teams. You would make a join table in the database (eg, `team_memberships`) which connect people to each of the teams they're on. This join table could also include information about _how_ that person is related to the team: when they joined, what role they have, etc. Edges can reveal this information about the relationship, for example: ```ruby team { # this is the team name name members(first: 10) { edges { # this is a team membership joinedAt role node { # this is the person on the team name } } } } ``` So, edges really help when the _relationship_ between two objects has special data associated with it. If you use a join table, that's a clue that you might use a custom edge to model the relationship. graphql-ruby-2.5.19/guides/pagination/cursors.md000066400000000000000000000030451514115062600216630ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Pagination title: Cursors desc: Advancing through lists with opaque cursors index: 4 --- Connections use _cursors_ to advance through paginated lists. A cursor is an opaque string that indicates a specific point in this. Here, _opaque_ means that the string has no meaning except its value. cursors shouldn't be decoded, reverse-engineered, or generated ad-hoc. The only guarantee of a cursor is that, after you retrieve one, you can use it to request subsequent or preceding items in the list. Although cursors can be tricky, they were chosen for Relay-style connections because they can be implemented in stable and high-performing ways. ## Customizing Cursors By default, cursors are encoded in base64 to make them opaque to a human client. You can specify a custom encoder with `Schema.cursor_encoder`. The value should be an object which responds to `.encode(plain_text, nonce:)` and `.decode(encoded_text, nonce: false)`. For example, to use URL-safe base-64 encoding: ```ruby module URLSafeBase64Encoder def self.encode(txt, nonce: false) Base64.urlsafe_encode64(txt) end def self.decode(txt, nonce: false) Base64.urlsafe_decode64(txt) end end class MySchema < GraphQL::Schema # ... cursor_encoder(URLSafeBase64Encoder) end ``` Now, all connections will use URL-safe base-64 encoding. From a connection instance, the `cursor_encoders` methods are available via {{ "GraphQL::Pagination::Connection#encode" | api_doc }} and {{ "GraphQL::Pagination::Connection#decode" | api_doc }} graphql-ruby-2.5.19/guides/pagination/custom_connections.md000066400000000000000000000146431514115062600241050ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Pagination title: Custom Connections desc: Building & using cursor-based connections in GraphQL-Ruby index: 3 --- GraphQL-Ruby ships with built-in connection support for ActiveRecord, Sequel, Mongoid, and Ruby Arrays. You can read more in the {% internal_link "Using Connections", "/pagination/using_connections" %} guide. When you want to serve a connection based on your _own_ data object, you can create a custom connection. The implementation will have several components: - The __application object__ -- the list of items that you want to paginate in GraphQL - The __connection wrapper__ which wraps the application object and implements methods used by GraphQL - The __connection type__, a GraphQL object type which implements the connection contract For this example, we'll imagine that your application communicates with an external search engine, and expresses all search results with a `SearchEngine::Result` class. (There isn't _really_ any class like this; it's an arbitrary example of an application-specific collection of items.) ## Application Object In Ruby, _everything_ is an object, and that includes _lists of objects_. For example, we think of an Array as a _list_ of objects, but Arrays are also objects in their own right. Some list objects have very fancy implementations. Think of an `ActiveRecord::Relation`: it gathers up the parts of a SQL query, and at the right moment, it dispatches a call to your database to fetch the objects in the list. An `ActiveRecord::Relation` is also a list object. Your application probably has other list objects that you want to paginate via GraphQL connections. For example, you might show a user some search results, or a list of files from a fileserver. Those lists are modeled with _list objects_, and those list objects can be wrapped with connection wrappers. ## Connection Wrapper A connection wrapper is an adapter between a plain-Ruby list object (like an Array, Relation, or something application-specific, like `SearchEngine::Result`) and a GraphQL connection type. The connection wrapper implements methods which the GraphQL connection type requires, and it implements those methods based on the underlying list object. You can extend {{ "GraphQL::Pagination::Connection" | api_doc }} to get started on a custom connection wrapper, for example: ```ruby # app/graphql/connections/search_results_connection.rb class Connections::SearchResultsConnection < GraphQL::Pagination::Connection # implementation here ... end ``` The methods you must implement are: - `#nodes`, which returns a paginated slice of `@items` based on the given arguments - `#has_next_page`, which returns `true` if there are items after the ones in `#nodes` - `#has_previous_page`, which returns `true` if there are items before the ones in `#nodes` - `#cursor_for(item)`, which returns a String to serve as the cursor for `item` How to implement these methods (efficiently!) depends on your backend and how you communicate with it. For inspiration, you can see the built-in connections: - {{ "GraphQL::Pagination::ArrayConnection" | api_doc }} - {{ "GraphQL::Pagination::ActiveRecordRelationConnection" | api_doc }} - {{ "GraphQL::Pagination::SequelDatasetConnection" | api_doc }} - {{ "GraphQL::Pagination::MongoidRelationConnection" | api_doc }} ### Using a Custom Connection To integrate your custom connection wrapper with GraphQL, you have two options: - Map the wrapper to a list object at the _schema level_, so that those list objects are _always_ automatically wrapped by your wrapper; OR - Use the wrapper manually in resolve methods, to override any automatic mapping The first case is very convenient, and the second case makes it possible to customize connections for specific situations. To __map the wrapper to a class of objects__, add it to your schema: ```ruby class MySchema < GraphQL::Schema # Hook up a custom wrapper connections.add(SearchEngine::Result, Connections::SearchResultsConnection) end ``` Now, any time a field returns an instance of `SearchEngine::Result`, it will be wrapped with `Connections::SearchResultsConnection` Alternatively, you can apply a connection wrapper on a case-by-case basis by applying it during the resolver (method or {{ "GraphQL::Schema::Resolver" | api_doc }}): ```ruby field :search, Types::SearchResult.connection_type, null: false do argument :query, String end def search(query:) search = SearchEngine::Search.new(query: query, viewer: context[:current_user]) results = search.results # Apply the connection wrapper and return it Connections::SearchResultsConnection.new(results) end ``` GraphQL-Ruby will use the provided connection wrapper in that case. You can use this fine-grained approach to handle special cases or implement performance optimizations. ## Connection Type Connection types are GraphQL object types which comply to the [Relay connection specification](https://relay.dev/graphql/connections.htm). GraphQL-Ruby ships with some tools to help you create those object types: - {{ "GraphQL::Types::Relay::BaseConnection" | api_doc }} and {{ "GraphQL::Types::Relay::BaseEdge" | api_doc }} are example implementations of the spec. They don't inherit from your application's base object class though, so you might not be able to use them out of the box. - Type classes respond to `.connection_type` which returns a generated connection type based on that class. By default, it inherits from the provided `GraphQL::Types::Relay::BaseConnection`, but you can override that by setting `connection_type_class(Types::MyBaseConnectionObject)` in your base classes. For example, you could implement a base connection class: ```ruby class Types::BaseConnectionObject < Types::BaseObject # implement based on `GraphQL::Types::Relay::BaseConnection`, etc end ``` Then hook it up to your base classes: ```ruby class Types::BaseObject < GraphQL::Schema::Object # ... connection_type_class(Types::BaseConnectionObject) end class Types::BaseUnion < GraphQL::Schema::Union connection_type_class(Types::BaseConnectionObject) end module Types::BaseInterface include GraphQL::Schema::Interface connection_type_class(Types::BaseConnectionObject) end ``` Then, when defining fields, you could use `.connection_type` to use your connection class hierarchy: ```ruby field :posts, Types::Post.connection_type, null: false ``` (Those fields get `connection: true` by default, because the generated connection type's name ends in `*Connection`.) graphql-ruby-2.5.19/guides/pagination/overview.md000066400000000000000000000014231514115062600220270ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Pagination title: Overview desc: Introduction to pagination in GraphQL index: 0 redirect_from: - /relay/connections/ --- GraphQL-Ruby ships with several implementations of Relay's "connection"-style pagination. You can familiarize yourself with connections in {% internal_link "Connection Concepts", "/pagination/connection_concepts" %} and see how to use them in {% internal_link "Using Connections", "/pagination/using_connections" %}. It also supports custom connection implementations and type definitions, which you can explore in the {% internal_link "Custom Connections", "/pagination/custom_connections" %} guide. GraphQL has its own [great pagination docs](https://graphql.org/learn/pagination/) for further reading. graphql-ruby-2.5.19/guides/pagination/stable_relation_connections.md000066400000000000000000000113611514115062600257340ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Pagination title: Stable Relation Connections desc: Advanced pagination for ActiveRecord index: 4 pro: true --- `GraphQL::Pro` includes a mechanism for serving _stable_ connections for `ActiveRecord::Relation`s based on column values. If objects are created or destroyed during pagination, the list of items won't be disrupted. These connection implementations are database-specific so that they can build proper queries with regard to `NULL` handling. (Postgres treats nulls as _larger_ than other values while MySQL and SQLite treat them as _smaller_ than other values.) ## What's the difference? The default {{ "GraphQL::Pagination::ActiveRecordRelationConnection" | api_doc }} (which turns an `ActiveRecord::Relation` into a GraphQL-ready connection) uses _offset_ as a cursor. This naive approach is sufficient for many cases, but it's subject to a specific set of bugs. Let's say you're looking at the second page of 10 items (`LIMIT 10 OFFSET 10`). During that time, one of the items on page 1 is deleted. When you navigate to page 3 (`LIMIT 10 OFFSET 20`), you'll actually _miss_ one item. The entire list shifted "up" one position when a previous item was deleted. To solve this bug, we should use a _value_ to page through items (instead of _offset_). For example, if items are ordered by `id`, use the `id` for pagination: ```sql LIMIT 10 -- page 1 WHERE id > :last_id LIMIT 10 -- page 2 ``` This way, even when items are added or removed, pagination will continue without interruption. For more information about this issue, see ["Pagination: You're (Probably) Doing It Wrong"](https://coderwall.com/p/lkcaag/pagination-you-re-probably-doing-it-wrong). ## Installation You can use a stable connection for _all_ `ActiveRecord::Relation`s by installing it at the schema level: ```ruby class MyAppSchema < GraphQL::Schema # Hook up the stable connection that matches your database connections.add(ActiveRecord::Relation, GraphQL::Pro::PostgresStableRelationConnection) # Or... # connections.add(ActiveRecord::Relation, GraphQL::Pro::MySQLStableRelationConnection) # connections.add(ActiveRecord::Relation, GraphQL::Pro::SqliteStableRelationConnection) end ``` Alternatively, you can apply the stable connection wrapper on a _field-by-field_ basis. For example: ```ruby field :items, Types::ItemType.connection_type, null: false def items # Build an ActiveRecord::Relation relation = Item.all # And wrap it with a connection implementation, then return the connection GraphQL::Pro::MySQLStableRelationConnection.new(relation) end ``` That way, you can adopt stable cursors bit-by-bit. (See below for [backwards compatibility](#backwards-compatibility) notes.) Similarly, if you enable stable connections for the whole schema, you can wrap _specific_ relations with `GraphQL::Pagination::ActiveRecordRelationConnection` when you want to use index-based cursors. (This is handy for relations whose ordering is too complicated for cursor generation.) ## Implementation Notes Keep these points in mind when using value-based cursors: - For a given `ActiveRecord::Relation`, only columns of that specific model can be used in pagination. (This is because column names are turned into `WHERE` conditions.) - The connection may add an additional `primary_key` ordering to ensure that the cursor value is unique. This behavior is inspired by `Relation#reverse_order` which also assumes that `primary_key` is the default sort. - The connection will add fields to the relation's `SELECT` clause, so that cursors can be reliably constructed from the database results. ## Grouped Relations When using a grouped `ActiveRecord::Relation`, include a unique ID in your sort to ensure that each row in the result has a unique cursor. For example: ```ruby # Bad: If two results have the same `max(price)`, # they will be identical from a pagination perspective: Products.select("max(price) as price").group("category_id").order("price") # Good: `category_id` is used to disambiguate any results with the same price: Products.select("max(price) as price").group("category_id").order("price, category_id") ``` For ungrouped relations, this issue is handled automatically by adding the model's `primary_key` to the order values. If you provide an unordered, grouped relation, `GraphQL::Pro::RelationConnection::InvalidRelationError` will be raised because an unordered relation _cannot_ be paginated in a stable way. ## Backwards Compatibility `GraphQL::Pro`'s stable relation connection is backwards-compatible. If it receives an offset-based cursor, it uses that cursor for the next resolution, then returns value-based cursors in the next result. ## ActiveRecord Versions Stable relation connections support ActiveRecord `>= 4.1.0`. graphql-ruby-2.5.19/guides/pagination/using_connections.md000066400000000000000000000115111514115062600237070ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Pagination title: Using Connections desc: Pagination with GraphQL-Ruby's built-in connections index: 2 --- GraphQL-Ruby ships with a few implementations of the {% internal_link "connection pattern", "pagination/connection_concepts" %} that you can use out of the box. They support Ruby Arrays, Mongoid, Sequel, and ActiveRecord. Additionally, connections allow you to limit the number of items returned with [`max_page_size`](#max-page-size) and set the default number of items returned with [`default_page_size`](#default-page-size). ## Make Connection Fields Use `.connection_type` to generate a connection type for paginating over objects of a given type: ```ruby field :items, Types::ItemType.connection_type, null: false ``` The generated return type will be called `ItemConnection`. Since it ends in `*Connection`, the `field(...)` will automatically be configured with `connection: true`. If the connection type's name doesn't end in `Connection`, you have to add that configuration yourself: ```ruby # here's a custom type whose name doesn't end in "Connection", so `connection: true` is required: field :items, Types::ItemConnectionPage, null: false, connection: true ``` The field will be given some arguments by default: `first`, `last`, `after`, and `before`. ### Opting out of default connection handling To opt out of GraphQL-Ruby's default connection handling, add `connection: false` to the field definition: ```diff - field :items, Types::ItemType.connection_type, null: false + field :items, Types::ItemType.connection_type, null: false, connection: false ``` Then, add any arguments you want (`first`, `last`, `after`, `before`) and make sure that your resolver returns an object that can fulfill the fields of the configured return type. ## Return Collections With connection fields, you can return collection objects from fields or resolvers: ```ruby def items object.items # => eg, returns an ActiveRecord Relation end ``` The collection object (Array, Mongoid relation, Sequel dataset, ActiveRecord relation) will be automatically paginated with the provided arguments. Cursors will be generated based on the offset of nodes in the collection. ## Make Custom Connections If you want to paginate something that _isn't_ supported out-of-the-box, you can implement your own pagination wrapper and hook it up to GraphQL-Ruby. Read more in {% internal_link "Custom Connections", "/pagination/custom_connections" %}. ## Special Cases Sometimes, you have _one collection_ that needs special handling, unlike other instances of its class. For cases like this, you can manually apply the connection wrapper in the resolver. For example: ```ruby def items # Get the ActiveRecord relation to paginate relation = object.items # Apply a custom wrapper Connections::ItemsConnection.new(relation) end ``` This way, you can handle this _particular_ `relation` with custom code. ## Max Page Size You can apply `max_page_size` to limit the number of items returned and queried from the database, regardless of what the client requests. - __For the whole schema__, you can add it to your schema definition: ```ruby class MyAppSchema < GraphQL::Schema default_max_page_size 50 end ``` At runtime, that value will be applied to _every_ connection, unless an override is provided as described below. - __For a given field__, add it to the field definition with a keyword: ```ruby field :items, Item.connection_type, null: false, max_page_size: 25 ``` - __Dynamically__, you can add `max_page_size:` when you apply custom connection wrappers: ```ruby def items relation = object.items Connections::ItemsConnection.new(relation, max_page_size: 10) end ``` To _remove_ a `max_page_size` setting, you can pass `nil`. That will allow unbounded collections to be returned to clients. ## Default Page Size You can apply `default_page_size` to limit the number of items returned and queried from the database when no `first` or `last` is provided. - __For the whole schema__, you can add it to your schema definition: ```ruby class MyAppSchema < GraphQL::Schema default_page_size 50 end ``` At runtime, that value will be applied to _every_ connection, unless an override is provided as described below. - __For a given field__, add it to the field definition with a keyword: ```ruby field :items, Item.connection_type, null: false, default_page_size: 25 ``` - __Dynamically__, you can add `default_page_size:` when you apply custom connection wrappers: ```ruby def items relation = object.items Connections::ItemsConnection.new(relation, default_page_size: 10) end ``` If `max_page_size` is set and `default_page_size` is higher than it, the `default_page_size` will be clamped down to match `max_page_size`. If both `default_page_size` and `max_page_size` are set to `nil`, unbounded collections will be returned. graphql-ruby-2.5.19/guides/pro/000077500000000000000000000000001514115062600163065ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/pro/checksums/000077500000000000000000000000001514115062600202735ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/pro/checksums/graphql-1.26.1.txt000066400000000000000000000002011514115062600232060ustar00rootroot0000000000000033537b3b1a5afef64ea25817d607afae983de531902c18dc5c99bd4b52538b5b03cb3f7032dc3a53b5690ef85a7abd7a0422cdead9a871a187b3bc755ba4062c graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.0.0.txt000066400000000000000000000002011514115062600252730ustar00rootroot00000000000000960ec05f3ef88fff70bef68ca02fa3d37b512f9ad668345a903766776aaca08fba52795a9625cde39f9c2080ae13ec6c76b8757f05c26ccdfb27edcc69112ca8 graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.0.1.txt000066400000000000000000000002011514115062600252740ustar00rootroot0000000000000037364b0f6dbb5e97203e30172d3ed21fdd3c90f78de3e93479d15008d00b170d4e1656dc86b686de69fe060f4573d743f341a1ee55eeb793426694619ffc3cb0 graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.1.0.txt000066400000000000000000000002011514115062600252740ustar00rootroot000000000000009de179b3c8a2f374c112146cbad6436cc72b553f2a7fb289171af50201137609545636aa5fa972c6fd19f371f8414c9339a708394169dc1582ebef04c68f9ecc graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.1.1.txt000066400000000000000000000002011514115062600252750ustar00rootroot000000000000002438c01b8bfd1ad23ff41f3dffd68b2a3cf150ca12f2dd45e2ba06fb063e6344dfca0d38e3d5f55693e050d9b44738e563abb484f24799fec80c6b78662075cf graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.1.10.txt000066400000000000000000000002011514115062600253550ustar00rootroot000000000000004fd53a85c197db33fdf2a73593a016dd687bd9e8a5e9eb01e8e09594bbfae2cd7f5a27a17a7555603e2ff14b779f26362091e478b37737b3ed8a1a7cb8b0fce2 graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.1.11.txt000066400000000000000000000002011514115062600253560ustar00rootroot00000000000000af885c3e261efe1ce3d14861d46d2150de3195d71debbfd0ac0de866eb7de45b6f5080a3686d8750522cd8f0eb499c6b1abb2e54bc9d449c62d9efe678440127 graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.1.12.txt000066400000000000000000000002011514115062600253570ustar00rootroot00000000000000dd4d30115510d1767dcbce8a64e91c241d48a4f6b4c9d57df29e334143431958783a17ed20cf7601675a1957b000c200dca4b9a649b1c433dbf879e5344d1c67 graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.1.13.txt000066400000000000000000000002011514115062600253600ustar00rootroot00000000000000545dc5c0ec289b6bec7c2ab59bf41708885f1e3a746dfd6b070d3d87eb82e4107ee154110b9340896144875384dca6ee7a4a5676b3301ed76884308d389fd96e graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.1.14.txt000066400000000000000000000002011514115062600253610ustar00rootroot0000000000000090f5193f787414b85ae649881d252dc0b0c1b6a880920ed117cea40aefa21350c0d0fc91e7a5a980c4958a5fdd7932874db5df966db81365a3882631fbae4eb5 graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.1.2.txt000066400000000000000000000002011514115062600252760ustar00rootroot00000000000000c512384b9d0482a44925a0e47026b6cd48f48282c3e22f9bb90fad6452dd5209f394752b0ecfe1e5ea41cd5f848325fd4fb9c68c54d63c36774fa568cfc93c01 graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.1.3.txt000066400000000000000000000002011514115062600252770ustar00rootroot000000000000004c46c862544ad3bd836949a57e7dc272a4bd5e6a71757879b6de8729d063c866d8ff2f2f6959001fc1120c4f748e25379039fa855fd9b1b717ae6ad5c43ed02f graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.1.4.txt000066400000000000000000000002011514115062600253000ustar00rootroot000000000000008320d4f89f1986c23e92cfe1dae0fad37c7ca6d7b39e5f05d110e7167aa3d0ce6ae995d514986941caa90f76aa3fa96679810e7fe3fe5232500c954c5d3645a3 graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.1.5.txt000066400000000000000000000002011514115062600253010ustar00rootroot000000000000006c6248cf76ea0ca250f9e0ff587e6475a51c3fa3b4cdb6a517a35baf9226d87be2b009f073a952ac33a19bbe46cdf44889466b160dfa20fc24d8a365bab4238b graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.1.6.txt000066400000000000000000000002011514115062600253020ustar00rootroot00000000000000875b3f09b699e006733a2ffee31379db3f775b79408b35726a24c8a00386b1a7fcfafc56c57163a71dadafa22fab24aa7dec5608feae1cce8c59bb652e60e439 graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.1.7.txt000066400000000000000000000002011514115062600253030ustar00rootroot00000000000000cc841f4c0365ddfd65b40b1af2c93217f6c5ab531331fa0e5e84e70dfbd7f997f20db2a5d741da7652c3efcb5dde42987668ed29387a378db22234b2aa18153a graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.1.8.txt000066400000000000000000000002011514115062600253040ustar00rootroot0000000000000066710fed7209fef5223602a3d8722c70c83f79540dff9752b42caad6ab53be7d74cd304dfc7e789ade3972d590ed3198aa42ea8e89dcb46c929fa1b9d3a997fa graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.1.9.txt000066400000000000000000000002011514115062600253050ustar00rootroot000000000000000bfb112eaca92cb98adb4beffed43f22a68993dacf67bf3631bec05f30b29954181629b5ab11d429619d75ec76c7295ae74f7eeba9940aadec2cdef7e5d96a95 graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.2.0.txt000066400000000000000000000002011514115062600252750ustar00rootroot00000000000000a4c9d7118ab03cb27bf49c97b0fe95e5695ace646bf9f459ec327bc18190dc9bcdcc772438873a7a7531213ba4d8d10af151e195a4b8971342230e5ad2d92346 graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.3.0.txt000066400000000000000000000002011514115062600252760ustar00rootroot00000000000000b1a909bf8582e6c253bdb901f9865634dd970519529eb34021c138b6cbfe76c1573756899d7966c16f32878dd11272d6f6c8be7abde3654c9a5969e93af62b0a graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.3.1.txt000066400000000000000000000002011514115062600252770ustar00rootroot000000000000006b23b11331e345a757ba31edb3af77e3174996603162dd1fb698971886545ff8344dfc39f21c6096d2206f7be1d7f529efbddc6f86cbe15ddf81232e4b631603 graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.3.2.txt000066400000000000000000000002011514115062600253000ustar00rootroot00000000000000cc215bc8fee73b02c40ce29070244a31072f42e8a68a3b7038ef449572eab977180d13bc61fa3cbd02cc6855fadaa05bcfc6db033b517e8b8136960a188bf01a graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.3.3.txt000066400000000000000000000002011514115062600253010ustar00rootroot00000000000000019e92e06ffab9cbecde167c72321136e0826db4bd4ae7a74c33b0d915e6b253c847f688f3efdc4a7f27428f0cd36b2ffa34fa02769ff3a431e101b88fcd7533 graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.3.4.txt000066400000000000000000000002011514115062600253020ustar00rootroot00000000000000e08061b7eae9ac0deb15a4d7361deddc9cd881d89931652806fb4a2a544c5b29e58245f850322c228dbbd3000920ce94d2de2bc2882cec29dc0c46698943058f graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.4.0.txt000066400000000000000000000002011514115062600252770ustar00rootroot00000000000000d2152d80aee8cd1699c4e64e28329c51ddee38ea6ea318f89c6e9bc996fbd68d4db28831fcedb5583ba92d499852dd7fc8ca99583376eacd28f00c908fec8f28 graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.4.1.txt000066400000000000000000000002011514115062600253000ustar00rootroot00000000000000e0a1bfe31eebec9523faf1710ef1887cf15bac93a9476778804958b646f9664bdbf304000cc568ccf87ca7d450aba646081bee295c998fd81491156890a1d092 graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.4.2.txt000066400000000000000000000002011514115062600253010ustar00rootroot00000000000000cce6aca47058577462d4bf51c02b578c6aaf7bb47335e3435129efc9662e37e13756542d40ce728702a373d4e93416bb72202dfa9781bcc3de61c81b29471a5a graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.5.0.txt000066400000000000000000000002011514115062600253000ustar00rootroot00000000000000753347081830d3007f568b04007cb401353130f3964ed5d75a65898b1751f88b47d571e43357e09173b6ba10ec954c946fe74adcf64b29a5fa799c50ec704a7a graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.5.1.txt000066400000000000000000000002011514115062600253010ustar00rootroot00000000000000f83113767fb51f584f0d27b34f44fccc8ca05fab61e150bd6dd9098c9b3aef41751c4337ca994e2b78486fdd71c623a6d24da985d3bcee983a33c7a798a25e2d graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.5.2.txt000066400000000000000000000002011514115062600253020ustar00rootroot00000000000000e56b5290ed8798c67ca8e2fb1d6af8895570893e4c71e7958f3051dfa24617ec7772d325e44f54dc320f2963e802b28ba8036f26a4932971c5b9a2181bd98c6b graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.5.3.txt000066400000000000000000000002011514115062600253030ustar00rootroot0000000000000092d3392668d3365d61d31d1906f19e54b4cc330383a2876997ed3cefa787cbcd51f009b6a30181f0631ee6a80a3402549f44d4905145aadeb7abe421167daa6c graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.5.4.txt000066400000000000000000000002011514115062600253040ustar00rootroot0000000000000077201981e1495aa32181b36ca444b020860f83e6a29d8a749beca11cb02d4ca8e25fdfad8dea1c0365b8616661d39452de72d861f0460828db03151a627fb7ee graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.5.5.txt000066400000000000000000000002011514115062600253050ustar00rootroot0000000000000093269e41b9069a070584b77722d412c3a43251deabe013090874a657a07f1180aec5d5a90091fa8cac7885147d83e9be0701f6e3bb52d3bc17c4759069882bb4 graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.5.6.txt000066400000000000000000000002011514115062600253060ustar00rootroot000000000000004e7776668f4e8f7897e41a2d8844c1684e914c477f1eb8f4aa204db9dca56c219f0a6d631e253b23d951d15a8e8bf106f786925655491a1f562ac270561f8f81 graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.5.7.txt000066400000000000000000000002011514115062600253070ustar00rootroot000000000000009ad14f1225195a537bb83daee54d2b35e1c29139177d3f85cf64e1bc7b8b052a476ea654577e816c8cc8aacb1b8f857f2328e895465bf6ad47da38ab35c33c7a graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.5.8.txt000066400000000000000000000002011514115062600253100ustar00rootroot000000000000000540ceba5c6b2379d803105458a91e5144c332440eb3f71b57442ae6d5dae18deff01fbf07cba8483081bb824c9870e6d63c39be4d27f078318781eb4533926e graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.5.9.txt000066400000000000000000000002011514115062600253110ustar00rootroot0000000000000073a2208665ee81b635863dbfe03eb291fafc29ef756d9e6a4172e9ad680b1fb63ba04e5ae8cd848480513e4300e61dcf0f6ac961fa4003ac2a30ec51d7a26060 graphql-ruby-2.5.19/guides/pro/checksums/graphql-enterprise-1.6.0.txt000066400000000000000000000002011514115062600253010ustar00rootroot00000000000000e135d50d7cd9c8e08ebaac02cfe6ddbbb31dea7c4c752c7acd3d327600ae8af0278a6534b3504b8186799286609051b1c38e7611b1659de23022b661763dd3c4 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.0.0.txt000066400000000000000000000002011514115062600237130ustar00rootroot000000000000004de5bbda7a3e9ed29eb3cdb25a0cea1a91f57dd34bb73827628f49a605cec244583cf0b9b6d9dfab932573f99c2ada4c98f4ef569fc82fa32b046e0ffb3e976c graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.0.1.txt000066400000000000000000000002011514115062600237140ustar00rootroot000000000000004951e730ead0e493c21ad337af84eb99551a1e4c66b35c8ebfabc000b1bc59d13c4c45c3081fa68719f56522ab1f1bc4c4258e634c0da3414cb82041ed2e122f graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.0.2.txt000066400000000000000000000002011514115062600237150ustar00rootroot00000000000000bc165db6aaf32d51e710626eb2baa36851e0d9ca4ff88676d0fdd9120264e599e326dd1602713197308d010392a46f7f3472e805b5a1677af18887e3e7b0bdaa graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.0.3.txt000066400000000000000000000002011514115062600237160ustar00rootroot00000000000000077630f850706f87104890dd7ccaca7fc01e44f73c2f5bf5750bfcb5b15a4b58e2028aec04ed3066abbeecf7863af399bfbb4ad1bbc7aea9c034faf949118b5e graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.0.4.txt000066400000000000000000000002011514115062600237170ustar00rootroot00000000000000e9be770ff2117a4692a11f5b5c8e5e9f5095bd7aa7ca24a9e085dd278a548caed28396d100302ec52ab295aafd7fab849c83420a49311034779ec30eb3b4a232 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.1.0.txt000066400000000000000000000002011514115062600237140ustar00rootroot00000000000000c934c912626c926921a4e5260edb3e1d108d3bd3b680137a3077a0baa6e802966399fcbb52c3b2d60333db9d8a5e508a4c8e3344ff2ef05555aeb76967537be7 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.1.1.txt000066400000000000000000000002011514115062600237150ustar00rootroot00000000000000deca840ea6676077e39608a63b3d33cbc032f69ab147d2a1c679da8852e087204b3c6b643cde3bde411d7d1bb7cf79bbc21b504fd08940ea5b6cd7cd868c3e12 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.10.0.txt000066400000000000000000000002011514115062600237740ustar00rootroot0000000000000070b8ebe4f3b0c99a16a544aa26bf0d9c8605c4e1aed1fd9374c93f7818bc08860690f60b3f28f48a6cfa8c5864312a2ea94b7c8caead40f6f609f07739d55ec7 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.10.1.txt000066400000000000000000000002011514115062600237750ustar00rootroot00000000000000d061d329ed1518519382f182ffc48ccbaa9cf7a2abea701a563477bfd3dd39418accc67449d6891afff6482c0c14f05a7a459ac42e801dac6fd1c91e6a0de974 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.10.2.txt000066400000000000000000000002011514115062600237760ustar00rootroot000000000000006f8c9be62438f95aa146e302c7234d55ea592fda72d483873b20140412cc912b3e1dba8789451448f600c15c38f40989bc34c42aa18ceb1f796cab50c774afb5 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.10.3.txt000066400000000000000000000000001514115062600237740ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.10.4.txt000066400000000000000000000002011514115062600240000ustar00rootroot000000000000006e900c64b27dbeee3180e5e624dd8eff409305bddd95bb651e8105a00a699bbcf9dc321df87c815372221e02b332ffbb9512800a60e0daf4ed323b181f4e681a graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.10.5.txt000066400000000000000000000002011514115062600240010ustar00rootroot00000000000000aff26d02fb3eb8751f763b12e4085c24e7efca383f1117b3b5ad119c328334f7f3abfdb294f9fc3f33a466c031f0ed856bf459d979eac93e7eff4f0f76610774 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.10.6.txt000066400000000000000000000002011514115062600240020ustar00rootroot00000000000000e304f7f23f6e940159d34f0d871054070a8235d657c141157072655d00787e9182629280509012160c757dcd1afd12982875dc0197e381393051d0798293cdb2 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.10.7.txt000066400000000000000000000002011514115062600240030ustar00rootroot0000000000000008d220a1b2580d9e763238d553838b692f26262be563b685e9ff69e02a6c03fe125647bd6b193256a4dab58aadeb45921ad986b51ad0d019e69b77e8ecc61650 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.10.8.txt000066400000000000000000000002011514115062600240040ustar00rootroot00000000000000de7618f67acf9016e74bde682a3fbcac61a422f6e61d06f85eebcf0aabb10111a55d2e05054bd68841909e0a53c73104c369c3e919447329ad6e1aaa2e8e7f26 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.11.0.txt000066400000000000000000000002001514115062600237740ustar00rootroot000000000000008f856d904af40ac0424743a1a40bad96408908202c031d7f0d67a74a085c000bb5db17e9e1c97dfbc79095b4006f82d18ca6f67fe830a655895ebda0e614ce71graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.12.0.txt000066400000000000000000000002011514115062600237760ustar00rootroot0000000000000075208ae66c220aa3d275a3765a7e15e9f589efb4fa7135fee57b33ecd13347f12ce8b4f5207b8457693a9b8f68eab5f11db746afbf2d0eba436396faf5502421 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.12.1.txt000066400000000000000000000002011514115062600237770ustar00rootroot000000000000000da30804e51647104a2b0be0bc9c5ca3ed43d8c64db33ae9afd7f7b73923a9243824e2d44a37cbb47372ec6a5c6b5aa54765d481dc51b885267ba7ab4672484d graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.12.2.txt000066400000000000000000000002011514115062600240000ustar00rootroot000000000000002a12902ed9321419fedf166021e50fdbfd695ca7b6245bf10a4354f95f38ac60de7bc4465ec873d6ed1af7adf0f631308739de799243e88a893f7527b8df9df9 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.13.0.txt000066400000000000000000000002011514115062600237770ustar00rootroot000000000000005446303fdfad4e2cb2bb47535d60bdf11a3e039c7b1edf8b02f9e8218ea1b87367f59726e6ad227e32d74a51f39f776b36d5f0fe59de1e72475a38376509cfa6 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.13.1.txt000066400000000000000000000002011514115062600240000ustar00rootroot0000000000000079f699934f0e9da8f328256b5c67144046341011e0629234d0391f206a807a00f457c71e00eb03b8fd4f0779156d189b54b56f6d7525904acb5ea19d061fceaf graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.13.2.txt000066400000000000000000000002011514115062600240010ustar00rootroot00000000000000e1aa0bc146fa154e3fb3655fd8e96e4fa7adc27b165ebf2c56880f64ac1938e3ba8ce8e7c7c02e921d54255ddfe9f7e44ecc3fad8675eaa0b1055813e989c2ef graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.13.3.txt000066400000000000000000000002011514115062600240020ustar00rootroot00000000000000a1f2ed08b503cf48d8fe7e0ac7f09d194e5f215de1ddce9acad8a0a6e9abc0865d62583b84af9e2a18a8ede3caa47d8d0b148caca986813bd4d03ff4f5c9ff5e graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.13.4.txt000066400000000000000000000002011514115062600240030ustar00rootroot000000000000003e1684ac0a06712bb565d0c4eff852f8240cee55824b75899d044db2815ae60c20fd367686a69e0b4f3bdd9764da18691d8af736561a13303d8be5cb9648b56a graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.13.5.txt000066400000000000000000000002011514115062600240040ustar00rootroot00000000000000d481fb6595d6b0cf5a5cf0fe2c29367dd4687c6fa8f7a12373b896dca6e0b4464ccaa6a46d8f8dcabc247ce0b1818161091b18f39ff14b8d2bbbc72769a0df23 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.13.6.txt000066400000000000000000000002011514115062600240050ustar00rootroot00000000000000e380c037088472eac1d71a64b419dc917eadbcd5e91d0273cf97d394d9833cbd93070d9ae3f8227e401a953a6982571713a3672df9b7d8ca37f1f216b9d3011a graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.14.0.txt000066400000000000000000000002011514115062600240000ustar00rootroot0000000000000022543434eb5bbb551f8a47fa2fa7f41f9210128b34e3bb8d88e47faf02fed606c9269d493fd3702cd8ae9861998ad967fff7095775a0fb8af8aa012b9dda06f2 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.14.1.txt000066400000000000000000000002011514115062600240010ustar00rootroot000000000000002c41c982e963f91f777e9b7c31dffc62d30e10cc741affdc168d94233016e3b07ae8ebe3c9631ea57b7a2433fd353f2087557dcfe2394d64e379485a20723893 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.15.0.txt000066400000000000000000000002011514115062600240010ustar00rootroot0000000000000006757f5a7d878584a29864cc3790e9687d119aaed5fe3c66afdf2ff498ceaba63c0bf9bc4fe81fdaad2b5e933acd7a889c33cec825abb673f6f624c16c5a110d graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.15.2.txt000066400000000000000000000002011514115062600240030ustar00rootroot00000000000000ec02be5c1b0e7defc6686d42d3033397a144de006ef7ae9ce0168d3aee459529edab8ca357a95f204010bfa7cbd14b7a0c7049892cad968ff2ede267c5cc5224 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.15.3.txt000066400000000000000000000002011514115062600240040ustar00rootroot00000000000000ba4c18f634b340b6064fbb919f4b33445d0c0c16e81f462d929c9437498c1349571f055d73e2bc0388a7e34a54316592a9f606cff12095b9e70483d962349a71 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.15.4.txt000066400000000000000000000002011514115062600240050ustar00rootroot00000000000000bca1561e9657893e6143404525b0857be42dbbf2148b64004e09207c5dce5b5c98bb431381989ac73b846d58e68d5eae12240ff01abf89660d0c7c66ec2f5b07 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.15.5.txt000066400000000000000000000002011514115062600240060ustar00rootroot00000000000000dad57401f7f2029b45dbd3b3c307025320b999e9d22396888cbfcec41ebac21838d4fb7ab63925ca3fc4fed67323842cd6788ccd20c8e6f315dd54f76d848e3b graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.15.6.txt000066400000000000000000000002011514115062600240070ustar00rootroot00000000000000476db12210ef3f92de3ec5743d06fa371495932773161f7e7dc574db0b8f45b5beef99d677394b3c4e5371a1b92ffb7c923e02c27f39ec3c570812b5480acaa7 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.15.7.txt000066400000000000000000000002011514115062600240100ustar00rootroot00000000000000fc69c5d1a8335ce8eba380e2d4aaf61e0f704e9dfd293fd435b485d3f84a191bb32b3eec91c8f718d307306abaea3b23f941e2e68cb8623c220d4009aa94b0fc graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.16.0.txt000066400000000000000000000002001514115062600240010ustar00rootroot0000000000000028dde609677a6303795e12452bad9e35b828bdc3cbfdaff5c40a8d30b6f1f53df6158fcb42659a5d55b89a696c9c0ec484c218c3df8c68abee4b02a141c5d284graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.16.1.txt000066400000000000000000000002011514115062600240030ustar00rootroot00000000000000d0d27966bcb3df9bb4f7b4188db4715321ce98beef0d57f759b003f79edf1d03dc6691380b0f4eef9f1124d5bc6932878e262af0ce98dcdd23a421b22c5232c8 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.16.2.txt000066400000000000000000000002011514115062600240040ustar00rootroot00000000000000b181b6fc70695498b5faa3216664b9849a6b8e32ae36517e3cd82ea6373e61821e4b0e193339ce2e92ee5528d9c694794bfeb27a3f2cd822c9576168e37e6f77 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.17.0.txt000066400000000000000000000002011514115062600240030ustar00rootroot00000000000000ade1d11d1a0dad72e0472ac4bbd157a444d179ffb420246cea1d4bdd45d241bd520753f57816df6851e553669573455117940859c49b9840790621c827acd8e8 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.17.1.txt000066400000000000000000000002011514115062600240040ustar00rootroot00000000000000594becf061695edb79ac93675a2c574cc06f7f4069d6af57974ba6da3b47a48876a0fce44dca6524042bc6dc97f1e415c534160424973ecf004efbea8d283b0e graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.17.10.txt000066400000000000000000000002011514115062600240640ustar00rootroot000000000000009c8b3e77aba8029499736dc4ce306654805d105fd9149e32c7078e59aaf573590f0c8590d2b714547d7033a0b964e7d4ab4deb8ceb469bdb6b92f0d155776c24 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.17.11.txt000066400000000000000000000002011514115062600240650ustar00rootroot000000000000004445d2a146ffba7b5d4c89826755984a17bc20cf6013f8271372d4feca6cd31b37be1314af9b2dc26b77978ad5681e75788b5c89532d4a093f9776d970d5eb8e graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.17.12.txt000066400000000000000000000002011514115062600240660ustar00rootroot00000000000000be3250e7a13552e41a8a27d3a9e25f2fcd6dd8e6433887f13ff5e0be03a28d67478e937c5eca33f9d3a322b36f1e5d3f596c7c5b026842e07d0603ed78872736 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.17.13.txt000066400000000000000000000002011514115062600240670ustar00rootroot00000000000000f14f76518749a8c6727efcfac579344c1fffc53f04cb26e0a94349f9f91a667529dd7fc1d838b82a478b50ae4ef11b03bff04e35add8d6035d11092c4bc8f625 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.17.14.txt000066400000000000000000000002011514115062600240700ustar00rootroot0000000000000079563aa9deb6b955e65c6e4a6c0326d98432932ad6c328886f7e4a13a0d22ba7813888cd07085c4503ade24068cb1a4d8a6b09e33b76b642ffc90bf560032a9f graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.17.15.txt000066400000000000000000000002011514115062600240710ustar00rootroot0000000000000040b16be98dd17dd8b41979d9f9015e72119a9913648d5d10ab91f1dc1ee07e391e9f54f116d7a258ef722720ed61983533df79b0b58647fb2cf0f3399aac675a graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.17.2.txt000066400000000000000000000002011514115062600240050ustar00rootroot00000000000000a33ca96756a41030479fd094c9cb907f28d95b182d52d8121e445cb76586ce653daefb6870ed8ef88dd33958ae981a3d1965c00d4f2890af0d637dd664fd9be1 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.17.3.txt000066400000000000000000000002011514115062600240060ustar00rootroot000000000000003987f32362d4bb4995078a0a6c3c9449ec65918aed02e49e9d1908ae6fb22f6acb2c56dbfddbad3072b68177b229965c441d85211341f46cd6969fba1f5545b2 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.17.4.txt000066400000000000000000000002011514115062600240070ustar00rootroot000000000000004d216428febffcc0c5c73c75a2f835470e60e86faa93d7cd5743dc51e4f0b68cd1da5f1e870cbd6d4e6616aa5ca3478fbe281b5b00a5913e21f35973228638fb graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.17.5.txt000066400000000000000000000002011514115062600240100ustar00rootroot00000000000000f5f32ea4acbcd18efffc7a8f076052d74b17eda16cbde21693c995c7428cddf5391579fb2b98c2839bcd11facb71edef1fa90e9dffdb309e6eea2672dcc99a61 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.17.6.txt000066400000000000000000000002011514115062600240110ustar00rootroot00000000000000a5dd58498ee11e2e1057151ba5e1c674f7adf4d6e1e47ad0af2a38ee7a075f5845a8b928620d63db8365eb12e669be586ec9b28adde8b63ba9fa67ead5080a82 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.17.7.txt000066400000000000000000000002011514115062600240120ustar00rootroot00000000000000d39669286b1f4e1f6227188755d59fdad2151ef67a6de3ca8deafb9c3eed69a12914d68005cc6cf9565c888e7cbd7c751db3824cc50389316329fa0723617ec8 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.17.8.txt000066400000000000000000000002011514115062600240130ustar00rootroot00000000000000bf8de39f503f423a10569fa534ac06243e118c9d2039d6ffb9b10251b516de218e5998943ee0fe7b8ddb6f6aa6b84bdaeb87480397cd76cb6de37205fae40127 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.17.9.txt000066400000000000000000000002011514115062600240140ustar00rootroot000000000000001914a9f65eb64f67497e8a5315b65e8bc087c808a98ad77ac790fc0ab1fdef73c06ed277317cae8e210f992bd9f59915e7191d4a504681bf78e888b5177f1638 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.18.0.txt000066400000000000000000000002011514115062600240040ustar00rootroot000000000000002dcfda18af745b557c62c2b60c83c96dcc767ef4f09c4ec723ae599677571204f9db59f4617fdb69eb78e24e205d91e81f236ab3aca491b1318055d1ae933b3f graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.18.1.txt000066400000000000000000000002011514115062600240050ustar00rootroot0000000000000034ebd540c1a341ab8fe8a59051af68c187311e407ec1ec9444ca5a2231c6b6c835ee13f9fa3bdde44b8b7d8439d2c8dce76364e8863badecb8c6fe67332ed93a graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.18.2.txt000066400000000000000000000002011514115062600240060ustar00rootroot00000000000000c863ba11770864257cb232fc1cc25a2386c3387bcab8aa95a309e4ee67bb6e184f5e6281b45820cc1383cd80e3cdee8a5c011951e7ac6d3397c3925879017777 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.18.3.txt000066400000000000000000000002011514115062600240070ustar00rootroot00000000000000c0c009deab18661ad98d30c44f1cbb33a44b6a21c806428bfc6b50f28e0fbe67c7b2fd4696b8e4992b9d1cee3d4c71d7b7b4de91dbf0c103715f1542ba6a39c8 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.19.0.txt000066400000000000000000000002011514115062600240050ustar00rootroot0000000000000070775118ede3e9600778bb92cc5b8a56f731ae133f820a7dd2e5d00b279a79e4a1e0be9e400b0fd092f1a8295d5cf10fa4b2931f834542998ede4327d25d9d12 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.19.1.txt000066400000000000000000000002011514115062600240060ustar00rootroot00000000000000aeff5df888036b4ccd8d7523673c077b2819015a0e1f27091b72de091a7d0cf609940ffba7cd73f3f48e0195c61c5e627fc86886775313f31beb31cb2d21e196 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.19.2.txt000066400000000000000000000002011514115062600240070ustar00rootroot00000000000000385d1311d4c5e41cf0c508b1ba6ee1edf16f29f010d913311b87d3b9f3bdc844dce970258fca3d606fc7d472f2024dbbd4d797c775c3bee455bfd158cb1e90fd graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.2.0.txt000066400000000000000000000002011514115062600237150ustar00rootroot00000000000000ec288ac8a27c79d493613abf9ae867a6ba3af28a029166d100c1153cad4c4039e8ba94beadf633d2889fe2af5c3bf91998c4ea67f00ac08150e9209b52c49938 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.2.1.txt000066400000000000000000000002011514115062600237160ustar00rootroot000000000000006362be7dc25124d8cda2f4ecace1b54088c8af7137f0a8789a80df63ace77ea25b5b9f09c72abb6e73deb204224d38f6f8de6a314fe255acca8fb8de3cc379d1 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.2.2.txt000066400000000000000000000002011514115062600237170ustar00rootroot00000000000000804dc71671ca8ed64f2fcf5fdfc7913758109c59b78a724dc6d6926829044aa08b354a862dcbc1bdb403def1f24e1e34eef2115778b671f87855cbfba62fa2b3 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.2.3.txt000066400000000000000000000002011514115062600237200ustar00rootroot00000000000000513828ef6e829ff917a08095443e51224f8a7dd5cbd2379e350e0505ad412d36fa9e151dc7ec034c65c4b564ff41ec4e40c9bbeb14501a245dd4bbfd61a64c78 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.20.0.txt000066400000000000000000000002011514115062600237750ustar00rootroot00000000000000813aa497534c608234e188a4272266cf2f2e14691c626c68c0ef1b05291dc305db1c274c57cf2d5bd83568fb7064bb121f8fecb617014496d23a98df3660486d graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.20.1.txt000066400000000000000000000002011514115062600237760ustar00rootroot00000000000000bb055c688d9c427a9f0535e22041bc7c414c74a9c40bd626a7ccc59595303afc7e3361c644edda5041350f814a59dfc64f403030a32b948e707e6a1dbc72872e graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.20.2.txt000066400000000000000000000002011514115062600237770ustar00rootroot00000000000000002710975d063bb3301eec6a2b52b068dfed50349bf44e7d7926709ce357cc70108bc05df1352929c8989b0425988e11ee2bc187967a0966211f30be5a0963b8 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.20.3.txt000066400000000000000000000002011514115062600240000ustar00rootroot00000000000000d75f4a170d3763ed3f01c241bdb01e21bfb5fabf607c13a45ed4815ac7d4f98ff44e4e6bc2ab7b91d380d5a510a21c4224d95ca61ee52e3d10992fe022605570 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.20.4.txt000066400000000000000000000002011514115062600240010ustar00rootroot00000000000000ce98318463a74c268ca618f46cf441859c2a374abd5908f1f25c788f934d87a9ab97377c3422882e8ad86da4d926cfbc30356856f6d2312c793a168abea8069f graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.21.0.txt000066400000000000000000000002011514115062600237760ustar00rootroot000000000000003ba6e99c5d9ff61a771f8e775027470b066ca8fd80b45926cfc5b9a89f09ea44bf616bc07099dc39c2f096bb70e0d331aa648de0779012be9fae6abd1e81fa5f graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.21.1.txt000066400000000000000000000002011514115062600237770ustar00rootroot00000000000000d3cb24e0212471783cb116ffd3aeac56007692d9107fd9f3d5b50b65057865b321ca52c049ec87b196129140d044282ae807aa1e14f36623b72c6c88f2b90956 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.21.2.txt000066400000000000000000000002011514115062600240000ustar00rootroot0000000000000061049f9ecb5e4124486dd881ef942503988413f24256048a7ef93ce7ccae2ab9f16a8915e5ace0f04c84d4e75732acac93e36af23e893eba7d3959cc4787fc9f graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.21.3.txt000066400000000000000000000002011514115062600240010ustar00rootroot0000000000000040342ee99d7aafd5de610927de407a3d736cf37afa3ecd9ecfb35abade00675d0f4156ad47f81871d63658a67ff59a3fb46660dfc28a10d21223ac40cdd13bdb graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.21.4.txt000066400000000000000000000002011514115062600240020ustar00rootroot000000000000007f47e41ca607a330a0dc988a9706fe8fa4065ee9045512ef0d5e9ebe12f53040e761cb9162448c1625af7d230baa2d24cdd3efbba0e4852bf855a075c2857bf6 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.21.5.txt000066400000000000000000000002011514115062600240030ustar00rootroot00000000000000d128ce5bcdf09b67de33cc8a32f3e8e6f3efb602f7c61bce7001ad7f616299923b90ada691d50a120a73478673cc7db49736822013556de86dd902209a5c5b0d graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.21.6.txt000066400000000000000000000002011514115062600240040ustar00rootroot000000000000003cfa5ee53774a042aa9cbdc2a64747265b359048db0087b24d084611c2c1a97a7f3f20547612d5681a338c32fd45c496c21a511872d098bde50ba8e66579c456 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.22.0.txt000066400000000000000000000002011514115062600237770ustar00rootroot00000000000000f35ee054e97240c089f1f56d233875e6038ac264facfeddc0aeb2abd5d1c7c0844e92f2ab3087c45c2999f4fd7bec8720bcfd4794af19dc5e565afa884e1be85 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.22.1.txt000066400000000000000000000002011514115062600240000ustar00rootroot00000000000000fb7fc671dec8dfc0528b42177b380ff7997340029746c367523aee8dd5bb2b335cd61b6663f5b7a198df5473fe61b31b2eadfb61fe93ca13bfe18d8294e80329 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.22.2.txt000066400000000000000000000002011514115062600240010ustar00rootroot000000000000003e7421186fdcc7ef7ab87f3a294b84d3478f47dbe54a5cf68ad739625c4cb9b58b3ac83c900039ee65b2e789b37d5faf9a67586c29045fc099bbd3e6f09eff85 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.22.3.txt000066400000000000000000000002011514115062600240020ustar00rootroot00000000000000300b9bb439118289cba69db994e9402697114d1b262b7c2879e49e19266f7b7b97d0aab45f54921c5449847579043867fbaf451165d616da78d945927415fdb9 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.23.0.txt000066400000000000000000000002011514115062600240000ustar00rootroot00000000000000c87ffc9ad12f452c013e996f2c4a792869c4a6b333c01091cb535ee62ccdc8f8546ae09738d08bd284bbfc27719b6cf22730c06678434e807407ce3b601dc090 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.23.1.txt000066400000000000000000000002011514115062600240010ustar00rootroot00000000000000cbcbceda448c0b31acfbb5fc5ac18dc1bd90787009bb9b17758629b3fc1b1fb697c64666bb864d2a3cf968ee14f0c7aaf5e9887c4eefded223689b414c0a3b71 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.23.2.txt000066400000000000000000000002011514115062600240020ustar00rootroot000000000000008006a4ce467f9fe0842757075fbbfd6dbeddafe5dc1e3de640a8fb3406126155cd782b16128fdf056d79d6681eff83c5f2b1e86b49460a7a0b23b2b231ae781a graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.23.3.txt000066400000000000000000000002011514115062600240030ustar00rootroot000000000000008bbab870128ce2e9283c41ef84959f99b15f003f7b0d499c95b2e40c0f1b706e65b3d9046749a56ba241046d2cdb5c9411c1a414efd87b7a9961595074c39983 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.23.4.txt000066400000000000000000000002011514115062600240040ustar00rootroot0000000000000084f8cf2aa279a63440dc412b7ccf6338df48b4b3f5aa21af99ad15febe667d277b725bb924e1ac48766ba72d02dcc0170f4d5cdb2c280615ec60a98839391d89 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.23.5.txt000066400000000000000000000002011514115062600240050ustar00rootroot000000000000006adfe5c8e153b43bd4fdb2f0dc79c2e5d035d3fe9e571c2f97255edd87d2419c91f8495bedaab3c3761d4e6b7e7b460c091d5189a184e6e2ed785ea06f204597 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.23.6.txt000066400000000000000000000002011514115062600240060ustar00rootroot00000000000000f910abcf08a2ad51e46f68f492b2d651fa5d5debf80761464ce1809d2672b1db1064db304f6f5bd19c2587792b160c9bb5fa526b371fd3a4060e8a0841f5452d graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.23.7.txt000066400000000000000000000002011514115062600240070ustar00rootroot00000000000000461c5c8d68892208c8fe457a51a6c18b253ec32cd37aface4fa2eae81fca94874c654a95a16f21571134476970a66304b261063c7fe2cd8a19daaba9bb90389b graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.23.8.txt000066400000000000000000000002011514115062600240100ustar00rootroot00000000000000e6f77862bf426401c4b86bfc92eae792da5ae3cc830c03ae6f90e80efa8ff1e324cfd4a49474832b0f82d32c2e568018b757a5efcbd896ed4de7ee5dc736e9e5 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.23.9.txt000066400000000000000000000002011514115062600240110ustar00rootroot000000000000003b93d5b119ca2b21784fb6b151f5e9f9b9630c8cd4f26995d87669097e2d19048588d4055dc230f2f926aa248f88bfbb4a4ec547c2f5ad6b801ff43b05c5c391 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.24.0.txt000066400000000000000000000002011514115062600240010ustar00rootroot00000000000000325cc56c6cb2773eb9341963371f1c80167168a76601a96a097e7d60f77bed0715ef8b21e5132555308295775d061674cf87f47ce390155496099fa1e49bb441 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.24.1.txt000066400000000000000000000002011514115062600240020ustar00rootroot00000000000000b6934de96b0d4f0758d0e84671d5e9d82e5e5790c01d3dbb53db1793aa4836e216d0962386542126f3b2dc1a27b35bec1ae802e644794a6029ce38314269d5f5 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.24.10.txt000066400000000000000000000002011514115062600240620ustar00rootroot00000000000000e53374f59ccb74dae6d99f2d9fffff6a932f442d31646aa4dd7a6725f434dd08925dba18e4c049432cebb48b0cdabdb72a0653b0c8eeb32399fb3a8cecb9380b graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.24.11.txt000066400000000000000000000002011514115062600240630ustar00rootroot0000000000000026b5a3d30b0866b87dbcc800f5860c92f982aa04fdf8a855674ffec3bc8ce1fbe041672df9d00c4318040c44239b9c3ab829c5f99f6568d0d37f3cea19ecdb81 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.24.12.txt000066400000000000000000000002011514115062600240640ustar00rootroot00000000000000019ea12ce24507d58afd8ffbcd4a8de990f8cdf354c7c273480dd09eca211a709dcb7e41cedb99813cdfc680bae8eab1f25826246ea85c5860898f60c34b3af8 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.24.13.txt000066400000000000000000000002011514115062600240650ustar00rootroot00000000000000566d9dfda5e0c39310f5c54e5c3cf44d3f7118224ac8a1aab2ccd2c9a46f34acaf29bf99a5baf2e2454a83c0b894cedff316a5b1e85d577d3c0721297afad78e graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.24.14.txt000066400000000000000000000002011514115062600240660ustar00rootroot00000000000000d3e3134b7e49c1f0c7c368c0fb5cdf0c27448b7527aac4cc7df012137b52919962b41ed0c3c503298232ccd7ea03a781be798b310af654906150b446a5043671 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.24.15.txt000066400000000000000000000002011514115062600240670ustar00rootroot00000000000000f2b2eb456344075b733c1fac38c258ec02b0fa492ecc124d6d5a090b7b949eed1166da008b90182db9d29f106236251b3a2cc61fd2e99fe7dba268e20c3a0ddd graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.24.2.txt000066400000000000000000000002011514115062600240030ustar00rootroot000000000000004b1fb57d91cd1ee1f94ecfdacd0b67585c3638c8cc1b06bfcdd7c8e050f15bba37c573fb1a394451ffcf5cc2077f39c24a77b2f130943538a3c01098d487228e graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.24.3.txt000066400000000000000000000002011514115062600240040ustar00rootroot00000000000000c0dc667c78c347daecfaca74da7e7cd230cd1c3992dc4c0b30014aa6b8add65ef7b110eca420495b93ee4f90a472297bde81e0758b77675389e3d9dd7edc0faf graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.24.4.txt000066400000000000000000000002011514115062600240050ustar00rootroot000000000000002a89e5dd34183dcbaf9474a26d51c51b7f37ef39aa09fe9e28de310910b00b81751a9d03e40b0c70dee7422d6b10bc92e6adae7fc480d8e0c943ee6ecce05761 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.24.5.txt000066400000000000000000000002011514115062600240060ustar00rootroot00000000000000a179aa5876ac3dc84d516ed73c08fdae2879b7ed8a725beb762860f02f671061d9e4f6569828bce0754da49ed3a1f46d134444a71ffd1a60d71078ee16260391 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.24.6.txt000066400000000000000000000002011514115062600240070ustar00rootroot0000000000000060c791368edeecb40481e573b129851090fc11f3b5f2b6a6a05ae1f7a64ff53e113addc456a4df469b9bcdf84199bc52381df6fc5cf03d65028023f5e3520be9 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.24.7.txt000066400000000000000000000002011514115062600240100ustar00rootroot00000000000000a6cfd98894639e5fbf7c687b60efcb66c7c0c7a9cc8a810083a2f9cc7d87a3ac51df3c3b6b4d49e4ea95cc7bae2e839177d83550cba992eaa99bfffcd85168a7 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.24.8.txt000066400000000000000000000002011514115062600240110ustar00rootroot00000000000000f684ab732ae69ead5c4726c0c7bf2ec72127061b499cfc7f144348fb7b584b3cfdd02f8382158f849b6d0a109bcf1d0597a9eaa6dd427e254a784337b57aec7f graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.24.9.txt000066400000000000000000000002011514115062600240120ustar00rootroot0000000000000006f556766da9333ddcf1d75e04707501e9f36420870da5df72e4c3e20d757fb8e157d3ea5fae5650090efea305bd7fce790cc5829ec08d322022c8f3c0c4ebff graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.25.0.txt000066400000000000000000000002011514115062600240020ustar00rootroot000000000000005cefd636eae11fec8127d2245d9cc2d630492311ab55ccae464058ea83920f24342d64d22afff325a1132f5a55f4c0721cc77077311b2ad906a39d4a7a6ec425 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.25.1.txt000066400000000000000000000002011514115062600240030ustar00rootroot000000000000002ce7d2337ba0ce219cb8c8cd80f39b6db3a523175ff1ef09410b01a4f6f19139b19af3b54174d8cd09ce6fdb0e7eb526825fc50433d64c3db36d2ddee916fb2c graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.25.2.txt000066400000000000000000000002011514115062600240040ustar00rootroot0000000000000039a4910d324054894d3b6695b9651040f7fdbb418450827409c6b8361a628a16a5a7ae6f1bb2d57b9b3784218862f7428572a0baf9d18ac3b9c0e5267018e479 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.26.0.txt000066400000000000000000000002011514115062600240030ustar00rootroot00000000000000c5e6955c2878ed82c168a6f73abee8e6ed5a79a6f57bc25131a44603e88c4bab17726f1f6c1c856ad3aeae96f8a19d38fae864019f6d2b64306f9c4e5433a6ce graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.26.2.txt000066400000000000000000000002011514115062600240050ustar00rootroot00000000000000c274b2993b36849ef08083eed7b9712d88eada464a893bdc123bbb7dd46e90417e3e3206e377d00a23a730a04170440375c6f2198418043d2fdc28baa0463e28 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.26.3.txt000066400000000000000000000002011514115062600240060ustar00rootroot000000000000001913e81ac6a1f6055b023b4e33d0188f801c722e4f6f18a410f2776169e1fa1dc87fad1e89416305ff4e08e352f9b7107a61fe9edf65d215ec2547cc4a78eb81 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.26.5.txt000066400000000000000000000002011514115062600240100ustar00rootroot0000000000000034218544d143f790a5424f12ec792f04b2d4e0e4244d549cb9baa6c3eacc1b68d2f0960a678479507c893050804d5cad916ba3ad9f8caec9c9958c7c02fd0dd5 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.27.0.txt000066400000000000000000000002011514115062600240040ustar00rootroot000000000000000f796160fcacd74386d1e955cafa7b51815593efcaa72d5f1583f024a4732abd8e97e6c2a9d83ff6a5f3f6c9906f367a002e11455db16fbbd71f4538c2552411 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.27.1.txt000066400000000000000000000002011514115062600240050ustar00rootroot00000000000000c0ea2ec42b02b4f03f6499dfaa153f53814f0b91638146e4d3b3f9021da2df1fd3bf8dbac150091d4ca988e2271addcd263624999700d16c53b77ee4dd430038 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.27.2.txt000066400000000000000000000002011514115062600240060ustar00rootroot00000000000000ffdaed66f84e10c2118ab0b4db4f2d958412380851edfc84c4b3c87f1b6c108399a0655912d750fdcc17393df7d803a345d02569846b8ef6fdd6a3f5d3ac8f83 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.27.4.txt000066400000000000000000000002011514115062600240100ustar00rootroot00000000000000aef6b82f70e6f264f8a762d3a2fce3d4b2f6779b07d91f15102a11dca76f0c1fcbfdaa46c436fe3407683a3f194a1bb39578934cdb8cbb10589c8c7526508228 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.27.5.txt000066400000000000000000000002011514115062600240110ustar00rootroot000000000000005107cca81a11674de03cc65f0c3a13389c4538225b10a8bcc0c6fc4aae2ec08e7c01e18ad5f86fa5db5fde01608a695fbdada4ea98fb4041dd4a3a7ed5019262 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.27.6.txt000066400000000000000000000002011514115062600240120ustar00rootroot00000000000000ecf249015ae4da921afc1291cbc7c8519804d47895d7de771760fb8c76a9bc19f3eba7e0d7cd07beb5a47c27974340d94b9dba1f1bec17428728f802cf157df2 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.28.0.txt000066400000000000000000000002011514115062600240050ustar00rootroot0000000000000046190abe8d74973ecc67c365029c861ef0b06ab06adad979cacf8dafa87df0b8a1b297ebc58867a62674e06afd9663467641e60da6a3e7c4ab60859fae409a36 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.28.1.txt000066400000000000000000000002011514115062600240060ustar00rootroot000000000000001393056efd26d98854bb54e345e9bb3dc5032648724540f5aa759c92e1a3076f7317a64e9a4275658b150b81e53e06185025848f467ea6d92af805197a6c9660 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.29.0.txt000066400000000000000000000002011514115062600240060ustar00rootroot000000000000008b7c83c448d7cadf1223c75f495b1342809f703374433a54cd184d5d4c2e934bf5f1ab3cebea1c901de42780e7a73a6f1fdea575400b32376efd2ef1befd0d63 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.29.1.txt000066400000000000000000000002011514115062600240070ustar00rootroot000000000000002f6aad9879bdf10f3089102c0f37b79233d3b6dd76267a3c52b286340f723723cc4aa0642eda555ce640e615c8af1b7c815db83160f00ef389aef949ebc62bb6 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.29.10.txt000066400000000000000000000002011514115062600240670ustar00rootroot000000000000002b45187577bcea131660cc2da1a7c846e61e6b5e3d6d4cd23890ce769b510feac4961c63412f4b8031dff232ae91ed62a97ea6824705eb08d76348e1680c907f graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.29.11.txt000066400000000000000000000002011514115062600240700ustar00rootroot00000000000000ea5f544ddad2a88d1813629567dbaa3c6e0a11408ddc0c31e0b2129bc599aa1af0b791b0307944d2d26689f58f611b8c0aaac14f85b7b5c9cd5c1b5938499a90 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.29.12.txt000066400000000000000000000002011514115062600240710ustar00rootroot00000000000000fea66a1684747ef85ea4d086f292cbb605ca2f28b0a705b47ae68b6701bd97c9ef462451767cab04b70516ac38b8a55069775803abc5c9289633ed1b73e19144 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.29.13.txt000066400000000000000000000002011514115062600240720ustar00rootroot00000000000000b87205dd28a9282ecd60724ca79b4e7e278e49388b6f3129a750fc0fc0da5ab7eaad8b58316d4a17227e05df65c9b673f8f49744509c4edaba49bfe8ce4d1d1b graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.29.14.txt000066400000000000000000000002011514115062600240730ustar00rootroot000000000000006267bbd7206fa50e69e96bfd7143ac118bcc51cc3a91b667499324f856d1894dec2a276603230a0661538989d4153f50e323b70bcf37982fd6bce8ed28ec7715 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.29.2.txt000066400000000000000000000002011514115062600240100ustar00rootroot0000000000000060d43612eaedfb0130e4e4c27f6f4239d6f5a1febca6eecd799b14923da2684830c75b5807d0c8894fa5194efa65b2800093147a4222090e60e532858315ba32 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.29.3.txt000066400000000000000000000002011514115062600240110ustar00rootroot0000000000000040aa14f24d3a3151de2015c5b3cd58a98ea7a2c0fccaa4663700bd0c804d68a129b9ceb63fc07b288ea814c74eeb4f20c82768781c99b8e80908531fb114f0d4 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.29.4.txt000066400000000000000000000002011514115062600240120ustar00rootroot00000000000000bfbc862b5f4f3e5a06517713e3bb618dce6b3e3f6feded200634ce97344651a98e408094cf7c02ee711008ded100049b3e743526f374f6ca7f9d38bccd08d004 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.29.5.txt000066400000000000000000000002011514115062600240130ustar00rootroot000000000000009c351ce7366d67f085125834bc82a0b4696c99a2398846b2536db06eff17aa041661ef4abe4adb98157724223a59853ce8d8c3fc4cf5b4447e1e9ec468f7eb22 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.29.6.txt000066400000000000000000000002011514115062600240140ustar00rootroot000000000000004c58ee6ebe6c1ceffd78e5802a965f54b7ed278c0e5069c6b2bac589ca59e543030456ac52827d3b89ddfe8019944336a519de6b529aaa982cec30e62326fe05 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.29.7.txt000066400000000000000000000002011514115062600240150ustar00rootroot000000000000008e1846cdd9f5d62fc2d72fe44b9270a1e9ab30a325430b7b1874c537e1df37d8f5d64db4ad26b96413ff7ccaf9d9a5d1f05da77c28dd6f61ffa04ec7eb39a2a3 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.29.8.txt000066400000000000000000000002011514115062600240160ustar00rootroot0000000000000042b3c1ff9c885cc981ac12e313bcc8a977d90a6baab2224e941ae59a435ee6e6c63af0d93f4c08879209222a9fbf83d93befa8d676c3ae73c56daf6c7024af91 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.29.9.txt000066400000000000000000000002011514115062600240170ustar00rootroot00000000000000a9b6a9d80900aefe10c6d88081a0f9245455517785b3a71e9ebb940f00f1c805e3d212ed2dba06989089151f2db676394454db3bd8e9dccf73b5ef294ffe3bdf graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.3.0.txt000066400000000000000000000002011514115062600237160ustar00rootroot00000000000000cf7d74176481decbc4b8eea28a1133076fd184856bbee95f583f9c0818771028ec65d55f7f711df439faf5c1c85a47f22e7f4165665261922aa8b6e0d0d950d4 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.4.0.txt000066400000000000000000000002011514115062600237170ustar00rootroot00000000000000b51cb1d642400b5a3099d0b9194b82050d812d4f1ba782c707f7f3090931439baf241e4eae745452399f48c60de64082689069cbf3fb1b371c943f2fd493dfe7 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.4.1.txt000066400000000000000000000002011514115062600237200ustar00rootroot00000000000000e565badaef42bdcf0c9dd679947d46bbb9149dfab40ed481f5195df6e24df4844df2c7fc333df52b7fac05d1008353d99d7930591bcaace4055d41bdd1358e08 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.4.2.txt000066400000000000000000000002011514115062600237210ustar00rootroot0000000000000026805a034902ede828fbf023b88aa7a9c03f0ee6dfc8e8dafb1404fdc1c64959a5b952d794d356ed08f05ce1e1bcd2d1be48054578003885ba91ad002d94c470 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.4.3.txt000066400000000000000000000002011514115062600237220ustar00rootroot000000000000001922b4218dc1549263354398c358fd79146de111b8e01d32408d0bcac382f4b90e27106e6338e36e9e36175761e8c0a55a2c86b01239cd64ff7a2ea9c2cba5ee graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.4.4.txt000066400000000000000000000002011514115062600237230ustar00rootroot0000000000000065231e030a03fb97fe4bb9d57d3c6586f8a243cc164dcf9e9119046a1ae96ec527d498639a1e434654747e236f412130219a3513152e9190974d2e0a8f00af2b graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.4.5.txt000066400000000000000000000002011514115062600237240ustar00rootroot000000000000002934e5e305b16beca9e37e6b55f4969e24877c86e68c2825ed8b8fae1e84c3ee97ecf3e9450c5d34d1bc1410a32c045f60ca65d544a9c960ab1d3ef5fb5ec18d graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.4.6.txt000066400000000000000000000002011514115062600237250ustar00rootroot000000000000002956ec0ff31daf1aa6c6b7078ca56807ebab9f24cf8be2cca2bb2e0da164ecb0efbe6f8a37751af0179ffd84c90243e136363a47db18bfa0bcc7acb1a264d903 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.4.7.txt000066400000000000000000000002011514115062600237260ustar00rootroot00000000000000f4d1816ae87e21d2d1f8d7ab1ba75606f4714259ef8190e8d24fe9dfa708c15311fddad571c6815b8bb4e324377a0634983f262f3d4bd6068625d71401f0d850 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.4.8.txt000066400000000000000000000000001514115062600237240ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.5.0.txt000066400000000000000000000002011514115062600237200ustar00rootroot00000000000000a2b3e35485b4cbd0656aa0b6a6583809e8db9364a913ef94415c40d26fb8b55201f77d91cc8c411f45e4ebf949530262478a99e4655a4573f3cf709f82a43ab7 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.5.1.txt000066400000000000000000000002011514115062600237210ustar00rootroot00000000000000a7a844b3e17989a6a4b7638627bc8045b0ce68330e683c03af4960470aaf53c381490bf16e1fb216d79c1c7db528e634d1c9559a1cc9635a39cf3bda84cf4502 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.5.2.txt000066400000000000000000000002011514115062600237220ustar00rootroot000000000000006578f9ddf46edd80fe156ac11799f1e979c581affbddca2ecadb4246934722666677a88e648e3d4799008f9c482f5ff4477e8cb62b4838196d07bf6665f64074 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.5.3.txt000066400000000000000000000002011514115062600237230ustar00rootroot0000000000000026d1e4db3e66dc9469053d424c29cd5a02f9d91d16d5db2eede9fe6c924640dedf27e304a99b38841550eaa1d0a1343b3f17cea0f40b6277d0f94a9df5257130 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.5.4.txt000066400000000000000000000002011514115062600237240ustar00rootroot0000000000000060c95c06fdf6b106683cd87ce993f2b8f6903f85050826d26bd5d579144c4ab71068166c7e9d6c47577c38cb93b2f4095f87b60a69e8ff391338d4c935c0e5e1 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.5.5.txt000066400000000000000000000002011514115062600237250ustar00rootroot000000000000008657b73725fe9390deeaf37f5d6a902cbd2939f8039a9ef0e96d3a7000ab5f835023aa323dad399ef8a00c8686dca5397295b8aadaa9367105976720c9dc9e61 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.5.6.txt000066400000000000000000000002011514115062600237260ustar00rootroot00000000000000928afc8c4526b1869a9e88b2e62115a97d9a062e5bc36a3393a85d887a038da293a08e9baa828d6121a1d0a03974aff7b91bed7cb38d30ebe5dbb5e3e8f96cf7 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.5.7.txt000066400000000000000000000002011514115062600237270ustar00rootroot000000000000008b59d45fd5bb4debf257f4215fcc0e304ac31bcf814cb2684fa49fd1696fbad35d88f6e578c502f927819bffd1efce4dd0909e59a51ba6a4af53618e5ff20165 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.5.8.txt000066400000000000000000000002011514115062600237300ustar00rootroot000000000000000119e593bdb84bc500b61bc4f4712d5f15b33f5fa5e6860f9e0ecdc0215ef7e61dd53472f5b03fee5ff149649712e9cf1ffc25141875c9cc4eab0cb2c9ec626b graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.5.9.txt000066400000000000000000000002011514115062600237310ustar00rootroot00000000000000d82db8271dd1bae9d6b49b0e3c9c9446dc95cdaa1405b99af6bf65c551dc8994c445137ec98e11c8b61363b9490ea2fc22ea98f966dbf645f0dcbef2419f3220 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.6.0.txt000066400000000000000000000002011514115062600237210ustar00rootroot000000000000000fa5c3a16722351ac3b57f54b2cab3dfced63a5cdf1b90d12824e6b69424135d4ae0187089b6b7cc27e5a47f1bc89e2ee111001142789eb1bdab062e048d466a graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.6.1.txt000066400000000000000000000002011514115062600237220ustar00rootroot000000000000007da501aaf5560330a34d52d8df2170b2dc1ad307b922abe2d6a2da117c3dad8281385bcd8a092a4ed6642e7bb91e114d552c859511523e8f719fca5ee50e8171 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.6.2.txt000066400000000000000000000002011514115062600237230ustar00rootroot00000000000000804a6575faed960eb1081ba5988fcf53d89c55f68b251397aa5a59fed1e9e0e4272bf18a27dfa03ea15d72271893be93e40423b54a8f2eece9a314ede8a5d517 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.6.3.txt000066400000000000000000000002011514115062600237240ustar00rootroot000000000000005e8d1483abc2f5e65e0cd7dddcf826b0fe2dda0166ae2e13e107bf10c0268c0267b21b851e4f9671877fb5a0cf9a71081e2536eaa76d3f5ee2adf9e899b6d751 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.6.4.txt000066400000000000000000000002011514115062600237250ustar00rootroot00000000000000c4cbd897270ecd150eb59784d7a568c61e9d9f7c9ad9a99cd7547be857cfde0ca268f1d6d0ce726d4f6003882253ab6a63d334e406a1fae4cb55b07fc9f61b38 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.6.5.txt000066400000000000000000000002011514115062600237260ustar00rootroot00000000000000ad004946d3f432fe8c3694e8a8410d6ce6f29a7fbed0d785d05e488e6ecb8ccf523d2d73514fcee8481ceb66053d9eb88297f4fab25d031938c08ee2dcc58968 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.7.0.txt000066400000000000000000000002011514115062600237220ustar00rootroot00000000000000377a41d7a5831d0d20176bd7ebc7ad4ba24cdf7bdbc2879eb35844ece7e48390ca598061b58b6fe60b965af7f9097b353dc30b9ce89180ac03bc54a4fe3ee59d graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.7.1.txt000066400000000000000000000002011514115062600237230ustar00rootroot000000000000000c22a1050619fc84f024a8928831c8b1dcc733b05742b249c2928d4d97f7bc4106839a1038e92671b42ac8bf2c0647dcdc09a6c7cd8ef240a67f841bf7ed3ffc graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.7.10.txt000066400000000000000000000002011514115062600240030ustar00rootroot000000000000009cc3900fa9b04a667146595141586592e1c7f72d4fb88d6af8e95b74d50cb8c096e5abd00d35e295a66dbdc7774b0afd311a20747e6051d786da36564282aeca graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.7.11.txt000066400000000000000000000002011514115062600240040ustar00rootroot00000000000000983b45f0aa5a7905d98cbe6571a4028fcbc115eff42deaa378edf5591b546a6b5e5cddfbbb39b5d363ffc16147dc72e056bd66c8f3e8adba97e58064f2c12695 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.7.12.txt000066400000000000000000000002011514115062600240050ustar00rootroot00000000000000739bd0c352ef4d183426504ddd41456d05191fcf3230214e9e84d7011a84c65263ec3299d9ef42a829e49d370ece69d15cd3f3d913bf63043fd95f55b1f6ac48 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.7.13.txt000066400000000000000000000002011514115062600240060ustar00rootroot000000000000007d9aae99f424473e4e07aa6e119645027d8da99f062c162cc7f67929fddd886192745383b70e053e2df6ae2176208c5781b92203f8a7194c1390ec13dce36a12 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.7.2.txt000066400000000000000000000002011514115062600237240ustar00rootroot000000000000005c8539eca13d99875f50d8a77c27327ab0c14eea88e31e0ffb78a81990f1e7bcdb34b2cc2ba0ec647cbc36ce74c4d990f6483381388166d4e6a9191c77c69d4d graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.7.3.txt000066400000000000000000000002011514115062600237250ustar00rootroot000000000000007c1076a9065fbb7fe69796e8bf33454e1ed59aac0fb967b08e7756193d3a9aa41ba4f92e36ddb9e7d50c73cce42d516c0d0d2492ba1ec3f1af005597513c6f1b graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.7.4.txt000066400000000000000000000002011514115062600237260ustar00rootroot000000000000003ab8ffacb8cd9ee0b22695b63e8ba4a4602007f54928cf4745b2d6a8866bc8291806ab492526e4ebb2c297ec27a7bb605afe674d440ee3027d4c192dc06978a9 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.7.5.txt000066400000000000000000000002011514115062600237270ustar00rootroot000000000000003ec4d622f1a1d2efc910872db03f79ca43c35294fa7a93adf5283c68faf21cb7b2a9116d47a8b0e49f174276b84751d8e9e16a1a51bc20ab5ea8a40df2b41183 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.7.6.txt000066400000000000000000000002011514115062600237300ustar00rootroot000000000000002f7fd5c57b8564bb9af06d82449a46957c6adf723dad61092d6e2fa91e79966fa15f6dbe49df90e99f56d51c9808c1c5e6f25c80855728923a69bd77646d894a graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.7.7.txt000066400000000000000000000002011514115062600237310ustar00rootroot00000000000000a3625b161363afb04bbf497ae1af1cc3c580753c95a2d281c1788f162fb146c92f4b73b7faf053cc73df6db61e90b4eb37d3cbe7247b9bc6edc92f217f6e691c graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.7.8.txt000066400000000000000000000002011514115062600237320ustar00rootroot000000000000003aea6af5e7ed2a5ebab879d3ea65fea6b60be60d7b3f3663d03831797b3970beb896a82a035b8782c693b0f9e36c9f3d3b937ea09b165ee1478f393881f5ba18 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.7.9.txt000066400000000000000000000002011514115062600237330ustar00rootroot00000000000000a9098604f33ebd469909ee7be84dbe8636adce7e91199c442d95ddc35da4773fe7117057cf1a0449611625c902de03a26d015f82b8917c97b8bbe5e4f1915d4e graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.8.0.txt000066400000000000000000000002011514115062600237230ustar00rootroot0000000000000066ee5869510364c01668107c6343085e9f2755a07fcd4fb65d251e57773fa317c24bb2a3da091f796523a30bad6f6d310bb50d0c57d3a29a3ff4bc65215e5c2d graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.8.1.txt000066400000000000000000000002011514115062600237240ustar00rootroot00000000000000924658f0f8cbd15afa5210d46c2e2c13a768ad0bb49e3bef00096b1d8836600a5e00d42ed7b3f04b49870c4042f97916e13f1a51d3ea367fdc5945fd4402df64 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.8.2.txt000066400000000000000000000002011514115062600237250ustar00rootroot0000000000000078623f5ef2359e170dc04eaaf1b5397b74082f962b35bda766e2574218ad55c2b490884f1f76e7bd66acd091f68539b6ecf5f8eedc63190c26aeedb976278562 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.9.0.txt000066400000000000000000000002011514115062600237240ustar00rootroot00000000000000cee3e8dfe8a1c7016ef9578edab3c81dc0ad7f87df1e97a860ea2ef455fb2ba88ecf8120529b0feea683ce623ce2b7e6172e97bf2850f73ba32b858410661676 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.9.1.txt000066400000000000000000000002011514115062600237250ustar00rootroot00000000000000ecde6cf51e0eb328ee6185334acc9f16b1ccae2568a8ea7f45d501a14a66ea2af76b41e240dd255e4d05e8fd5b8f3d6866c02daa7f30a6e0b1053ced95bee1e4 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.9.10.txt000066400000000000000000000002011514115062600240050ustar00rootroot000000000000008a7526d55d9dd3e4ba042b4e950105a9b99b09373fdecf8017ffb002bcb1aacbef6ebdd213588ec96558e49c2f23e9136f3167c928156a815045176ff2161b70 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.9.11.txt000066400000000000000000000002011514115062600240060ustar00rootroot000000000000006ff8342db34dd6f7a7a7e11b2925df9205810ca4ce15ba2ec8a2ec79eafebdd0c1d7c706f82d6b5fe536d7b19c4ab4eda2c0129f1d0db9d6f597a2c92f5b38d3 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.9.12.txt000066400000000000000000000002011514115062600240070ustar00rootroot00000000000000ce8e9583918491e341bc47eb6a08712ea310d91361f7324e9e10574e60cde062deecf58306204267a08bb8a8ffdbb85fe73fa8382f91fb51cbc6f3ea7919df9f graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.9.13.txt000066400000000000000000000002011514115062600240100ustar00rootroot00000000000000074391c2bdc294a1ec027d043a6bf172f70bf5ff75f0062821cabd5ce7469c88a3b60a0ed644e8bae178fdd9db26acef20b0721fcc708c1825da256ec5f2b4fb graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.9.2.txt000066400000000000000000000002011514115062600237260ustar00rootroot00000000000000a2370a8984ef9acb0664cc982bfc22fa2fc33df631b9e5b80fa8a2a9e8ef68d0c296f1f297e405c44b123445de67edc83dfcd6960b53063b0add7906202e5fb8 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.9.3.txt000066400000000000000000000002011514115062600237270ustar00rootroot00000000000000f70be733046923edab517e4bfbada4dcbc9f9234d4706bce59a8d9f23100545d994e03c49ebe525ad4435d33b5abfe25191c690fd20d8e16ea0aa0979438f21e graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.9.4.txt000066400000000000000000000002011514115062600237300ustar00rootroot00000000000000b9b8771b0dea2b4fe2d426ae0c84a522884c2b36cedd1095ded49fef9bc90c0373d70ff871635223f07f12ac9a067978d91d4b42292070e44f2a5f223e5f4d88 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.9.5.txt000066400000000000000000000002011514115062600237310ustar00rootroot00000000000000de80808d62c6f49cc391ad2f0badc6040ac174271656649f0837aa11bb9188a5faa7b33c6a7640129f4b2df4cb1a00c823efcceff4f96aeac77655be4c51bd20 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.9.6.txt000066400000000000000000000002011514115062600237320ustar00rootroot000000000000003e14feb6dd6a14d26c76f3acce640845f786961d4718730d4509e3b72990cc2a184db8e6d17f612fdcb1cd5f5e85a02f72a8bc69124b7173f0019664baa5cdf2 graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.9.7.txt000066400000000000000000000002011514115062600237330ustar00rootroot00000000000000aac0fdf50112fb896616ce4dffe1e427dce076f7d13cfa6b6f62ef97c26eee56c3ff4e8e2e088ecf3dee263d841c294651dc65514159b3f4cab0cd6e53f1e0de graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.9.8.txt000066400000000000000000000002011514115062600237340ustar00rootroot000000000000004da4d21bec267d782224732143535a3d9bd210c1b5ed9df56f9b21ffc9415ac04687b2ff01c4c6d8dd0c82613ce323db08e96b727e95a62855c47112c49d07ab graphql-ruby-2.5.19/guides/pro/checksums/graphql-pro-1.9.9.txt000066400000000000000000000002011514115062600237350ustar00rootroot00000000000000055f07c55bf22cda1ac6ce58b8acb74812ae150c3235ba7769b47933f827b4956fe9c036a40d48a4051554d161d9274c21dac57694daff1a0a8ee2558b838f2f graphql-ruby-2.5.19/guides/pro/checksums/pro-1.26.4.txt000066400000000000000000000002011514115062600223530ustar00rootroot00000000000000d1099e2f64e9ad256d86a624904a6a5fdd28241bbfa478fcd25bc32af53e5200892ce8e4808be0bcdd8b42a74e5463565002eb8810928748d3390c5c7f4eb736 graphql-ruby-2.5.19/guides/pro/checksums/pro-1.27.3.txt000066400000000000000000000002011514115062600223530ustar00rootroot00000000000000eaecab7fe1956b08d6bd11009bc984c4876acc93ba1977b2da2af3dc1c0dfd6974d1d98a25fd8005aea6890a4cb01bea0d5d9621dfa97f549e7b9c9d3afd944d graphql-ruby-2.5.19/guides/pro/checksums/pro-1.27.7.txt000066400000000000000000000002011514115062600223570ustar00rootroot000000000000003cee596b97b156879fce33e22a5e6ccea3bc2a4f44d27451d4a32d910cb108bce0f83a2121fd7b247ade237040a86b824b37f63dc2cff7e26b73398fb4468d27 graphql-ruby-2.5.19/guides/pro/dashboard.md000066400000000000000000000051621514115062600205630ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: GraphQL Pro title: Dashboard desc: Installing GraphQL-Pro's Dashboard index: 4 pro: true --- [GraphQL-Pro](https://graphql.pro) includes a web dashboard for monitoring {% internal_link "Operation Store", "/operation_store/overview" %} and {% internal_link "subscriptions", "/subscriptions/pusher_implementation" %}. ## Installation To hook up the Dashboard, add it to `routes.rb` ```ruby # config/routes.rb # Include GraphQL::Pro's routing extensions: using GraphQL::Pro::Routes Rails.application.routes.draw do # ... # Add the GraphQL::Pro Dashboard # TODO: authorize, see below mount MySchema.dashboard, at: "/graphql/dashboard" end ``` With this configuration, it will be available at `/graphql/dashboard`. The dashboard is a Rack app, so you can mount it in Sinatra or any other Rack app. #### Lazy-loading the schema Alternatively, you can set up the dashboard to load the schema during the first request. To do that, initialize `GraphQL::Pro::Routes::Lazy` with a string that gives the fully-qualified name of your schema class, for example: ```ruby Rails.application.routes.draw do # ... # Add the GraphQL::Pro Dashboard # TODO: authorize, see below lazy_routes = GraphQL::Pro::Routes::Lazy.new("MySchema") mount lazy_routes.dashboard, at: "/graphql/dashboard" end ``` With this setup, `MySchema` will be loaded when the dashboard serves its first request. This can speed up your application's boot in development since it doesn't load the whole GraphQL schema when building the routes. ## Authorizing the Dashboard You should only allow admin users to see `/graphql/dashboard` because it allows viewers to delete stored operations. ### Rails Routing Constraints Use [Rails routing constraints](https://api.rubyonrails.org/v5.1/classes/ActionDispatch/Routing/Mapper/Scoping.html#method-i-constraints) to restrict access to authorized users, for example: ```ruby # Check the secure session for a staff flag: STAFF_ONLY = ->(request) { request.session["staff"] == true } # Only serve the GraphQL Dashboard to staff users: constraints(STAFF_ONLY) do mount MySchema.dashboard, at: "/graphql/dashboard" end ``` ### Rack Basic Authentication Insert the `Rack::Auth::Basic` middleware, before the web view. This prompts for a username and password when visiting the dashboard. ```ruby graphql_dashboard = Rack::Builder.new do use(Rack::Auth::Basic) do |username, password| username == ENV.fetch("GRAPHQL_USERNAME") && password == ENV.fetch("GRAPHQL_PASSWORD") end run MySchema.dashboard end mount graphql_dashboard, at: "/graphql/dashboard" ``` graphql-ruby-2.5.19/guides/pro/encoders.md000066400000000000000000000103041514115062600204300ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: GraphQL Pro title: Encrypted, Versioned Cursors and IDs desc: Increased opacity and configurability for Relay identifiers index: 6 pro: true --- `GraphQL::Pro` includes a mechanism for serving encrypted, versioned cursors and IDs. This provides some benefits: - Users can't reverse-engineer node IDs or connection cursors, removing a possible attack vector. - You can gradually transition between cursor strategies, adding encrypting while supporting any "stale" encoders which clients already have. `GraphQL::Pro`'s encrypted encoders provide a few security features: - Key-based encryption by `aes-128-gcm` by default - Authentication - Nonces for cursors (but not IDs, that would be silly) ## Defining an Encoder Encoders can be created by subclassing `GraphQL::Pro::Encoder`: ```ruby class MyEncoder < GraphQL::Pro::Encoder key("f411f30...") # optional: tag("81ce51c307") end ``` - `key` is the encryption key for this encoder. You can generate one with: `require "securerandom"; SecureRandom.bytes(16)` - `tag`, if provided, is used as authentication data or for disambiguating versioned encoders ## Encrypting Cursors Encrypt cursors by attaching an encrypted encoder to `Schema#cursor_encoder`: ```ruby class MySchema GraphQL::Schema cursor_encoder(MyCursorEncoder) end ``` Now, built-in connection implementations will use that encoder for cursors. If you implement your own connections, you can access the encoder's encryption methods via {{ "GraphQL::Pagination::Connection#encode" | api_doc }} and {{ "GraphQL::Pagination::Connection#decode" | api_doc }}. ## Encrypting IDs Encrypt IDs by using encoders in `Schema.id_from_object` and `Schema.object_from_id`: ```ruby class MySchema < GraphQL::Schema def self.id_from_object(object, type, ctx) id_data = "#{object.class.name}/#{object.id}" MyIDEncoder.encode(id_data) end def self.object_from_id(id, ctx) id_data = MyIDEncoder.decode(id) class_name, id = id_data.split("/") class_name.constantize.find(id) end end ``` Note that IDs are _not_ encrypted with nonces. This means that if someone can _guess_ how IDs are constructed, they can determine the encryption key (a kind of [known-plaintext attack](https://en.wikipedia.org/wiki/Known-plaintext_attack)). To reduce this risk, make your plaintext IDs unpredictable, for example, by appending a salt or obfuscating their content. ## Versioning You can combine several encoders into a single chain of versioned encoders. Pass them to `.versioned`, newest-to-oldest: ```ruby # Define some encoders ... class NewSecureEncoder < GraphQL::Pro::Encoder # ... end class OldSecureEncoder < GraphQL::Pro::Encoder # ... end class LegacyInsecureEncoder < GraphQL::Pro::Encoder # ... end # Then order them by priority: VersionedEncoder = GraphQL::Pro::Encoder.versioned( # Newest: NewSecureEncoder, OldSecureEncoder, # Oldest: LegacyInsecureEncoder ) ``` When receiving an ID or cursor, a versioned encoders tries each encoder in sequence. When creating a new ID or cursor, the encoder always uses the first encoder. This way, clients will receiving _new_ encoders, but the server will still accept _old_ encoders (until the old one is removed from the list). `VersionedEncoder#decode_versioned` returns two values: the decoded data _and_ the encoder which successfully decoded it. You can use this to determine how to process decoded data. For example, you can switch on the encoder: ```ruby data, encoder = VersionedEncoder.decode_versioned(id) case encoder when UUIDEncoder find_by_uuid(data) when SQLPrimaryKeyEncoder find_by_pk(data) when nil # `id` could not be decoded nil end ``` ## Encoding By default, encrypted bytes is stringified as base-64. You can specific a custom encoder with the `Encoder#encoder` definition. For example, you could define an encode which uses URL-safe base-64 functions: ```ruby module URLSafeEncoder def self.encode(str) Base64.urlsafe_encode64(str) end def self.decode(str) Base64.urlsafe_decode64(str) end end ``` Then attach it to your encoder: ```ruby class MyURLSafeEncoder < GraphQL::Pro::Encoder encoder URLSafeEncoder end ``` Now, these node IDs and cursors will be URL-safe! graphql-ruby-2.5.19/guides/pro/home.md000066400000000000000000000002611514115062600175570ustar00rootroot00000000000000--- layout: guide doc_stub: false outbound_url: https://graphql.pro title: GraphQL::Pro Home section: GraphQL Pro desc: Overview of GraphQL::Pro features index: 0 pro: true --- graphql-ruby-2.5.19/guides/pro/installation.md000066400000000000000000000044111514115062600213310ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: GraphQL Pro title: Installation desc: Get started with GraphQL::Pro index: 1 pro: true --- `GraphQL::Pro` is distributed as a Ruby gem. When you buy `GraphQL::Pro`, you'll receive credentials, which you can register with bundler: ```sh bundle config gems.graphql.pro #{YOUR_CREDENTIALS} ``` Then, you can add `graphql-pro` to your Gemfile, which a custom `source`: ```ruby source "https://gems.graphql.pro" do gem "graphql-pro" end ``` Then, install the gem with Bundler: ```sh bundle install ``` Then, check out some of `GraphQL::Pro`'s features! ## Updates To update `GraphQL::Pro`, use Bundler: ```sh bundle update graphql-pro ``` Be sure to check the [changelog](https://github.com/rmosolgo/graphql-ruby/blob/master/CHANGELOG-pro.md) between versions! ## Dependencies `graphql-pro 1.0.0` requires `graphql ~>1.4`. The latest version requires `graphql =>1.7.6`. ## Verifying Integrity You can verify the integrity of `graphql-pro` by getting its checksum and comparing it to the [published checksums](https://github.com/rmosolgo/graphql-ruby/blob/master/guides/pro/checksums). Include the `graphql:pro:validate` task in your `Rakefile`: ```ruby # Rakefile require "graphql/rake_task/validate" ``` Then invoke it with a version: ``` $ bundle exec rake graphql:pro:validate[1.0.0] Validating graphql-pro v1.0.0 - Checking for graphql-pro credentials... ✓ found - Fetching the gem... ✓ fetched - Validating digest... ✓ validated from GitHub ✓ validated from graphql-ruby.org ✔ graphql-pro 1.0.0 validated successfully! ``` In case of a failure, please {% open_an_issue "GraphQL Pro installation failure" %}: ``` Validating graphql-pro v1.4.800 - Checking for graphql-pro credentials... ✓ found - Fetching the gem... ✓ fetched - Validating digest... ✘ SHA mismatch: Downloaded: c9cab2619aa6540605ce7922784fc84dbba3623383fdce6b17fde01d8da0aff49d666810c97f66310013c030e3ab7712094ee2d8f1ea9ce79aaf65c1684d992a GitHub: 404: Not Found graphql-ruby.org: 404: Not Found This download of graphql-pro is invalid, please open an issue: https://github.com/rmosolgo/graphql-ruby/issues/new?title=graphql-pro%20digest%20mismatch%20(1.4.800) ``` graphql-ruby-2.5.19/guides/pro/privacy.md000066400000000000000000000041111514115062600203020ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: GraphQL Pro title: Privacy desc: Privacy Policy for GraphQL::Pro index: 7 --- The following statement describes what data GraphQL::Pro collects during normal operation and how that data is used. ## What Data We Collect And How It's Used GraphQL::Pro collects the following kinds of data: Kind of Data | Use ------ | ----- Cookies | User authentication HTTP traffic information, such as IP address and bundler credential | Authentication, system operation User-provided profile information, such as contact email address and company name | Business communications, newsletter delivery (if signed up) Billing information | Recurring payment for GraphQL::Pro license The `graphql-pro` Ruby gem collects no data and never "phones home" for any purpose. GraphQL::Pro is not directed at children under the age of 13. If you are under age 13, please do not use GraphQL::Pro. ## Third-Party Services Use of GraphQL::Pro includes the following third-party services: Name of Service | Use ------|------ GitHub Pages | Website Hosting Digital Ocean | Cloud Application Hosting Heroku | Cloud Application Hosting Bugsnag | Application Monitoring New Relic APM | Application Monitoring Papertrail | Application Monitoring Stripe | Payment Processing Buttondown | Newsletter Management Google Apps | Company Infrastructure ## Data Security GraphQL::Pro does not collect ["Sensitive Personal Information"](https://gdpr-info.eu/art-9-gdpr/). GraphQL::Pro's systems are secured with strong, unique passwords and two-factor authentication is enabled wherever possible. You are responsible for using a strong, unique password to log into https://billing.graphql.pro. ## More Information This document is managed [on GitHub](https://github.com/rmosolgo/graphql-ruby/blob/master/guides/pro/privacy.md). You can use a GitHub account to watch for changes or subscribe to a [public RSS feed](https://github.com/rmosolgo/graphql-ruby/commits/master.atom). For questions, comments, or requests regarding this policy, please contact [info@graphql.pro](mailto:info@graphql.pro) graphql-ruby-2.5.19/guides/queries/000077500000000000000000000000001514115062600171635ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/queries/ast_analysis.md000066400000000000000000000103131514115062600221750ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Queries title: Ahead-of-Time AST Analysis desc: Check incoming query strings and reject them if they don't pass your checks index: 1 redirect_from: - /queries/analysis/ --- You can do ahead-of-time analysis for your queries. The primitive for analysis is {{ "GraphQL::Analysis::Analyzer" | api_doc }}. Analyzers must inherit from this base class and implement the desired methods for analysis. ## Using Analyzers Query analyzers are added to the schema with `query_analyzer`, for example: ```ruby class MySchema < GraphQL::Schema query_analyzer MyQueryAnalyzer end ``` Pass the **class** (and not an _instance_) of your analyzer. The analysis engine will take care of instantiating your analyzers with the query. ## Analyzer API Analyzers respond to methods similar to AST visitors. They're named like `on_enter_#{ast_node}` and `on_leave_#{ast_node}`. Methods are called with three arguments: - `node`: The current AST node (being entered or left) - `parent`: The AST node which precedes this one in the tree - `visitor`: A {{ "GraphQL::Analysis::Visitor" | api_doc }} which is managing this analysis run For example: ```ruby class BasicCounterAnalyzer < GraphQL::Analysis::Analyzer def initialize(query_or_multiplex) super @fields = Set.new @arguments = Set.new end # Visitors are all defined on the Analyzer base class # We override them for custom analyzers. def on_leave_field(node, _parent, _visitor) @fields.add(node.name) end def result # Do something with the gathered result. Analytics.log(@fields) end end ``` In this example, we counted every field, no matter if it was on fragment definitions or if it was skipped by directives. If we want to detect those contexts, we can use helper methods: ```ruby class BasicFieldAnalyzer < GraphQL::Analysis::Analyzer def initialize(query_or_multiplex) super @fields = Set.new end # Visitors are all defined on the Analyzer base class # We override them for custom analyzers. def on_leave_field(node, _parent, visitor) if visitor.skipping? || visitor.visiting_fragment_definition? # We don't want to count skipped fields or fields # inside fragment definitions else @fields.add(node.name) end end def result Analytics.log(@fields) end end ``` See {{ "GraphQL::Analysis::Visitor" | api_doc }} for more information about the `visitor` object. ### Field Arguments Usually, analyzers will use `on_enter_field` and `on_leave_field` to process queries. To get a field's arguments during analysis, use `visitor.query.arguments_for(node, visitor.field_definition)` ({{ "GraphQL::Query#arguments_for" | api_doc }}). That method returns coerced argument values and normalizes argument literals and variable values. ### Errors It is still possible to return errors from an analyzer. To reject a query and halt its execution, you may return {{ "GraphQL::AnalysisError" | api_doc }} in the `result` method: ```ruby class NoFieldsCalledHello < GraphQL::Analysis::Analyzer def on_leave_field(node, _parent, visitor) if node.name == "hello" @field_called_hello = true end end def result GraphQL::AnalysisError.new("A field called `hello` was found.") if @field_called_hello end end ``` ### Conditional Analysis Some analyzers might only make sense in certain context, or some might be too expensive to run for every query. To handle these scenarios, your analyzers may answer to an `analyze?` method: ```ruby class BasicFieldAnalyzer < GraphQL::Analysis::Analyzer # Use the analyze? method to enable or disable a certain analyzer # at query time. def analyze? !!subject.context[:should_analyze] end def on_leave_field(node, _parent, visitor) # ... end def result # ... end end ``` ## Analyzing Multiplexes Analyzers are initialized with the _unit of analysis_, available as `subject`. When analyzers are hooked up to multiplexes, `query` is `nil`, but `multiplex` returns the subject of analysis. You can use `visitor.query` inside visit methods to reference the query that owns the current AST node. Note that some built-in analyzers (eg `AST::MaxQueryDepth`) support multiplexes even though `Query` is in their name. graphql-ruby-2.5.19/guides/queries/backtrace_annotations.md000066400000000000000000000065761514115062600240570ustar00rootroot00000000000000--- title: Backtrace Annotations layout: guide doc_stub: false search: true section: Queries desc: Use the GraphQL backtrace for debugging index: 12 experimental: true --- `context` objects have a `backtrace` which shows its GraphQL context. You can print the backtrace during query execution: ```ruby puts context.backtrace # Loc | Field | Object | Arguments | Result # 3:13 | User.login | # | {"message"=>"Boom"} | # # 2:11 | Query.user | nil | {"login"=>"rmosolgo"} | {} # 1:9 | query | nil | {"msg"=>"Boom"} | ``` The backtrace contains some execution data: - `Loc` is the `line:column` of the field in the query string - `Field` is the `TypeName.fieldName` of the fields in the backtrace - `Object` is the `obj` for query resolution (used for resolving the given field), equivalent to `obj.inspect` - `Arguments` are the GraphQL arguments for field resolution (including any default values and variable values) - `Result` is the GraphQL-ready result which is being constructed (it may be incomplete if the query is still in-progress) ## Wrapping Errors You can wrap unhandled errors with a GraphQL error with `GraphQL::Backtrace`. To enable this feature for a query, add `backtrace: true` to your `context`, for example: ```ruby # Wrap this query with backtrace annotation MySchema.execute(query_string, context: { backtrace: true }) ``` Or, to _always_ wrap backtraces, add it to your schema definition with `use`, for example: ```ruby class MySchema < GraphQL::Schema # Always wrap backtraces with GraphQL annotation use GraphQL::Backtrace end ``` Now, any unhandled errors will be wrapped by `GraphQL::Backtrace::TracedError`, which prints out the GraphQL backtrace, too. For example: ``` Unhandled error during GraphQL execution: This is broken: Boom /Users/rmosolgo/code/graphql-ruby/spec/graphql/backtrace_spec.rb:27:in `block (3 levels) in ' /Users/rmosolgo/code/graphql-ruby/lib/graphql/schema/build_from_definition/resolve_map.rb:57:in `call' /Users/rmosolgo/code/graphql-ruby/lib/graphql/schema/build_from_definition.rb:171:in `block in build_object_type' /Users/rmosolgo/code/graphql-ruby/lib/graphql/schema/build_from_definition.rb:280:in `block (2 levels) in build_fields' /Users/rmosolgo/code/graphql-ruby/lib/graphql/field.rb:228:in `resolve' /Users/rmosolgo/code/graphql-ruby/lib/graphql/execution/execute.rb:253:in `call' /Users/rmosolgo/code/graphql-ruby/lib/graphql/schema/middleware_chain.rb:45:in `invoke_core' /Users/rmosolgo/code/graphql-ruby/lib/graphql/schema/middleware_chain.rb:38:in `invoke' /Users/rmosolgo/code/graphql-ruby/lib/graphql/execution/execute.rb:107:in `resolve_field' /Users/rmosolgo/code/graphql-ruby/lib/graphql/execution/execute.rb:71:in `block (2 levels) in resolve_selection' ... and 65 more lines Use #cause to access the original exception (including #cause.backtrace). GraphQL Backtrace: Loc | Field | Object | Arguments | Result 3:13 | Thing.raiseField as boomError | :something | {"message"=>"Boom"} | # 2:11 | Query.field1 | "Root" | {} | {} 1:9 | query | "Root" | {"msg"=>"Boom"} | {} ``` graphql-ruby-2.5.19/guides/queries/complexity_and_depth.md000066400000000000000000000222401514115062600237100ustar00rootroot00000000000000--- title: Complexity & Depth layout: guide doc_stub: false search: true section: Queries desc: Limiting query depth and field selections index: 4 --- GraphQL-Ruby ships with some validations based on {% internal_link "query analysis", "/queries/ast_analysis" %}. You can customize them as-needed, too. ## Prevent deeply-nested queries You can also reject queries based on the depth of their nesting. You can define `max_depth` at schema-level or query-level: ```ruby # Schema-level: class MySchema < GraphQL::Schema # ... max_depth 15 end # Query-level, which overrides the schema-level setting: MySchema.execute(query_string, max_depth: 20) ``` By default, **introspection fields are counted**. The default introspection query requires at least `max_depth 13`. You can also configure your schema not to count introspection fields with `max_depth ..., count_introspection_fields: false`. You can use `nil` to disable the validation: ```ruby # This query won't be validated: MySchema.execute(query_string, max_depth: nil) ``` To get a feeling for depth of queries in your system, you can extend {{ "GraphQL::Analysis::QueryDepth" | api_doc }}. Hook it up to log out values from each query: ```ruby class LogQueryDepth < GraphQL::Analysis::QueryDepth def result query_depth = super message = "[GraphQL Query Depth] #{query_depth} || staff? #{query.context[:current_user].staff?}" Rails.logger.info(message) end end class MySchema < GraphQL::Schema query_analyzer(LogQueryDepth) end ``` ## Prevent complex queries Fields have a "complexity" value which can be configured in their definition. It can be a constant (numeric) value, or a proc. If no `complexity` is defined for a field, it will default to a value of `1`. It can be defined as a keyword _or_ inside the configuration block. For example: ```ruby # Constant complexity: field :top_score, Integer, null: false, complexity: 10 # Dynamic complexity: field :top_scorers, [PlayerType], null: false do argument :limit, Integer, limit: false, default_value: 5 complexity ->(ctx, args, child_complexity) { if ctx[:current_user].staff? # no limit for staff users 0 else # `child_complexity` is the value for selections # which were made on the items of this list. # # We don't know how many items will be fetched because # we haven't run the query yet, but we can estimate by # using the `limit` argument which we defined above. args[:limit] * child_complexity end } end ``` Then, define your `max_complexity` at the schema-level: ```ruby class MySchema < GraphQL::Schema # ... max_complexity 100 end ``` Or, at the query-level, which overrides the schema-level setting: ```ruby MySchema.execute(query_string, max_complexity: 100) ``` Using `nil` will disable the validation: ```ruby # 😧 Anything goes! MySchema.execute(query_string, max_complexity: nil) ``` To get a feeling for complexity of queries in your system, you can extend {{ "GraphQL::Analysis::QueryComplexity" | api_doc }}. Hook it up to log out values from each query: ```ruby class LogQueryComplexityAnalyzer < GraphQL::Analysis::QueryComplexity # Override this method to _do something_ with the calculated complexity value def result complexity = super message = "[GraphQL Query Complexity] #{complexity} | staff? #{query.context[:current_user].staff?}" Rails.logger.info(message) end end class MySchema < GraphQL::Schema query_analyzer(LogQueryComplexityAnalyzer) end ``` By default, **introspection fields are counted**. You can also configure your schema not to count introspection fields with `max_complexity ..., count_introspection_fields: false`. #### Connection fields By default, GraphQL-Ruby calculates a complexity value for connection fields by: - adding `1` for `pageInfo` and each of its subselections - adding `1` for `count`, `totalCount`, or `total` - adding `1` for the connection field itself - multiplying the complexity of other fields by the largest possible page size, which is the greater of `first:` or `last:`, or if neither of those are given it will go through each of `default_page_size`, the schema's `default_page_size`, `max_page_size`, and then the schema's `default_max_page_size`. (If no default page size or max page size can be determined, then the analysis crashes with an internal error -- set `default_page_size` or `default_max_page_size` in your schema to prevent this.) For example, this query has complexity `26`: ```graphql query { author { # +1 name # +1 books(first: 10) { # +1 nodes { # +10 (+1, multiplied by `first:` above) title # +10 (ditto) } pageInfo { # +1 endCursor # +1 } totalCount # +1 } } } ``` To customize this behavior, implement `def calculate_complexity(query:, nodes:, child_complexity:)` in your base field class, handling the case where `self.connection?` is `true`: ```ruby class Types::BaseField < GraphQL::Schema::Field def calculate_complexity(query:, nodes:, child_complexity:) if connection? # Custom connection calculation goes here else super end end end ``` ## How complexity scoring works GraphQL Ruby's complexity scoring algorithm is biased towards selection fairness. While highly accurate, its results are not always intuitive. Here's an example query performed on the [Shopify Admin API](https://shopify.dev/docs/api/admin-graphql): ```graphql query { node(id: "123") { # interface Node id ...on HasMetafields { # interface HasMetafields metafield(key: "a") { value } metafields(first: 10) { nodes { value } } } ...on Product { # implements HasMetafields title metafield(key: "a") { definition { description } } } ...on PriceList { name catalog { id } } } } ``` First, GraphQL Ruby allows field definitions to specify a `complexity` attribute that provides a complexity score (or a proc that computes a score) for each field. Let's say that this schema defines a system where: - Leaf fields cost `0` - Composite fields cost `1` - Connection fields cost `children * input size` Given these parameters, we get an itemized scoring distribution of: ```graphql query { node(id: "123") { # 1, composite id # 0, leaf ...on HasMetafields { metafield(key: "a") { # 1, composite value # 0, leaf } metafields(first: 10) { # 1 * 10, connection nodes { # 1, composite value # 0, leaf } } } ...on Product { title # 0, leaf metafield(key: "a") { # 1, composite definition { # 1, composite description # 0, leaf } } } ...on PriceList { name # 0, leaf catalog { # 1, composite id # 0, leaf } } } } ``` However, we cannot naively tally these itemized scores without over-costing the query. Consider: - The `node` scope makes many _possible_ selections on an abstract type, so we need the maximum among concrete possibilities for a fair representation. - A `node.metafield` selection path is duplicated across the `HasMetafields` and `Product` selection scopes. This path will only resolve once, so should also only cost once. To reconcile these possibilities, the [complexity algorithm](https://github.com/rmosolgo/graphql-ruby/blob/master/lib/graphql/analysis/query_complexity.rb) breaks the selection down into a tree of types mapped to possible selections, across which lexical selections can be coalesced and deduplicated (pseudocode): ```ruby { Schema::Query => { "node" => { Schema::Node => { "id" => nil, }, Schema::HasMetafields => { "metafield" => { Schema::Metafield => { "value" => nil, }, }, "metafields" => { Schema::Metafield => { "nodes" => { ... }, }, }, }, Schema::Product => { "title" => nil, "metafield" => { Schema::Metafield => { "definition" => { ... }, }, }, }, Schema::PriceList => { "name" => nil, "catalog" => { Schema::Catalog => { "id" => nil, }, }, }, }, }, } ``` This aggregation provides a new perspective on the scoring where _possible typed selections_ have costs rather than individual fields. In this normalized view, `Product` acquires the `HasMetafields` interface costs, and ignores a duplicated path. Ultimately the maximum of possible typed costs is used, making this query cost `12`: ```graphql query { node(id: "123") { # max(11, 12, 1) = 12 id ...on HasMetafields { # 1 + 10 = 11 metafield(key: "a") { # 1 value } metafields(first: 10) { # 10 nodes { value } } } ...on Product { # 1 + 11 from HasMetafields = 12 title metafield(key: "a") { # duplicated in HasMetafields definition { # 1 description } } } ...on PriceList { # 1 = 1 name catalog { # 1 id } } } } ``` graphql-ruby-2.5.19/guides/queries/executing_queries.md000066400000000000000000000143711514115062600232430ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Queries title: Executing Queries desc: Evaluate GraphQL queries with your schema index: 0 --- You can execute queries with your {{ "GraphQL::Schema" | api_doc }} and get a Ruby Hash as a result. For example, to execute a query from a string: ```ruby query_string = "{ ... }" MySchema.execute(query_string) # { # "data" => { ... } # } ``` Or, you can execute multiple queries at once: ```ruby MySchema.multiplex([ {query: query_string_1}, {query: query_string_2}, {query: query_string_3}, ]) # [ # { "data" => { ... } }, # { "data" => { ... } }, # { "data" => { ... } }, # ] ``` There are also several options you can use: - `variables:` provides values for `$`-named [query variables](https://graphql.org/learn/queries/#variables) - `context:` accepts application-specific data to pass to `resolve` functions - `root_value:` will be provided to root-level `resolve` functions as `obj` - `operation_name:` picks a [named operation](https://graphql.org/learn/queries/#operation-type-and-name) from the incoming string to execute - `document:` accepts an already-parsed query (instead of a string), see {{ "GraphQL.parse" | api_doc }} - `validate:` may be `false` to skip static validation for this query - `max_depth:` and `max_complexity:` may override schema-level values Some of these options are described in more detail below, see {{ "GraphQL::Query#initialize" | api_doc }} for more information. ## Variables GraphQL provides [query variables](https://graphql.org/learn/queries/#variables) as a way to parameterize query strings. If your query string contains variables, you can provide values in a hash of `{ String => value }` pairs. The keys should _not_ contain `"$"`. For example, to provide variables to a query: ```ruby query_string = " query getPost($postId: ID!) { post(id: $postId) { title } }" variables = { "postId" => "1" } MySchema.execute(query_string, variables: variables) ``` If the variable is a {{ "GraphQL::Schema::InputObject" | api_doc }}, you can provide a nested hash, for example: ```ruby query_string = " mutation createPost($postParams: PostInput!, $createdById: ID!){ createPost(params: $postParams, createdById: $createdById) { id title createdBy { name } } } " variables = { "postParams" => { "title" => "...", "body" => "..." }, "createdById" => "5", } MySchema.execute(query_string, variables: variables) ``` ## Context You can provide application-specific values to GraphQL as `context:`. This is available in many places: - `resolve` functions - `Schema#resolve_type` hook - ID generation & fetching Common uses for `context:` include the current user or auth token. To provide a `context:` value, pass a hash to `Schema#execute`: ```ruby context = { current_user: session[:current_user], current_organization: session[:current_organization], } MySchema.execute(query_string, context: context) ``` Then, you can access those values during execution: ```ruby field :post, Post do argument :id, ID end def post(id:) context[:current_user] # => # # ... end ``` Note that `context` is _not_ the hash that you passed it. It's an instance of {{ "GraphQL::Query::Context" | api_doc }}, but it delegates `#[]`, `#[]=`, and a few other methods to the hash you provide. ### Scoped Context `context` is shared by the whole query. Anything you add to `context` will be accessible by any other field in the query (although GraphQL-Ruby's order of execution can vary). However, "scoped context" can be used to assign values into `context` that are only available in the current field and the _children_ of the current field. For example, in this query: ```graphql { posts { comments { author { isOriginalPoster } } } } ``` You could use "scoped context" to implement `isOriginalPoster`, based on the parent `comments` field. {% callout warning %} Using scoped context may result in a violation of [the GraphQL specification](https://spec.graphql.org/draft/#sel-EABDLDFAACHAo3V) and break normalized client stores, which assume that a given object always has the same values for its fields. See ["Referencing ancestors breaks normalized stores"](https://benjie.dev/graphql/ancestors#breaks-normalized-stores) for details about this pitfall and alternative approaches which avoid it. {% endcallout %} In `def comments`, add `:current_post` to scoped context using `context.scoped_set!`: ```ruby class Types::Post < Types::BaseObject # ... def comments context.scoped_set!(:current_post, object) object.comments end end ``` Then, inside `User` (assuming `author` resolves to `Types::User`), you can check `context[:current_post]`: ```ruby class Types::User < Types::BaseObject # ... def is_original_poster current_post = context[:current_post] current_post && current_post.author == object end end ``` `context[:current_post]` will be present if an "upstream" field assigned it with `scoped_set!`. `context.scoped_merge!({ ... })` is also available for setting multiple keys at once. **Note**: With batched data loading (eg, GraphQL-Batch), scoped context might not work because of GraphQL-Ruby's control flow jumps from one field to the next. In that case, use `scoped_ctx = context.scoped` to grab a scoped context reference _before_ calling a loader, then used `scoped_ctx.set!` or `scoped_ctx.merge!` to modify scoped context inside the promise body. For example: ```ruby # For use with GraphQL-Batch promises: scoped_ctx = context.scoped SomethingLoader.load(:something).then do |thing| scoped_ctx.set!(:thing_name, thing.name) end ``` ## Root Value You can provide a root `object` value with `root_value:`. For example, to base the query off of the current organization: ```ruby current_org = session[:current_organization] MySchema.execute(query_string, root_value: current_org) ``` That value will be provided to root-level fields, such as mutation fields. For example: ```ruby class Types::MutationType < GraphQL::Schema::Object field :create_post, Post def create_post(**args) object # => # # ... end end ``` {{ "GraphQL::Schema::Mutation" | api_doc }} fields will also receive `root_value:` as `obj` (assuming they're attached directly to your `MutationType`). graphql-ruby-2.5.19/guides/queries/logging.md000066400000000000000000000013111514115062600211270ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Queries title: Logging desc: Development output from GraphQL-Ruby index: 12 --- At runtime, GraphQL-Ruby will output debug information using {{ "GraphQL::Query.logger" | api_doc }}. By default, this uses `Rails.logger`. To see output, make sure `config.log_level = :debug` is set. (This information isn't meant for production logs.) You can configure a custom logger with {{ "GraphQL::Schema.default_logger" | api_doc }}, for example: ```ruby class MySchema < GraphQL::Schema # This logger will be used by queries during execution: default_logger MyCustomLogger.new end ``` You can also pass `context[:logger]` to provide a logger during execution. graphql-ruby-2.5.19/guides/queries/lookahead.md000066400000000000000000000070411514115062600214360ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Queries title: Lookahead desc: Detecting child selections during field resolution index: 11 --- GraphQL-Ruby 1.9+ includes {{ "GraphQL::Execution::Lookahead" | api_doc }} for checking whether child fields are selected. You can use this to optimize database access, for example, selecting only the _needed_ fields from the database. ## Getting a Lookahead Add `extras: [:lookahead]` to your field configuration to receive an injected lookahead: ```ruby field :files, [Types::File], null: false, extras: [:lookahead] ``` Then, update your resolver method to accept a `lookahead:` argument: ```ruby def files(lookahead:) # ... end ``` That argument will be injected by the GraphQL runtime. ## Using a lookahead Inside your field resolver, you can use the lookahead to check for child fields. For example, you can check for a __specific selection__: ```ruby def files(lookahead:) if lookahead.selects?(:full_path) # This is a query like `files { fullPath ... }` else # This query doesn't have `fullPath` end end ``` Or, you can list __all the selected fields__: ```ruby def files(lookahead:) all_selections = lookahead.selections.map(&:name) if all_selections == [:name] # Only `files { name }` was selected, use a fast cached value: object.file_names.map { |n| { name: n }} else # Lots of fields were selected, fall back to a more resource-intensive approach FileSystemHelper.load_files_for(object) end end ``` Lookaheads are _chainable_, so you can use them to check __nested selections__ too: ```ruby def files(lookahead:) if lookahead.selection(:history).selects?(:author) # For example, `files { history { author { ... } } }` # We're checking for commit authors, so load those objects appropriately ... else # Not selecting commit authors ... end end ``` Nested lookaheads return empty objects when there's no selection (not `nil`), so the code above will never have a "no method error on `nil`". ## Lookaheads with connections If you want to see what selections were made on the items in a connection, you can use nested lookaheads. However, don't forget to check for `edges { node }` _and_ `nodes { ... }`, if you support that shortcut field. For example: ```ruby field :products, Types::Product.connection_type, null: false, extras: [:lookahead] def products(lookahead:) selects_quantity_available = lookahead.selection(:nodes).selects?(:quantity_available) || # ^^ check for `products { nodes { quantityAvailable } }` lookahead.selection(:edges).selection(:node).selects?(:quantity_available) # ^^ check for `products { edges { node { quantityAvailable } } }` if selects_quantity_available # ... else # ... end end ``` That way, you can check for specific selections on the nodes in a connection. ## Lookaheads with aliases If you want to find selection by its [alias](https://spec.graphql.org/June2018/#sec-Field-Alias), you can use `#alias_selection(...)` or check if it exists with `#selects_alias?`. In this case, the lookahead will check if there is a field with the provided alias. For example, this query can find a bird species by its name: ```graphql query { gull: findBirdSpecies(byName: "Laughing Gull") { name } tanager: findBirdSpecies(byName: "Scarlet Tanager") { name } } ``` You can get the lookahead for each selection in a following way: ```ruby def find_bird_species(by_name:, lookahead:) if lookahead.selects_alias?("gull") lookahead.alias_selection("gull") end end ``` graphql-ruby-2.5.19/guides/queries/multiplex.md000066400000000000000000000104371514115062600215350ustar00rootroot00000000000000--- title: Multiplex layout: guide doc_stub: false search: true section: Queries desc: Run multiple queries concurrently index: 10 --- Some clients may send _several_ queries to the server at once (for example, [Apollo Client's query batching](https://www.apollographql.com/docs/react/api/link/apollo-link-batch-http/)). You can execute them concurrently with {{ "Schema#multiplex" | api_doc }}. Multiplex runs have their own context, analyzers and instrumentation. __NOTE:__ As an implementation detail, _all_ queries run inside multiplexes. That is, a stand-alone query is executed as a "multiplex of one", so instrumentation and multiplex analyzers and tracers _will_ apply to standalone queries run with `MySchema.execute(...)`. ## Concurrent Execution To run queries concurrently, build an array of query options, using `query:` for the query string. For example: ```ruby # Prepare the context for each query: context = { current_user: current_user, } # Prepare the query options: queries = [ { query: "query Query1 { someField }", variables: {}, operation_name: 'Query1', context: context, }, { query: "query Query2 ($num: Int){ plusOne(num: $num) }", variables: { num: 3 }, operation_name: 'Query2', context: context, } ] ``` Then, pass them to `Schema#multiplex`: ```ruby results = MySchema.multiplex(queries) ``` `results` will contain the result for each query in `queries`. __NOTE:__ The results will always be in the same order that their respective requests were sent in. ## Apollo Query Batching Apollo sends batches of queries as an array of queries. Rails' ActionDispatch will parse the request and put the result into the `_json` field of the `params` variable. You also need to ensure that your schema can handle both batched and non-batched queries, below is an example of the default GraphqlController rewritten to handle Apollo batches: ```ruby def execute context = {} # Apollo sends the queries in an array when batching is enabled. The data ends up in the _json field of the params variable. # see the Apollo Documentation about query batching: https://www.apollographql.com/docs/react/api/link/apollo-link-batch-http/ result = if params[:_json] queries = params[:_json].map do |param| { query: param[:query], operation_name: param[:operationName], variables: ensure_hash(param[:variables]), context: context } end MySchema.multiplex(queries) else MySchema.execute( params[:query], operation_name: params[:operationName], variables: ensure_hash(params[:variables]), context: context ) end render json: result, root: false end ``` ## Validation and Error Handling Each query is validated and {% internal_link "analyzed","/queries/ast_analysis" %} independently. The `results` array may include a mix of successful results and failed results. ## Multiplex-Level Context You can add values to {{ "Execution::Multiplex#context" | api_doc }} by providing a `context:` hash: ```ruby MySchema.multiplex(queries, context: { current_user: current_user }) ``` This will be available to instrumentation as `multiplex.context[:current_user]` (see below). ## Multiplex-Level Analysis You can analyze _all_ queries in a multiplex by adding a multiplex analyzer. For example: ```ruby class MySchema < GraphQL::Schema # ... multiplex_analyzer(MyAnalyzer) end ``` The API is the same as {% internal_link "query analyzers","/queries/ast_analysis#analyzing-multiplexes" %}. Multiplex analyzers may return {{ "AnalysisError" | api_doc }} to halt execution of the whole multiplex. ## Multiplex Tracing You can add hooks for each multiplex run with {% internal_link "trace modules", "/queries/tracing" %}. The trace module may implement `def execute_multiplex(multiplex:)` which `yield`s to allow the multiplex to execute. See {{ "Execution::Multiplex" | api_doc }} for available methods. For example: ```ruby # Count how many queries are in the multiplex run: module MultiplexCounter def execute_multiplex(multiplex:) Rails.logger.info("Multiplex size: #{multiplex.queries.length}") yield end end # ... class MySchema < GraphQL::Schema # ... trace_with(MultiplexCounter ) end ``` Now, `MultiplexCounter#execute_multiplex` will be called for each execution, logging the size of each multiplex. graphql-ruby-2.5.19/guides/queries/perfetto_example.png000066400000000000000000003375711514115062600232540ustar00rootroot00000000000000PNG  IHDR +eIsRGBeXIfMM*(i>tASCIIScreenshotLiTXtXML:com.adobe.xmp 1 2 1 2 1532 1 Screenshot 737 Ij@IDATx|E7=RBP?QDD, XPTEDT iRX Uz %$ۙp]K>,# |R`ڴi~ae߸n ٲu9@@@@@@ȐKvn^&$Zv,25iPyᯒ?1ң;\yg; ֗/f M}[jT\RO~U:d,aΫ\)Z>>I~ck쵷~՜i|>      PTvrK~~~bZ׻WHc]z> e239]Jʍ]:SVIm笇 2K ͱ[YL0ߥc[n/KuwsgH1Ukęt7ԃ_,A     @Q ،ɻ=>yc|t>3TG UԬ!Ƽ!?,Xmsw3gZerybCl>?LQzu#wQ;zOo;}<`_l ;zr._{+g@@@@@"Ѐ`֫R\Y3䱧GJjj&yb9ֵ`tjF??F>t3zQcIE(.#,`J}~n%Ce.U*ݽ^B׾z@,'>zg!     PVCc.e˔ԲSl%5/~c˯+~*eyҤQٹ?&鵪ȅ+]&g&={W[p     XUC| btW-gWbk,Kyۺ?.)3xde֜/61/)\Hk)۴Oګ~I@@@@@(V=U@O`9UqxcݶUUJMKc߫޽CDEFZ)앟.Փ й>M7t2R[/k6:k      E-`??һa0K6n5Wc6m yj9c $15?6˪ UL]U+YyRPCY֟)s~##lLRnB:q8؉a@@@@@"}e#.hZ1Qo9c|6'@V7x`J7"<\BCCNG &KpHd@    .(ZLE (    Xg      PL i#    ߺ      @1 _L/<     [}zr6     T16     o      Ss     %@߷'g     PL i#    ߺ      @1 _L/<     [}zr6     T 0-5:@/יD@@@@/666CÆ m"@`ΝR|yD[@o111}/~ E3}B'.#'k^php̥D0@@@@@&th+     v?()iVU;pX>;_Y.[rJ3rLjJ|_̙3b>ˮ]WBBӧvUNm˙T[JFFyӜ9s$-w};    W'^OΜ?7nyV\KW37?˗b^gOXwiӭ/-?i?E}7@ԩSg#G?~|Wk޽pBYzȊ+e矗oF~iQੴn:IOO˦oͳz`ʷ7{l[C@@@@;vf=vBN:-e6?/tj\bЃPԋ@,jWRv˶JϷ~?6gfL#15/w ILL&M=<G//_\?3fyZXjKڶm ,[|X7o.ҪU+ ٳG:$^{oRz{ݺu}W_-QQQVm`@@@@wE-[#GNH iâ$wvԍn%+eŚ?nrMLT޶g!WcXtK[iXoL9y {Q iָޱpC-m*XJJ;J2فCGc:}V]NlX>'a2r6ҵSk{$4$XBCCvbZWۜMUDQ?TreQ_ ,0~9x0a4jHڷo8p@l"=eRn6UN/͛?c믿dȐ!2w\iذ_^/^,5j ɓ'KHH$%%z@= P)33S#FhѢo    (Zvrӧtѳ!:#nt=2X -?yWO{5AԸzYTy7dwlHiڨs@Z{6o!%Knǚ2Q_زmHʒF˽w,/"^zKfN+z^l,ꁔVr CI=$s.O:v(*pX>ƍu|Sjp>4l0Qu>ܔ?sf͚P@Az@oР~c~z][)p5M    $ stl%Ith9rT(QAU9 qO*{ /s-dtZmk~ߨ{C=`/keO6U˒k۱TEB';{н%Ko#Zoj͓yTW"'OQoJz12E4痢we/QSק /_^On5܏XdI9qTZUOk m*W ~!}԰Bj~TP_ ӡC9vSߚ6m˼[̓|м@@@@\)PemQjTU`^[*7Ӈt*&m<{;G׽M:G==]"MIURo3ieLhzuӆFbSY[VySgc_oO yHW:H֔Ykt_MZ=PRhl2_vy OmjgR 'ԲRjt]+U{߫J*K_=PoԹsg=d{/jݻۜ:u$ӦMKJz+J~lٲz5.]d2|iܸ1~+MV@@@@(H2X3o6gӇu 8Z.VNWeT+aovO&a Ց-wz[^i=kIq#KE{ut*(zE2%rcVza˶z{^r;}IrrttnR|9aC٫|8z ~d#eetp7J1̤q#B쀴#g2m_x[WS4zh1M{=q亖I'QM=jիk{𛶵iFZj6ϯj( U>55UOky|@@@@C@e^)8nU<6Xzaprs挦)ntf_gʪa}ԗ)[V2iָ4cWԪ.Sg~7gXܶq\׬=<#TudURNMGo=D㯒cѲryTD|6][nDz(fxB8z;z CʰR|*؟eLWgHMK!Ns1!)H6ͯ2eSM@@@@g ;SesoWeLCGQC kK^9y/\g=[II޴Q]=\m+1Wv }˫We5ɠȍ=H #קn sS61hZ 4z7m\WƝTpC1졅[nReLo&ʫ+UNm}T,wŹ!VL6kbJ[PN]QCٓZ晶9alҜ_MEn=~Qc#   .`/hݝ>9{LGw)bcc 4l&9F@19gΤ&EU~5NeRj[)7YpnDzUSC|!x$N7IܑF=$h=zgn_LK9d;Êf#@ƽV0CFQ7G(@ nH 8"ep@Qo9Wsˡ`.Kmtc =mVym1xp6J%{A}{{~-cz`4v򊏀To|iU*gWz@@@@D -Jwe>>^ʤ<7eɭƏ7M"k(6Lě$\'*x    @Ne`>zݘЩC674MDYsM=MԷ<Ϝs\ߍMj@@@G^r*5ְ8`mRt/f9T#L~GR}SsS&@@@@.(nK<$VCfWUR|ScוcNB@@@ -K1 uRͶ>V[-9Lu/!\W[ X--d4ς;ZE@@(~']eN})䦴h"=i=OO Zqs`x@ j)U-9LuWzpW(R  +Ex?pnÌjDgzZ7]deb5Uܔu2DU g](@@p@QMgg.:m߯&aFLIR)SAdо" @ /|s  [kxG[~'8|` " )X@@@9 jiQt'nC)γT#TR/$wL . @ _?g xzncL<([OnՇn/AwWR#P/ oc;W3A@`   IިI2/21/rSMgge&t=G_qIWz7[5X  PcW@@"abL8hӻ%<$ܩ@ 뚨 GUS`/@@]Im  Bnn]abTR)> |Sھy]9+@(ZE@@~Q=+^mwٲoQc"  "@W$ PlǽVpߗ~}~޽$qªhQ uL  X `@*/eRxM"&X/6[E4@@|L]PN Y OЩCOpШ9P! \Bjl    kƑZ@"(#'[Tw\ y06{ޕE   JԤ.(E|/##JA@@@\'@uԄE$Pv[Zw궮y    @ _<;g@ ; _v[m    PX~Ybo %c7"mfv .)n@@@@t\ 6>E`ΝR|y$ pbbb\R @oW pR/f߆-Zբi ڃ      0f ڵe۶WݺeEJ3VK2)6|~'F@#*aFS'OIffU5"$0 PN*d˶]}^ޭU9V@s$!!ܠҥKKٲe, ?_hDG9ϕxuCN-4l@@<[7̱٩Je\M^KTT)2}h;~R X@l"۷oڵkmVn6s…RfMU[R@ `fspÆ\7rq6m~`͚52eٷoަ /ZHC>3d+`2xڵEMD*~6 N\93֡[DG^uʹ[ m_ ǾE+)տʔI)Z  KŊ8_Q-1$Wn@lCTѢ}C,]%dޡu>dZwr$v1Ffrd"J5> gff!H      @ /|s     6[J xw͚ryV,@@@FɁxi3 G lvR3A>$@@@@*Π:ƞEA៛@pPz   8,`99#aZ "     =2@@@@@`H( W&a^k@@@@yMKכS# @1P~5a 3Yo1&   E$/a@|Cz}:r   x#ӯ"C@@@@@ Q@@@@@O W!     c;D(ڵEMpJB6m:'s\@@@@;T\bXغuJw\;Z `EDMpJBW\0_9@@@D@#OSrYj^rh&"͚U<@@@@(UD}a&0E @@@@@Z^{h8     _` $&U@@@@@ | ޖ@ 7..Ms    @A*@GԄD)O@@@@[@܎Ot^@@@@@@{%     -eXHHM#IBCC'NJppa}H     Z#Gʙ3$׀Æ ]{TjCBعs/_^"##  (x3.V pϩZrsb.V3O1Ȝ"     k"    O'Wff[yZƝp1~ZR6lSRc:'uHc[f\<@̙3b~ٵkJHHrS>@@@@@CiWJe.&Hɠn }b˜iͲv'ExL9W*+k/Y"TO-/>Y<}]{*ڦz1/)HE }lܸQF!իW׭?~1n^֭{'%JڵkK߾}څ J͚5VZn;#    N)IPPIHN%+-7Z[o/O<Ϛ4V.'KunzKdl_NLL&Mzxg5͛7OnFر 2䒀'CMJٲ[{͛KpplٲE?D(SXBڵk'Wց۷rǏyM*UH"O@@@@|T!}, "#;orLi2fБclNڦVWVӜ6!I>^?ce0Oq;etjh܇?,5JOA~[/_+JY;Ɋ+~u$PD;ANrҢE 0ܹsg{„ tR*ʩ ̛zX0vXQC˒&k֬)SȾ}-Z$!JΝ֮]+R$@@@@ȇ@X*pt.YDW,'+  6ZʔfW7o|Oծ5r ytؘKN!4$D>9_a>xī$ͯRF9U>g`j: i~Օ}?/P\Q MN>K+~C:m.Gd܎}ՏҵSٲ}{w%]{nh\x;Uo۷ ȶmD=PU 1Tޛ6m6mw!=z5L {ҧO ݻw\߿z뭺Lj[W^y%sB    NjUi'_=os;#qh07!re#R9,YN^}nԍ.ݮo'=!jre*W8uFNIY:pz'0}2x0pOE+>wIn{{T[tULˍ`,JIkHמ\2on\c<ufrmF2ճ;w͓yTBC'OQoJz12z皦ٳiϞ=JTT2֓'Ojuqqq}ʕ+'0 j!    \CQ}{*u[3j\l+*z?.{ƍzJ?y楷$5άYdԨQrQ^;v5jT#ҩS'9s~uVX5W=-)K{e@@@@@ [I{BƝ0;`v+KJuZ9Z/O?J~6z?xgWS?44jbܲeDsNp=UD!r8^%axlF.1GRZo+^q>z1I7c^ueq2er?s>*O<?ү5e$?yL{%)Y"sO -S^?TTIO꫆QczwnUkƍ$OVCDMt̔%+>cr0;=0cW(_VJ1]~jT?3PC43zik RCcGɿ;SI/_g1QnZZ36ZA 6iPGV~Xr;{}.6*]Qdq*؟eFv4~G /?=xK%{xkd [VgW˶m$@@@@ȟ.ԧW|ԸRNc=88HŒap&2Bwj\n3Xb.t}z"xE~?5~ca}='U; 6(DRRR# ~n Ե#:mۗ]P߻9ߊ{n].n1=1$Pno \R9&aOz9Tqoyxc:v%xyf@@@@X{l;yҜgLe^˱аa9y໚ 2jz3ş>#@rns]MpY)S:j>7*ˆQFo6g9.$)P :YO\ pvܩkS@~^s1#`~3IoPk Pxog͑\+3o}7.9YKYo34$Xcհ #oim     @j[c;:,Ou3\y؈     kΞ0Jߖ y     ^'h ^.v'C>     G ;@ Jz@@@@@| ^磪b     ~{kg(}{=@@@@@ Xs ^j??     ,j ?~cSY@@@@@ U^ w΋    [@IDAT {UM     P99c9DFBP    @^7+LJf o99ea{U    PWy\HXUɡS  *F@xyG@@<@Wk@KX^ETCWr\hV@!@?  #`+H_ yO-)2'zAY9M6YD@%@ [Vt @X8@@ ׏#^!P=m(JHW@(j|@@@@#@=Ԋ s`p t;!   ͲgHHM#ݴIBCCmTI3 e%%^?4P    6l!>,sN}v111>|!75ϸ|s#9ׂo__γg2oe؉2%1WoeCCB@@󶯍Jm͘+mZ\eWW/4sdxYK 淾*ϝ@WګɩgO6;}.[:rH    5'a%*Z/ѥns%uRޒ(_N{0OB[_gky  ^&!}ϟ^Ou}/ &7yTOkK7?.3 n.cZx'eeQ'_Ko1mvjZiҰ z^R>{Ml#}szۚ7-ۥnz}KCL @@!PGDHpF|hV K'_HVJd:-F*ـ  ;NU/^('hc'+#HppMF`|u4#?-Z! 4Yb.7˹Yl0MOOT5\O^PT_jU%D^Vj}za%K*bKm_W-c7`Ԫddfʱc't_`f  APVGPo6oT IY\&]A%ٕr~op*կ#~ưIs?$e+a /3g%ic I]թ@HYr7@@'V7nlvH1ޥjcTw7@@@ ӍET5<|~лd\S cJWՋ%Y"_[*^7k 4 @@ލ ׯrc{9iLU9h~y<}_^rE9z\QZ|OS/oUJnL_&.ɩR"$D>tCֲ`js^g set@@@ +X!4̓ƼeFO{sR .tJHXՅaZjg&fo0q;  )n /#y^߾/SzEiqMyTIL*C@<hh     #@V| 7I߽:b]Fʮ:H5IvLLGF!kovE9O~C|I{͗&o~h_A_PAG@@@@@{E      TA@@@@@ & PTGCVJBrzQ5"    jF/%K6f@@@@Xfu{I2o^Eu}'o[Jhpw4AmWK˔mIeU$$(q    x==2*pL="n%߮#R>rVݲjo:E%œ?e;>*G@@@@HrUˤVHY{Xn>rٰrx4z+I2 χu+IوYA}]D@@@@vz{Su{Zp[9R_\SxhK#R[%JVsD J^Ϋ$@@@@s)h'p:)U~>zV}Iv>K_&he@@@@@hҧh9*E*d!^!BY|FT6&ٶѥ1?|.ʒw @."     ~@@@@@>p9@@@@@3     )      G-ꋄ    )@߼     @1ȵΝ; ݻN3B<$~Kˉy^k>yY9)~ C\&k?555\V -5YCJm#8:    C 3A@@@@@K     @kոBՉL8Nԯ2g$w~sT>ouʕu[\?"#eS t{;֦URɪͮkjbgZQmm׺m%KHJ ۋ:g     r}ͮgKrrˆѧ%9%%CW-U&?#rj㕍KrٞgYG 4i@ 6É])oI]$44Dԫkʅ6!H/fI7KZZ,\qyUIMM ҹ[= r0ܹd3]gX5kjUv7ߗӧJґ92a@@@@@5jnjiwCwiҢt,*z%jը.#r\wߗZ`$$&JլI#ٽg秃֓ ҡ]FАP?}:k^mܮO ,ۡTW, KηJO IJ~@@@@@. yԔSeҺe6*Udk/ʏƛ߯jHgRVC4*2Q~ L^ds哏&[ ?cXG dݻч=ǿ:J]VVҺuoGF}go x@\ee˘7ulJ͝CMi從ysDX&վ޽NHDD,1 tj)op6dXj!垻o2&^Ů>O֬ST0~o%<,L7+ǭEC8傃t0Kdz؏?RoSo|n7Aw0*H?Ï۶usy|TE'@ $@BJS(XADEQ|W v""Ez/BHo,wl0<Ïsf7={f8c:ul)X?szF+ѧ{طu-UKoX[oĘs=eC E\|zu7DZznÆ5?B:yFrKWyO7m >^qXҿelH;ou0"7/fϚ9WQcl4pGRDHrpp0?Cxw2ak`l4twZ3(@ P(@ P(P1oMm>xU`@[ڝs1j0+G˳ ~?Ti޴)6lhghl'pˍSfWJjy[N:%N^=t×AkSn^DTt,-HX-^u_#To~9bܘ,1]U3̛o87WXLA|Ӿ ލ%"xLQwSXWf<0N}W˻l: P(@ P(@ P< @K&*~=t|?zg+npssUi`j;7FFf&$~J־X:ѯO5kD)7Jڧ׹5KX=J>5o\8}&kT_&|wVY]@xDx7Xo"r¿Y+W,Gmz,ig^*ݼX^+LU9ڴ2^.6䪶EI]<X 2o/A3zҗR-~w=-c WOˏ^jT\; _ȸbֿ ԅ< d d )Lp P(@ P(@ PXܝ%ne2-uݳw3I(T$gN3]Jl|WϨ]zKҹ 'N3 ҹG Ыk}|}<g=ʝ]z˲~j0[8!A~Wީ* 'u@}G P/pr7-vB.$'b݆z dc;Zǟ}gxyy#@gzZN7t/cHj!bL21#vګ(@ P(@ P*VB{_HvnZSHop sݬk<<7yw߮>srk^ss|#ٳQ! =[)ќxn\>߱>^ NBSZ`?vl\%i.,,,q|ˊ[.J۞^0>2Hѣ=:s}\IrrNϿ "4"jԱnUwVHp8\#?;8??_-â%KuΖS/AvWY+(@ P(@ P*G. ϳb5oI<{oڪweOvQ11%cGl\E+2\ ,j`ƍ=׵\^i";+9ʾd3:&\GFFr+K ';.nچ+S(@ P(@ PSk'+[M!x P(@ P(@ T@(@ P(@ P(@ ` k(@ P(@ P(@ PNZ(@ P(@ P(@ 4a (@ P(@ P(@ P 0_26(@ P(@ P(PR&(@ P(@ P(@ :kSS(@ P(@ PJ 0_҄5(@ P(@ P(@Z'{` P(@ P(@ P@IK(@ P(@ P(PuOL P(@ P(@ P()IP(@ P(@ Pj)c)@ P(@ P(@ P%KV&';+sM PZ:v(@ P(@ PU.`PXYGٳge ԩS*덯(_oeX.VSl|͉kQ"zE& 0OM~v6 P(@ P(@ P@/#W(@ P(@ P(@ d_ODAAf<ժfXr⥙'S`ogXFEz)hBdgaw̬,kYn^N=lCBR2<6cS4R۱P& D͂owݵ)[~. w£C lyWa^}24jܦ\;Ë#*|oMkyvhcڮ2VhʼSQ {"q";8y=6>1^ZS`)Pz|9 :4|'pCSk\[ PbN+g𳘰x= N*ft/;`O__fxqzӦ@øx^-^뉀w_Llj$4t׻`jqw 4`Lĥ'`gHK\ NQ1θ;4)n~It#Q!Y# .Mݚ`g^z?Jd&vv>f=Ws,i[閻wS/ۙw4cD\9zy"'Bã0}hӪnC:/{n Gc~u1ο ̹c۲9cj~"cqǃOÿΜ ǣQì3㝗``_[|U/4kw^^.4Ym T@Fb>~ /<_)P@ئ z TٵGehxcjpr@=|֣Jz+p  5ā%ajr3 :_[ipƜVƣTi_ڴ\]j}Z^/wUMO8hת_v_X׌Ů;Ku?]]:y?^{/?|n {]lز ׌j=P:DđlEM<6*B jW0zY>(@byHLB0x;ŚY PB /Y |I \@Fx C'*{^̩p PIUe?ǫn/TJ4OƋo~w>6s%77~mR\cQw9}]$$&c,|qgw +VAH9 i+gZ/3ң_!##;~|BbTƚCC?)9U>h˝ M l,%P9q*ܳ_*8`̏:', 3U qѡϯTAZ1-,Q<% T@FtnCސjo @ -dLZgJ9d^1?+;j@%Z#FTOJaER?W< Er r q ljD}xmHLJx t,+QoZƌ !<?j(6ث ѓ8zy_mZ^OOv=?Xz%cŅ*o/EDT gޏ1p$~ZnAzu[^^a7չl(o3C?PCs{8rrDʎ=ѥc;c>R}"G#)HZ퍺ةq?,^{Z}QrI/͏L䄨*qΊ2"M<@VEK T@aa?\>{^rWyغd1Y;%7wCV&wA h'0Bz{/g嫎yigۙx=E,K(Γ/-ùfe@'-k߫inY?j@̚~>hݰ Z ʯAwG oL}#'`ч*׹ҫ{'Q)rOƤ85ЯqЩN3m{PLFn\=]ʥS&GT?- JrgBLL }M]2H@~:t`UNrʽh Ufs- ~ Ko1QuKMZek7 /|6ʎY=u5%9qᐋk [y^ P ,~nz1t`S+ncoc(KF^?? >RuQ #L9h>8i6N}Gc'kٙ*F]V6@J)=~ OQRd\9u;\?|HWoݹ1wL="B¢Tww]' <{K߰so |nXd\1JϮSlSiyG S w۷m+rA*[ImH~jT%{Z)ؼmWq::h@ D(Jy<ahک[Qp?>xzu@H_ G'7 l!vmy Y4l>BlD;[ CXHQ{ܚ]EuG}.zn^q DEC=Ѯxuo/н|9K`?^VFٙ8b"Z,ڂ!';V}M=dac&Eb >Nٲ+f`h\YGcO/kuV{U ՎQ,onC̀Yꗀ?g=:Fg9l{>m:D>D:nu=\]Ao߳79ANB+ndžԻ oDŽ)u:ݛUwoGfz=ㇳ+oU`,+(N+a+|^P$ LUQQ@zWg' q/BU*ps? )hv6/|~v&FiYvJsF^BS~4i{>wFߞ]*n 7SRA}Sο|[c/{{4l`lUizo9.ڙ0kѷw".!QZFrlX~ gϼ/6h iz̬ਤ5juW/)"ACoޅ~p[LoB"` dvhe_%?p<wrj]LwRwUOsm U}"T{ v&}Am0SukpppV2]zW.MmPuh{a/ߒu`>2$V}n|aÅ:gsDn7ʧoE\T3r1@Ɵ?}Гx l#+w"G\%2QIM\FHѼy'(@ e;iX(@ X T7o O?~>mvQw\_t8o#X{GmV>~c6Rƒ!RTkcMEC֓7u,i 9[V☺0m=ae9m}#?ˀU(@Z)p(@ϕ,ۘcʿy Fɔ%o>U7Xqݦ <9T>\zK`+FM<A䫑ʿ y Cor[O;<0kZPFaʤ+u Aw_YYw$PQ1ŀf^Mpp!l?)i鈌Eg~(]. 9:^tKzׄuBVf^Z[)*Ao)mJNŁ6GD[cō[vX˽bGk6#Kna1p\~f/ qtfܥWN{C[>ntbr/|;~_%wJ>=5F]}yLgP[7HJ L(Ӧ\(CtՊj P@ H 7|JؽT~C'ׯ z  K|dyc⭏Mn?`ڜto`_zq]|ի_1V*]Tb9&Z_Z 9Pm*=- ?-58M %r^9ԫ#{]RenaEEP_{TŅԈS +9 +^>r\U)P0䢀 +y% "z}NܤsKzM[wA.ѽ32TtO#N*eQm՝ .ֹ>a|[͢TP~ዋ0suQ*ϺvO!Cozsbs`Ns[s UB 9u27GL+aBtn/yыSCF\U2niZMIg0EjJ{r2W Zus&ч[S)^}zU_@u9^*xx "-ۦr] O." {˝4! v药>X 4屡ND-J$Cׁjmpn}i}g7Couŭ,,[y}U.D??/JS5T QVh^ZVr Y lLU+Pq*ޒb4BTS=s-:[.3)2lj15ߊӣK^zlǸ⏿Зs@P <6[egobs%U`+oM5ԁ~Y1#t[FϳoG +LhNg~ƃ0eU0R󢻦M= ~u<}eRDtqF#S~ pup!`7 `onT_+IsSKwIӎw~{v'7xJi-V3]x),,^ڶUQ^F^~ сz9Kz]H/GKHKFfWO]_CzP JUرu|Q5K`J#];kոO>|;S__'HTy~IɩƒU|Qj@);'n6Z T@vZ%o]urnPCTuLwH&sխ-UZ< :uyַV$sTZt4i1@َ"E~F`cͻԛJn䣳* 2cU.zVm.17'[[CNҁz uFld0cpn!7e0߼bB~!}XbܚߵH: ~*߿O;5_A䋴wH`3վ9C \@)s쏛PuR qy< u#;Uqk%PZS }bWg'"vxm't(5h'IMXZIWwX#o<$apAX]~8qh|"SdaGwJS=mRa(P/\Ƿw K;Wr:U7cNJS,`,+cf=TFcjl*|i{֐ ~c]UZX<[2o%`-/t|K `i:şfs Ѿ'V80a'7ˠA@IDAT` 19U_Բ§(K@j@FG/ڤ§F_to.>89r'C|q 0d# AץFb懲h>`x&M50uУ4 0:w^9ܯBKϪ"-]?8C rl58oA{SCoK|=+;Ciw *y./QH^Z P,Ynb{*!DnV\㾝š}vj7m_Az[>+8{RHPrۉ_j ~ES@Ret}vVQW|q@f~.]'9lySo2 UnJ.$~؇wJ}-"]/9{&u{{#U=&AwsԩQ.kiė;jaO7<(P ;%އ]8~:qiH Oܚ r쐫RuV@}cA,C}Vi`>N=.Sߪ'NNB_ iz _Jsrܙg5G&|^w\'gӗK..jb5o|>yoiyL.2h3i.S"m[POoy PN ;>ae<}@XXOcE0N@>_:ZP\Wg.Zb؋qU7/nQ}\; sst獊$ R15  ps*ZW(bYi P [}PX5.#4E:{K`qŃFG%oQWVq~YԷ [ƣRب"vX"{`O㋰1/erYg?ZCD6}l~ GǡKja: i g(@r $YV#^!W궀m7V[zWueߨ/x ƺr #oYgLh|0gqu?zw\W;uif Ѡ'?mAash6RbLwy;.::`sנGl;]ϙ'`-m#:U߼; [y`:6"&u8N)xlpTcϚbŀ-Q-S#ِA~٩DƷCA>w@HVDKoZ^ES)GX(@ HR#sĔ yj`m h]%(@:*.z4@uiQ $Bz}^x銘Zո1vg:\ ;{D8SdܸzV߼0{倞΀YKz#C^^x?R٥J3]า{'1Xh=Ri(y{g\w=7Ǜ!tsPY/~To{gt~ "X Pى({zZmԸ]3trR}jLzn慟EpةA_< 'Non[m_YA~buؘ5r<TDӦ/Z:YPz'W|Y?+}Fp8~yenw*څq; _q%>bk)@ P(@ P(@ P6J P(@ P(@ P@`v=_l-(@ P(@ P(@ P6YXI P(@ P(@ P] ׮狭(@ P(@ P(@ `& +)@ P(@ P(@ PK|(@ P(@ P(@ da%(@ P(@ P(@ Pv 0_/(@ P(@ P(`S,(@ P(@ P(@ .kR(@ P(@ Pl 0o(@ P(@ P(@%zZ P(@ P(@ PMm(@ P(@ P(P][K P(@ P(@ P)MVR(@ P(@ Pjbk)@ P(@ P(@ P6 m.ʞ={V^ PB:u]p; PzkX\)Ers Cr`qU \_kM)PN k{׺ (@ P(@ P(@ p,YUq5rŌ^mNz"Z=yy5B4DzD4]j ؀z$[=zVj oOEm2HyFؐ.{u$jҳu3)|Քg|Շg#SgN P(@ P(@ PuH:dT(@ P(@ P(@ P TjJ⬱(((nnVu5#ǒc/ͼ<{;{x6?7¿/zw\|ՋOIMGwW";; }ffe!(8 [sp*(>hf#cӲ:c̫D=+(@ P"faO1j9H <"kקaS*Q:yR*-ljGuo?~s(Pp'`f;1|`rSMw?6a=(@:!z7<6f܅3eR*gɑ=]110g}q&8u=t Av]yQmw  6=_4ݏ»싂3LĕGYs@V-vA~'GGsM8r+%p8xNaSL_ǫ=0"cqǃOÿΜ ǣQ_qh԰/yŒwזagbyG(@Hp2}r@H OB04ݺ4-E_ : `ƀ'J P q} Wh aN׉(l?QTGw{{4/-Zpuqh_zͽkjʨCMz&thF+0ص0^yg)~b^~" ,~9ث'P}g./xoWs*ILDɆ[Tҏ:TQ[ P dƦ"rG5 b(P2ct 0(!΃KSꂀF==ywUNg޲mAb〣Qfl_jUnus(kox·_@%77~mR\cQw9}MHLX"lٱ\/k^z_X tp(K}kH~9L IQ=N;Ro3o$%RmA\32LLnAg2uO>t`ⷵNgg'>P@ @aR(RRud =:N T@~z6r3x<:cS)zsz e zXA P(T\F--T=s ն6Fr1_) (^Zm[ş"1)xe7U\Zzc܅ R$mǏ;m28t$8m^{?mVSӦA;,T1RDD`~Nfg8{D@JWrXPzҲEsĪ2ɠHNIÂ,/r՗߯SMH P(*?`v>JfP2S*5(PUaR'%. q] PJ:N[NUR$ (@ ] ;.Ud ݭה;~i8=s|uZ*4PꨊIV>'_Z;_ Hw3p8|{`MЃJQbaXaA_-у>O,o,KL|}?sU0F)^>@rAذeϾ c/>*܂wxyCuw ܩ@Q*OŃxH_ŋ0$u1D7&^y9y/?Koɗ?bUG}p*^@op.~-UG P'a/hrO|_#7*A #,{vLGVH, \MCe(@H={V˚|wF^vQ6xͨ.5inw]"ǡ9q>cO%W)*7 W k;NA{GEzQ?XfJz:XC']soxtrrB~ƬR0f(P1gsӏh]MqBsѽecѦ]ٻ1gph_=x\̢/|헏kS+ G-.AM}GqTx4jkq嶎{(Ps2ҰeLѢ8_sj9sm3#L8*֟ibf5"߻{gJоY:ho ꛔvRcV>7eYh5iaΉ_|YiՅ<5ucN "̸Z 'Oy[?_c1V'jB#XO?:[ݹYocܨa8 }̬,ܗ$UOCEo|T@y9 'rAd/< ۚ6ޯ6.N@Q~󎤗/ (P=ixzp ='m0U@-u?<~]՝*g~ޅ9i[ΖyyT P@!t>JρNj~b߶k)@# zvKcѷ'P9ِH={Rx{*ݹS>'gGpK8\RbǖXt,tioYKXj*m/ER9D47\1J?SR!} kǕ ƺ(64Tn>=o8_;$iݣNO$;unZ ;n]KgBѳ['=~}۸ 瞑f^۫+⓬5FvNQJ?(@ PBbO x\Υ1v*=E«QG Prs0ぷ1|-wcOߊs(P^-W=:,r*zu 3C TE/?x7/PZ\[P|1`+oW6T9^խrŘ-j=MRI?Ͼe_5(3h{.h+ԏg<sdL{QG^[=_zAg܈e_l'8A PU'u<9q*PۣKԳ\P|x񣭪o`}@;;{v͑`V{kߊ3@5 o4þ0ӘԏrGYs@}C?xZ?V?hSR:"%9N/T0}F]k'Nkbe||S X~-K=)@(XQBN +;YL#Nc9C l2(9%3]lVY#=m[=kDJ[A|-2 x7)ϣַ [n+~ 6&.m{9_`zrΥ-4(@ TI Ul<%8^@ t3]q$?l{}ĸHtw1:/ Dzw@BL^_pb"S7X=سWcu^^}3'(@jNHFE_f#rVO˝I,ꫀ{Ƹz|5ӥXLq$.5tI;7P,52^uЫMŸs/޶G#c:[*oy`NSj@Rdncֹ[;Uz&lC=!QO(P5*պw{ݚrgluKѝ7-|A >[t?~C'?m ɍ}q>b/ P 㓑!)Tqm ~0d_ {G {q"? j< 'GweUd@ <\7 }秤,QU*xyq |5 gGt~ }C76lIeJ^[}T73;;$Ǎ[f.߰\1);IdJ*8[Ooy'(@ P(&;NgaW 1kwi ] (-g)@(7h7ꜜ]!~)+2fJ-a&p]__)@[EC𹬿Ϸ3]upWC }*tu4Ǥ(Pc\\UQgt;[/ |1O톨tFqn{i԰D]1֭kw '1|5 檈ptƔ8mED[hU8C P"H6ÌTe"wUe3y, PG XݵLVĭS=>mK4RzKzmo_#k>v0“S zW{G'烧t}ZvR:].A P f`“^;mFw-7u^7#fYA b˫pJӔAS(*] L}9/4/trcso8y xN ˍ ! * P0rrt_y5V|13v2yjJwժ}\owU3buWd7 kYT\ Z62 0]Sٳg%앻*Wɺ'bYz@Zٽ&.F^o1ھn|0q^t8w=P뭖I/Z:$`V̿~<5Q<<><뭺y&Z}{y)[uU!PN:U9@)rѭm۶k VSZ`P<8\D 뭂A; "Z)0@%V e`JT!(@ P(@ P(@ `%(@ P(@ P(@ PTjJgF jUWQ3r,9fɩigcs#wWt4pwEAa!sаgfVο5ݬ?7/B ڐ3!Ϫ^fc!Ю:7YA P'p&OވWPy)@%D<{Pc3ez Pbsd+t!ߡ?6Vb^X_ME%}@hVJAAb7(LqGK瞧'SZ}p׋qϸѾf! :Y^xFZ)/O zhyTiwS/gL+GjTË́Ga'ЦUK]t~?''J ?b,7Asc/YuU՞z6FFk9GLgX9<!K]nC!(/o@әS;_x#j@V\~6AfGl;̀IPU$O z *d%#Pn]Foޏf@Sఝ7FŠ{Koa4/G]puq67'Z^0sZ2*"ТyS^Ic|b%{&_3+,/_|p2u~c/h BtWo~/|?Xw+*㔸O P(&pvo&R5P`Ǟfň8K:)e?U??= g2OuG@OnH:):qbBy$(@'~}O9)Q+1[ u..ސ肸mݸ fl}@ϟ<|#cӯ~Ͽ99eǞCwyd}/.–2~NK@*geC!\|2#=222so'$!F٫;aH={KŶq23[:S'&`,tz{a[w A P*;)!1{Mz@v g>憚 #i ~y?Di:8پ'NKJW\rXh ZhXI.{R9Ϊ85NE7fH P(91=VG)@H k>|Y~L P dF#PnktPP6TcI%*8$(SJ;wL Pb#?Të )qPwjԦp$2bXSۏE"6xyEpPHɇAKfq*eWpIEK5| fMA_k_=!qƲ'`ч3W cT11ZrǁٰeϾ c/>*Jc9x5TS*GT?-[4bb!~طxqS2)/= |)?R}\]*oӹ"H P| gD%񹀿|`H OBVpRăT$HGV _?(@& ~ Kq>S)[40c&])inTfTŜ(P)ȂdqJ窀t_hTy=e1Yyz엠NUr;rȠFs$wT>_ܯ̔2'$,J~wur^Oz ؟䄎r){7S(U;U^6N?d_PlwwwC.Ml>J}6rq( R;nlz</֏+b@9?~\mځ\w[zSP@ DȂCN, G@UH?Kuܰ\SG1(@ |ϒ^\ԿFz#"CՠGsϡ߿~˯SE։1u{wAA>m[X }3\\u-! tZwvZ!7Z (.PJhxJA!~g⎕\h6rn*.8u|?\ڱnIQĽֻ{gJоY:ho ꛔvRN۴ѽ-B#_440/BB^^^.A.DHqۻN=Dp }[U"x٘=f/qLH8䂃;2%QCGn8x$ckаzuA*_@z;dtz!v&4˃Okx݁U(@H : ^am֮JiU @!Vc>^|w`c[CWww|B\r:۷qӬrr_l{FBlJ$6 _.y #M o`iz2C^JNo"=3Nww)@ \SnqC7?]foB5-%%0D3MvvDīM!lߔfTΥ۴_ ;z݃^\s(]oHNI|888j~yc ywxzwnv*ַ/S՝ `4zH[t ;󖠽g,૬8[w266`0F"%b1111P Qf`?\w6t|߽{~NAh MPITAnJ/ 戅:ٹ:7F1 4/Hc4i $@$pd0=$`~6%-/#8w5V4rY2#6.5?-߿}; WNﺯ.JDEԬ5_jiV k5$hR\1OUh ia pmR`B0IGL W%5+Ty vT5d0L1^9m_AMCqxFqʽaE~,kHH%fBi &5ے;\THơ?~/ơTÒ2%Ƶ)~U,c@`%!†߯YJ=[0Sb-|Gd|~ᘟ*\v3ܐ^=_֝|~3^~jG"d0o[8dL8̚wr2|U`ızD{Ĭ-w ar+ 3ŷ]{Y6 iBXCFW Ofصjj2 k6#l5s: D/ټub2YJaHHA 3lO܇0/'~feȍT0y dᣪ-둴q: 3/+]CcB$Дٶ3`b9ov,D i&-+qRmj|J4b@IDAT @ 52ipXķރ[&4+jc !Fwތ3*XT e `mk'LV0b2Ky:`#aguwϸ׊$ƖJ~,.8كnBӴ &HNGY~#P> M~5IB;<!yAk>iW47ɀMIVXĀ.g3ѡv27.u8mµ+  h; `gc@wu/M@Cn&9|{9^w;l= eܓ{ie:$?5< ~Y 3[U˞KrlQ 4Nrb3M 41cIoxW~kb]qN!*+``nBϙGi4d<6bZX.N(ԍ1xJ: &$@$@@Yq*K¨'/S/a "G#e%M Z|/fFpibߓA @ $;r#R|&̈́S#bPT ;'QaG0h_fC i'0HWYn(GRL&HH$\h;NE({xx5&췬H'Ǽf×LQ[M8duU"8o:ʆB6ss! |#*&#ώHH: Dals1yp ZrBhL|'BjgjEu^]x)=ΝhR/_bd;d @Hp~skL kNvp7t\Vب|$%7kxlI$@$@@IA93o_e"@H[1ٹh@7޷ }+FЋ!旯!x%h%ٙid4v6¬k)0w*N8O(03UBB/5iL (G5 >&]HSLk鎚<`jMHcqtU_Fu]Kaf43M_oPSԣ+X޹ 7,s8p¤F&BCC7vE$PIPPPC> @ 筩7DS8!FĿ"v4\/ ԋ@S=oZ%V\<zqwtu|*{&glx;֚߱7WK$@$Іx{ Y8t0@$xAiȨwĖ$@$@$@$@$@$ЊPߊnB$@$@U zzlp<2$kY 4@):m@wJ$@$@$@$@$@6p$@$@$P7ICGyhi;y @у(HyA$@$@$@$@$@mmFq$@$@$ ]ݦa/#/~a\# @%@\6 @ `>w> qgá#Q=hJrQ s+keҧ=k#    ('  z̽gW[lGc+ݜOWj{u PϿ   |&][a8᫈Y]!evTߎ;M$@$@$@$@Vs$@$@$t4>{#M_A [1jmno1F$@$@$@$@$p%I t,+?z v'mZt.Xw, \m&PҼ\q6z\ 4S!CAHHH5-{&  VKR{v?q`7( b[91h($ëMYHHHHHfos@8aho"##ےh6t*iӅW{`dI@CGKk)vH5w YHJO[3g@ 8: tpR>o1mY_+?Vy~ݎ#Ƿ?D !P-䉎S~7mc%@}7>$@#gq؊C[cM[!@>mNq$@$@$Ќ:3!w?+'}ӌ@[$L>mq$@$@$@$@$@ #@x6 [.C3ߥp롯1{zhKJPVQof5p$@$@$@$@$@$jҧDR2P^^n;[[JȱU+rranfW'fHpժgɃ-+*PTT G{gAa!""cнL_RZ3՚`iiaR&eeeHLNCWіHHe #7g8Zo vm@G[z}?W c.G5(LGЍ`neը~و]Ƨec ^o=X2$ E HN؂0-m=vl@$@am7n][1Yɸ(~} ,z a;"e.l,qa{Z[P!ތo? /Ǟ=qc<#|i:iYFC ÔL*=3JLH܂{ -*3s3̻"L9onHL\"vbܭVpqXYY6g^Œ7HI WAӥ3RAj I)Gн7NFaaQl?۟/22'aP 9~_ ÇSK0Y3$@$@O zwJlu·EYآŻHg[87l=ܤ75շm tfHLsiɲ7h"^fl -/)CVD F3t&skK ~$hCm{K101er^_[*$Y5õ|gyjy͑ Νڍ૆_qWcAF WhuO  +ee-SVX,=cmWk7w~p%=T\K18@QFZ[]tHE`$@$@$8<A# tPq9AoPS|¦Eua5ˤ{P}r5^B r$c%Ʃ5=w^6cν*}D$lKH디!JӋKDڏU3Km2צxWwND@OÛ!ƍF\C~~>O-Ɲ6\U}Q2ߝ0}8U&O&xvrCDT,"梄p˜&P(L,{.  @("+D|1h 0!SŒwG7M=E(=z#$Z WlܚGXuzsc3QW$@$@@ȍDƑ51ΆHmU̱fkqv*Jˑ+Kaܴ)/.O RdVX!<5u{ws~ dC ޟ# ҤO~oP^~JŗBR6 ]x޷/Z%*\9Hܰ`G$*iqSKiK2H A=O/ 9A59(ʄ+@(0h[ ~`"\8"r  H2Haܸ؊HCȏMFƾpf)inMʠLDkUM:@Lee~N Bm{BA˅mzk W4W 7\y[ P[An#ɜDnf'~ye^OZYY! 5p,Lh!w  GXXX.S3 M-^pJ;r8$@$3jK~@BL8Б(--Ä7Sav CRm^%f^u?]kF'Bj( ͉H>@'K7ŋ{ɐibGe M~߯i˵5}8> 44!̩<%OGMx$@m/JR37ρL7N"?g26N9dg$ʄt- q_X9J鄶f Wֵ B-ښS̬lt ?.5q\:c҆76{#ЄZIėvKj.S78;'9̛3%@ `5ïkxucr ?^ ;Bn8H>CAa>Ei񔉟W7\u ^ba<.xʓ'?<4X*xNQq-lk:{ aBG)Ŧ N 'd:.>Yq  'xeŕ&CJ m~=a_kQZQę7qSfyMi6 cA|"xgNq`p M -%-~D>CJ3l-H&gzD{[3C$44ZoUZ~ vSS3}vQ.L *n/m{\λUKp+m˗t[RR (8”r]־ȯiݟK7q2jFeC_ٸe8 z8|ju_P(4סPԯ+X EI 2j7i(L-Y]9S/6>x8pk}۵ r򕿀{q\aHHe$<XYo Y&'7?/@S7k>Bc XS/U>SD u-z 2q?kޝ\43mXUOOofmS Cwe'-£+^|b1_ ~*iYk"^IH@IaՐ'T ߾\gKNiK=ghk}I-~ZYS\NVωOPgeMG4!<lq 4@yI83ս!MUWj {)2K;+=  O@ pj旚s r$@@m2߭P;Ng{v%?gJ?5)<QjArv% ׯ2?H~hMguJN`aGGZ#>z_Ƒ]'˰Y t赧#<KsCv+b㇐EO@jgWįEiK? $R3P--.BӼFIHH`&L||:܋=*/iW4Z{Y 4@pkzǢ8]e|e*g(I<=nhN]`_V4g8Hg~43V\=y+~JBy f}qBO9mcnZgÕ t$I;hvsr^A6k |)A K)7wDѳC}1]a*?iR[9:!dPʮ@-A@7rS-iApdnixF}>2V-5'6Cmĺ9 D|8*aϙ1YWҏ|j= @{'fD}1aȜ~dZ]az&o;mlV,)ętߡƯ݆lq*S _<6.t׹XXgLO8#Z;G"KV_&w l<4ieHHH 7YeSS3{Vjkv?LPqv3"#;GĬ?!lquC:B8cȋвx%f#PrlVϋ_.lmsTY0@$@$:dF@jg.KsW$[l9  D @8fGoFjj%TZ̦+euOnH,ٕ䙍с[gG ]<R/dj.6S -`-6[dh ܣ3a!M١a ]OX 4@̞%엚UOH yfbpFi:\<"7>=7k./A )i{y@S(/vhd [#`_7c}TY28 MdϹ/A BL!Md\]oR @'PVT\h5'ԣ yjIYC`` 5Y+aWVSό0L:yIRX{J+dFkWytx[(HJ$@$@ $kL4YVO@MWkhBt_u0;p@TָPƵf+ 3% :Ӯ؞H4֒Z0qXX]ܺmN砹'+(L}tϹ4dYL e; Գ7Ͽzc 4@][:be : Y II ykRh~VhNHHHe 8Kһ>pU8J_dΐ@QZЎb      Fvù\  8[]rcu>qGP1BgBIw3mIHHHHHMM6NHH.ކn ȍ:fګ1B%Piß2d;     K{8s  h|gA էi 4@i^.J `ak+'6g}     h(o   I ďAO_iFH!hΧ!XHHHHH==UHH7ާ$j!8BS;_E(-Iճv9zc%     vGvwK   h[l= dRt:Jx8܃=1B#Pzy*IHHHHH]]V.HHsKK߸H8LxAB,<)[7y@] TO]XF$@$@$@$@$@ra$@$@$θAJ_G_iFH&i(+,pRS @'@\ - _xq(!c9O18 @"@Ǻ\- J1t>;]cz#$(L9$@$@$@$@$@s$@$@$Ъ [Y#ƻ]9< ?yz=t : @&@\: 3wܪҮނ|U%pL4/҆liҧi{     L|w87   Ech t"RS"=I /:B-cIHHHH:: ;_O$@$@m??^c/gFyt<Q'ԢxIHHHHHF0%  h㪛04'eH. @yq14 {k\- T!@ L ~&`9(Dl-ؿ>dګ1 h]`ig @(HHH3 6ǩMq'^{ BBƴ6˼켻'3iQZ2^} iuow#     CuHHHZ-AMYc̯K~Xc(+Cn4; 4ec֧]xxx} @3l5 FϚF,]=46%G$rO nX8 타Ŝ@EI Ȥ@H5iH*>kU0MG[eϭ@ Z*9 䦛?,v?. Ykq<~A`? ˓nVoD9fM{v'Qvrq?=ъUHC5ې@ Yk83 XrlФO[K# @ \p> $o4?Og ۇlzzSvo#gN$@$@$@$@$(obHHHA[Gkn>Mbcw!eF=I 5qms5 4f5S|=xyV䗗#%-(/@Rjy{Б8z<LXfd AQQ1Ue)]xD4<:ݭ0~^~^߸Bq<X[[g#5=Y9 3gHH(*_Q fUbxAZSIv&_{)oHMgsvx (# C}Ҥƺ)sGЩw%Ţ(# 6wC`0HHHHH:"oز/J'`X"1q۵³܍Ĕ4ɷxœ\7g^Œ7HI_ pŸ򒩸~jEm?_119ܥ@cp "'Gnz[FHH:&Ҙ(䷫g8W? >T/kG/LѷwoY9aɘz 3Qqtϵe) t0-&ϾV"q ع fM=OUBO \qT5,kvqBzĬi 5xLџ|q~ر z6oߍKgLR'!\bp~L[Нj/?'T#^Ve7 J/ Fŏ`R   Ge(5k̭Jv>IQI_fQNa&f'lGNZ"lN8Ýw DžHy^cL>[V⅙B:M}3\k= @G%bt:ï7e yfNk)R ?n0$&{:/Vq&k( wA ᯿a`ȡxϕ]{ïQ:9pϣ pckac̈!B?TĿ'>_rBHU]5ٺK&p2*NmhuSfFm!xEz39NfHHc(KJ@yN /~Y<]k ,l𻐫t3L/-L߂GsmgLԴ(/ɪ$\_Xr\1ڠ< +2ʹ%#ŗw>bҶ!o=?Dx_vDf$@$n%'v-׎nzxiwߚ+= Yw1>[^iO(s¦xuϥ0%^[F=cv/"p86/\swKڬ`s4>0u 55~)!c_Ý;lݠں'?-%`t?-_emKh'e T0vW7KOpH33icaמ%W>Aزs/ƞc1TH9ZH ^ N`3~]+R i"~hzztG<=4Y$ÿbR05$C0%,N,H6ًЮxpϔԥ֊6ܨpshkqJM;% +M彻⒑sQ6bo|6B޶(QتOyko0* !9"~ȸX*W1gl>_ qJAV2p8.:ng6۷ _mYaJ^`˪uwg |ʍ퍵eD/I%,{eLb[  O,9z59mvs& 9;Yy>p$.0B:؀~ZhcRPҒ#+?žpu2`/هD 7~.X~/ T;)8vuٯ=ƙgc#Nl?/NEw/ 1a1RLz=' K!03{Cyi6yv‚`xt`48 [B-ܟl1}mli?BŦ?1{5vbj] f(?=_ -"6;/b pב)ːy82jZU o$h|{2bfQtݪRa!:3Ẇ}~~x%     0h1A}?L_P(n4M/ 8'ii^j*wS)@IDATa_[LDMs'V@y?ĤTM_[?ɗ2rP*~4q۟Wr?Euvi%LIsF⇽4{T"6?F &d?K_|^q&o`2iL\ؖHHӅ |/NaxJCp|Fj_>bv<&z >&_3TI`ooanA}!7ƞTu2||O+uX@/w&o%BDH8yV mǤFb}1:<ӀNuLeļSTXRr#C SSrqiX .س}] JB8aa^_#F=ŝSg%{"$ ;aQz:u6wBNxb~7+6sZat[GHݦ] uw4q     Rm&"Rťe(7/lO=Lx0}s/kP\\E^Hpb{Ϫv=\ՂEӼKq-^YНyaW*ahLfFTIx ryrN[!OSS/U>R.\U[jZo'p2&~YobҢ[4D"si`&L%I?vǁ}?zY!gvr@  C@˄)k' dqSpBYo&!pkPHQdFnPt> 15y9,3/7 OȰ諷tDpL^c𢇐o6}Q}?ӻtRqi_dOE9q [o|^I=$ge@mU~_qW:MD O?U5)ڂ,v嘽>8Hy0JsSPnx7΋u%пӐJ>&nXhUކp1z[\iͅDnxk       h_R1oV{M/,8;:R4B`.5ׂe/_a 1gEB.ǖ|+ój/Y^5[3`iRGm\)̐4'$hڨjx,A!T 5_k +oR$׹iAh`R @$P*҆]A)-+ùO/Tլ>5No_lUPqp[^Q.3I96.N H/ƮG^hj=gV}~oR$f?`cx$J̛p3f==Rw*~֯0gzđh;iv&&=ӄ~E".#U &l^Ge^C3]&gk]2#OHaw;6!cpՕc|e-RmvI1?MsFW8pٗ q\o/7~oX4\b&6 |ueݳ ?EI/(C =WK)h| ^;5_v|2@Rf:~)|{Ef/Y0aV=,:ya/_z t;*89|Rm!e_*rD w v^OjvU)a8+ Nɞ}NkXbHHHHHH8`[MO_544XHm vL$` %B ݻԄ&N.|mfhv6 N<+xoo?rOgW,X˰fFCիV]gW%Ԓ~T]yJAsa*OoCOXL?WiҦ쩪[ggNczY(N:P#UpM׸'n#IUZۣl@q~Jp5:[-p0nO^*/L&zѷP!NwSӷm-thݛ\#㑪 R.xstvƉqh 9|] >omNq%PݮMc{b;   6G<' *Cf90?GxOmjNVL`jꖇvkR z&UMh ^M5"aan)T~U~-2 qZ^הYNЯuE|~^k)&61l ~d}wT͏՟D(?:e& 9g1+4+7L>ٱ d-N2 ԟ@/aM   vB"/.) _q`'&.D5̯!1Bc NKM߷S օ/ SbiR8x+#Y*O]`'h֧.,#     T\\   vL46 unYezn6V36cM_ccKoS]'Tei; @vb3O[[dWtKVWɰC9/+ @=P_HB$@$@핀;F~Aj+b$F= 2 BPQZ+tk|)rur      *(I  Hj~͝-h؎{9 |ߢۄ]'XjGV:Ca6Є?~Zrt& KǺ6ƚ'^5J&_-$@$@$@$@$@$@u HHH:,tĭI_xՈAj$@$@$@$@$@$@-DaHHHH[-jN={󈱵.Q G $@$@$@$@$@$@gMHHH70r%ͺb-,Qu$>FɳxMK{g b jdA$@$@$@m@) ۀaz/1 HHHZ--q`>?sxm[OoUTP[ 43 0'   D4/+Ч3i&[/ )tUYN$@$@$@$@$@$L(o&HHH">CInz- V( + @ɑC   hRwnFKa>]ĮKWU%|UEB!$@BR]k{AXYu V嵋 EE,4Q")'̄{i$@R~$9s|/Wo3癈%Ue?   @9/TD@@fR~MFzKPM{W-eǖT   @ /cPC@֙t<{]4jvM<n};fέ},   7@@J ,Z%?xyV zij7 WFNOS=z   e$@ i@{R49F򥄴KLZC6@@@@P! @NMqWԬ!e׶[Mپw3    @9  P~勼]kwDժ]?-ڨV`e$)=>P@@@C p Tet6sZ|zBgF#"  +@`Ũ T&׵ܪڝ{y]U6yyVfm   @K6  P~]إ?xqzb@e_vdeE   P Rԡ   PnuލN3ϭk^}96JӀ_`m;T-NnNwGaXZ*5jRwmg"߲h6@@@`6A@@쏔+0ڎ;yFD֧8##  e-P#7lP=8H[TGCv(jS90g:iK.z.dܮe[S42!JV"@;uTy>U[tk׮,VW>kU罪=:sr2凴7^nY-k%OGwB>o0"a"g\Xi"j"@JjFr  b{*F].P }JI.S&   @@XE@@ՠKw9Ɋ XۓTL 6#  @)lWHp7k\\#=''G1q 8rrr-?7jƭwɅĤ RNn3R/5 qjV4~ʾTo}Oٛz6y_S}nupjng@@ܯdg!7=]b#ij_-Otզ=^|^3>][|`@@+nؿsj5kӦxaoh٬`3s33򭳂   P3(s췥nP?kISխ_}>WTMug)a}wcO6gz6 I03is6m߱[Y&}(&6^3T{u)#4O ?5k7uTV-KZe+謱'6>O ӳ.8{;K4hu{N;?a^3iy,_Qpp]w=?.YsN?= ѰA:oۤvЁ}t˽B@N9a[n@@ fgy/ᱽ˞Te)\+M9 *$g۶<yFBcͅf- @@@\:-5QV-4z#1x1"cuݿtWF{.橷#mٶc~e튌e'ݭTmݖ_{OߛB' /-RmMP?9e 쫷Mf}Z'}{&+##P_ rx<ծS[ )"e[ 9w?׀'7ggz잛nt{Imۮw$vT|@`:VߤBF?wO KcϿF&'~`@~h؟xGN&w^ݏW^BCSʦߙ|ͥ:y`+unٷ{h5fU_W;/[>޹6:6p;s{Wܐgou(L佾*,# P=ky/$f*]ȎUzn3bsS 5)}v'*[/հȜNx\b֔ 5Oɐ: 嘜iZ3.>uv0O9OMwURIyX0zM[2@@@_Br~n{I,]rinĹoz;l5v0%gmAP?۶lm.W׳#ԷOLq$& `;]zv[mҦyw(fY_zL;k4oO4];C ko67Wf]CI[RU/|uYF@%X[G3A[}u~DOIg5qy oRP^_j-AM)@F  T@MۿZOj}1,ЩGgImcu\0}8݌~t{,Nqvev rGFLtC{&W08{sO(5-M?|L@Ȧ3 2ymڣLscHv<ċc&77J*M{NcMOconLtM|7fGKj  PuZ>K:$-6bx}\6Sǘ=Cu j{% ⱎ  T@A'Uw>_ƿF' .RϾVFFn2Lb;n:}<<p.63ikl ys5[`Fy^VZ<;+w5Si_ֻ>6no__q &o|[vD]V>{=WTH7];!>bbs/eVݨ/Y٬GLVnuϿտLs7TΗ[@uvyOޭQf|w(~p$5}<3߮=-]@ss c(   #ziZ޽IbmА|jOU0lr#M{7Ԥ ؂uJn %M,rM'dS䔦9_Zzyvi}J!aRCŪ%aN0js* >gEPD {%k{NASFV+ a$8>oC@bU3gA yAu8!ىt+ť͂a%)oQ74$/cu8@@NtdXo?)7ݤٟQIcVAe@@@*,@@S '6B௧3͟h2榸 tG w'77Dm@@@  _aԜ@<9I:Tr2 Rrqn[-%Ҝ_c]]k6i{ƌTy)Vr   pdYΎ ܔd9:y㔢WlZ[t>vd%ſf70č_q/@@@#+@sv@@dܞ7# ._/BVtnn 8^̍4}JNPdbb@@@W5gB@*pƧ٠VnE`䀡IK|zt迪%?@@@#+#@@@@@(eH#     YG֟#     P&˄F@@@@@8?gG@@@@@L #      pdYΎ     @/FA@@@@@ ?@@@@@2 _&4      d9;     e"@Li@@@@@#+@sv@@@@@D0     GV     2a@@@@@#@@@@@(2iF@@@Wm C@@#û5     xraÆ GZ`֭G !gF\d%VIQ#Ո|*@jV -׀Nj,,@e7ڵk'>ݠ5AZMx"筲&y 2XUw>>o5坮IJs     L5{C@@@@@)P)}#ݸeBռY999K(t 8nLQ6nոN.Tp7$&^prrszlmؼ]5TB5Sz;Z-7wWRrܭNRځ,  @nz;K/[pT#t'.f\QacoIcƕϫwEGfOE@@*4ez.(+2Fzi$H]rjӪy>SȘ8<:Ee/G֟hͺv7.t Ǎċv}LKK 5a?SzêW;9ulVLLQ   dئ콅|KQ"lUժ+轈7Wp߳?,瞦Zc@>?M6өRthJcgGҴBiۺ]vGŪYF:}w~\;&uj(L`?(:*# Gvg5ijz;sxJlT_pw7k~g؛?C0oeR\w׌?SFh@3il ?5k7uTV-KZ)qXƞ;3Tkܠ>=邳Ǻ؝ߚ*_n]Yh(#>gղz~\\~{aPu6)x,_=|?y %%mR #ځ.o8儡o;@@MMURF"Тa#_;bʥ6ڱy+%'*!9(zJO߫e+ESK}mmruW   @|hfMԶU k|9rD ۀ!۞j4b{ַإ|Y"5t@o Iw+9%U#sAlx&Iy u={,kL\o0Siz{zoWз l{/SZ׮dzvZ7GM1-"M1{tU͌`'~ ~xJ9-" @vn6s֝_|*Uf?Эo\~UEjwhOkֿR)C=sW=zz}p՝ϛ&uFm%Ĭ47*y_ޤV(50yI@@*#Pa+r+u7 {ʼ]7Qsv~Iz׽Zwu5rn2޶a)kN9ĥYweK OM|ӯJ+/t7-n2OM3KݓXh>߾l3FkIkI&y::~[{FdCI={]O 2F-LȷԭSGif`   dGGxp|l{puB[~Q|ru~+;첾7oK=vЊ-ռa {1oz?<کykOVNnJwCn ]=yTVOX{´] JZkRb_:@@l*,nOiϿzäyLz팈RW5wwlVԂM)͗EjNJ׌jߣwg}ęm͛6n &MGS6nYڴqw,gg;ގ%ۤ6O,<}u; lڵQ;Đ~.}R`9@%\M~ w.m:}  )`GT3s_Kûu4svYLQz㴋̤~GsWԱY++%KֿǞ:AzyZ9oHuӘs)z‚C?}1cz м?~Ks})%?Uw|x^6ni~\[q7fI해4iӨ֋f2su4cWY ]t±]SoҘqj?ߥձOzh};[?]u5k_M`?L=񛮝?Y[s  hUnVO?Յ'3Ok){Tf2o3|Rt&KQzv,56y"k[qۆt~V?u6FFh)'|j??IwEykwtrϼ'nסFt;NwvzNwטU7ݟ3QoNV|Gwa_pݳW@@jŚ?ɳo>gsT3㯔c0dRf^{s2O}9WZ=Blo؛t>3pc?  lrjρ.(Rjwq7;nKn4mo{W`7umg[z[xʳn[gیKNt랺v{vif&5PYLs#3+Co_u闍ҙ~籺^,Ηngo@d[b%zosˍB뢡'i>܍ݖȽIHW[M5W? wn?O7)}B6U-ZKzi~<2ͳϾvQ ^@@6ă·H\jRpÚnٛ%KwNuݮu+8=YFst5I[c<|?'w?Wǟ6sXW?(O o_~a"O=ZդcWWl\۟7j׶^hx*k'gW6oXpòil?zw=V2P_z>@@RqJ뗗ҡyEm'-=>ZGE%&()-)̛}m=Gι9j_uk,`Fv9kl9n"ߖ᭵5zrzۮv:jCZ84pul: =&ҵ/YT?!loy"cWkT~߭ɛ@;If2.jkx/& &m%Ǐ&nij~J).ťYquV+2_o5Գ7E6F@@b?[la'-8s< {?HE&]ApkPXE*-mܼ]q-@ e'*)@IDAT-^ 7M3cS4 m3߭Xׄqa&t|&߼y-xs;ɣ n.v=5#յU? [;3AvNvϫ?yv<ۊ{iuv'Ĺud7w{e[/.)޴AWyG13쪜Ej95;y["wNӛ)of>Muֈw-ԕ-&ķguu Cw?+~_^:igGOZ6 PMU8|ގ(?'ym*ज@@"cY#;@u R@jTRq֯97OUL^54X ۊ6oQ yw(ukw {&ճ`^#LzJLޢ6VzܿN\T)n{QuV]# {zqmUG$_TV?ܮc;O]o8_?{W)fbM);dn7^&zɾo,];wkծ(XA@^o,G   Pr(\3o$eЙ6{# edg+bo?UmͼjŮLKa仟e@@ /5@@($0\u*YP'PooH FZi@y+GZ =C@@@@@KMEE@@@@@*3@@@@@J-@TTD@@j kS<4O~~PIz  @|/څ  T;x޽Q9ڸ @@@@"XGi)5jL@@js  5TeNgt z.@@/@\!     @  _d.@@@@@ 1W     P׀7KD@@@F4tعS'X'|̈́ծS@@@j Tnk@@8OL@ :[׽ЪiE@@#$@sZ@@*Zsdl#dLM|^+_@@F>eH+  T9췥k:  N?@@@@@%      @     T&o"  P<5@Fskm+VGN>ƒ>vce(   1Ÿ     TraÆj@% PnZ/#PEU7nV >om"*F*/gʿ\@V,zHԩ!u@lMvډbx % Y+I%JV(>keH+F[iSUHSU9      @@@@@ҧ8[+$8X͛5.ٞBnP99SnV;BuwCbR)'7W ֆոQC5+t\S?e_~ բyyw6lPmX@@@!K{w+nia\PmlG'ow:D@ /i|a JQ^jڽ7߿rVP ;:[-ܠ(;*V͚4o۸}Ru5'NiGI0= nWO+Ҍz;{_ۮ2  Ta)߀}j*o# @EnGXfWjjiF\~Y 3퍁?ڬ%%Ц;۴}ne4]qbb5O'1Y2BIcco8lYA'Zjy]҂Ml5DWզÙާZM=֝%bur:DDCQ}=џg&PoR`=`z   T]IJb"D)&+Ea@@#/PatZ4kZhg_曷̱''wL0G X]t}6;lؔEllGdTB&n,;@~yCiʣϺ>IEv8s6mғjr~=ϿQȦR)ap0Ş3';.flI;]zv[m26>Aϼߺe3bGփfB)\}{ '?0P7jN@5 kvuO?؛ f.WnN0t{R>DEQb  T()#!;¿dedJ:o)-߸XWoYam[rLv/{b@@Wԗsꙩwz;s>.N02\`VXfN֓ۈBx";K~3u)5=]OV(a,߸MC섺۴qݻk@.M3骋Wߪ]4rh?ovm7\WB['xIH;}ijlnT>,@ZZC   T]:X'ބRe"=4olulE}: \3%ۏQzf?wt=fz3YE?%S  @MۿZOj}if:uhz㯔cRl2mlz??n ߴgW6ζy_plwA^&nGl#b kTm={\?}gws\^M蚢4#m::udeCg&mm'^T3ioiM/ڰ=vti줿Id'T.M:   T*\3Ǎ镟r\ϦL3?3+Co_ϛ6M3c5]-Q<z5gg^,ڨ&@@|*lă·#MnwWxI}skg_fo2;4Ω۵']o֤>]rmW/إyLNkӫo}O=wϧ=[<Ɂ&P]}a׺&߾ >bLP|JM{^rnvB}vŎ7?jcqmIN7lJLs7#8*M߶XF@@/`ٱх:j+7)&_3R]zrH3?HǶ=NMM7.\{ nufU+T   *z:C݋lڦ )UwlfVnGTlڛT50(X&N`&92ަK߂},j=-=<P]%na {S`w;C`ÆQY:uU PZ%~ZVN.2W?j5K{nf4-g߫?L,w􎸳6iYy&Q9PqծYGmXaBic҅>o *>P؀m#@U5g:2G{۴2v З6xn/*o۱"o۲Kcmr~ۮ'_/(:  UW +*(ѥx ֦vwZMgNeTВ?(3;S^ 5Zs%'0HS$ @@ 8"#+8 Q"Qc@%RƜ7HrRm>Q ?Q_rMuWRnlR_v]5nj_v_-zK%+CߛOi(W~=wm&+p9 !  UM kF]) JRP%ـ'o+@e\_. u)#4[^ɍ1@@*^W|9#   @h7#k Ub@KREQzuwnn׼~n1H{tNKUxᝧ"  W^ @@@+;BA'+ؿRrel45A]}rc`v)Ѳn+  @ @lN  @v,Sg!f*['w(4 fy<  pp98/j#  OV-۹>A?-bĆYe/'C&:5_K6qmc@@b_1Μ@@@@@r _4     @g΂     @ /W^G@@@@@bW3gA@@@@@\+/#     P1+ƙ      P˕@@@@@Y@@@@@(WK     Tq,     rq@@@@@*F8s@@@@@U8     #@b9      *@\yi@@@@@ _1Μ@@@@@r(i@@@l3/qzpjsm\  P_E@@@@@  6x`8[n=]>k5m"+JFЍ!F\d%V Pc՘^h;uTcap*֮];Y } |j»5V>o坠5A[Mx g2 y)tͼNRF@@@@f     LrMS-WlQL\Bs7o\EƩufύZqƝvr!1)E vMfggkjܨ;MʾTm߹[>Jjʷ/)9(ծomsul6vV@@@)ّthsr\g5hLӂU%7pt7+;9j P?-ӓ/pA]1XM&W vDDkoWVSK1qzyzu26_)>;&_?7lueo\>.eee)--CԲyIMN)#}?.UN{/gڷm_Xy 74scᑻn1nV@@@Y)7;_&ׇ@ڗE[v.u>?M6XNMKӕ7ޣYke~K ۶u 77(ʻ쎊U&4ut6tT]M0;&ujb^,1IpgiϾW:gs󯽫wk3#wy*~k@@@LY;)7#2u @)ei]_RA5@L(AA:&MVG+hfFY?ճ)jlzghmھcLz뮸@1ړO}zϤ֚t:x;RBh-[Fg=pfA}zvgu;a}XݺN==PT_}OcG'٫}&=ϒ嫵5 %u駸'!6UggU}PqMq {MN@;O1u[ޤdWm>^x}/@@@j $%/'\2׉@ dfe(6)F-NUk*ƌOMRBjUӴ@\:-5QV-4󳯕krt;&o#Wdtn{jjV/ގVe/kWd-8n%j~.m<{ޤ:i }9onqb5[&O3{rM@Ѐ=\eaF k^^Ϟujk_6Pdtlp7AoX)MZ>{ݺxIppRn/ozTf'd~[߸ޛ#ƺ]T#w'~X xZV5 )t|ڡJ$aR@ OKzjNBw<4]ϚFmp(y&'ifd''-ԾϤŎ0Anέכ|Q#S辰_L?LM`o: zu?F_y[ 6O|fr*~g5ݾЊ׹e߾orPS W]2^K]-e}J-./ݏc`Nv+((/SNN][GTE =+   5B 'z3;_eCGUXc]2d3_oz?_ZꂹG#c櫥[BJ6R>V\'Ծg_O/Ol1B+4~cb?=X?l*%$'mR@@@j"'AYGWK:e Mi:[ڲ.6Z};nʙjZyn{:6mZ7aFs҂?W{zly̹Լ1|{F:Afy M9)^8"X?Ҏo_;KQ z켫5Sw`Xyjsnw"77G6}i޶?\cz м?~K؞:^ <: oONƇpL7"9qh@RReg\`ݛzϫx3VZ=h[[ ac 捆KfOuͩ#%K-첷2#Kvn#>2Lbe/ʵ/\dY%":Cغ"CeEOe=Ͷlپo察452-rkW]7`dJ&I /ZL&6_βsWT̬l9hrn9ʚC&-M%9|8ձٙ߹e!2>yv7_ u7<~ZGVcƓ6Q>ԧ`R\ ̒']l1֝Iloc=insZ5Uϑ*ULJM3LNʉdʝ+oϳxMeSTqimuL=9<>K"з Fd[>mZ\np`^;S+2fnij{yN\k!DYҦ!@@@I:=O5y0캦kFkE"_ltofԂs0tշ݉fDfQک(f{9?`WA%7}IL:(UCBrFבzrgȹg&)iif(-M]ϭ7LrrJlu2`Mq^:2^ϩk:!M^1"~0?`$u~0o,KZhc-=C2uTq'   ^ wo\G j\GF{pݤ#ie_;9v4~v.Y frܥ8 5g8:jWٯ2F~d':wd;Ek@_EK-&HߢNYGr6~ܴVFwk;(4sI3oO{^ǂU?ٹt_o/]- 7K>U}߰Fc{rn߉2tdؤv~*v@Mi{nt#jPxC<_m|w2Ie؋l*߷nj_s,Ob{Y*Nx ]#xVܧw )\] zI▋ Wu^c`~,#  x@v(cw8iFnks6g]f|fɴQJXSG-V.-:- {cu(_zF[‚G$Oe&-Yn=?W[yoDS}x=5줾Zድ+l= oiFg 9EcDnWND=gIT>?{LxkؼI*c{] ի֔{\}"9rհ)R3v!S۝&u#H[K܁{Bh2}WsR/L{' -t>dž q|">֭oжm(-"@mݨCE86];6;Bh8vonǑ[%OS՘AO'-Wq̈́8lrk9%#ݹK^gٴ'奟˫?,rY5N G[_΄L?FԒGn:Ͷn+fqV\]JzVIS|rߒ﷮*x]הԵ̈́{To)iWGuZ׮)ϝ;ҭ~VP&P8MEC@@@G ;f V.%EkbFw;G*:nYho xܪMNr[L\oCz|qu }`sss }L0[5ؼ\o}W 9+VKI|H"U[%o@@@9}%I5#)/0~I:[W/H=Ҟ%%+ˎ״>kfw@@@L 7v >`iޮzt<m{@@P ZHBGkYcڼIl@['}"   99к=~o;OZ4)*! 7 O'&o{/   *'6On8&M][]&RSu'[ww;Wʨ3o,׷g|H]լ#AW_$͚uM.ugmX-׋w׹ /qTD" 0¿">U @@@@@{#@@@@@*T'@@@@@ uF@@@Nn_7ҤI___㏥qzF,>b[G[Xc@Wc͵@@@@@^^-))IG}3K.}䨏.qob{ÉEZ+G[v؎T    @yL3ھqңY.4O̖];7wt2BV^,?6&͢JڵJ~J׮g!|s.ҦM?ٲe| `iڴT*OJ֬Y")Nl@F@LB@@@@ån(iҴkΐ Ȕ k?n.???v7^]<Sc{cOʈ'ɠS/_{9ߞ[AKQ=_ @)/TD@@@𜀦չΡSR` QiyJFF,iII=(ћ5fԿ./d<YҼEEsyx8IH)m?V~iX)E_<笧 Esn@'@@@@>Fn,YpB0rd_|lmBKDSQ(Nq2lG@@@8inygfe߾RV#{/&ҠakI5i}ٹ|s۝EHO5)* pvoš~hK$ u  p<Gc@@@@L //sL\9TB""ȧ5}BLG§nkNܲUo39-ҥE^nϺuh[+Pm۶i9^)7k^2Vky=,p2}R>{W߰T~]֝Nq VCVCR99v/XʦS@lb)ʰ]?-ʰWʨ3o,AͲ!>u$`KҬYij~JZ6ſ|w'6CUue@@@@*@Q[`w4T\; DE>Ǣ1     3p9     cQ@@@@@ʙ9r@@@@*Pe'z-C>Y9opiW9KЪ,#@@@@@BTGGGG\$Y`۶m7ʍߵr(/[,7|ͣB*ߵ r%\=.J5U L PRtkԨ],8>k߷Ѣ.'8 ]+8~ooH W>pe     X     @(Ք>,kU/Jl͕}:wp͓RN-sٴe9 g;/IDATPݐ|(EIn^dddJH`dNNDo!իKӤɎ]Ҽ)ϱtPBCH\یٱ[5n'     mZ4~5S[NNgo/zRڞ5SCh|O^JF=dsl{O>ڱ^4ع;N.t4WMc]7IܾҜ%,kп4w=>J#6oId4ΖL91W 7ԭ]CIen׿/!Q^2Ex`5םK~X.>DWtӱeTcXA@@@@޹]rƕ=pփ1 %:KzB~R5J5KtVX_G|UmXNKOo[D5w_|4_G. RF5it-[!'ٗNJj;復ɀ]m'3)w ]s>][ ޛmPE6     rv{=~^O0y{bN*9}j\VC?<QYYYQ䕃d&M6%~7|3Z\rh0ϵcO2gzgvˎm\{y|@˨a{vcR;.7Dː~݋M ݱ{eSVM3~ctnZ3žGw~㯲ҺES}եy?`<\'+V~R5̾ *͛bꬕ=qүgg۔v?d;V4 ҷ)öEn>@@@@@ $g.KM)ë(Sg&JzbE1;[phYctԪ! Ց?JL~z-g$\w]~5 ?w7ۛ oݞ!~]_ޱ}1%c^ӽhw4Ы<6=I)4Obw?K\O%&(KLPpJEp?ݥY͑]ډ^Q5_ّ (A/рMן?.|P0)q^ 4A]EFNO=8is%zAeSGL|hH tz"%?)aUCh=jb vڋ:vD=gnN=D0}%M ]>>ǻ/tFhk 0נqc)ǿsrM,Vls ܶFǧ*wxҦ5#tW3,y?$71״Q,R׍jT&#]3UT )|fi\9qLayy6ȡv{_>.^&Pn}@f1~]oefiqVW&o|1;wK\s|Z I^xctBg^?q?vL7YG|̗US]Έ7&6d@@@@<#`9G횟Bp`=fG?Jۆe{jU6בܮ_Gǚ`d59ݿs+>֭+-mٴ =Q+$&!!+˼"#9vXѴ7)iifQ捁3M/t`r#5/:32%zTu:CCNjNR89w|v\'{f\+}v\%wz^\-P޿oٻwJNU)ru]gKe֗B6o'f(=&No߻1t۶V_ 7~iIJ9$w\o M&<=C^˹{˖}_Znn۪MC^ uz4v4spj%YV Vj`R?g# 5)voDk:nq2/i^; 95_*=AÉeK;    x@زAf kUZi 1^eέ2I[{yq.<]dwfr e|SMgayr {{f^׀9[u)fPnҘS$~ !    &s"$".#n\#]YGVm6>ͧ߂ĺY``_O{`#>+mW/L ]/Xjvn(-n޵t"?-n8=?vfa(!A5eOmw:gWp     r%Kvn{.ݥR~ЦQYs5دyT gq#_ӱY`siMzh9;?_)[Ԗ@ejlkU,%7lٗh?.@݃5@@@@@y& NTGW/vv'HWKNFcn"~r퀞ҪF׭.}/~(d]V]I\?.@@@@A {׎`Ml[m'$a-ˁʀfMsvukɊm ,9#@@@@@X7@@@@@JA)$      iq·     @)/TD@@@@@=-@@@@@(J     xZ9      @Pi@@@@@O 8C@@@@@*M"     iO!    8֝DYx}ը-a5D8*h/C){F3 @@@@@8nR}H p|۶m;8J$wDLTB};!4@Jߵ&J,TT>>nv:konXA@@@@N@NCd$ >WWs /рҟ~YϱA=qk6=7ox`8hmҠ^m?w$qKs+xASr+ectlؼU&]:^$;;[3# k\nt#RIevߒǞ}]ª:gJp ҟxST9ΐA۝Y@@@@@JA ;ze74-W;b$7&鱀ԫ227-?_"gG ]Eu( jTϿ^6n`OݲʘM} }9|0Z3_z v=g?@L8L伳 ]UWޕG(     wnI-A?}g\е.>:*>++ULG=¤od5fzM-Um| Z:b; &?S޶]v쌕lΓ} 2|XF  ;)lᝏ?eH^rjnoQ̂mz=T[CSy-۷ƌp֌_riݢ>TFCQz* 頤<\'lgٯ+QaҼ)Z>'zvMiG~7V#5z̓i7]E)    xB@7`O!sHK2 /\:ujՐȇ~%y&?F [.kлMk7nOKːO./WX߾s̘+}w _:N9&zu'_z^{_7))_,Ats˾ENI=;./4?9ҽK;{\юs݆2X)P~w;Gۻ_bﶝ ڵ3@@|SΔHע~-L!y폭vzh6Sd^#@@@@(m8{ 3c+1y/'EbM/3 ᯗrfQX.NѾ0Ҍ{;uڢnG;5)mxkzdDUykB:/?q}kw2b5er5/e_ZGN:m)]~] 6o|jr.~g5dȀ^v+V?7ekǸ3KdDl5o-\qٲuK+[ 1SYt?‹fg qn:t|ϦS<;̨$_,Z"g6> > ,     !7?"Or"OeC>DsS$>RU&Oҟw>Z(o8? ҢY#Ywj+?ZVԂqX3ES4kP3_8vَ_fu6]f.85=iD4e5cF5o,hzk}@^H;E3v$)9NxZta}`W|g:#ι&;!~)fU^];ȅWOu     PI$~Ow%{ %zM'q#M9Й_U d3hJfVU q6 7[۹k/vaGS9r4N K7?[sGIdm6x@~];~Z2bٱ;V?u5ңK{6W$ZEzkz,DK+?95O0S*e kQ#}3B;\u     prƋdf8[Ey{/3?Ƥ9o\-¹h2|ۃnWϤٹ+VGFHfV)ȹMҡM ;89;+,DɆM[[t_=ou$-#C+#t(Q6ơ6nPy=_:xPH+MBѠaeӜ|OϼC.phߦs=3l:\˔iIԣfqX]:AjjyᎪ|"    '\ ;>֭I0i}Mة|2$۱*&~˕rduj#o?nWjfU˥r?5޼EmRhz͟#ut濷4qn7Ztr-1;l@G넵q WKmc醒ө8C tǏ?F*|:3 TI4Qݭ4oȭ3W;7$qnuuvj2%vRŎ =̼_|ݧϡ]fF~!    'P <#]\e]- W\BjXJ[O7[~M?O7{ILoa:͓.v/7pL{\Fnǹ^F5j9r5SJ`gƴ+')w>,=9y낏K{ .1<@ey}Eo?ɮ2}N3۴^+RV|1;wK\s|Z I^xctBg^?q?vL7J&{<4eysg&7:Wΰ)9M7S@@@@@R`NBQ_Z8ϩ#]'u:? -nݺwֶmو tPkymG/xc]GݧI9G7B 8mz.Mz]+n9=#ӼX#ny)XQ#ª͟P'@tt&rN];Iy };!7p };zR ]; }+I_EdKUI;݌Ϗn'gg')XqK7R$fBݢ)z/i\/*دDkOD_Iutݵh^q-k_ӔHPT     )Wd֧7=ɯcIѹ@@@@@rbw߲ "5>yI塕9f~XG9fc"?M @@@@@@vKOЪإh-)K`*,UR$:/r3Zɘ6Ҹ_U sVXb=֤cP 0¿ [@@@@@N@΁D3?g_צsB%9dJM$W$IͶ LF\ڳ -]HP)I [@@@@@N@I44 h̭<۟7䟠ِw+a+vP 6o@@@@@rr$uB~ Aɒ>RTQW | V@@@@@N7nZ_]m(9$k??1,s\Uc/Zx[/^a?|n@@@@@8ϒ;A@@@@b^u@@@@@#@. }*|c*ߵ Pr+>.      e-@G@@@@@jJoK ڵWLʾ zdI|~_qlڲMƎRnH>"U$7/O222%Jm2''G¥zdiRRdǮXis۷?1S }ؽRv ql@@@@: ٻx$ 9⽧J񓦵"X;=_o296'ntFf-/'MMԫfu//͙+̾_ͲK#COS1aVtxNK&OlIOϔǎK/cqպ rӝH]OLJ'Ma{}.'^xS4j kY?lǛR5yvT roV@@@@@o޹]S->m/ys7௣g>~`j,irwϗ٣ϓ;u/>R בnKl|ԪQM>{iڸ=wVH+c66SdRZ|ou2wWIpgޙ'o>4nXOy]yΛmX>@@@@v];/=093veɅۗ+2QYYY6د:|&M6'ǟ}#W1k%獖cO2gzNϔ4o;c%ۤd_B`$%QHǤw>\o!{ɩJ㼐"4oڽbg dhܾ7f=_e˥u2S E]itDO5y~]N4 \ϲ_W97!BySL2|Pٹ'NlҎl@~9owz,)_7n`S@@@@f4ٳK|W%WΈ!$ WKD땅rԩUC֫#~Z1HnR k@~@n7At=--C>;4_c}=2cK'nI4;pJ U|mx{}ޤܿ|%.'w%&8%U"|Pޟ@H.lS$33K ^ e%RvM1wlm;Au;k% fͧ)4UdyEri6Sd^#@@@@VX=,>Y$@( y)|H2S['_&?0[>3w_rypڍ fdK'- 6ZtT {v=25w KtϾ5л[G129 ޲vl's? ۶.?߮ >5OM3 2d@/eӌcܙ͈pjZ⢳e묗Vз4Wcn-N3`y10!vsv-ѧ_sVٱ;v$_,Z"g6> > g}@@@@@o@+ [MQ6>M:V=]ʗӪy'dϿ;-7LJן~@M~xiѬ wj+?ZVԂqX3ES4kP$yo]aVfu6]f.fjn+5h"-?kB:^KI%׼y\,K:^H;E3v$)9NxZta}`瞇wv7RiJPP%յ\xTXF@@@@J@G;ʑFq@zkON(X@ t׺"%ٌ:?)aUCh=j8,!&ްnm-^VqPo8~t=gnNiuZ\sM[ۼڄN۸A=ж[ls[w] 4m+.mZ>]t1nȹ 5G}; 55cnGg,     ^ 7NGL:x?[Q;tt(&cqǦ2n叵 ً?rnFk{IIImyutXIh7ŕl*VN%f w4  kh)vM7t /Z 7i_`ޣ3ؤN錂*UM{eHzw ͛6r;Nۙ LګZtd _u8+jN&7?U}Z5sN'     /X4[8K_q,=9y낏K{ .1<@ey}Eo3?ɮ2}N3۴^+QV|1;wK\s|ZI^xc7Wμ b1~0n[Kj}8':Wΰ)9M7S@@@@@0#s u3}~$#.>֭+h۶mMkP3ɱ_$&!!++;ێ|wQVeEޤN\[捁B:?c4WPu=L@`qܞkO(hZY#ª͟ຟ[ ::@TEqr)W@ Z?;ys.ˇ߷/w?c}+?Ϣ<]Ip~7T2ݷ1w<5`ǜ&]-Kzg:IlQ~/i\/*o7՞`}!UJ|Z=5.\zz@@@@@rHp{}9Lezǜ@@@@@r $U8m|v9w%95[瘝}epym97     ({(YcwۣtJ]{JIמy_B|ֈ2?%>GGJF}$$(@X-&{Ie_d)     e&rX|:_rX:H_IMd>2DGkQ.YFKyHǓ@@@@@wWu3T-r"m_Bn[cڼIOjs.@@@@@r#);.PyaR'T ;iпaxc>_f@@@@@,5/t:êW ?KЀ>0 VGU #=)@@@@@(m-L     x@9     -@i@@@@@2@@@@@@0#     @     }@@@@@< @Ȝ@@@@@ _´     |6mڔp @@@@@(ER JNNtlO-RO. cWD? *     @9e=[wIENDB`graphql-ruby-2.5.19/guides/queries/phases_of_execution.md000066400000000000000000000015211514115062600235360ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Queries title: Phases of Execution desc: The steps GraphQL takes to run your query index: 2 --- When GraphQL receives a query string, it goes through these steps: - Tokenize: {{ "GraphQL::Language::Lexer" | api_doc }} splits the string into a stream of tokens - Parse: {{ "GraphQL::Language::Parser" | api_doc }} builds an abstract syntax tree (AST) out of the stream of tokens - Validate: {{ "GraphQL::StaticValidation::Validator" | api_doc }} validates the incoming AST as a valid query for the schema - Analyze: If there are any query analyzers, they are run with {{ "GraphQL::Analysis.analyze_query" | api_doc }} - Execute: The query is traversed, `resolve` functions are called and the response is built - Respond: The response is returned as a {{ "GraphQL::Query::Result" | api_doc }} graphql-ruby-2.5.19/guides/queries/response_extensions.md000066400000000000000000000016771514115062600236350ustar00rootroot00000000000000--- title: Response Extensions layout: guide doc_stub: false search: true section: Queries desc: Adding "extensions" to the response hash index: 12 --- During query execution, you can add to the response's `"extensions" => { ... }` Hash. By default, no `"extensions"` key is present in the result, but if you call the method below, it will be present with the given values. To add to `"extensions"`, call `context.response_extensions[key] = value` during execution. For example: ```ruby field :to_dos, [ToDo] def to_dos warnings = context.response_extensions["warnings"] ||= [] warnings << "To-Dos will be disabled on Jan. 31, 2022." context[:current_user].deprecated_to_dos end ``` That would add to the final query response: ```ruby { "data" => { ... }, "extensions" => { "warnings" => ["To-Dos will be disabled on Jan. 31, 2022"], }, } ``` Values written to `context.response_extensions` are added to the GraphQL response verbatim. graphql-ruby-2.5.19/guides/queries/timeout.md000066400000000000000000000060111514115062600211710ustar00rootroot00000000000000--- title: Timeout layout: guide doc_stub: false search: true section: Queries desc: Cutting off GraphQL execution index: 5 --- You can apply a timeout to query execution with the `GraphQL::Schema::Timeout` plugin. For example: ```ruby class MySchema < GraphQL::Schema use GraphQL::Schema::Timeout, max_seconds: 2 end ``` After `max_seconds`, no new fields will be resolved. Instead, errors will be added to the `errors` key for fields that weren't resolved. __Note__ that this _does not interrupt_ field execution (doing so is [buggy](https://www.mikeperham.com/2015/05/08/timeout-rubys-most-dangerous-api/)). If you're making external calls (eg, HTTP requests or database queries), make sure to use a library-specific timeout for that operation (eg, [Redis timeout](https://github.com/redis/redis-rb#timeouts), [Net::HTTP](https://ruby-doc.org/stdlib-2.4.1/libdoc/net/http/rdoc/Net/HTTP.html)'s `ssl_timeout`, `open_timeout`, and `read_timeout`). ## Custom Error Handling To log the error, provide a subclass of `GraphQL::Schema::Timeout` with an overridden `handle_timeout` method: ```ruby class MyTimeout < GraphQL::Schema::Timeout def handle_timeout(error, query) Rails.logger.warn("GraphQL Timeout: #{error.message}: #{query.query_string}") end end class MySchema < GraphQL::Schema use MyTimeout, max_seconds: 2 end ``` ## Customizing the Timeout Window To dynamically pick a timeout duration (or bypass it), override {{ "GraphQL::Schema::Timeout#max_seconds" | api_doc }} in your subclass. To bypass the timeout altogether, `max_seconds` can return `false`. For example: ```ruby class MyTimeout < GraphQL::Schema::Timeout # Allow 10s for an incoming mutation, but don't apply any timeout for an admin user. def max_seconds(query) if query.context[:current_user]&.admin? false elsif query.mutation? 10 else super end end end # ... class MySchema < GraphQL::Schema use MyTimeout, max_seconds: 5 end ``` ## Validation and Analysis Queries can originate from a user, and may be crafted in a manner to take a long time to validate against the schema. It is possible to limit how many seconds the static validation rules and analysers are allowed to run before returning a validation timeout error. By default, validation and query analysis have a 3-second timeout. You can customize this timeout or disable it completely: For example: ```ruby # Customize timeout (in seconds) class MySchema < GraphQL::Schema # Applies to static validation and query analysis validate_timeout 10 end # OR disable timeout completely class MySchema < GraphQL::Schema validate_timeout nil end ``` **Note:** This configuration uses Ruby's built-in `Timeout` API, which can interrupt IO calls mid-flight, resulting in [very weird bugs](https://www.mikeperham.com/2015/05/08/timeout-rubys-most-dangerous-api/). None of GraphQL-Ruby's validators make IO calls but if you want to use this configuration and you have custom static validators that make IO calls, open an issue to discuss implementing this in an IO-safe way. graphql-ruby-2.5.19/guides/queries/tracing.md000066400000000000000000000047271514115062600211460ustar00rootroot00000000000000--- title: Tracing layout: guide doc_stub: false search: true section: Queries desc: Observation hooks for execution index: 11 redirect_from: - /queries/instrumentation --- {{ "GraphQL::Tracing::Trace" | api_doc }} provides hooks to observe and modify events during runtime. Tracing hooks are methods, defined in modules and mixed in with {{ "Schema.trace_with" | api_doc }}. ```ruby module CustomTrace def parse(query_string:) # measure, log, etc super end # ... end ``` To include a trace module when running queries, add it to the schema with `trace_with`: ```ruby # Run `MyCustomTrace` for all queries class MySchema < GraphQL::Schema trace_with(MyCustomTrace) end ``` For a full list of methods and their arguments, see {{ "GraphQL::Tracing::Trace" | api_doc }}. By default, GraphQL-Ruby makes a new trace instance when it runs a query. You can pass an existing instance as `context: { trace: ... }`. Also, `GraphQL.parse( ..., trace: ...)` accepts a trace instance. ## Detailed Traces You can capture detailed traces of query execution with {{ "Tracing::DetailedTrace" | api_doc }}. They can be viewed in Google's [Perfetto Trace Viewer](https://ui.perfetto.dev). They include a per-Fiber breakdown with links between fields and Dataloader sources. {{ "/queries/perfetto_example.png" | link_to_img:"GraphQL-Ruby Dataloader Perfetto Trace" }} Learn how to set it up in the {{ "Tracing::DetailedTrace" | api_doc }} docs. ## External Monitoring Platforms There integrations for GraphQL-Ruby with several other monitoring systems: - `ActiveSupport::Notifications`: See {{ "Tracing::ActiveSupportNotificationsTrace" | api_doc }}. - [AppOptics](https://appoptics.com/) instrumentation is automatic in `appoptics_apm` v4.11.0+. - [AppSignal](https://appsignal.com/): See {{ "Tracing::AppsignalTrace" | api_doc }}. - [Datadog](https://www.datadoghq.com): See {{ "Tracing::DataDogTrace" | api_doc }}. - [NewRelic](https://newrelic.com/): See {{ "Tracing::NewRelicTrace" | api_doc }}. - [Prometheus](https://prometheus.io): See {{ "Tracing::PrometheusTrace" | api_doc }}. - [Scout APM](https://scoutapp.com/): See {{ "Tracing::ScoutTrace" | api_doc }}. - [Sentry](https://sentry.io): See {{ "Tracing::SentryTrace" | api_doc }}. - [Skylight](https://www.skylight.io): either enable the [GraphQL probe](https://www.skylight.io/support/getting-more-from-skylight#graphql) or use {{ "Tracing::ActiveSupportNotificationsTrace" | api_doc }}. - Statsd: See {{ "Tracing::StatsdTrace" | api_doc }}. graphql-ruby-2.5.19/guides/related_projects.md000066400000000000000000000141041514115062600213610ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true title: Related Projects section: Other desc: Code, blog posts and presentations about GraphQL Ruby --- Want to add something? Please open a pull request [on GitHub](https://github.com/rmosolgo/graphql-ruby)! ## Code - `graphql-ruby` + Rails demo ([src](https://github.com/rmosolgo/graphql-ruby-demo) / [heroku](https://graphql-ruby-demo.herokuapp.com)) - `graphql-ruby` + Sinatra demo ([src](https://github.com/robinjmurphy/ruby-graphql-server-example) / [heroku](https://ruby-graphql-server-example.herokuapp.com/)) - [`graphql-batch`](https://github.com/shopify/graphql-batch), a batched query execution strategy - [`graphql-cache`](https://github.com/stackshareio/graphql-cache), a resolver-level caching solution - [`graphql-devise`](https://github.com/graphql-devise/graphql_devise), a gql interface to handle authentication with Devise - [`graphql-docs`](https://github.com/gjtorikian/graphql-docs), a tool to automatically generate static HTML documentation from your GraphQL implementation - [`graphql-metrics`](https://github.com/Shopify/graphql-metrics), a plugin to extract fine-grain metrics of GraphQL queries received by your server - [`graphql-stitching`](https://github.com/gmac/graphql-stitching-ruby), tools to combine multiple local and remote schemas into a single graph that queries as one - [`graphql-groups`](https://github.com/hschne/graphql-groups), a DSL to define group- and aggregation queries with graphql-ruby - Rails Helpers: - [`graphql-activerecord`](https://github.com/goco-inc/graphql-activerecord) - [`graphql-rails-resolve`](https://github.com/colepatrickturner/graphql-rails-resolver) - [`graphql-query-resolver`](https://github.com/nettofarah/graphql-query-resolver), a graphql-ruby add-on to minimize N+1 queries. - [`graphql-rails_logger`](https://github.com/jetruby/graphql-rails_logger), a logger which allows you to inspect GraphQL queries in a more readable format. - [`apollo_upload_server-ruby`](https://github.com/jetruby/apollo_upload_server-ruby), a middleware which allows you to upload files with GraphQL and multipart/form-data using [`apollo-upload-client`](https://github.com/jaydenseric/apollo-upload-client) library on front-end. - [`graphql-sources`](https://github.com/ksylvest/graphql-sources) a collection of common GraphQL [sources](https://graphql-ruby.org/dataloader/sources.html) to simplify using `ActiveRecord`, `ActiveStorage`, `Rails.cache`, and more. - [`graphql-filters`](https://github.com/moku-io/graphql-filters), a DSL to define fully typed filters for list fields. - [`search_object_graphql`](https://github.com/rstankov/SearchObjectGraphQL), a DSL for defining search resolvers for GraphQL. - [`action_policy-graphql`](https://github.com/palkan/action_policy-graphql), an integration for using [`action_policy`](https://github.com/palkan/action_policy) as an authorization framework for GraphQL applications. - [`graphql_rails`](https://github.com/samesystem/graphql_rails), Rails way GraphQL build tool - [`graphql-rails-generators`](https://github.com/ajsharp/graphql-rails-generators), Generate graphql-ruby mutations, types and input types from your ActiveRecord models. - [`graphql-ruby-fragment_cache`](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache), a tool for caching response fragments. - [`graphql-ruby-persisted_queries`](https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries), the implementation of [Apollo persisted queries](https://github.com/apollographql/apollo-link-persisted-queries). - [`rubocop-graphql`](https://github.com/DmitryTsepelev/rubocop-graphql), [rubocop](https://github.com/rubocop-hq/rubocop) extension for enforcing best practices. - [`apollo-federation-ruby`](https://github.com/Gusto/apollo-federation-ruby), a Ruby implementation of the Apollo Federation [subgraph spec](https://www.apollographql.com/docs/federation/subgraph-spec/). ## Blog Posts - Building a blog in GraphQL and Relay on Rails [Introduction](https://medium.com/@gauravtiwari/graphql-and-relay-on-rails-getting-started-955a49d251de), [Part 1]( https://medium.com/@gauravtiwari/graphql-and-relay-on-rails-creating-types-and-schema-b3f9b232ccfc), [Part 2](https://medium.com/@gauravtiwari/graphql-and-relay-on-rails-first-relay-powered-react-component-cb3f9ee95eca) - https://medium.com/@khor/relay-facebook-on-rails-8b4af2057152 - https://blog.jacobwgillespie.com/from-rest-to-graphql-b4e95e94c26b#.4cjtklrwt - https://jonsimpson.ca/parallel-graphql-resolvers-with-futures/ - Active Storage meets GraphQL: [Direct uploads](https://evilmartians.com/chronicles/active-storage-meets-graphql-direct-uploads) and [Exposing attachment URLs](https://evilmartians.com/chronicles/active-storage-meets-graphql-pt-2-exposing-attachment-urls) - [Exposing permissions in GraphQL APIs with Action Policy](https://evilmartians.com/chronicles/exposing-permissions-in-graphql-apis-with-action-policy) - [Reporting non-nullable violations in graphql-ruby properly](https://evilmartians.com/chronicles/reporting-non-nullable-violations-in-graphql-ruby-properly) - [How to GraphQL with Ruby, Rails, Active Record, and no N+1](https://evilmartians.com/chronicles/how-to-graphql-with-ruby-rails-active-record-and-no-n-plus-one) ## Screencasts - [GraphQL Basics in Rails 5](https://rubyplus.com/episodes/271-GraphQL-Basics-in-Rails-5) ## Presentations - [Rescuing Legacy Codebases with GraphQL](https://speakerdeck.com/nettofarah/rescuing-legacy-codebases-with-graphql-1) by [@nettofarah](https://twitter.com/nettofarah) ## Tutorials - [How To GraphQL](https://www.howtographql.com/graphql-ruby/0-introduction/) by [@rstankov](https://github.com/rstankov) - [GraphQL Ruby CRUD Tutorial](https://www.blook.pub/books/graphql-rails-tutorial) by [@kohheepeace](https://twitter.com/kohheepeace) - Rails/GraphQL + React/Apollo Tutorial ([Part 1](https://evilmartians.com/chronicles/graphql-on-rails-1-from-zero-to-the-first-query), [Part 2](https://evilmartians.com/chronicles/graphql-on-rails-2-updating-the-data), [Part 3](https://evilmartians.com/chronicles/graphql-on-rails-3-on-the-way-to-perfection)) by [@evilmartians](https://twitter.com/evilmartians) graphql-ruby-2.5.19/guides/relay/000077500000000000000000000000001514115062600166225ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/relay/range_add.md000066400000000000000000000005541514115062600210540ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Relay title: RangeAdd helper for mutations desc: A helper for Relay's RANGE_ADD operations index: 1 --- Relay specifies `RANGE_ADD` operations for adding items to connections. GraphQL-Ruby ships with {{ "GraphQL::Relay::RangeAdd" | api_doc }} to help implement this. Check the API docs for a usage example. graphql-ruby-2.5.19/guides/schema/000077500000000000000000000000001514115062600167465ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/schema/definition.md000066400000000000000000000174701514115062600214310ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Schema title: Definition desc: Defining your schema index: 1 --- A GraphQL system is called a _schema_. The schema contains all the types and fields in the system. The schema executes queries and publishes an {% internal_link "introspection system","/schema/introspection" %}. Your GraphQL schema is a class that extends {{ "GraphQL::Schema" | api_doc }}, for example: ```ruby class MyAppSchema < GraphQL::Schema max_complexity 400 query Types::Query use GraphQL::Dataloader # Define hooks as class methods: def self.resolve_type(type, obj, ctx) # ... end def self.object_from_id(node_id, ctx) # ... end def self.id_from_object(object, type, ctx) # ... end end ``` There are lots of schema configuration methods. For defining GraphQL types, see the guides for those types: {% internal_link "object types", "/type_definitions/objects" %}, {% internal_link "interface types", "/type_definitions/interfaces" %}, {% internal_link "union types", "/type_definitions/unions" %}, {% internal_link "input object types", "/type_definitions/input_objects" %}, {% internal_link "enum types", "/type_definitions/enums" %}, and {% internal_link "scalar types", "/type_definitions/scalars" %}. ## Types in the Schema - {{ "Schema.query" | api_doc }}, {{ "Schema.mutation" | api_doc }}, and {{ "Schema.subscription" | api_doc}} declare the [entry-point types](https://graphql.org/learn/schema/#the-query-mutation-and-subscription-types) of the schema. - {{ "Schema.orphan_types" | api_doc }} declares object types which implement {% internal_link "Interfaces", "/type_definitions/interfaces" %} but aren't used as field return types in the schema. For more about this specific scenario, see {% internal_link "Orphan Types", "/type_definitions/interfaces#orphan-types" %} ### Lazy-loading types In development, GraphQL-Ruby can defer loading your type definitions until they're needed. This requires some configuration to opt in: - Add `use GraphQL::Schema::Visibility` to your schema. ({{ "GraphQL::Schema::Visibility" | api_doc }} supports lazy loading and will be the default in a future GraphQL-Ruby version. See {% internal_link "Migration Notes", "/authorization/visibility#migration-notes" %} if you have an existing visibility implementation.) - Move your entry-point type definitions into a block, for example: ```diff - query Types::Query + query { Types::Query } ``` - Optionally, move field types into blocks, too: ```diff - field :posts, [Types::Post] # Loads `types/post.rb` immediately + field :posts do + type([Types::Post]) # Loads `types/post.rb` when this field is used in a query + end ``` To enforce these patterns, you can enable two Rubocop rules that ship with GraphQL-Ruby: - `GraphQL/RootTypesInBlock` will make sure that `query`, `mutation`, and `subscription` are all defined in a block. - `GraphQL/FieldTypeInBlock` will make sure that non-built-in field return types are defined in blocks. ## Object Identification Some GraphQL features use unique IDs to load objects: - the `node(id:)` field looks up objects by ID (See {% internal_link "Object Identification", "/schema/object_identification" %} for more about Relay-style object identification.) - any arguments with `loads:` configurations look up objects by ID - the {% internal_link "ObjectCache", "/object_cache/overview" %} uses IDs in its caching scheme To use these features, you must provide some methods for generating UUIDs and fetching objects with them: {{ "Schema.object_from_id" | api_doc }} is called by GraphQL-Ruby to load objects directly from the database. It's usually used by the `node(id: ID!): Node` field (see {{ "GraphQL::Types::Relay::Node" | api_doc }}), Argument {% internal_link "loads:", "/mutations/mutation_classes#auto-loading-arguments" %}, or the {% internal_link "ObjectCache", "/object_cache/overview" %}. It receives a unique ID and must return the object for that ID, or `nil` if the object isn't found (or if it should be hidden from the current user). {{ "Schema.id_from_object" | api_doc }} is used to implement `Node.id`. It should return a unique ID for the given object. This ID will later be sent to `object_from_id` to refetch the object. Additionally, {{ "Schema.resolve_type" | api_doc }} is called by GraphQL-Ruby to get the runtime Object type for fields that return return {% internal_link "interface", "/type_definitions/interfaces" %} or {% internal_link "union", "/type_definitions/unions" %} types. ## Error Handling - {{ "Schema.type_error" | api_doc }} handles type errors at runtime, read more in the {% internal_link "Type errors guide", "/errors/type_errors" %}. - {{ "Schema.rescue_from" | api_doc }} defines error handlers for application errors. See the {% internal_link "error handling guide", "/errors/error_handling" %} for more. - {{ "Schema.parse_error" | api_doc }} and {{ "Schema.query_stack_error" | api_doc }} provide hooks for reporting errors to your bug tracker. ## Default Limits - {{ "Schema.max_depth" | api_doc }} and {{ "Schema.max_complexity" | api_doc }} apply some limits to incoming queries. See {% internal_link "Complexity and Depth", "/queries/complexity_and_depth" %} for more. - {{ "Schema.default_max_page_size" | api_doc }} applies limits to {% internal_link "connection fields", "/pagination/overview" %}. - {{ "Schema.validate_timeout" | api_doc }}, {{ "Schema.validate_max_errors" | api_doc }} and {{ "Schema.max_query_string_tokens" | api_doc }} all apply limits to query execution. See {% internal_link "Timeout", "/queries/timeout" %} for more. ## Introspection - {{ "Schema.extra_types" | api_doc }} declares types which should be printed in the SDL and returned in introspection queries, but aren't otherwise used in the schema. - {{ "Schema.introspection" | api_doc }} can attach a {% internal_link "custom introspection system", "/schema/introspection" %} to the schema. ## Authorization - {{ "Schema.unauthorized_object" | api_doc }} and {{ "Schema.unauthorized_field" | api_doc }} are called when {% internal_link "authorization hooks", "/authorization/authorization" %} return `false` during query execution. ## Execution Configuration - {{ "Schema.trace_with" | api_doc }} attaches tracer modules. See {% internal_link "Tracing", "/queries/tracing" %} for more. - {{ "Schema.query_analyzer" | api_doc }} and {{ "Schema.multiplex_analyzer" }} accept processors for ahead-of-time query analysis, see {% internal_link "Analysis", "/queries/ast_analysis" %} for more. - {{ "Schema.default_logger" | api_doc }} configures a logger for runtime. See {% internal_link "Logging", "/queries/logging" %}. - {{ "Schema.context_class" | api_doc }} and {{ "Schema.query_class" | api_doc }} attach custom subclasses to your schema to use during execution. - {{ "Schema.lazy_resolve" | api_doc }} registers classes with {% internal_link "lazy execution", "/schema/lazy_execution" %}. ## Plugins - {{ "Schema.use" | api_doc }} adds plugins to your schema. For example, {{ "GraphQL::Dataloader" | api_doc }} and {{ "GraphQL::Schema::Visibility" | api_doc }} are installed this way. ## Production Considerations - __Parser caching__: if your application parses GraphQL _files_ (queries or schema definition), it may benefit from enabling {{ "GraphQL::Language::Cache" | api_doc }}. - __Eager loading the library__: by default, GraphQL-Ruby autoloads its constants as-needed. In production, they should be eager loaded instead, using `GraphQL.eager_load!`. - Rails: enabled automatically. (ActiveSupport calls `.eager_load!`.) - Sinatra: add `configure(:production) { GraphQL.eager_load! }` to your application file. - Hanami: add `environment(:production) { GraphQL.eager_load! }` to your application file. - Other frameworks: call `GraphQL.eager_load!` when your application is booting in production mode. See {{"GraphQL::Autoload#eager_load!" | api_doc }} for more details. graphql-ruby-2.5.19/guides/schema/dynamic_types.md000066400000000000000000000254341514115062600221500ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Schema title: Dynamic types and fields desc: Using different schema members for each request index: 8 --- You can use different versions of your GraphQL schema for each operation. To do this, add `use GraphQL::Schema::Visibility` and implement `visible?(context)` on the parts of your schema that will be conditionally accessible. Additionally, many schema elements have definition methods which are called at runtime by GraphQL-Ruby. You can re-implement those to return any valid schema objects. GraphQL-Ruby caches schema elements for the duration of the operation, but if you're making external service calls to implement the methods below, consider adding a cache layer to improve the client experience and reduce load on your backend. At runtime, ensure that only one object is visible per name (type name, field name, etc.). (If `.visible?(context)` returns `false`, then that part of the schema will be hidden for the current operation.) When using dynamic schema members, be sure to include the relevant `context: ...` when [generating schema definition files](#schema-dumps). ## Different fields You can customize which field definitions are used for each operation. ### Using `#visible?(context)` To serve different fields to different clients, implement `def visible?(context)` in your {% internal_link "base field class", "/type_definitions/extensions#customizing-fields" %}: ```ruby class Types::BaseField < GraphQL::Schema::Field def initialize(*args, for_staff: false, **kwargs, &block) super(*args, **kwargs, &block) @for_staff = for_staff end def visible?(context) super && case @for_staff when true !!context[:current_user]&.staff? when false !context[:current_user]&.staff? else true end end end ``` Then, you can configure fields with `for_staff: true|false`: ```ruby field :comments, Types::Comment.connection_type, null: false, description: "Comments on this blog post", resolver_method: :moderated_comments, for_staff: false field :comments, Types::Comment.connection_type, null: false, description: "Comments on this blog post, including unmoderated comments", resolver_method: :all_comments, for_staff: true ``` With that configuration, `post { comments { ... } }` will use `def moderated_comments` when `context[:current_user]` is `nil` or is not `.staff?`, but when `context[:current_user].staff?` is `true`, it will use `def all_comments` instead. ### Using `.fields(context)` and `.get_field(name, context)` To customize the set of fields used at runtime, you can implement `def self.fields(context)` in your type classes. It should return a Hash of `{ String => GraphQL::Schema::Field }`. Along with this, you should implement `.get_field(name, context)` to return a field for `name`, if it should exist. For example: ```ruby class Types::User < Types::BaseObject def self.fields(context) all_fields = super if !context[:current_user]&.staff? all_fields.delete("isSpammy") # this is staff-only end all_fields end def self.get_field(name, context) field = super if field.graphql_name == "isSpammy" && !context[:current_user]&.staff? nil # don't show this field to non-staff else field end end end ``` ### Hidden Return Types Besides field visibility described above, if an field's return type is hidden (that is, it implements `self.visible?(context)` to return `false`), then the field will be hidden too. ## Different arguments As with fields, you can use different sets of argument definitions for different GraphQL operations. ### Using `#visible?(context)` To serve different arguments to different clients, implement `def visible?(context)` in your {% internal_link "base argument class", "/type_definitions/extensions#customizing-arguments" %}: ```ruby class Types::BaseArgument < GraphQL::Schema::Argument def initialize(*args, for_staff: false, **kwargs, &block) super(*args, **kwargs, &block) @for_staff = for_staff end def visible?(context) super && case @for_staff when true !!context[:current_user]&.staff? when false !context[:current_user]&.staff? else true end end end ``` Then, you can configure arguments with `for_staff: true|false`: ```ruby field :user, Types::User, null: true, description: "Look up a user" do # Require a UUID-style ID from non-staff clients: argument :id, ID, required: true, for_staff: false # Support database primary key lookups for staff clients: argument :id, ID, required: false, for_staff: true argument :database_id, Int, required: false, for_staff: true end def user(id: nil, database_id: nil) # ... end ``` That way, any staff client will have the option of `id` or `databaseId` while non-staff clients must use `id`. ### Using `def arguments(context)` and `def get_argument(name, context)` Also, you can implement `def arguments(context)` on your base field class to return a Hash of `{ String => GraphQL::Schema::Argument }` and `def get_argument(name, context)` to return a {{ "GraphQL::Schema::Argument" | api_doc }} or `nil`. . If you take this approach, you might want some custom field classes for any types or resolvers that use these methods. That way, you don't have to reimplement the method for _all_ the fields in the schema. ### Hidden Input Types Besides argument visibility described above, if an argument's input type is hidden (that is, it implements `self.visible?(context)` to return `false`), then the argument will be hidden too. ## Different enum values ### Using `#visible?(context)` You can implement `def visible?(context)` in your {% internal_link "base enum value class", "/type_definitions/extensions#customizing-enum-values" %} to hide some enum values from some clients. For example: ```ruby class BaseEnumValue < GraphQL::Schema::EnumValue def initialize(*args, for_staff: false, **kwargs, &block) super(*args, **kwargs, &block) @for_staff = for_staff end def visible?(context) super && case @for_staff when true !!context[:current_user]&.staff? when false !context[:current_user]&.staff? else true end end end ``` With this base class, you can configure some enum values to be _just_ for staff or non-staff viewers: ```ruby class AccountStatus < Types::BaseEnum value "ACTIVE" value "INACTIVE" # Use this for sensitive account statuses when the viewer is public: value "OTHER", for_staff: false # Staff-only sensitive account statuses: value "BANNED", for_staff: true value "PAYMENT_FAILED", for_staff: true value "PENDING_VERIFICATION", for_staff: true end ``` ### Using `.enum_values(context)` Alternatively, you can implement `def self.enum_values(context)` in your enum types to return an Array of {{ "GraphQL::Schema::EnumValue" | api_doc }}s. For example, to return a dynamic set of enum values: ```ruby class ProjectStatus < Types::BaseEnum def self.enum_values(context = {}) # Fetch the values from the database status_names = context[:tenant].project_statuses.pluck("name") # Then build an Array of Enum values status_names.map do |name| # Be sure to include `owner: self`, the back-reference from the EnumValue to its parent Enum GraphQL::Schema::EnumValue.new(name, owner: self) end end end ``` ## Different types You can also use different types for each query. A few behaviors depend on the methods defined above: - If a type is not used as a return type, an argument type, or as a member of a union or implementer of an interface, it will be hidden - If an interface or union has members, it will be hidden - If a field's return type is hidden, the field will be hidden - If an argument's input type is hidden, the argument will be hidden As you can imagine, these different hiding behaviors influence one another and they can cause some real head-scratchers when used simultaneously. ### Using `.visible?(context)` Type classes can implement `def self.visible?(context)` to hide themselves at runtime: ```ruby class Types::BanReason < Types::BaseEnum # Hide any arguments or fields that use this enum # unless the current user is staff def self.visible?(context) super && !!context[:current_user]&.staff? end # ... end ``` ### Different definitions for the same type You can provide different implementations of the same type by: - Implementing `def self.visible?(context)` to return `true` and `false` in complementary contexts. (They should never both be `.visible? => true`). - Hooking the types up to the schema with different field or argument definitions, as described above For example, to migrate your `Money` scalar to a `Money` object type: ```ruby # Previously, we used a simple string to describe money: class Types::LegacyMoney < Types::BaseScalar # This graphql name will conflict with `Types::Money`, # so we have to be careful not to use them at the same time. # (GraphQL-Ruby will raise an error if it finds two definitions with the same name at runtime.) graphql_name "Money" describe "A string describing an amount of money." # Use this type definition if the current request # explicitly opted in to the legacy money representation: def self.visible?(context) !!context[:requests_legacy_money] end end # But we want to improve the client experience with a dedicated object type: class Types::Money < Types::BaseObject field :amount, Integer, null: false field :currency, Types::Currency, null: false # Use this new definition if the client # didn't explicitly ask for the legacy definition: def self.visible?(context) !context[:requests_legacy_money] end end ``` Then, hook the definitions up to the schema using field definitions: ```ruby class Types::BaseField < GraphQL::Schema::Field def initialize(*args, legacy_money: false, **kwargs, &block) super(*args, **kwargs, &block) @legacy_money = legacy_money end def visible?(context) super && (@legacy_money ? !!context[:requests_legacy_money] : !context[:requests_legacy_money]) end end class Types::Invoice < Types::BaseObject # Add one definition for each possible return type # (one definition will be hidden at runtime) field :amount, Types::LegacyMoney, null: false, legacy_money: true field :amount, Types::Money, null: false, legacy_money: false end ``` Input types (like input objects, scalars, and enums) work the same way with argument definitions. ## Schema Dumps To dump a certain _version_ of the schema, provide the applicable `context: ...` to {{ "Schema.to_definition" | api_doc }}. For example: ```ruby # Legacy money schema: MySchema.to_definition(context: { requests_legacy_money: true }) ``` or ```ruby # Staff-only schema: MySchema.to_definition(context: { current_user: OpenStruct.new(staff?: true) }) ``` That way, the given `context` will be passed to `visible?(context)` calls and other relevant methods. graphql-ruby-2.5.19/guides/schema/generators.md000066400000000000000000000072471514115062600214530ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true title: Generators section: Schema desc: Use Rails generators to install GraphQL and scaffold new types. index: 3 --- If you're using GraphQL with Ruby on Rails, you can use generators to: - [setup GraphQL](#graphqlinstall), including [GraphiQL](https://github.com/graphql/graphiql), [GraphQL::Batch](https://github.com/Shopify/graphql-batch), and [Relay](https://facebook.github.io/relay/) - [scaffold types](#scaffolding-types) - [scaffold Relay mutations](#scaffolding-mutations) - [scaffold ActiveRecord create/update/delete mutations](#scaffolding-activerecord-mutations) - [scaffold GraphQL::Batch loaders](#scaffolding-loaders) ## graphql:install You can add GraphQL to a Rails app with `graphql:install`: ``` rails generate graphql:install ``` This will: - Set up a folder structure in `app/graphql/` - Add schema definition - Add base type classes - Add a `Query` type definition - Add a `Mutation` type definition with a base mutation class - Add a route and controller for executing queries - Install [`graphiql-rails`](https://github.com/rmosolgo/graphiql-rails) - Enable [`ActiveRecord::QueryLogs`](https://api.rubyonrails.org/classes/ActiveRecord/QueryLogs.html) and add GraphQL-related metadata (using {{ "GraphQL::Current" | api_doc }}) After installing you can see your new schema by: - `bundle install` - `rails server` - Open `localhost:3000/graphiql` ### Options - `--directory=DIRECTORY` will specify the directory where generated files should be saved (default is `app/graphql`) - `--schema=MySchemaName` will be used for naming the schema (default is `#{app_name}Schema`) - `--skip-graphiql` will exclude `graphiql-rails` from the setup - `--skip-mutation-root-type` will not create of the mutation root type - `--skip-query-logs` will skip the QueryLogs setup - `--relay` will add [Relay](https://facebook.github.io/relay/)-specific code to your schema - `--batch` will add [GraphQL::Batch](https://github.com/Shopify/graphql-batch) to your gemfile and include the setup in your schema - `--playground` will include `graphql_playground-rails` in the setup (mounted at `/playground`) - `--api` will create smaller stack for API only apps ## Scaffolding Types Several generators will add GraphQL types to your project. Run them with `-h` to see the options: - `rails g graphql:object` - `rails g graphql:input` - `rails g graphql:interface` - `rails g graphql:union` - `rails g graphql:enum` - `rails g graphql:scalar` ### ActiveRecord columns auto-extraction The `graphql:object` and `graphql:input` generators can detect the existence of an ActiveRecord class with the same name, and scaffold all database columns as fields/arguments using appropriate GraphQL types and nullability detection ### Options - `--namespaced-types` will generate each one of the `object`/`input`/`interface`/... types under separate `Types::Objects::*`/`Types::Inputs::*`/`Types::Interfaces::*`/... namespaces and folders ## Scaffolding Mutations You can prepare a Relay Classic mutation with ``` rails g graphql:mutation #{mutation_name} ``` ## Scaffolding ActiveRecord Mutations You can generate a Relay Classic create, update or delete mutation for a given model with ``` rails g graphql:mutation_create #{model_class_name} rails g graphql:mutation_update #{model_class_name} rails g graphql:mutation_delete #{model_class_name} ``` `model_class_name` accepts both `namespace/class_type` and `Namespace::ClassType` formats. This mutation also accepts the `--namespaced-types` flag, to keep it consistent with the scaffolded Object and Input classes from the type generators ## Scaffolding Loaders You can prepare a GraphQL::Batch loader with ``` rails g graphql:loader ``` graphql-ruby-2.5.19/guides/schema/introspection.md000066400000000000000000000145471514115062600222030ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true title: Introspection section: Schema desc: GraphQL has an introspection system that tells about the schema. index: 3 --- A GraphQL schema has a [built-in introspection system](https://graphql.org/learn/introspection/) that publishes the schema's structure. In fact, the introspection system can be queried using GraphQL, for example: ```graphql { __schema { queryType { name } } } # Returns: # { # "data": { # "__schema": { # "queryType": { # "name": "Query" # } # } # } # } ``` This system is used for GraphQL tooling like the [GraphiQL editor](https://github.com/graphql/graphiql). Here are the default parts of the introspection system: - `__schema` is a root-level field that contains data about the schema: its entry points, types, and directives. - `__type(name: String!)` is a root-level field that returns data about a type with the given `name`, if there is a type with that name. - `__typename` works a bit differently: it can be added to _any_ selection, and it will return the type of object being queried. Here are some `__typename` examples: ```graphql { user(id: "1") { handle __typename } } # Returns: # { # "data": { # "user": { # "handle": "rmosolgo", # "__typename": "User" # } # } # } ``` For unions and interfaces, `__typename` returns the _object_ type for the current object, for example: ```graphql { search(term: "puppies") { title __typename } } # Returns: # { # "data": { # "search": [ # { # "title": "Sound of Dogs Barking", # "__typename": "AudioClip", # }, # { # "title": "Cute puppies playing with a stick", # "__typename": "VideoClip", # }, # { # "title": "The names of my favorite pets", # "__typename": "TextSnippet" # }, # ] # } # } ``` ## Customizing Introspection You can use custom introspection types. ```ruby # create a module namespace for your custom types: module Introspection # described below ... end class MySchema < GraphQL::Schema # ... # then pass the module as `introspection` introspection Introspection end ``` Keep in mind that off-the-shelf tooling may not support your custom introspection fields. You may have to modify existing tooling or create your own tools to make use of your extensions. ### Introspection Namespace The introspection namespace may contain a few different customizations: - Class-based {% internal_link "object definitions", "/type_definitions/objects" %} which replace the built-in introspection types (such as `__Schema` and `__Type`) - `EntryPoints`, A class-based {% internal_link "object definition", "/type_definitions/objects" %} containing introspection entry points (like `__schema` and `__type(name:)`). - `DynamicFields`, A class-based {% internal_link "object definition", "/type_definitions/objects" %} containing dynamic, globally-available fields (like `__typename`.) ### Custom Introspection Types The `module` passed as `introspection` may contain classes with the following names, which replace the built-in introspection types: Custom class name | GraphQL type | Built-in class name --|--|-- `SchemaType` | `__Schema` | {{ "GraphQL::Introspection::SchemaType" | api_doc }} `TypeType` | `__Type` | {{ "GraphQL::Introspection::TypeType" | api_doc }} `DirectiveType` | `__Directive` | {{ "GraphQL::Introspection::DirectiveType" | api_doc }} `DirectiveLocationType` | `__DirectiveLocation` | {{ "GraphQL::Introspection::DirectiveLocationEnum" | api_doc }} `EnumValueType` | `__EnumValue` | {{ "GraphQL::Introspection::EnumValueType" | api_doc }} `FieldType` | `__Field` | {{ "GraphQL::Introspection::FieldType" | api_doc }} `InputValueType` | `__InputValue` | {{ "GraphQL::Introspection::InputValueType" | api_doc }} `TypeKindType` | `__TypeKind` | {{ "GraphQL::Introspection::TypeKindEnum" | api_doc }} The class-based definitions' names _must_ match the names of the types they replace. #### Extending a Built-in Type The built-in classes listed above may be extended: ```ruby module Introspection class SchemaType < GraphQL::Introspection::SchemaType # ... end end ``` Inside the class definition, you may: - add new fields by calling `field(...)` and providing implementations - redefine field structure by calling `field(...)` - provide new field implementations by defining methods - provide new descriptions by calling `description(...)` ### Introspection Entry Points The GraphQL spec describes two entry points to the introspection system: - `__schema` returns data about the schema (as type `__Schema`) - `__type(name:)` returns data about a type, if one is found by name (as type `__Type`) You can re-implement these fields or create new ones by creating a custom `EntryPoints` class in your introspection namespace: ```ruby module Introspection class EntryPoints < GraphQL::Introspection::EntryPoints # ... end end ``` This class is an object type definition, so you can override existing fields or add new ones here. They'll be available on the root `query` object, but ignored in introspection (just like `__schema` and `__type`). ### Dynamic Fields The GraphQL spec describes a field which may be added to _any_ selection: `__typename`. It returns the name of the current GraphQL type. You can add fields like this (or override `__typename`) by creating a custom `DynamicFields` definition: ```ruby module Introspection class DynamicFields < GraphQL::Introspection::DynamicFields # ... end end ``` Any fields defined there will be available in any selection, but ignored in introspection (just like `__typename`). ## Disabling Introspection In case you want to turn off introspection entry points `__schema` and `__type` (for instance in the production environment) you can use a `#disable_introspection_entry_points` shorthand method: ```ruby class MySchema < GraphQL::Schema disable_introspection_entry_points if Rails.env.production? end ``` Where `disable_introspection_entry_points` will disable both the `__schema` and `__type` introspection entry points, you can also individually disable the introspection entry points using the `disable_schema_introspection_entry_point` and `disable_type_introspection_entry_point` shorthand methods: ```ruby class MySchema < GraphQL::Schema disable_schema_introspection_entry_point disable_type_introspection_entry_point end ``` graphql-ruby-2.5.19/guides/schema/lazy_execution.md000066400000000000000000000061401514115062600223330ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true title: Lazy Execution section: Schema desc: Resolvers can return "unfinished" results that are deferred for batch resolution. index: 4 --- With lazy execution, you can optimize access to external services (such as databases) by making batched calls. Building a lazy loader has three steps: - Define a lazy-loading class with _one_ method for loading & returning a value - Connect it to your schema with {{ "GraphQL::Schema.lazy_resolve" | api_doc }} - In `resolve` methods, return instances of the lazy-loading class ## Example: Batched Find Here's a way to find many objects by ID using one database call, preventing N+1 queries. 1. Lazy-loading class which finds models by ID. ```ruby class LazyFindPerson def initialize(query_ctx, person_id) @person_id = person_id # Initialize the loading state for this query, # or get the previously-initiated state @lazy_state = query_ctx[:lazy_find_person] ||= { pending_ids: Set.new, loaded_ids: {}, } # Register this ID to be loaded later: @lazy_state[:pending_ids] << person_id end # Return the loaded record, hitting the database if needed def person # Check if the record was already loaded: loaded_record = @lazy_state[:loaded_ids][@person_id] if loaded_record # The pending IDs were already loaded, # so return the result of that previous load loaded_record else # The record hasn't been loaded yet, so # hit the database with all pending IDs pending_ids = @lazy_state[:pending_ids].to_a people = Person.where(id: pending_ids) people.each { |person| @lazy_state[:loaded_ids][person.id] = person } @lazy_state[:pending_ids].clear # Now, get the matching person from the loaded result: @lazy_state[:loaded_ids][@person_id] end end ``` 2. Connect the lazy resolve method ```ruby class MySchema < GraphQL::Schema # ... lazy_resolve(LazyFindPerson, :person) end ``` 3. Return lazy objects from `resolve` ```ruby field :author, PersonType def author LazyFindPerson.new(context, object.author_id) end ``` Now, calls to `author` will use batched database access. For example, this query: ```graphql { p1: post(id: 1) { author { name } } p2: post(id: 2) { author { name } } p3: post(id: 3) { author { name } } } ``` Will only make one query to load the `author` values. ## Gems for batching The example above is simple and has some shortcomings. Consider the following gems for a robust solution to batched resolution: * {{ "GraphQL::Dataloader" | api_doc }} is a built-in, Fiber-based approach to batching. See the {% internal_link "Dataloader guide", "/dataloader/overview" %} for more information. * [`graphql-batch`](https://github.com/shopify/graphql-batch) provides a powerful, flexible toolkit for lazy resolution with GraphQL. * [`dataloader`](https://github.com/sheerun/dataloader) is more general promise-based utility for batching queries within the same thread. * [`batch-loader`](https://github.com/exAspArk/batch-loader) works with any Ruby code including GraphQL, no extra dependencies or primitives. graphql-ruby-2.5.19/guides/schema/object_identification.md000066400000000000000000000043751514115062600236200ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true title: Object Identification section: Schema desc: Working with unique global IDs index: 8 --- GraphQL-Ruby ships with some helpers to implement [Relay-style object identification](https://relay.dev/graphql/objectidentification.htm). ## Schema methods See {% internal_link "the Schema definition guide", "/schema/definition#object-identification" %} for required top-level hooks. ## Node interface One requirement for Relay's object management is implementing the `"Node"` interface. To implement the node interface, add {{ "GraphQL::Types::Relay::Node" | api_doc }} to your definition: ```ruby class Types::PostType < GraphQL::Schema::Object # Implement the "Node" interface for Relay implements GraphQL::Types::Relay::Node # ... end ``` To tell GraphQL how to resolve members of the `Node` interface, you must also define `Schema.resolve_type`: ```ruby class MySchema < GraphQL::Schema # You'll also need to define `resolve_type` for # telling the schema what type Relay `Node` objects are def self.resolve_type(type, obj, ctx) case obj when Post Types::PostType when Comment Types::CommentType else raise("Unexpected object: #{obj}") end end end ``` ## UUID fields Nodes must have a field named `"id"` which returns a globally unique ID. To add a UUID field named `"id"`, implement the {{ "GraphQL::Types::Relay::Node" | api_doc }} interface:: ```ruby class Types::PostType < GraphQL::Schema::Object implements GraphQL::Types::Relay::Node end ``` This field will call the previously-defined `id_from_object` class method. ## `node` field (find-by-UUID) You should also provide a root-level `node` field so that Relay can refetch objects from your schema. You can attach it like this: ```ruby class Types::QueryType < GraphQL::Schema::Object # Used by Relay to lookup objects by UUID: # Add `node(id: ID!) include GraphQL::Types::Relay::HasNodeField # ... end ``` ## `nodes` field You can also provide a root-level `nodes` field so that Relay can refetch objects by IDs: ```ruby class Types::QueryType < GraphQL::Schema::Object # Fetches a list of objects given a list of IDs # Add `nodes(ids: [ID!]!)` include GraphQL::Types::Relay::HasNodesField # ... end ``` graphql-ruby-2.5.19/guides/schema/root_types.md000066400000000000000000000025341514115062600215030ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Schema title: Root Types desc: Root types are the entry points for queries, mutations and subscriptions. index: 2 --- GraphQL queries begin from [root types](https://graphql.org/learn/schema/#the-query-mutation-and-subscription-types): `query`, `mutation`, and `subscription`. Attach these to your schema using methods with the same name: ```ruby class MySchema < GraphQL::Schema # required query Types::QueryType # optional mutation Types::MutationType subscription Types::SubscriptionType end ``` The types are `GraphQL::Schema::Object` classes, for example: ```ruby # app/graphql/types/query_type.rb class Types::QueryType < GraphQL::Schema::Object field :posts, [PostType], 'Returns all blog posts', null: false end # Similarly: class Types::MutationType < GraphQL::Schema::Object field :create_post, mutation: Mutations::AddPost end # and class Types::SubscriptionType < GraphQL::Schema::Object field :comment_added, subscription: Subscriptions::CommentAdded end ``` Each type is the entry point for the corresponding GraphQL query: ```ruby query Posts { # `Query.posts` posts { ... } } mutation AddPost($postAttrs: PostInput!){ # `Mutation.createPost` createPost(attrs: $postAttrs) } subscription CommentAdded { # `Subscription.commentAdded` commentAdded(postId: 1) } ``` graphql-ruby-2.5.19/guides/schema/sdl.md000066400000000000000000000071371514115062600200620ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Schema title: Parsing GraphQL Schema Definition Language into a Ruby Schema desc: Defining a schema from a string or .graphql file index: 7 --- GraphQL-Ruby includes a way to build a runable schema from the GraphQL Schema Definition Language (SDL). {{ "GraphQL::Schema.from_definition" | api_doc }} returns a schema class based on a filename or string containing GraphQL SDL. For example: ```ruby # From a file: MySchema = GraphQL::Schema.from_definition("path/to/schema.graphql") # Or, from a string: MySchema = GraphQL::Schema.from_definition(<<~GRAPHQL) type Query { # ... } # ... GRAPHQL ``` Definitions from the SDL are converted into Ruby classes, similar to those defined in plain Ruby code. ## Execution You can provide execution behaviors to a generated schema as `default_resolve:`, which accepts two kinds of values: - __Implementation Object__: an object that implements several methods used at runtime - __Implementation Hash__: keys and nested hashes provide procs for execution ### Implementation Object By providing an object that implements several runtime methods, you can define the execution behaviors of a schema loaded from SDL: ```ruby class SchemaImplementation # see below for methods end # Pass the object as `default_resolve:` MySchema = GraphQL::Schema.from_definition( "path/to/schema.graphql", default_resolve: SchemaImplementation.new ) ``` The `default_resolve:` object may implement: - `#call(type, field, obj, args, ctx)` for resolving fields - `#resolve_type(abstract_type, obj, ctx)` for resolving `obj` as one of `abstract_type`'s possible object types - `#coerce_input(type, value, ctx)` for coercing scalar input - `#coerce_result(type, value, ctx)` for coercing scalar return values ### Implementation Hash Alternatively, you can provide a Hash containing callable behaviors, for example: ```ruby schema_implementation = { # ... see below for hash structure } # Pass the hash as `default_resolve:` MySchema = GraphQL::Schema.from_definition( "path/to/schema.graphql", default_resolve: schema_implementation ) ``` The hash may contain: - A key for each object type name, containing a sub-hash of `{ field_name => ->(obj, args, ctx) { ... } }` for resolving object fields - A key for each scalar type name, containing a sub-hash with keys `"coerce_result"` and `"coerce_input"`, each pointing to a `->(value, ctx) { ... }` for handling scalar values at runtime - A `"resolve_type"` key pointing to a `->(abstract_type, object, ctx) { ... }` callable, used for resolving `object` to one of `abstract_type`'s possible types ## Plugins {{ "GraphQL::Schema.from_definition" | api_doc }} accepts a `using:` argument, which may be given as a map of `plugin => args` pairs. For example: ```ruby MySchema = GraphQL::Schema.from_definition("path/to/schema.graphql", using: { GraphQL::Pro::PusherSubscriptions => { redis: $redis }, GraphQL::Pro::OperationStore => nil, # no options here }) ``` ## Directives Although GraphQL-Ruby doesn't have special handling for directives in the SDL, you can build custom behavior in your own app. If part of the schema had a directive, you can access it using `.ast_node.directives`. For example: ```ruby schema = GraphQL::Schema.from_definition <<-GRAPHQL type Query @flagged { secret: Boolean @privacy(secret: true) } GRAPHQL pp schema.query.ast_node.directives.map(&:to_query_string) # => ["@flagged"] pp schema.get_field("Query", "secret").ast_node.directives.map(&:to_query_string) # => ["@privacy(secret: true)"] ``` See {{ "GraphQL::Language::Nodes::Directive" | api_doc }} for available methods. graphql-ruby-2.5.19/guides/search.html000066400000000000000000000045031514115062600176430ustar00rootroot00000000000000--- layout: default title: Search ---

Search:

graphql-ruby-2.5.19/guides/subscriptions/000077500000000000000000000000001514115062600204155ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/subscriptions/ably_implementation.md000066400000000000000000000274321514115062600250030ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Subscriptions title: Ably Implementation desc: GraphQL subscriptions over Ably index: 7 pro: true --- [GraphQL Pro](https://graphql.pro) includes a subscription system based on [Redis](https://redis.io) and [Ably](https://ably.io) which works with any Ruby web framework. After creating an app on Ably, you can hook it up to your GraphQL schema. ## How it Works This subscription implementation uses a hybrid approach: - __Your app__ takes GraphQL queries an runs them - __Redis__ stores subscription data for later updates - __Ably__ sends updates to subscribed clients So, the lifecycle goes like this: - A `subscription` query is sent by HTTP Post to your server (just like a `query` or `mutation`) - The response contains an Ably channel ID (as an HTTP header) which the client may subscribe to - The client opens that Ably channel - When the server triggers updates, they're delivered over the Ably channel - When the client unsubscribes, the server receives a webhook and responds by removing its subscription data Here's another look: ``` 1. Subscription is created in your app HTTP POST .----------> write to Redis 📱 ⚙️ -----> 💾 <---------' X-Subscription-ID: 1234 2. Client opens a connection to Ably websocket 📱 <---------> ☁️ 3. The app sends updates via Ably ⚙️ ---------> ☁️ ------> 📱 POST update (via gem) (via websocket) 4. When the client unsubscribes, Ably notifies the app webhook ⚙️ <-------- ☁️ (disconnect) 📱 ``` By using this configuration, you can use GraphQL subscriptions without hosting a push server yourself! ## Ably setup Add `ably-rest` to your `Gemfile`: ```ruby gem 'ably-rest' ``` and `bundle install`. ## Database setup Subscriptions require a _persistent_ Redis database, configured with: ```sh maxmemory-policy noeviction # optional, more durable persistence: appendonly yes ``` Otherwise, Redis will drop data that doesn't fit in memory (read more in ["Redis persistence"](https://redis.io/topics/persistence)). If you're already using Redis in your application, see ["Storing Data in Redis"](https://www.mikeperham.com/2015/09/24/storing-data-with-redis/) for options to isolate data and tune your configuration. ## Schema configuration Add `redis` to your `Gemfile`: ```ruby gem 'redis' ``` and `bundle install`. Then create a Redis instance: ```ruby # for example, in an initializer: $graphql_subscriptions_redis = Redis.new # default connection ``` Then, that Redis client is passed to the Subscription configuration: ```ruby class MySchema < GraphQL::Schema use GraphQL::Pro::AblySubscriptions, redis: $graphql_subscriptions_redis, ably: Ably::Rest.new(key: ABLY_API_KEY) end ``` That connection will be used for managing subscription state. All writes to Redis are prefixed with `graphql:sub:`. There are also two configurations for managing persistence: - `stale_ttl_s:` expires subscription data after the given number of seconds without any update. After `stale_ttl_s` has passed, the data will expire from Redis. Each time a subscription receives an update, its TTL is refreshed. (Generally, this isn't required because the backend is built to clean itself up. But, if you find that Redis is collecting stale queries, you can set them to expire after some very long time as a safeguard.) - `cleanup_delay_s:` (default: `5`) prevents deleting a subscription during those first seconds after it's created. Usually, a longer delay isn't necessary, but if you observe latency between the subscription's initial response and the client's subscription to the delivery channel, you can set this configuration to account for it. ### Connection Pool For better performance reading and writing to Redis, you can pass a `connection_pool:` instead of `redis:`, using the [`connection_pool` gem](): ```ruby use GraphQL::Pro::AblySubscriptions, connection_pool: ConnectionPool.new(size: 5, timeout: 5) { Redis.new }, ably: Ably::Rest.new(key: ABLY_API_KEY) ``` ### Broadcasts If you set up {% internal_link "Broadcasts", "/subscriptions/broadcast" %}, then you can update many clients over a single Ably channel. Broadcast channels have stable, predictable IDs. To prevent unauthorized clients from "listening in," use [token authorization](#authorization) for transport. Broadcasts channels use the namespace `gqlbdcst:`, so you can provide capabilities to receive them using `"gqlbdcst:*" => [ ... ]` in your authorization code. (If you're using [encryption](#encryption), the prefix will be `ablyencr-gqlbdcst:` instead.) ## Execution configuration During execution, GraphQL will assign a `subscription_id` to the `context` hash. The client will use that ID to listen for updates, so you must return the `subscription_id` in the response headers. Return `result.context[:subscription_id]` as the `X-Subscription-ID` header. For example: ```ruby result = MySchema.execute(...) # For subscriptions, return the subscription_id as a header if result.subscription? response.headers["X-Subscription-ID"] = result.context[:subscription_id] end render json: result ``` This way, the client can use that ID as a Ably channel. For __CORS requests__, you need a special header so that clients can read the custom header: ```ruby if result.subscription? response.headers["X-Subscription-ID"] = result.context[:subscription_id] # Required for CORS requests: response.headers["Access-Control-Expose-Headers"] = "X-Subscription-ID" end ``` Read more here: ["Using CORS"](https://www.html5rocks.com/en/tutorials/cors/). ## Webhook configuration Your server needs to receive webhooks from Ably when clients disconnect. This keeps your local subscription database in sync with Ably. ### Server *Note: if you're setting up in a development environment you should follow the [Developing with webhooks](#Developing-with-webhooks) section first* Mount the Rack app for handling webhooks from Ably. For example, on Rails: ```ruby # config/routes.rb # Include GraphQL::Pro's routing extensions: using GraphQL::Pro::Routes Rails.application.routes.draw do # ... # Handle webhooks for subscriptions: mount MySchema.ably_webhooks_client, at: "/ably_webhooks" end ``` __Alternatively__, you can configure the routes to load your schema lazily, during the first request: ```ruby # Provide the fully-qualified class name of your schema: lazy_routes = GraphQL::Pro::Routes::Lazy.new("MySchema") mount lazy_routes.ably_webhooks_client, at: "/ably_webhooks" ``` ### Ably 1. Go to the Ably dashboard 2. Click on your application 3. Select the **"Integrations"** tab 4. Click on the **"+ New Integration Rule"** button 5. Click on the "Choose" button for **"Webhook"** 6. Click on the "Choose" button for **"Webhook"** (again) 7. Enter **your URL (including the webhooks path from above)** in the URL field. 8. Select **"Batch request"** for "Request Mode" 9. Under "Source", select **"Presence"** 10. Under "Sign with key", select the API Key prefix that matches the prefix of the `ABLY_API_KEY` you provided 11. Click **"Create"** ## Authorization You can use Ably's [token authentication](https://www.ably.io/documentation/realtime/authentication#token-authentication) by implementing an endpoint in your app, for example: ```ruby class AblyController < ActionController::Base def auth render status: 201, json: ably_rest_client.auth.create_token_request( capability: { '*' => ['presence', 'subscribe'] }, client_id: 'graphql-subscriber', ) end end ``` [Ably's tutorial](https://www.ably.io/tutorials/webhook-chuck-norris#tutorial-step-4) also demonstrates some of the setup for this. ## Encryption You can use Ably's [end-to-end encryption](https://www.ably.io/documentation/realtime/encryption) with GraphQL subscriptions. To enable it, add `cipher_base:` to your setup: ```ruby use GraphQL::Pro::AblySubscriptions, redis: $graphql_subscriptions_redis, ably: Ably::Rest.new(key: ABLY_API_KEY), # Add `cipher_base:` to enable end-to-end encryption cipher_base: "ff16381ae2f2b6c6de6ff696226009f3" ``` (Any random string will do, eg `ruby -e "require 'securerandom'; puts SecureRandom.hex"`.) Also, return a header to client so that it can decrypt subscription updates. The key is put in `context[:ably_cipher_base64]`, and `graphql-ruby-client` expects to find it in the `X-Subscription-Key` header: ```ruby result = MySchema.execute(...) # For subscriptions, return the subscription_id as a header if result.subscription? response.headers["X-Subscription-ID"] = result.context[:subscription_id] # Also return the encryption key so that clients # can decode subscription updates response.headers["X-Subscription-Key"] = result.context[:ably_cipher_base64] end ``` (Also, if you're using CORS requests, update `Access-Control-Expose-Headers` to include `X-Subscription-Key`) With this setup, - `GraphQL::Pro::AblySubscriptions` will generate per-subscription keys (using `cipher_base` and the subscription ID) and use them to encrypt Ably payloads - Those keys will be returned to clients in `X-Subscription-Key` - Clients will use those keys to decrypt incoming messages __Backwards compatibility:__ `GraphQL::Pro::AblySubscriptions` will only encrypt payloads whose `query.context[:ably_cipher_base64]` is present. Any subscriptions created _before_ `cipher_base:` was added to the setup will _not_ be encrypted. (There was no key to encrypt them, and clients don't have a key to _decrypt_ them!) ## Serializing Context Since subscription state is stored in the database, then reloaded for pushing updates, you have to serialize and reload your query `context`. By default, this is done with {{ "GraphQL::Subscriptions::Serialize" | api_doc }}'s `dump` and `load` methods, but you can provide custom implementations as well. To customize the serialization logic, create a subclass of `GraphQL::Pro::AblySubscriptions` and override `#dump_context(ctx)` and `#load_context(ctx_string)`: ```ruby class CustomSubscriptions < GraphQL::Pro::AblySubscriptions def dump_context(ctx) context_hash = ctx.to_h # somehow convert this hash to a string, return the string end def load_context(ctx_string) # Given the string from the DB, create a new hash # to use as `context:` end end ``` Then, use your _custom_ subscriptions class instead of the built-in one for your schema: ```ruby class MySchema < GraphQL::Schema # Use custom subscriptions instead of GraphQL::Pro::AblySubscriptions # to get custom serialization logic use CustomSubscriptions, ... end ``` That gives you fine-grained control of context reloading. ## Dashboard You can monitor subscription state in the {% internal_link "GraphQL-Pro Dashboard", "/pro/dashboard" %}: {{ "/subscriptions/redis_dashboard_1.png" | link_to_img:"Redis Subscription Dashboard" }} {{ "/subscriptions/redis_dashboard_2.png" | link_to_img:"Redis Subscription Detail" }} ## Development Tips #### Clear subscription data At any time, you can reset your subscription database with the __"Reset"__ button in the {% internal_link "GraphQL-Pro Dashboard", "/pro/dashboard" %}, or in Ruby: ```ruby # Wipe all subscription data from the DB: MySchema.subscriptions.clear ``` #### Developing with webhooks To receive webhooks in development, you can [use ngrok](https://www.ably.io/tutorials/webhook-chuck-norris). It gives you a public URL which you can setup with Ably, then any hooks delivered to that URL will be forwarded to your development environment. ## Client configuration Install the [Ably JS client](https://github.com/ably/ably-js) then see docs for: - {% internal_link "Apollo Client", "/javascript_client/apollo_subscriptions" %} - {% internal_link "Relay Modern", "/javascript_client/relay_subscriptions" %}. - {% internal_link "GraphiQL", "/javascript_client/graphiql_subscriptions" %} graphql-ruby-2.5.19/guides/subscriptions/action_cable_implementation.md000066400000000000000000000016121514115062600264470ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Subscriptions title: Action Cable Implementation desc: GraphQL subscriptions over ActionCable index: 4 --- [ActionCable](https://guides.rubyonrails.org/action_cable_overview.html) is a great platform for delivering GraphQL subscriptions on Rails 5+. It handles message passing (via `broadcast`) and transport (via `transmit` over a websocket). To get started, see examples in the API docs: {{ "GraphQL::Subscriptions::ActionCableSubscriptions" | api_doc }}. GraphQL-Ruby also includes a mock ActionCable implementation for testing: {{ "GraphQL::Testing::MockActionCable" | api_doc }}. See client usage for: - {% internal_link "Apollo Client", "/javascript_client/apollo_subscriptions" %} - {% internal_link "Relay Modern", "/javascript_client/relay_subscriptions" %}. - {% internal_link "GraphiQL", "/javascript_client/graphiql_subscriptions" %} graphql-ruby-2.5.19/guides/subscriptions/broadcast.md000066400000000000000000000126531514115062600227100ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Subscriptions title: Broadcasts desc: Delivering the same GraphQL result to multiple subscribers index: 3 --- GraphQL subscription updates may _broadcast_ data to multiple subscribers. A broadcast is a subscription update which is executed _once_, then delivered to _any number_ of subscribers. This reduces the time your server spends running GraphQL queries, since it doesn't have to re-run the query for every subscriber. But, __take care__: this approach risks leaking information to subscribers who shouldn't receive it. ## Setup To enable broadcasts, add `broadcast: true` to your subscription setup: ```ruby class MyAppSchema < GraphQL::Schema # ... use SomeSubscriptionImplementation, broadcast: true # <---- end ``` Then, any broadcastable field can be configured with `broadcastable: true`: ```ruby field :name, String, null: false, broadcastable: true ``` When a subscription comes in where _all_ of its fields are `broadcastable: true`, then it will be handled as a broadcast. Additionally, you can set `default_broadcastable: true`: ```ruby class MyAppSchema < GraphQL::Schema # ... use SomeSubscriptionImplementation, broadcast: true, default_broadcastable: true # <---- end ``` With this setting, fields are broadcastable by default. Only a field with `broadcastable: false` in its configuration will cause a subscription to be handled on a subscriber-by-subscriber basis. ## What fields are broadcastable? GraphQL-Ruby can't infer whether a field is broadcastable or not. You must configure it explicitly with `broadcastable: true` or `broadcastable: false`. (The subscription plugin also accepts `default_broadcastable: true|false`.) A field is broadcastable if _all clients who request the field will see the same value_. For example: - General facts: celebrity names, laws of physics, historical dates - Public information: object names, document updated-at timestamps, boilerplate info For fields like this, you can add `broadcastable: true`. A field is __not broadcastable__ if its value is different for different clients. For example: - __Viewer-specific information:__ if a field is specifically viewer-based, then it can't be broadcasted to other viewers. For example, `discussion { viewerCanModerate }` might be true for a moderator, but it shouldn't be broadcasted to other viewers. - __Context-specific information:__ if a field's value takes the request context into consideration, it shouldn't be broadcasted. For example, IP addresses or HTTP header values probably can't be broadcasted. If a field reflects the viewer's timezone, it can't be broadcasted. - __Restricted information:__ if some viewers see one value, while other viewers see a different value, then it's not broadcastable. Broadcasting this data might leak private information to unauthorized clients. (This includes filtered lists: if the filtering is viewer-by-viewer, it's not broadcastable.) - __Fields with side effects:__ if the system requires a side effect (eg, logging a metric, updating a database, incrementing a counter) whenever a resolver is executed, it's not a good candidate for broadcasting because some executions will be optimized away. These fields can be tagged with `broadcastable: false` so that GraphQL-Ruby will handle them on a subscriber-by-subscriber basis. If you want to use subscriptions but have a lot of non-broadcastable fields in your schema, consider building a new set of subscription fields with limited access to other schema objects. Instead, optimize those subscriptions for broacastability. ## Under the Hood GraphQL-Ruby determines which subscribers can receive a broadcast by inspecting: - __Query string__. Only exactly-matching query strings will receive the same broadcast. - __Variables__. Only exactly-matching variable values will receive the same broadcast. - __Field and Arguments__ given to `.trigger`. They must match the ones initially sent when subscribing. (Subscriptions always worked this way.) - __Subscription scope__. Only clients with exactly-matching subscription scope can receive the same broadcasts. So, take care to {% internal_link "set subscription_scope", "subscriptions/subscription_classes#scope" %} whenever a subscription should be implicitly scoped! (See {{ "GraphQL::Subscriptions::Event#fingerprint" | api_doc }} for the implementation of broadcast fingerprints.) ## Checking for Broadcastable For testing purposes, you can confirm that a GraphQL query string is broadcastable by using {{ "Subscriptions#broadcastable?" | api_doc }}: ```ruby subscription_string = "subscription { ... }" MySchema.subscriptions.broadcastable?(subscription_string) # => true or false ``` Use this in your application's tests to make sure that broadcastable fields aren't accidentally made non-broadcastable. ## Connections and Edges You can configure your generated `Connection` and `Edge` types to be broadcastable by setting `default_broadcastable(true)` in their definition: ```ruby # app/types/base_connection.rb class Types::BaseConnection < Types::BaseObject include GraphQL::Types::Relay::ConnectionBehaviors default_broadcastable(true) end # app/types/base_edge.rb class Types::BaseEdge < Types::BaseObject include GraphQL::Types::Relay::EdgeBehaviors default_broadcastable(true) end ``` (In your `BaseObject`, you should also have `connection_type_class(Types::BaseConnection)` and `edge_type_class(Types::BaseEdge)`.) `PageInfo` is broadcastable by default. graphql-ruby-2.5.19/guides/subscriptions/implementation.md000066400000000000000000000033601514115062600237660ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Subscriptions title: Implementation desc: Subscription execution and delivery index: 3 --- The {{ "GraphQL::Subscriptions" | api_doc }} plugin is a base class for implementing subscriptions. Each method corresponds to a step in the subscription lifecycle. See the API docs for method-by-method documentation: {{ "GraphQL::Subscriptions" | api_doc }}. Also, see the {% internal_link "Pusher implementation guide", "subscriptions/pusher_implementation" %}, the {% internal_link "Ably implementation guide", "subscriptions/ably_implementation" %}, the {% internal_link "ActionCable implementation guide", "subscriptions/action_cable_implementation" %} or {{ "GraphQL::Subscriptions::ActionCableSubscriptions" | api_doc }} docs for an example implementation. ## Considerations Every Ruby application is different, so consider these points when implementing subscriptions: - Is your application single-process or multiprocess? Single-process applications can store state in memory while multiprocess applications need a message broker to keep all processes up-to-date. - What components of your application can be used for persistence and message passing? - How will you deliver push updates to subscribed clients? (For example, websockets, ActionCable, Pusher, webhooks, or something else?) - How will you handle [thundering herd](https://en.wikipedia.org/wiki/Thundering_herd_problem)s? When an event is triggered, how will you manage database access to update clients without swamping your system? ## Broadcasts _Broadcasting_ updates to multiple subscribers is supported by GraphQL-Ruby, but requires implementation-specific work, see more in the {% internal_link "Broadcast guide", "subscriptions/broadcast" %}. graphql-ruby-2.5.19/guides/subscriptions/multi_tenant.md000066400000000000000000000111501514115062600234400ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Subscriptions title: Multi-Tenant desc: Switching tenants in GraphQL Subscription execution index: 8 --- In a multi-tenant system, data from many different accounts is stored on the same server. (An account might be an organization, a customer, a namespace, a domain, etc -- these are all _tenants_.) Gems like [Apartment](https://github.com/influitive/apartment) assist with this arrangement, but it can also be implemented in the application. Here are a few considerations for this architecture when using GraphQL subscriptions. ## Add Tenant to `context` All the approaches below will use `context[:tenant]` to identify the tenant during GraphQL execution, so make sure to assign it before executing a query: ```ruby context = { viewer: current_user, tenant: current_user.tenant, # ... } MySchema.execute(query_str, context: context, ...) ``` ## Tenant-based `subscription_scope` When subscriptions are delivered, {% internal_link "`subscription_scope`", "subscriptions/subscription_classes#scope" %} is one element used to route data to the right subscriber. In short, it's the _implicit_ identifier for the receiver. In a multi-tenant architecture, `subscription_scope` should reference the context key that names the tenant, for example: ```ruby class BudgetWasApproved < GraphQL::Schema::Subscription subscription_scope :tenant # This would work with `context[:tenant] => "acme-corp"` # ... end # Include the scope when `.trigger`ing: BudgetSchema.subscriptions.trigger(:budget_was_approved, {}, { ... }, scope: "acme-corp") ``` Alternatively, `subscription_scope` might name something that _belongs_ to the tenant: ```ruby class BudgetWasApproved < GraphQL::Schema::Subscription subscription_scope :project_id # This would work with `context[:project_id] = 1234` end # Include the scope when `.trigger`ing: BudgetSchema.subscriptions.trigger(:budget_was_approved, {}, { ... }, scope: 1234) ``` As long as `project_id` is unique among _all_ tenants, that would work fine too. But _some_ scope is required so that subscriptions can be disambiguated between tenants. ## Choosing a tenant for execution There are a few places where subscriptions might need to load data: - When building the payload for the subscription (fetching data to prepare the result) - `ActionCableSubscriptions`: when deserializing the JSON string broadcasted by `ActionCable` - `PusherSubscriptions` and `AblySubscriptions`: when deserializing query context Each of these operations will need to select the right tenant in order to load data properly. For __building the payload__, use a {% internal_link "Trace module", "queries/tracing" %}: ```ruby module TenantSelectionTrace def execute_multiplex(multiplex:) # this is the top-level, umbrella event context = data[:multiplex].queries.first.context # This assumes that all queries in a multiplex have the same tenant MultiTenancy.select_tenant(context[:tenant]) do # ^^ your multi-tenancy implementation here super # Call through to the rest of execution end end end # ... class MySchema < GraphQL::Schema trace_with(TenantSelectionTrace) end ``` The tracer above will use `context[:tenant]` to select a tenant for the duration of execution for _all_ queries, mutations, and subscriptions. For __deserializing ActionCable messages__, provide a `serializer:` object that implements `.dump(obj)` and `.load(string, context)`: ```ruby class MultiTenantSerializer def self.dump(obj) GraphQL::Subscriptions::Serialize.dump(obj) end def self.load(string, context) MultiTenancy.select_tenant(context[:tenant]) do GraphQL::Subscriptions::Serialize.load(string) end end end # ... class MySchema < GraphQL::Schema # ... use GraphQL::Subscriptions::ActionCableSubscriptions, serializer: MultiTenantSerializer end ``` The implementation above will use the built-in serialization algorithms, but it will do so _in the context of_ the selected tenant. For __loading query context in Pusher and Ably__, add tenant selection to your `load_context` method, if required: ```ruby class CustomSubscriptions < GraphQL::Pro::PusherSubscriptions # or `GraphQL::Pro::AblySubscriptions` def dump_context(ctx) JSON.dump(ctx.to_h) end def load_context(ctx_string) ctx_data = JSON.parse(ctx_string) MultiTenancy.select_tenant(ctx_data["tenant"]) do # Build a symbol-keyed hash, loading objects from the database if necessary # to use a `context: ...` end end end ``` With that approach, the selected tenant will be active when building the context hash, in case any objects need to be loaded from the database. graphql-ruby-2.5.19/guides/subscriptions/overview.md000066400000000000000000000053241514115062600226110ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Subscriptions title: Overview desc: Introduction to Subscriptions in GraphQL-Ruby index: 0 --- _Subscriptions_ allow GraphQL clients to observe specific events and receive updates from the server when those events occur. This supports live updates, such as websocket pushes. Subscriptions introduce several new concepts: - The __Subscription type__ is the entry point for subscription queries - __Subscription classes__ are resolvers for handing initial subscription requests and subsequent updates - __Triggers__ begin the update process - The __Implementation__ provides application-specific methods for executing & delivering updates. - __Broadcasts__ can send the same GraphQL result to any number of subscribers. ## Subscription Type `subscription` is an entry point to your GraphQL schema, like `query` or `mutation`. It is defined by your `SubscriptionType`, a root-level `GraphQL::Schema::Object`. Read more in the {% internal_link "Subscription Type guide", "subscriptions/subscription_type" %}. ## Subscription Classes {{ "GraphQL::Schema::Subscription" | api_doc }} is a resolver class with subscription-specific behaviors. Each subscription field should be implemented by a subscription class. Read more in the {% internal_link "Subscription Classes guide", "subscriptions/subscription_classes" %} ## Triggers After an event occurs in our application, _triggers_ begin the update process by sending a name and payload to GraphQL. Read more in the {% internal_link "Triggers guide","subscriptions/triggers" %}. ## Implementation Besides the GraphQL component, your application must provide some subscription-related plumbing, for example: - __state management__: How does your application keep track of who is subscribed to what? - __transport__: How does your application deliver payloads to clients? - __queueing__: How does your application distribute the work of re-running subscription queries? Read more in the {% internal_link "Implementation guide", "subscriptions/implementation" %} or check out the {% internal_link "ActionCable implementation", "subscriptions/action_cable_implementation" %}, {% internal_link "Pusher implementation", "subscriptions/pusher_implementation" %} or {% internal_link "Ably implementation", "subscriptions/ably_implementation" %}. ## Broadcasts By default, the subscription implementations listed above handle each subscription in total isolation. However, this behavior can be optimized by setting up broadcasts. Read more in the {% internal_link "Broadcast guide", "subscriptions/broadcast" %}. ## Multi-Tenant See the {% internal_link "Multi-tenant guide", "subscriptions/multi_tenant" %} for supporting multi-tenancy in GraphQL subscriptions. graphql-ruby-2.5.19/guides/subscriptions/pusher_implementation.md000066400000000000000000000272151514115062600253610ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Subscriptions title: Pusher Implementation desc: GraphQL subscriptions over Pusher index: 6 pro: true --- [GraphQL Pro](https://graphql.pro) includes a subscription system based on [Redis](https://redis.io) and [Pusher](https://pusher.com) which works with any Ruby web framework. After creating an app on Pusher and [configuring the Ruby gem](https://github.com/pusher/pusher-http-ruby#global), you can hook it up to your GraphQL schema. ## How it Works This subscription implementation uses a hybrid approach: - __Your app__ takes GraphQL queries an runs them - __Redis__ stores subscription data for later updates - __Pusher__ sends updates to subscribed clients So, the lifecycle goes like this: - A `subscription` query is sent by HTTP Post to your server (just like a `query` or `mutation`) - The response contains a Pusher channel ID (as an HTTP header) which the client may subscribe to - The client opens that Pusher channel - When the server triggers updates, they're delivered over the Pusher channel - When the client unsubscribes, the server receives a webhook and responds by removing its subscription data Here's another look: ``` 1. Subscription is created in your app HTTP POST .----------> write to Redis 📱 ⚙️ -----> 💾 <---------' X-Subscription-ID: 1234 2. Client opens a connection to Pusher websocket 📱 <---------> ☁️ 3. The app sends updates via Pusher ⚙️ ---------> ☁️ ------> 📱 POST update (via gem) (via websocket) 4. When the client unsubscribes, Pusher notifies the app webhook ⚙️ <-------- ☁️ (disconnect) 📱 ``` By using this configuration, you can use GraphQL subscriptions without hosting a push server yourself! ## Database setup Subscriptions require a _persistent_ Redis database, configured with: ```sh maxmemory-policy noeviction # optional, more durable persistence: appendonly yes ``` Otherwise, Redis will drop data that doesn't fit in memory (read more in ["Redis persistence"](https://redis.io/topics/persistence)). If you're already using Redis in your application, see ["Storing Data in Redis"](https://www.mikeperham.com/2015/09/24/storing-data-with-redis/) for options to isolate data and tune your configuration. ## Schema configuration Add `redis` to your `Gemfile`: ```ruby gem 'redis' ``` and `bundle install`. Then create a Redis instance: ```ruby # for example, in an initializer: $graphql_subscriptions_redis = Redis.new # default connection ``` Then, that Redis client is passed to the Subscription configuration: ```ruby class MySchema < GraphQL::Schema use GraphQL::Pro::PusherSubscriptions, redis: $graphql_subscriptions_redis end ``` That connection will be used for managing subscription state. All writes to Redis are prefixed with `graphql:sub:`. There are also two configurations for managing persistence: - `stale_ttl_s:` expires subscription data after the given number of seconds without any update. After `stale_ttl_s` has passed, the data will expire from Redis. Each time a subscription receives an update, its TTL is refreshed. (Generally, this isn't required because the backend is built to clean itself up. But, if you find that Redis is collecting stale queries, you can set them to expire after some very long time as a safeguard.) - `cleanup_delay_s:` (default: `5`) prevents deleting a subscription during those first seconds after it's created. Usually, a longer delay isn't necessary, but if you observe latency between the subscription's initial response and the client's subscription to the delivery channel, you can set this configuration to account for it. ### Connection Pool For better performance reading and writing to Redis, you can pass a `connection_pool:` instead of `redis:`, using the [`connection_pool` gem](https://github.com/mperham/connection_pool): ```ruby use GraphQL::Pro::PusherSubscriptions, connection_pool: ConnectionPool.new(size: 5, timeout: 5) { Redis.new }, ``` ### Broadcasts If you set up {% internal_link "Broadcasts", "/subscriptions/broadcast" %}, then you can update many clients over a single Pusher channel. Broadcast channels have stable, predictable IDs. To prevent unauthorized clients from "listening in," use an [authorized Pusher channel](#authorization) for transport. In your authorization code, you can check for a broadcast using `.broadcast_subscription_id?`: ```ruby # In your Pusher authorization endpoint: channel_name = params[:channel_name] MySchema.subscriptions.broadcast_subscription_id?(channel_name) # => true | false ``` ## Execution configuration During execution, GraphQL will assign a `subscription_id` to the `context` hash. The client will use that ID to listen for updates, so you must return the `subscription_id` in the response headers. Return `result.context[:subscription_id]` as the `X-Subscription-ID` header. For example: ```ruby result = MySchema.execute(...) # For subscriptions, return the subscription_id as a header if result.subscription? response.headers["X-Subscription-ID"] = result.context[:subscription_id] end render json: result ``` This way, the client can use that ID as a Pusher channel. For __CORS requests__, you need a special header so that clients can read the custom header: ```ruby if result.subscription? response.headers["X-Subscription-ID"] = result.context[:subscription_id] # Required for CORS requests: response.headers["Access-Control-Expose-Headers"] = "X-Subscription-ID" end ``` Read more here: ["Using CORS"](https://www.html5rocks.com/en/tutorials/cors/). ### Payload Compression To mitigate problems with [Pusher's 10kb message limit](https://support.pusher.com/hc/en-us/articles/4412243423761-What-Is-The-Message-Size-Limit-When-Publishing-an-Event-in-Channels-), you can specify `compress_pusher_payload: true` in the `context` of your subscription. For example: ```ruby # app/controllers/graphql_controller.rb def execute # ... # Somehow detect whether the client supports compressed payloads, # for example, User-Agent, query param, or request header: if client_supports_compressed_payloads? context[:compress_pusher_payload] = true end # ... end ``` This will cause subscription payloads to include `compressed_result: "..."` instead of `result: "..."` when they're sent over Pusher. See docs for {% internal_link "Apollo Client", "/javascript_client/apollo_subscriptions" %} or {% internal_link "Relay Modern", "/javascript_client/relay_subscriptions" %} to read about preparing clients for compressed payloads. By configuring `compress_pusher_payload: true` on a query-by-query basis, the subscription backend can continue to support clients running _old_ client code (by not compressing) while upgrading new clients to compressed payloads. ### Batched Deliveries By default, `PusherSubscriptions` sends updates in batches of up to 10 at a time, using [batch triggers](https://github.com/pusher/pusher-http-ruby#batches). You can customize the batch size by passing `batch_size:` when installing it, for example: ```ruby use GraphQL::Pro::PusherSubscriptions, batch_size: 1, ... ``` `batch_size: 1` will make `PusherSubscriptions` use the single trigger API instead of batch triggers. ## Webhook configuration Your server needs to receive webhooks from Pusher when clients disconnect. This keeps your local subscription database in sync with Pusher. In the Pusher web UI, Add a webhook for "Channel existence" {{ "/subscriptions/pusher_webhook_configuration.png" | link_to_img:"Pusher Webhook Configuration" }} Then, mount the Rack app for handling webhooks from Pusher. For example, on Rails: ```ruby # config/routes.rb # Include GraphQL::Pro's routing extensions: using GraphQL::Pro::Routes Rails.application.routes.draw do # ... # Handle Pusher webhooks for subscriptions: mount MySchema.pusher_webhooks_client, at: "/pusher_webhooks" end ``` This way, we'll be kept up-to-date with Pusher's unsubscribe events. __Alternatively__, you can configure the routes to load your schema lazily, during the first request: ```ruby # Provide the fully-qualified class name of your schema: lazy_routes = GraphQL::Pro::Routes::Lazy.new("MySchema") mount lazy_routes.pusher_webhooks_client, at: "/pusher_webhooks" ``` ## Authorization To ensure the privacy of subscription updates, you should use a [private channel](https://pusher.com/docs/client_api_guide/client_private_channels) for transport. To use a private channel, add a `channel_prefix:` key to your query context: ```ruby MySchema.execute( query_string, context: { # If this query is a subscription, use this prefix for the Pusher channel: channel_prefix: "private-user-#{current_user.id}-", # ... }, # ... ) ``` That prefix will be applied to GraphQL-related Pusher channel names. (The prefix should begin with `private-`, as required by Pusher.) Then, in your [auth endpoint](https://pusher.com/docs/authenticating_users#implementing_private_endpoints), you can assert that the logged-in user matches the channel name: ```ruby if params[:channel_name].start_with?("private-user-#{current_user.id}-") # success, render the auth token else # failure, render unauthorized end ``` ## Serializing Context Since subscription state is stored in the database, then reloaded for pushing updates, you have to serialize and reload your query `context`. By default, this is done with {{ "GraphQL::Subscriptions::Serialize" | api_doc }}'s `dump` and `load` methods, but you can provide custom implementations as well. To customize the serialization logic, create a subclass of `GraphQL::Pro::PusherSubscriptions` and override `#dump_context(ctx)` and `#load_context(ctx_string)`: ```ruby class CustomSubscriptions < GraphQL::Pro::PusherSubscriptions def dump_context(ctx) context_hash = ctx.to_h # somehow convert this hash to a string, return the string end def load_context(ctx_string) # Given the string from the DB, create a new hash # to use as `context:` end end ``` Then, use your _custom_ subscriptions class instead of the built-in one for your schema: ```ruby class MySchema < GraphQL::Schema # Use custom subscriptions instead of GraphQL::Pro::PusherSubscriptions # to get custom serialization logic use CustomSubscriptions, redis: $redis end ``` That gives you fine-grained control of context reloading. ## Dashboard You can monitor subscription state in the {% internal_link "GraphQL-Pro Dashboard", "/pro/dashboard" %}: {{ "/subscriptions/redis_dashboard_1.png" | link_to_img:"Redis Subscription Dashboard" }} {{ "/subscriptions/redis_dashboard_2.png" | link_to_img:"Redis Subscription Detail" }} ## Development Tips #### Clear subscription data At any time, you can reset your subscription database with the __"Reset"__ button in the {% internal_link "GraphQL-Pro Dashboard", "/pro/dashboard" %}, or in Ruby: ```ruby # Wipe all subscription data from the DB: MySchema.subscriptions.clear ``` #### Developing with Pusher webhooks To receive Pusher's webhooks in development, Pusher [suggests using ngrok](https://support.pusher.com/hc/en-us/articles/203112227-Developing-against-and-testing-WebHooks). It gives you a public URL which you can setup with Pusher, then any hooks delivered to that URL will be forwarded to your development environment. ## Client configuration Install the [Pusher JS client](https://github.com/pusher/pusher-js) then see docs for: - {% internal_link "Apollo Client", "/javascript_client/apollo_subscriptions" %} - {% internal_link "Relay Modern", "/javascript_client/relay_subscriptions" %} - {% internal_link "GraphiQL", "/javascript_client/graphiql_subscriptions" %} - {% internal_link "urql", "/javascript_client/urql_subscriptions" %} graphql-ruby-2.5.19/guides/subscriptions/pusher_webhook_configuration.png000066400000000000000000001236421514115062600271060ustar00rootroot00000000000000PNG  IHDRTh! iCCPICC ProfileHWXS[RIhH #E^%BBP# ]Qq-,6"u`CM N9g3fPefAĤdP(@DE(MHk֒X:_Eq@ N89  t@\ XM @%8]5$8U6~{@t%YtGQV ''ͅ>V99 V"Cl]LfaY.R!Eroa-C-YaLM *\8"?f l0!f|؞-B{4+ǩYh ;"\gy/dD16ia 3bd<3!eń}fEFMFl0h^ - 束,yQ\ 0X]rߒ(=V3v@T3{5lG(\rbep?X@ [*2sy6trͨGtD1 |Q=^}z1&4a07bqWǽ(hԸIVYe5EkVYjsƗ?86m=O{vNN&:L]oLuzz|z'`|X٬J֠~X~AAACazvA#=FS]377~obj`̤乩iii}3YYusy. "â%jdɷbmErXZݲYXX7X0mmlm^N033fO̻XձqJ'''$NH\x9I+ԒLJOޕ<45`ꆩ}L9tghȞqlLC)=)ّZPjHAg#כy=OH_ޟQ1W_egn|U5ݘCI9*Pd ҝ5gVwenInO[ކAapMcNLهk1wgqןd~" S/2\ToqKK^d[T^vibŏ DDXrk+VlZ[z̶JK?\ȪUV׬!kr&kZZ_톙.VLغQ2eѦ5>WeTݨnܬy[[xߪlmmmo5AQq~֮]_u=wwߣgu n;mo>}-ood65כr9GGJM=-I-GC淺6cV/>>rܓO=n~tg<v¹s?qBEG/\jtñtl|˵{R^WO]vz7"ntߌyִ[=ɾn{>P~PPa?s׿Q̣{9_<=W޳Gߋ%y18Jj7oN|>5]λ4?Oφg&}bk#9##l![z`Cx]= *^RAdE) gRq3J l`/9zzaE`/E7‡7:Z"22e'${y;D|\:;G'kꞍC pHYs%%IR$iTXtXML:com.adobe.xmp 360 1620 1 QH@{@IDATxUHHtwt !v- *t HAlE1FJP@Z{v]g?oo̽{kfΜstPHHHHHHHHHHHHHw 7 TF            P IHHHHHHHHHHH $@$@$@$@$@$@$@$@$@$@$@$*q3 P{HHHHHHHHHHHB% n&           *Tx @T$@$@$@$@$@$@$@$@$@$@$@$@            @ HHHHHHHHHHHP=@$@$@$@$@$@$@$@$@$@$@$@P7 *HHHHHHHHHHHH *Tf           B  @J@L$@$@$@$@$@$@$@$@$@$@$@T            P IHHHHHHHHHHH $@$@$@$@$@$@$@$@$@$@$@$*q3 P{HHHHHHHHHHHB% n&           *Tx @T$@$@$@$@$@$@$@$@$@$@$@$@            @ HHHHHHHHHHHP=@$@$@$@$@$@$@$@$@$@$@$@P7 *HHHHHHHHHHHH *Tf           B  @J@L$@$@$@$@$@$@$@$@$@$@$@T            P IHHHHHHHHHHH $@$@$@$@$@$@$@$@$@$@$@$*q3 P{HHHHHHHHHHHB% n&           *Tx @T$@$@$@$@$@$@$@$@$@$@$@$,lظEΝ.GRH!>p֬N4L6.#G9e=P+U &IA$ fl WhBr_ *T;w (Zƍ'QM[yHZ=Һ%mP    ண P (!    x g.*U,.],ۙG`AaeÆ-vcNSXaI6v.$^ׯ_}eDyf$@$@$@$@$liѺ s 1LLC$@$@$@$@"g*˕[nE4o]R% Nh&V`&vљntq+jIJ*.9.[%KW \A2IR@>u}3$@$@$@$@$pH.tx]G1CHߴ~:ziGAHHHHH LxJ ټe ovʵk$YDgCGsT1"3 @֬I;x\=cҭ@پci߰ J\    Y L:z<.      Dh*n5U(T6wdZ@;*M.],;v!egSƌp"իhrYj?~RRH.s甆 jK2%=Z.@[eoL+&K4}eXIk*ۮ8W^sɥ>I<<J \|Ed,wr"ugvSĹ9|wZԯW;I.鞦 BEs"^;'    P@l+WeOֹLwW|yi?;ǐ4qk/z+2,:!a Z>oHɝm!   H,(=P˗X_`5qT0٢[RAG C :"-Hg?Wj)UVuyL*2Y6 a)ٲe1+U[oդ㝡:y2`QH!RtYL LI#h% ,t uVz~GN8Ҽ7+T)sLҿϛfDn!ΕÙ iJ~9X[=W;1uw_xLɑ#~ǥVjwMQkG:`JZJ}27L= Dh`Vd(oYՊ)bZ:|N( *pC(`aRH4.]i-] 4Oe ;>V + 1rʻ . $>qj|RhfUt>|$*ٲfGeTZ'}TarnBxGo,QLRJE67~̜@&LL-E IE÷zN^z԰kᆫ_FsL& fiO] 8֬ 0t$[` Pdʔ,c4ԗ_ }?Lƾ;lÿu q`4ר1;}{&P*ŧ\P=Vd{[hNwզq,pHHHHnbPdt|kGJTz~Yrއ*+.]r,Qw`y1rtX=^Ab)Mf%pֵӾ>= [Ǵ*'zt}EҧOgcؠwȪo.0pZo Z&od>GOO+2̝QjJ4p=ֳ{)R6kܪR+X0 3-6ú\;g FC=*N!   HB(]qT6O~q+ K܂ ^:!h`Xb+5,ܭL1_ۙx(o+v2s!_hk.΂kVl ;f PWw)؎C4~}X8G+1k˰]j:?xcyWZhhϟn*5o 7E[RZ%^r#wL$@$@$@$@II0+k׮}JHb;1ϱ)Gbk66o71(6t@e *N0wZX>nI2Mc@,mgݢa`XZ&˝P`#ڥhY٥. ( m ֬ujW7 W    8PxdLE7 x]r?0Z8yJn5nw_5Р[[E *%4 \D%)SFponN>c,f|7΅r~mZڹsY,Y-lPlFuy(jnXZxekVB?*>&    (@HXeD&%Ks6jbǟؓ.{5֤ x E!pug)LaiRxə Q-= , Tw5#_hVZo`w _]vZ4+Væ&b|SlV-Y|ug>ׁB$@$@$@$ ĹB%U[6`u-V!SV.Vʫ )n^ټw?-f[l{ Px4N^ΩȠP9+W`>y /]lV߯p.6aD#!&yw!u֧W0돪ֆ(q=Mny΂ Gޡ 9ob36u=`V $SG/#zuj3v/VęGAwV,]KmA|d0˗B%~%hş/4ؘ0<~ڴW&>{=IHHH'8W#Z7^r_nu5e]da~M|( HSpA3N%O63׮#ɓmH$#{=Lǡܹ́ޛ峮mI^gB06|P~]0 ,XAI0AkrIɑ=a]UHYɓ'a O=eAݧZΝOrg-x1̽qt^2ob3g|_-7LP8}w*ނjU+6իr.܋6. $^bÚ )faauc+W(YGg >l{,O{XQ\rb2U+mk.mZ7sg`e}{%s-^nr]GBZGV`L]MZvhO|Yi!d1>v() PFu~Q8Or| y7kz,\Lm~3J!jRXaɜ)Z 6ˑaV>ȏ;Wewo=1YҪeS_IڝA +jlM $@$@$@$@$" \>@K=֨^E&$Z~^HkbdEKjeވ _~TkwGtiPFtVIQlUvɓӴq ƍʧv:qS] gYj ג"E %1M)jGcᬌ t14f̜Jy77S[b]{QO<ߨ |g {wPbиΐwF \Kj~UDUjcw~d~4Q3R]F-_ƽc>W9@$@$@$@$@ "/^tCɰnʀマlG|KA'e֝x C#i-Q_z*ejz墪@(_(:L8; 6oCU|や)Y깋9_Z%Yrӝn{|գ-[,tn=i?ۖvy    DL ^\~' (N| V`b-J:KB$VQMa0jx: uL2dPO)T(c]RݿC!R݂ n9;nhs{3N!^3sd|00kԈZdɒG\~D})ߦiؠ@;_/S    H,k wI$qC0Pw׌UPv+# 2>A{=uTr/Ay˺B~#If:J̙d>PRU5oJdۖRjؽ7v)L$@$@$@@*O'NFG>#{TpܧN}ȯLZ;v¸,BSde$odd\=[Vɞ=IK$@$@$@$@$<S/W׻59խ{Z%p'J:ܰ `rqi)*CX`O1*&A^ԝ0\z($@$@$@$@M&"Xfӆ/a[&aA_t$&y`ՀLz!    =S/Abq%ψ_t$P^RAebrFu?_T:@/*3- @!h* HIHHHHH1k$Uɳ,4 ֢_\O$@$@$@$@Q%@JT1= @@O6_ivS    3TjHHHHHH *&K\RҦ=ܦ+lv:UiIHHHH/Ds @!pU  HHHH-T>M$@$@$@$@$@$))H7 q'HHHHHHHHHHH(*Ta *T5D$@$@$@$@$@$@$@$@$@$@$@ P@/HHHHHHHHHHH Pw'           J zx$@$@$@$@$@$@$@$@$@$@$@qG c= $PT$ &          ;TkHHHHHHHHHHH HWǽC$@$@$@$@$p('Mzd<,     -T"í$@$@$@$@$@$@$@$@$@$@$@$ qfbYsD%) wbsy$@$@$@$@$X B%^Y @P2,HHHHHHHHHHH B%^Y @P2,HHHHHHHHHHH B%^Y @P2,HHHHHHHHHHH B%^Y @P2,HHHHHHHHHHH B%^Y @P2,HHHHHHHHHHH B%^Y @P2,HHHHHHHHHHH B%^Y @P2,HHHHHHHHHHH B%^Y @P2,HHHHHHHHHHH B%^Y @P2,HHHHHHHHHHH B%^Y @P2,HHHHHHHHHHH B%^Y @P2,HHHHHHHHHHH HXOE$@$@$@$@$@ 3K".ߥagqkr)E/"3%Ic&    K {x$@$@$@$@$@EUl=q]NHu^_sa[.҈ΖDRB$@$@$@$@$P],HHHHHH9|VK/bJeJ"?"G&3+\wҸ.RdOTr.)n SĄ)WH%c**Ux)H \|Y]NG8@qB KW#jﱸ}O { ]ׯ_$Ib~+9g_ʁW_}&M%3o 6J+٥d8'HҤICR >P\ _ $ $@l3 @@irI4iHT$E+WŋhI<_@ *d-3s@!(\KI#@ ?׬㋝eݺ 3䙧IՖ*:0QJ,WL[$3ݴSmc8/sҾf+/sQ߬xoh\JLߞP @#e҃I0e3";N+G%d:F"-P(Fb6$Quvu9.%$|`򯖝TjRF\C$>${v&9u[LLBd`V;P s0ڵkrU9s~䐱h)RΓ7O'x#(s$`P7̙s䴚Vx(Q瞂I3MrɒrYu7ȂI737@̟Д}ݰ   Kn`b);OI*R| 铫˯H2%YŔpR|: g.ٱmΕKr!)Յ~pq-{BJ(TRN-YdcǎJYܸO+|4C PM|ײc.yS{ `@⠀yOzu-4~3s Đ* .KBcX @ =bX7_P*IǗ1>bȃ]7eQ*INT@Wtuc u͓LfMJ/"ŊyFl? ӧK+ŋVdM $J 9u7g jQ-kw\G!'@Jȑ p,[Rʗ/+ݦ}H0i|d*kkOis_ːɂHHHH *@7_L1$%*Zk0uI,q`[O$r9\C8{Ew:he?ϒ)(/J oƍ[:*T<8 FgF-ST?Lmߥ)F ,($#e׎mRTIɞ=%|"ʔ E|l>.Wr*ap@bxbpa>|ԙ|֬_q7|i-sk ZoڴٌG~@ru2}ϦSU>RX֝YJShS e#gvizwccb*Yl*Ӥ" IVJ"=GypCq}y^MGT!믿eJIwݰݮ;/3fɱߏʫSFeJSjUVXn.mVK"mG?U̷l&[~sΗK䞦M<;~\V,_{e=r]s%5=41c<2/LkڴͮjZ!l,ΝWS"Rt \ΛP.[.~ 3Q Od#ռܹKfDԨQ]jg&FXUAeu2{<9zY_B9yMD_`ɼ!cɟLSCɭDΏ}˚5a*Ν;>UaOȴieYw'Zha} j[ymN'}o6{J,f5Ž3 O:}Fu<-6öm姟fށgϞ5J,&hMhYu3+WNQ[ԯkc{_et9k>|D $[RhQiٲ1=^]( KG+xnU)x7|+͘1M) $[O\43~fY.h9^Re@R`m!Km?V?IJc{!cI4 Kڴi{,(G#/̣̿Kb%b-\5'_퉭vȒpNp~Cm;:k_ 9zCʨu~}zHX-vXTqN]{icSӹzZ>|tFQÉXZdQ߲Ŵ#*p0 :X'LXW*wT.%-4Dg3ϴQ3J#[D8q4S]n6S̙sO?2 DP;˸vE3&/u({?ȑ36o٪sdǟʨCb*^,]&#Fk6[ ,sDF ,嵛-Io𡍚 ²e+㏧H~= :-w# Ut`/eoJF ݛc/i`@u{nVG ҥ}Wn-#ltF*F~]DG唩S&nz+$B&g=6/ϿMV7lŠ?NRznJtQ~p{C2 j@7oQNYG1n%@9_hY&g}oȁ":ȝ ј ^\' (B#^ڪPMwÞhGס_A}θ{^YxO"sgg<\@Aȸ#Zʷ}鳉2pz7uXtQLuI\?ySgNVy xu[suٴiYp$y՗*)$@$PugKv_"~cܟrelX$B---s 2Qc[/k{(={V)Unu7T#~ϝR<>e;~G=B Ap]e{(e'OM,TXKl ypWӏIunstbg2~#O:%(*ީlP 0rgڛ}Jʒ^; oߡ (>8Y舴gn=U@ICKjw訉jXqyuN^'hʔ)ŋNH}YGZGraΜyo ;d:ܛ4X8ЁsˇN=2Nׂ 21odf8fv=V>kcЃmoqi xWDΟ;~|W-U}<}>(5B-z~~Nqrj(nZja8]T+X)/^(oCgs"̠q`LuF3͓X_({Wz_3 2f̠TnUk{mWߔǿSdm(mjש)I$6lh 굨kE-=9@"OCqN/Ϩ{W/Ev)j`3(hErac4[u_fx-2eJkyd*yQTdWKj Ov#L`Wx^+\F#[_N-PٸqϠY^k׮XyˢEK̪2KKv0r6k,z/ݭtUcN}X͘9[z'4zO|dHAUYkwx(ހ5"UVRTPH @اʍsvvl9yằN{@5CJU^`WLa1 x_z <)Ԩ̝X-4kz{{/TGz#&h 4w74 ]u!-[)QZ7T@7=S4MnG?Ղ_JG|s_ɟ~MQl,I<,e蠜>,e>>ӨK;zvo.9xzTɬm䊚 Xnlz^Y5{>w`%oi Sl|| M u]t=ѩOE{N1`xlVҮozڱsoGF!bcP0Ɋue3R^M# 7gJtPPG:w-Lq͛X]9m3@>G( 9z=_Um 9s:DkCYa SJ[j%2lctvcnr]{c#XJTu2e4cN|ݟ{lO@D݃N}**{f0 ~x|-[6W*S܇Ұa=gy)3z2={eU ;Vйf1a/=ZpϽmFmC448֭[x(Sq֬q5}!pKSp .#VҦMK$P(Ք2&Bl EW'o2ŝw7 ;w:Ptg##Xx| /<2ŝ!; E:|9xpGG,5P8ʟ@yVtPA?Qhct;iV+h4x^64]^1yF3_*! q܂;CF lP{3_F(zg3Bn xO{zj0IkwP( <7PBCb`g1k]<)tPX,q}It=x6 %ȋP7_Q%bd;;PA\N#F @x 48K&GΟHk#?s $,ҧ&ޗFZI-%iJRm1 >/+c3ݮ.,x{ eKJm-|Z71e 9k1tp;xȻQؾ0٤BrwNGNj 20 u+m)yYF|(S~B [L#һPLEfݍcƽ5DWnukkan$(8Ͽf)?ywQ&@;vxJT-FPh"]ác'ˤDl^;Rk*X0zSV`5>o jLMG4"D7nL㜟}1ӑfE;fs<_jA(tZ)S{ͮ2)(}LyGL~L^xҌPG'?N=,3E#gT߁|<.As`ABh)Pzg׾jB f DzŁ*n X 2dL>Z+ HTux6 JJ~oAJ%Yλ4*`o6e?AG*~(7=;?ÝW-ZT:j'XZ Uk.]^reDucA~_p+PŋX&Mi.tnƝwi~49gq(]hYA-\$WΜ6]7@`bJ1``>FCqX? w3g1y*7N#?yww enL}bvcyfebv,ߧ?1bx3ۨ"kӳ;c(CtꚺǨ[YŞ=H>`~0nML=o ŊRѭ:O W/xjSb9+am\jaafy20MLzvb vӂeXMa0JRkfu>T;kNl\[Z!7~̜@>3nJ,Muڴi,60}Ŧ\7+n! -3_v@HBnt+ /}{m=(l~ޛ-4TgTށ|<.As[`@6.]\zxr#ҵ9s|п~+)}kΞqɰ>T ;h%wQOw}G~UG||F}#~/+c:u[[P &ہk׮[9ѿϛlF XFg/)`ΥKVTI~F)}I+U)H6.;͏?6dQ2D YnKGiLA3@i!_y\ 1^F~XBa Z!M[7'Nx1Rpecr^ L+R{H]}Χ6pө32+q ^Sę ,neY?Nϥ3-\%vpsbWl*uӾίËo&y(S$ė GVPa{oyu՘?0;ϑ;!s[`(2c+!-Pj%_:P ݹivV(S߸:j}iWŮ4:|Sl1 @]6i\lɻ]vLSr| ;!=كA0r/-|D!C0(>ҸqjԨtz[(A+Slw`Ҹ 6:Gfnc+z fm  ܪSUmʧ;K~[JeoL5}=eSA#! ٢ȡP:.<tz;#'& "Vb 3:oq׃g vĈ $;|t?`K1k`A~(; ڑB!~LX٘- f?4kS#ؔE]];1Sڵ}_jTirу-+_J%cqxRgUu=|&9, . !իW5Vx_UreK԰bBl ^V=rE . }IfM[Mg[ϩ [Ϟ䣏ӑ4+G(:|+/>8$f{TdѮs2frԒ6i<(/+8Pf/0E btl_^u5<ύ%;#vc:W`؈"+"ζe˖UVCL5_f$ Ў3LPv6a,~%8 m Vrc6&#>E{̚+g'My O:ՙd͚\wn KP`WBP#Jzm-q1LW\kPQhȄW!WFYbW# N7O. \Ν&=P{lYiWEu(͖-Lpmw?d:aMfu/!|!G0拗.|úE Իa]L6. ӈhRu춍%pG9z3W:' ھb$͇N2;O?0noq}h|>m t`͂Q{a$[Zhf;'=޺u be \,Y} 8?>g; .27רQl1r@;vӟrڭfDe} v]t &#)>9ժ߀DgyܣisoqRhޛtJjU+}QpIKhZ TܣOӹ>6E}`]:+3m 0_eAe5FRLI .2N/j-19/w9QqfcBgc;w[ [pqNo\w6MbfȐ nB.]tזTWB#>j r*5]f5:VaeϑA 9zCU"XWvo EOk AEY58gG2UVYT;MIb(tK>+3RDE S mVuW}iԨ,[O H'5l-[ -[>yT~ 8۩5LWXEdXd@d:YљY>]RVA-_3^シ^;;t|EG6>ѸkzyguO<&q}p \*[5o#S~^m,oFjCmN^<3v0.\l+QqP Ph$8~Gf\ :X"EU0 ېkɷ~A G5N1yF9@izj:BCepT0zpکS{ED{}(MtD7{^顖$ Ħڭt)]t ӧO/C`FȀE=ލswYWbgC~4D @B P0S"_MW аN6uFQa*\ۅ4ec1J}q)?2Gz8'}&/U^}3hiCP!¢.m08 :1.QOϿ4dž:嫥dxj`Ub-):"]pV,PEVDy4|^)mجRi?s:|R7hE6 cuɦ~<ĜyEvmC{ L.9zmcs<0ٳ`Y2pap YB7b%m2&׸=J BgΜ1Dg X 41eނ0-?L Q|֯mb*U(MupmݺSz"]A jqĠ|{S,[#mB+"H-yxȾ)/W,/Uqk =V8)tL$.naW*ċ,i*iCWϮS|ۿlF-[JɣqDcs`beGMgֱ9ǥ}B]ָP+ʕ+~f)P~j(K,sFVw%}p+vvr P!fx":tL|gAGb%wm$2/94ir~~ch\Z_pѸα<܂p/>(< .$uv ?g.0'8&שUZ%`}*)P >+utQe~<}{(RgxqD%_ƌBrUxQ晃bg:EUV= 0倎|0iyT]׮ɔO?|{xm#0OLև,R :Pjnޛxwf͚E obPY. ym,ߔ=mPD̙bV2. Dt5+[FM IH > dK#%+*7N.uޟu%Kfa~8?P }R`0WP-Sfigʁ_f3v7:I̙7EyruQm: *ގv:`; x&z2tBLp(СyG=֞ms^r+X8U(_Q`+raٷR\))Z6:x/'W^3ڐ p[--Ŋr,xp)RPF kEGgZJZPp~wv0y\1jI}aMx$<[}Wd`x}?}`_ Q8}Z]?;oxv{V~聖n~s21D?tp,YܜFnڼŸNqWdI0b>%v⣓s2Fw5٩|Ǝ.;I\9Uɾqw/@]}O>f)i'B (G"& 6L`W!`d $6P\  !izvgwvs;=3|B3sν|sg?sI#=>0'uڷ'O2;hZ uUק?oܕn`6mܘy'w(CD;mo7L@~Ĉ_\7wm'".d_/JƅboSb8|*P7R[t>ŔW߽~?-Owo;MK7fOq?.>wסi/T<<}I7v=3yNWqxxmWg?<v|:]|EusZ# Okpgo>t[^\&yD~$Py7վ~b=*+^9# ũOsk^>k}qbw=Y哟H:'}㍯iEݘ_nbs+^/c|aJZOV\!@l\.'?'V_q5FkoMťg;[3}8w- 9Wocx[{{ժtm1X+?M𶷜\O=EXC&.( S7qw?8Lr;~kOUϕxT1-7N[;:>[b(dzm({Wsmw3>ʿs.7 3~xGg*[:QuA ԱX={ځ%`hteGHF[ kQݭ[/Lqѩv(7󅿩Fb:-`|Ow_&mo~*e&To{/X$7#J",8/[NT+]w'.HF_o~wfy$d ESPyXގg| ?FvLE|'.lqNgxӍąp:S[ؙׄ~339}c6 WmqwmOu&{4WڕŨLaөx>Dxlƚ?%nOݓzt٥sR_,z /K֟必\5"/)b6 ץ y*CyT؞G!N3yDhnXyRbzmy9ſg]o]מλ}U('P >9U.=.{m[Ϟq&Pﱦ TݏS ~zm1?I?Pxv@& PIX=Rڱܿ'{<_ StŅźG->x/9.=~mq˚Fzyf=ÁXܑ~tUWC?Yh6E|Q)”_)-wE)y(UR L&{y-b]4X;3&81XHlhS+;=|4wwR~Sb<֋6)k5,Gs%xG*zEylP;Z3Bg7L@%zG)exMOfQ@25w5b!?_o~';u쟑#(K _ @`-Wۚ9D*CtX^z022Z;*%5S.[<1Вfq,@\ ۱=ŴڤngR_OAJ7L@%FfbkXG ,-u{M_gddW  @g@`F ZowtFz'V5Jѧ[i  @ @ @GZ%4T  @ @ @uP+ @ @ P@P @ @ @@z @ @ @@1bZC  @ @ @r @ @ @Tj %@ @ @ TʩG @ @# P)5 @ @+ P+ @ @ P@P @ @ @@z @ @ @@1bZC  @ @ @r @ @ @Tj %@ @ @ TʩG @ @# P)5 @ @+ P+ @ @ P@P @ @ @@z @ @ @@1bZC  @ @ @r @ @ @Tj %@ @ @ TʩG @ @# P)5 @ @+ P+ @ @ P@P @ @ @@z @ @ @@1bZC  @ @ @r @ @ @Tj %@ @ @ TʩG @ @# P)5 @ @+ P+ @ @ P@P @ @ @@z @ @ @@1bZC  @ @ @r @ @ @Tj %@ @ @ TʩG @ @# P)5 @ @+ P+ @ @ P@P @ @ @@z @ @ @@1bZC  @ @ @r @ @ @Tj %@ @ @ TʩG @ @# P)5 @ @+ P+ @ @ P@P @ @ @@z @ @ @@1bZC  @ @ @r @ @ @Tj %@ @ @ TʩG @ @# P)5 @ @+ P+ @ @ P@P @ @ @@z @ @ @@1bZC  @ @ @rZ) IDAT @ @ @Tj %@ @ @ TʩG @ @# P)5 @ @+ P+ @ @ P@P @ @ @@z @ @ @@1bZC  @ @ @r @ @ @Tj %@ @ @ TʩG @ @# P)5 @ @+ P+ @ @ P@P @ @ @@z @ @ @@1bZC  @ @ @r @ @ @Tj %@ @ @ TʩG @ @#08[-ˍ:ww5  @ @ @@u#T[)I @ @* P)5 @ @. Pn$ @ @ P@Ўl @ @ @@ @ @ @@B;^  @ @ @VJ @ @ @ T x&@ @ @ T[)I @ @* P)5 @ @. Pn$ @ @ P@Ўl @ @ @@ @ @ @@B;^  @ @ @VJ @ @ @ T x&@ @ @ T[)I @ @* P)5 @ @. Pn$ @ @ P@Ўl @ @ @@ @ @ @@B;^  @ @ @VJ @ @ @ T x&@ @ @ T[)I @ @* P)5 @ @. Pn$ @ @ P@Ўl @ @ @@ @ @ @@B;^  @ @ @VJ @ @ @ ql֌}Ҕ6#k[2;>J4 @ @XykWt?D4۾|,ZHW?oyjg1 4ҏ]HifDvO/M=-5Q~fzŅ.;);i?ǜzoB @ @@ee=kܿʷwL}|u3r7N_^LY:u^76+6 iFz? @ @"`2yW_+БVl3=Z3+z– ”yʰyIV.L/i Sb{wN#:_l6 @ @X F^ܐڰuݟNi ,MG[>9.Z$m]KFN>W]<x}494 ];z`ږG\y~O2an @ @ @( P]?%N7ǷХ{_+F%9 e[k .IN)埃V!+6 @ @ P)Y[G>ul]L'$i2Yqw7m݀e\I7 @ @ @n|o])I?=~De.K=^dyzIs\b ҜwwVG @ @f$ P|E)t;]ELXGhZ\qz,ckKJz֛??6kK&,qbM @ @dck[D.ϋww+Ǐv&[GeG^Lfj'gH+'Zix3e ?2 z*^y @ @,6#TQKl^ɸMό +Ɲϥ`@#]Ryb#-o~S?5z.8'1.`9QK~pz;6+ҽOty)=K @ @ @` 2zxIlZ#9%i.=ZͥhE2qs8}gݕṣKbʓ/<{F^s̉Q*iat{w @ @ *7[gI>8&ńSL5,W}@jXaC_z"?6-?5<ߞHYP%r׾?= @ @ @` 4t8M˦:qkX޷]HPS3[dh9yo{x4zl$-l-g/I9ytH:G~[(%ҕy.s{=iU^~9Kbi#퀦ipʞ~J7[Sr? @ @[kۮbKWKGOGL?si`PZbm%ɶ|t9H9{;RJKrcdv#@ @ @+ PG}aYySlCgQ)l|'K֤^f#Y|Kw\vYeZ:v4WkC9v' @ @X cWCka@PNns @ @(`Q{]  @ @ @*}q)L @ @( P)׵ @ @K@ @ @ @@{]  @ @ @*}q)L @ @( P)׵ @ @K@ @ @ @@{]  @ @ @*}q)L @ @( P)׵ @ @K@ @ @ @@{]  @ @ @*}q)L @ @( P)׵ @ @K@ @ @ @@{]  @ @ @*}q)L @ @( P)׵ @ @K@ @ @ @@{]  @ @ @*}q)L @ @( P)׵ @ @K@ @ @ @@# @ @ @FTR @ @(V@Rlk8 @ @ PU@RUJ9 @ @ @XJ] @ @ @@UJU) @ @ @b*v @ @ @U*U#@ @ @N @ @TTR @ @(V@Rlk8 @ @ PU@RUJ9 @ @ @XJ] @ @ @@UJU) @ @ @b*v @ @ @U*U#@ @ @N @ @TTR @ @(V@Rlk8 @ @ PU@RUJ9 @ @ @XJ] @ @ @@UJU) @ @ @b*v @ @ @U*U#@ @ @N @ @TTR @ @(V@Rlk8 @ @ PU@RUJ9 @ @ @XJ] @ @ @@UJU) @ @ @b*v @ @ @U*U#@ @ @N @ @TTR @ @(V@Rlk8 @ @ PU@RUJ9 @ @ @XJ] @ @ @@UJU) @ @ @b*v @ @ @U*U#@ @ @N @ @TTR @ @(V@Rlk8 @ @ PU@RUJ9 @ @ @XYbjIENDB`graphql-ruby-2.5.19/guides/subscriptions/redis_dashboard_1.png000066400000000000000000003116001514115062600244610ustar00rootroot00000000000000PNG  IHDR$ iCCPICC ProfileHWXS[RIhH #E^%BBP# ]Qq-,6"u`CM N9g3fPefAĤdP(@DE(MHk֒X:_Eq@ N89  t@\ XM @%8]5$8U6~{@t%YtGQV ''ͅ>V99 V"Cl]LfaY.R!Eroa-C-YaLM *\8"?f l0!f|؞-B{4+ǩYh ;"\gy/dD16ia 3bd<3!eń}fEFMFl0h^ - 束,yQ\ 0X]rߒ(=V3v@T3{5lG(\rbep?X@ [*2sy6trͨGtD1 |Q=^}z1&4a07bqWǽ(hԸIVYe5EkVYjsƗ?86m=O{vNN&:L]oLuzz|z'`|X٬J֠~X~AAACazvA#=FS]377~obj`̤乩iii}3YYusy. "â%jdɷbmErXZݲYXX7X0mmlm^N033fO̻XձqJ'''$NH\x9I+ԒLJOޕ<45`ꆩ}L9tghȞqlLC)=)ّZPjHAg#כy=OH_ޟQ1W_egn|U5ݘCI9*Pd ҝ5gVwenInO[ކAapMcNLهk1wgqןd~" S/2\ToqKK^d[T^vibŏ DDXrk+VlZ[z̶JK?\ȪUV׬!kr&kZZ_톙.VLغQ2eѦ5>WeTݨnܬy[[xߪlmmmo5AQq~֮]_u=wwߣgu n;mo>}-ood65כr9GGJM=-I-GC淺6cV/>>rܓO=n~tg<v¹s?qBEG/\jtñtl|˵{R^WO]vz7"ntߌyִ[=ɾn{>P~PPa?s׿Q̣{9_<=W޳Gߋ%y18Jj7oN|>5]λ4?Oφg&}bk#9##l![z`Cx]= *^RAdE) gRq3J l`/9zzaE`/E7‡7:Z"22e'${y;D|\:;G'kꞍC pHYs%%IR$iTXtXML:com.adobe.xmp 548 2054 1 9@IDATx @יe,^DAd nq[m4IM;iI6LIMIf:ͤiڦ46&N\5%*lW+e?]{+Qr9.{wʕA0 ܒkkkoɎS$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@` ?|\Ss4g'$@$xo&R ]%#      1G&              1D€14* #@asHHHHHHHHHHHHHH` 0` 6J$@$@$@$@$@$@$@$@$@$@$@$@$@$0P0Ɯ=&             C( Cͮ =1gIHHHHHHHHHHHHH `$@$@$@$@$@$@$@$@$@$@$@$@$@$@ccoc              1D€14* #@asHHHHHHHHHHHHHH` 0` 6J$@$@$@$@$@$@$@$@$@$@$@$@$@$0P0Ɯ=&             C( Cͮ =1gIHHHHHHHHHHHHH1WvH`ЦLF9oDkwG=:~~ ԯ܃`؃ =ͭW- (  r6?IK!hB<#_Cx-GQsf"_@Xb/pT)_?ssaM&?0WyIH]A/W@Ur/^x 33 Ht&            ( tiH"8l gt$b?|!q1PV7aʗD>m'180fTw8?@( *#            P&f"U ty{! C5:op<7븊ݏ(ύY"6"d 65?HHHHHHHHHHHHH`4P0iH%nhu¤%s1{1~z]to:i-B%wGGEi;U>}~&-. 8@E$@$@$@$@$@$@$@$@$@$@$@$@E€"I;$@7->tKSDT,?`L~iCg}#wR&ʶ({u6%ӟbK^*            0`iHV :P㟡 D@A,H۵3N)njAą#L눞U]Nm" @a 0L$0 J9mt4"a¬nAMÔ/Fgm= 8o`\L70qlu7ʻ$@$@$@$@$@$@$@$@$@$@$@$@$0C X& r?hsaky )J]U:T\ gNn"y.$@$@$@$@$@$@$@$@$@$@$@$@$ ,$@c8/>k8̣OM_@ %ZOg%.D_W]'8描#J`EyHHHHHHHHHHHH!@a7H`PEe(ƥOO#B'O"0&??(&-0J@{-}}>^"nøiS3 #w2IH o~~v N=ctPd^6&]fԾ')28,p8SwZ$].~}R^ +͑gXb!            [0$@׌8wjtO=uvWk&FjoY0"ǽ"iiGܟ_w`P导ZP0:'&           a Pp F$pGwS3NozQ4~d.R 1Qh?S?({iIR8̏9._0[B=2˒ #@apvH`xz1pQxDMj!4nbW.BxwR/ „Ц$IAK@mTB_}`HHHHHHHHHHHH<0!'  4 " P"&"s#_5E ɳbZDMz Ÿ6WkD0 XHHHHHHHHHHHH`yy2om2ϼ+V{rTl$-yII%B;J ܰe"(cH%g].>;Lc>B 0~nXNl E3/ݵiɈj17;ۏ;‹B`]XfbPfol-#           zFע~ .i񙋱郷QLum'݈\l_ Ʒ{rjqߦ}xk7L'D{VbЋykWp]?z!q10=?0G*z.WVǐx*d?JM \߈ic: 0䤄?qزtd̛; xٴCBߍ\]}qޓ[aU}"-:DNC&r`y*AxV<Ývn3*@E Rq`n?%s|}xx໾l0^6u[gNŏF|}9~% ]-p깟|I0(u<)Q>]fQPS#IHHHHHHHHHHHnUUً x' ݊{<)n돭-XV|Qn+`Ar]cCg<(w ^"yx;""ŒO帗J[^řQ 3ڰ&2:/]u6ǿ}"PBǭX?؈q1bh4N1P+>Ít|(}闸tX<0FJ@P0µ ꇡ7~twK?.1!hEKGK |ьlq= ⠐0he>AN-Kyn0N=; .4ZDEF{ulh\Ϫc/TՏ M N%S]]B7&6.!_6VhHHHHHHH>{MKg}xx;l;W݄staFJN.2cYŅޱQc@ʎƙsse86Y-/]Շp @̰Ѱz0L K.s6}D_iiGS$@$@$@$@$@$@$@$Ypf/橌y/ER{[ϕ+e-:!櫕x+Qu/?Q!,I,gcy6$يvʣ@`Q@Ȥh4M-mMSu(kh8"gg!s#gS_cTJlUp_჏0.9YE̢ *WaYzNu- PLj:w^ _[  H-%a]3յ(@g6VbeA*}-5 tTborע+8w;Tjt&_Sw1-WGe-iĄ0&w5]6]8 \_.֤r#![S֗ 3|^W&c8{59kP=nbGDA%P-)! ۝yd#*Bk W*^AZ9o$= Ix}Wǚ-oXplP%Djv/[ :H\8h,IhFmIq/RޓSة:p~Cشֶ2> +m }K8qӄ3: {$}C:=ٻͼO$@$@$@$@$@$@$@7# HG8~rטBX.A I+.6/m})6绥Wupt{%__21oy%RTnSPᐱ *F`+w"QPٙ`j(>>՗n؜|DZ^zv+Fm.FXb";m79 6@яlOú{ ]1!S3Ew ;G[kNľ}ʗ(݈m)$@$@$@$@$@$@$@$uYKbV4AgZN|| ^ ˔ƛL6"/͉(@)+lwa:&"ޅ^%*v=Υ(@mF /?o)}v! Phɭ!sDTaM8evBlO@k`lTԶ8KGtxsxCkCnHE̦y-9B]V>D;@@h0ο }zGދ3rYNDx0C)9yTw߱۸{Z 9'+" 8i-/yM 9ӣN~D`j[$M)(. )!%mvƅK@iT!:1Z$hDž:]ІۅA `_]c$(D twX?$tp=]ڎh/\_?{e\׎Ɏ}M~h*!*< z} 샾*}׉ .Y,=`y ]@߈Ua'u(D#\lc:Ҁ ;dS Ytm^ꬮAXgSMe2,vGɈD``'ZA Hm>aqX#vdD繿LNtAh3aBX|di-st]z(r!yEԄP& ]h&80t4WkA=vI#Ӑ29s]~=tܪGD 6*=r=]lLN@DkvK[+Pc~/g&[]^¶ffTuL_VIhF31m MJ %Ht)Mjڤd~_8 X : 5hl1M2-kE91       ŽC߳g%Zuvxk,?œx7]Wf~;_8 +̰Mm']Wh߿~&sK/Vq%%~S>8W;i*`}U 5t[[>1dn.U(sMLlz8'_Bj+m>7֜¸6 r,~klyw-I#/a(o+oa*ܼrعu.~330njνqU{έ g|{Il~301Qh: H][PV@ͯ!Eѿr[W|br +ED0Lh(@B5.Վ`.YxDm,(>;ɘױi3r{E 6D,\86ԔTnGCjs,3ݱȞmߞz I7?aԗ'=)E:3[-fρ(cݥǰP&GslR~ENssi25IEn1+WePуƒSDg}K9<:z4Ԧ<Ժ9Px ) Ȥ\̝dNXYSSEׁ\1q*̙$É^dg8EC션3}R\Љj[!23(|/!}M,Դ "9n׹e%@ dteR܎~ WAQNЈɫh%U_8,n,oj:)℟l`-CmؐљX1C]8Љ>1tJ>fDy$|K}ϔ7`||ey A&fSzY >V'iQp VD[*e\KUi+d,MR͕HQafy6'0YKd&;i[$32+"~wcžSQbc'zx5vVõ*Bӱ I.U× >IR|>~rYUCޅM/㱡{s1_$hH"tX!J"sxGı߼/}"*i+qYmߥL-_`[od;zn{QW(=߳Uv*?zg( @J pJr)yۡ RFWKvVm\]"rڡZڷМ¬>p(}RsiƱ3-woeMrM[wt'vvH{$-t;W攲߆حxkB)#W*xmxD$@$@$@$@$@$@$@&idFItqW1-\;_Zkl: =7՛om~oZah1gz.We/KhgwbuBd8k+@"y{#l1}(~ur}5Cܰ⦕sc«[6yK4z~j[3ZϿk;wuÜw"\6>%VȫȓX83!"'OLoTW"t76w`P1 0BO~bys{O_ʸ1ꗿ^L[fR~?%OO7pѱ$Ͻ& \_s1$)i,w 6)[ d 7/B~ `CՑ2_U 8Q&sq>+)Ѧz}T]u0xل*\;2&YDrkNK}fGO>KF1ώyd5}Vaz+%VMrzjgD!tmfG8 {Mi” '`ZP8 ެH=udg zN׌J v2,(U>2hR. >$:/^f&B/ٷAxӌT_<P]zue嫦9Z@dqVz, &d Um,]І|R1SPRomٚeܬľ`|ȊoE>F W/Iwx'&g8| E%ȝc.(aK_!ڕǺ F{?]qYoNʪm6d۴">R bq||m09" ݯl%zr1&!J~ڇدDv;ll% B),+~+ʄm0\HʿLIpDn lwۉ_A'%R`."TQg vMq/01$@$@$@$@$@$@$@$0270 [%{t^PwN``[񫧰@zwe/tz$mھy[C.V#5XaMMo޾-V*=wSpq㹥-5*nJ\IQ"rѬݹAwU]rzbZ* FDxi+ڟObCy["zY =^^ںyFcb,qOGO3ID6 ӟ鳮?^  mh"0{smRM8n[nhE]SduUMA'F-u-{e/LEʸr|3f!WmRi%?-z[W^!p F2V'F:*!_wZw3GJrF\ i.8f^ 6u4BCjg $PL0؂#DA"Pi^UaA6nۮˆ#ɲ|_T{'E=u(JGΤ 0\x6IQԀ)c쌘"+E`;@ȹS-NsDƥ@ASCtދc9.IH0QaQՆABJʷFTWa@r_j|@V'$OEjg(L] ;(ێЌJˊ`./Fr8.f\:Y򬙊g`W/!.\#ao'FG[0D`kQ(Gqee- \@al:PmA<> D8UnGSS)tHrڠuL̟Z57&`阫c        P ?] w g ͵9kyW ;9HCT&AN@raH._ҝq}ɝt0ܛnZ-+g.uqᙵ&b70+ meW od<٫zһ94RB4J[€L;!&EyqJ)Hbe+dxhhl KaWOniHi3qaׅRH99ScuVrY>؎vy[1 qupXLl0ՙyا.4_6C)x fLx3{j_."-hZ;}= 0OE8$XsոC,_'a ZɎA)Eb@ `JmY fK]BGQ\ ܀€=xt1%C[{7@YCU P9M*`Δy9阨wWAS wӷӗVody ѢR"Hyl?-WAI,|2;}h͟Ua A8>}l4@o8kF@ɏ^ÕU eA@ޝ61 廍[ t56c`0n!yxg4{C[^VҕxȂ۶uЄXY3 UJv7YVC6ZA&;$[qnm8õV9 Cص~/oe{kGQ/t=hmspto8P. =I))H4!s2_H .zc(0iREQp;_f~ u:瀭4/z ⫫v=C1Zb0SVu\Џ<[fHt!F~{w @%:QWpnlByvb\3 Zd S'keA H4/禕rkDoZ*~1̓'i1ȇX_3c'͙#^eC ;eg:&2aWFOF:xN$@$@$@$@$@$@$@%p]0+PIdtxufH鵍XNJ :[]AG}oȭ2?hgŀ|.T]!*BY1*>m"]v['l@^zmwo#?q+Ӧt8^g%rzD/GwS emLҝ0@ 8f""/#bf N\)D=+1͟PڛצOcŪթm5pY|uEnJ/ɪ2nƝPwgqd~,$F#|pbMH|H-hu˳6a@GKNtSDH Cjj.SFbE0XU^'d`<..ch.y|3@DT+iv{S%Jɬ"ԣ;2G\ /Fۇ8w{a$ KBV8(l51IB}Z㊯z5%zQWd|>#ͫИX,ܵ}]&7F2n Js]i K 9*gy=m=^A$;OVڶ^BG{+P߀ع7yQ୽a&woz*Ar 6fΑ۵(koSh2 DBw/_q4^zZ$eb6$vۋQcƳ#ngp[V+aaXb7Bigv"eۀiƠح!ݟWʪ39gQwٚ6{W-&_Q9;.$]CPXsZaO!K&g-(ܷ DqinqKKդl0I%TV|re(bSN oX:n# X| ~eW"DlT̄r㝭KNE! IJ{2e.46ԣy\hhD{Kڇ]Knn7J86ȽKƁ2f} &$tw-{Ksk=Ze       DJeIKc39 #z^Fd_ãKPǤ- KCX4yHOP9}7n@R^k~0 :L4(4ji*Z;o*I%"4_B3Wu(-h f!DTL'8 XVk<)_;~^VOEH1 ۟0*^^B㧧Yj*.\St/OƂbe1C"C5A|j'+#fO!7*< T62:aǢ"fED@d\*҆ӂ['W,09.c&DZԓhiq3X2xdD^nZ[7"sehlkERHP$s7 oP EOyLkZP_l:oy_ r<Ύb0s̲|ѬR Cۿ-oqVebú=؊&O.YF! 12f wT}jm .Γv̇kHHHHHHHHB*{Tg%/ϸᾹ 9sW`gd$!k?y}ZꓫHm<󡹫rT}8#H=gx}3`DQ2];vlܯ,E2B7MVu ( L%u93W_@7A ;_wP?\-zZ};q W[34vQD[HIAK\4*C {{qtF*,[lz` )?> >g!VZe2r1/TN+Sg T e7[kDf UCu䏬빾*^6"[xj^IeOS5BAdtrކX̘j 9 -lYYΫ\ I+e.CvEa[@Lzn}޹`o,+sk ѯ:z{P(:ovƩ6B'}(kqkPc.0MGaMoh +5N+Ay*mMgPc0!:6BJ$^B'&Nm.>v;'Ȏ) t̯AhHHHHHHH`>GUlYq[ƭZ]|^J~HKruzPQmJ!gq\-JYTn,=qmZ}-#9+ l#Qix]C8b٫C߲R]DCXf]O]%l}tubM(*ej᣺PH</00@V>$<ʔ[ R~݅RK?x2oH֯b_eŎJ؁J ( vm%{D\0\8qm6,GYٚ;ZD{Me5g`U@ZUꂯ5M2`aRT:- qFl|tB+$@$@$@$@$@$@$@$xwĬv>6`54yTzB0~~ӱ鵗"˞1T/7YNVfkM+prņ77G14Ox<>Ϭ@zo:w/VlkUɫ6a!9'_3GWpg"rX&̏%0݌NoJH2mMoz۶nˇڏLM#GMMB98[e/NDliMD8]rjL~M|ouoX8Z#8.H_&th=zŝPEѧ)6ŁSqzB' %% ѝ.3I @V? UgQY{i2abQq' tҕ>wE7 y/SmP C +N;jm֟?w,B\j*NCDDBي u5(S]*QR~uΛ&{'v] nHiGM)/FX$}%f#cJ 4hP3j6lSr7:'Cw?YȚ0q| n&%\dE&5QajkF3(=jB 5Sfc9Ѐhǡ|?~ۘ\x4%&zWTIM9N)T  N-lHcJf&"ssQFA@$~jdFfSD,*N|ZuD`\˜ 4@$@$@$@$@$@$@$@fO)mǖwmEb{o9q.aNJUSc 8sک݅Xbn[@ֽ& ʱ]Xi#[rZ)Vl{j󐾬x`m] ā=}/UqrV٢"}FY5^{yk/C^o~[^MD:E,#ssNYdB"%{R 8+7q7&7`G|lnFw6<U΍"` ^)+Eׅ5Z8?%mHNHK-*@!@Vv+nZU"P`܆C`Bě5Ac:S2؁?BUWTNI)(4p :sph'|SsgvkN@e--lcTu0b\h/ Isa۳bT]I {x0M 0 m_;+ 1pފ(~u ZkM~?V!]R۹8Vg*|x< Laotփ?B%OPC±0$r#g<klj}I^ N[py8repbaɴ5ɋBVwTb_-뤼*2GN엇ȿk5^Y1Go!<'qX~!_f4ĚiCnY^ni.e;ĈÅyg{z? S,| @,~hو6FF#/nux9BAO+J"k0NJNu8ܱ2 \GapױaQtFyd ]\j8BΖPO[+bO.j8`C hh$mRY >w6]o6RBJ0/Y3ⵇ7? <7Vkpߓx:ȧJ+\:S~% 0\f<`6Bz uS!nk ӰauM [a̻fw㼋:GymܐFSy ?ѫ% 8Dž9HX ?8~rl%8@޲Lwݍ͌ۂc{d {{T-+u^J?]% GV; >؋sE8^RaB72#C" ҀӅе9w!zO+[]H3٪h:t(:s 0=>Ao(]V~K/,Cw>K+2 %ĩcnquUtv*D&ېXZiBƬH߃}WoЙs{RsioAm8^ M-(.pJ^s lAƮ ̝'3 RzR/"+)Y8sjJAD371އwc\1||RV |/$0i0DqM@PxxD&J%ZR >KЮFGkܭג_[ĭ(KRI  )#N>=ܓd$ܯluz\6L3JFlT&`Ў&W uwfpi˾:A /Q^fpG]5EڙbzۻmF0s?l;;um;<6L8_ߐ6=A]͘x.ҴvZEգf}?*Ahu:mź1#S{uah>GgNeW^,۬mC9qF&0"͌$ᚇf    @zN(X=wڹpwFtL7gUoG}4/nGW/ץel}Ӆ7]2  `?$ nVOF -t#OĊ##ʌьg<<}:E)C[d=&us?|Vʎtߙ_W|E?aN!g>B?! eHzq.0#ꬫ5,4|u֞ MH#\u?ӄ|UlЗu+S%߽;[6te8GiCa:;MgǣuJ f̜ 6R#>҇!uX-o0yzu)@SdZjZsआi%ci:#!9b]o~Cg)߷\}9X#|%E::wc꡴#}ɻ;jڵAE:43]BƸ1+M^%;!O1 61kV Nesi%sOXuU_E'>_7 TGu"mt\=t]7JV1Y/6|L(cl_PleҚi0fv`J2y\ك   #Vܣ\+O02@G|A?+G$t]hs7#/ZgzS~,`nLwjޗß.PIQʴcgSD@,m3o8AtR2ם7׫pM3ä(v{f꜡wT fd ^ .eSv5_v sĒkOkݯf+,'giœ^oFHO/FLp948zؗOGp5WMg5eP_/?M׿'^wg`A0뮧E>5(%1ۮ'C 'y;brҫ5@?IY>r2d,9SIFNVi=,K7ZOu^ƭ:W_@ :Gd˾sZacGkzod H8q|x$rxP335ә믯cKSNMfe}Xo]/G[ Yo1zasEBS%"9xZ52Cg[WXHLFb I   1 Ȉ]k8~ށ޺%_.f䁵׷q7;Dc֏~y=wg.qȾ@3~w`)N0sf9 &mz')e\Mjv%Z$Ysv$Jv hX@@@@RX8j՞mqNU;ڕ\4Cce[?_!& }8% tƥ_ШsL^'t;;2_[ V)VZIʘ]Q} dG_ :Sb5/A %lK*j\9@@@@ 7j B08k,X:TyEY_Ǵ`Çɑ1N#?wƘQ\/~^Ýc}C}uf۲MwoF"&|ZVpfZn[2AN~y1pR^7Քcǎş  k%'6|8t (@@@UX8N-!C5u%_'?^3b@e.}h 7preyY:iF8k~A=HYѻhjw4o/]?':['9BOeqH-|@4|8c."   I 0 3G@`i\T pjs/~a?Č&`6a'ח.Rq/Ti#|kφM:~`& k)/ ^.ͻ+t䃿GBS      @? /QM:ӄ8~)B@3IOwH>B;T!bɗ5aNF?_ծ_M?"CZ~vmRC2NKY}<5@@@@@@ϖry$ A֐8 ? j ѐa7mΞuc@Ӯ&@hؑ| vYXa*[}a !     )r!54v=k[0G?XcyCc.'~wKg{J_X1|63D@@@@@@A)@`lT:|F9?tсd:+˔\6,Y(}y>w:qp)O_9yℯ]o-g2.U?ݬ!C*}i=sJs ׉#޾"     )t1 gܛjN_`I5s53uSo'[4& ؁Ca}[dn8}^󴆍yJ=JC Ӥo_z￞Mf      腡Z |KyaykXqӦj\>2kę'3M0lhS9n"0߯"     #@`S* yg>tN9yta_mFN g J5}@G;6dxF @@@@@@φL+@}u\W\ac;{kQ,ʋUȚ2oMc/<713Wrk      @ H"&Y!@j lÚ_>Pkj=A'GM\@$J;d҆Q s3R97ޟ$#@@@@@@Q]H\6ύx1 xv7@@@@@@@z'0wI       ,@`@*_       @/ % @@@@@@@HeRP7@@@@@@@z)@`@/I       @* W!       Kz Hr@@@@@@@RYT: @@@@@@@^ K@#       աn       R^@@@@@@@T 0 uC@@@@@@@       |u        0$G@@@@@@@ OS @@@@@@@`ĀH       @  "       ۑ@@@@@@@ 0 /D@@@@@@@ q#%       )/@`@_"*       @$nGJ@@@@@@@R^DT@@@@@@@ H܎       )         )@@@@@@@HyRQA@@@@@@@ 0 q;R"       %       $.@`@vD@@@@@@@  HKD@@@@@@@H\H       @  "       ۑ@@@@@@@ 0 /D@@@@@@@ q#%       )/@`@_"*       @$nGJ@@@@@@@R^DT@@@@@@@ H܎       )         )@@@@@@@HyRQA@@@@@@@ 0 q;R"       %       $.@`@vD@@@@@@@  HKD@@@@@@@H\H       @  "       ۑ@@@@@@@ 0 /D@@@@@@@ q#%       )/@`@_"*       @$nGJ@@@@@@@R^ =kH@@@@@@@ϠwF_~M'x?CG8q%\Pc XOt F@@@@@@&`R VEVXp# Z#      ),``0qu;4O֞M;C} ZoiO,2bS(       YϿpM&"3 #VpW5i@#\d@ `2ӑ6z{`y/o&='͝o lmBς7#{Q#     }!.:o݂Uy }VF$0>8hՒvZHv搝l.V"Umm7EʛC~jkJKs|n=ՙR*m_Wwߢ 6h FxiDn.4u[,rܣ5wj5Ev#̪u^jP~+ jSfCef Kw㾎v=B_GϛzzʦgēeZS2d%%ZrfoUۧ]rBfc_њGDyEza^\}> v!     xܻ9nO,טH)vj`Xk_^(uQa@@@@@6}r9pVCO;CCϻD#T5Ss<"wuNv0~f]v'r`} _J"{fhs1JV5|L1kJC?%`;WGh-k*bE2b\ (]BV[haTt]+ )ccJVݞ0BvyTuM<|_ݻTzm*_rnqH6(rK,;8QanAV 1}uk^ؠ-:/-&("LMIxG{a1`/mOM PdUkOб^uk n~[]J0lP@®5w͎IdO0pϧ/ 5|&hUZ \P@O:-"[kʹ=-y .2;mkf[t9`·智T{oGF     @n}:t{: jegl귳]'Tʧ@ 4ߙ%'0$7T vEY%NYY*L*_kdUO:h%|ނr=t|͜l ؼ2 D4ob}WeQZ֕uKEmTq*ͻ*܊PAZ =5pܺ?B0G0uϳh _G٪̌qQ%[CbM 0[FhV[BJo,2CBD-֩b .|%O;ιq m{t"kebl#5tIٱiF(^<_X9_ȱ ^)ߺ&[PʲUyC44_5/.5AZ~O?~)'xUN*0Cض Ζue*۪BfJ9!E~Uq|ԫTf)=0:0ڃp&xe`0Tb=sֳ`5w"-}ʭZ\L]<ѧ`!     0#csǝr9Wɟ.e'I`yo/}m:p׭P^;_cvvh!Vڪ{)t. 54ss%W!FRۡә[ X{? vNSvh[9"7QE oаYK>/ja>JL}5Mb',U%QF10#Y,y/UbiIV'kY{_ˌm4:=1R@H( yuY PBߴ`; - yr Jܨ-=qEU~ \УO̯c)} }&g@U뺿}Եl#     00:-״.KJ_l%, W5~ăvfm4Ë7ljI~>uL.RF-uܰ\5S'53H tF ءvpY?t`hwLSf#̼|cWaꈺWf>+ =ڻݖourdtۋjQSO+QoTxwZjLP@U3D)nq$prGq0a5׬h{ֽnJU[S,sc4gzciтN]~K5P<L rw&؎wM&QhIl!     0PDC]\ikUpUv{5m zm zi{Ҩ-&/nԻqs@{{m핎-ϡc}ѐqcp<[pm 5Ghn5\ u5͜ /H')5eb_>j\!_%7-crܿQ.zQYGۃGVYQy6j6G"OHrwW/^w]6fܤEl/fA˂8:u;R- $CtZlT28C1z!l^} EZ4j+KJԎߍ#qS}`-;66;!dcze}/      ̿tx۶ֻP{isk~ =x77WPԹj1j屻 qj0~6 ]7=չjxkEzuQa`GU*1]ʊ*\S LvEUlV$Pxy\W-Ԛ|RbH%qQef;X[w94־8l-QUo^͉5)$f޾N/7f:}Br&D RWQ ,nj06l|K[ k%W;Oώy@mAuڶ+c7pm%++Ӷjf#Ymb%peٞmݟnӢXG '}45c(ǟ66g)+ک%f3Gi?} @@@@@H}(UW.{a:>ߕ&7+{z($#Lav 0{ӮAд:|KQڏWȑKT$ TvC (ז=;;ǻ _Lʳunf#+Qɥ5ᆰ$NPiGҝʙlWL؉}23W1:{`T0 >IEޭjv~o1|02VCKĞـ9*6znwŀ@RhG"UZA͍Zri<<4{Dy3)]gT+s $(R_5kKDݪt Кuujj5_& o[S\i"IWSBb24miKƱN5>[urTt[Q e!kJ5mZ:A2dzSLRًy-|YՕ[d˵d ;A0p98S݆^Nh 7tNm(zyN%I]9 0(B@@@@8uޯu uh#{чU+uCW|KzOtޮta׍+]q/`!g?qB]cwtœ=2'?o8>\8uj14lm<<( f4N-^Ӣ*SpNRVR:\mļPΕv>8WOZ s'a晴65^OTkm{6 y_ EoT.g ( Owj_PɩGlO#0;ym7d"~Jԩ߱᭥AmE=,s|vjM'whh.Qw~QNJn)kϨzqmڅ&nA䘘bS!Q;5NR rBdjy^Is&׊.ppR5HwkD{>ߨޒstAcz8؋Qr@@@@@8C3׭7˲t/31} w|Wo9+Ү`G9ojWn;umf&\ ZdV)O冦SuS^BSQ s;tM\r~ejm#t|tG_6mqʈ+oڛKsiZMpKsZ6q(=n=QfDW YYd4uM;N5&1 R p!c5wڟ*=%H yșhUz6'Xmk'AGd!     0Ӕ~d~:!nGv6Aܣ/Cf$(}//FZWA|r3t=^EZ+奝ʹ5^f54+VẮ2 wPi+m9;jz%!7sRrT]FMf#/}&dZQj88ewLOy*wf-~]^?_qi[<k\p5s^_v!Zb/Erv|ԟTpl|6ctR#JC[lSbx>7nGyBvPN;2BnFJ:AHV^Y0Qe;k*c(l      0k7XgՎqi\ 2ikowitsf.9o냭o;݊ Qm$ySSLvz4AE?b\7Aʜ}QrϏ=2 .m~< ,x[1~WK2@@@@@M1US~voz} W\'c7)ctn%nU';ԮVkoEc0jK4RzK:% 7*g{Ne:V_'Etԕ l/.VN GVjtƺxPyggyiUb]p|k ?oí*kǣW'*wzrNTU9x{# , |ּMOUd29u@Z[k_<9w'^oK5k̘KJVވԩ`GvѲey>"?h2%Ǩ>u+JlAEZtepf&鑗b/BEsy@B"3m{{}[gb~z>1VӔR|Eh@@@@@H!sܪ^my}8G7Cٲ:}Is Vg>ڨy?.iR5C ~ḻq~?xN{iBWiS@i]F ~{ЩB`@2@O. ɩٚZ5"uְlC{ׂ}gzLGʛUho Avȭ +*ײqN+7ӽ]-}xWr-C̴mW**-o^1Q&oOUU:T뒐/;LҒҤҫ;yǭ5P%؁hϢ7$ץM Ge9U8Q Wʴh_)˕RM{QO7D/TacƊϞ6.[aG>:،4b;\g!Lgļco]cKdե8dۄ8Kil"     7iWm5kz\niJɭw2 m}MnQy'P[/P3"z>~Յ+:ha٭ڕj(\Lޘv2,m6c7j^Pkm:f;eЭh]~W!T ؝YTX 4_Ӣ iVa*#ᜩ^#4*U]Ds&TE8-ЮgX Y kڨv;sS 3T+aEKTu:*{M>f.qZMKOSê5VYwjugK=P,8ѶSw{'zgktVB*-0soݦqn_<3ZRbx]TU@ Ng-.V㞚&̨5/WkJrb9G =wV-]z-l֤UKt`dTfj|3Z@{`*`Oz}fU3CF > m>iSSy´yɓa[LJ@@@@@K`.2mYױx-W)Kzmkp+g⩁YһfjǵvYYzk$#PJ9:z* :XY%;Z ݄Ee`,Jߗ7\f;% UW TJʍ;\Wi (VUtwIy{U9L Й*͌`l=|UTmJSu#tK'V)Ayg:4o8V|y}El@@@@@3ޏC>}cku҇^kFmnګgmҳvFk5Žz#=t.~~H齭Jiwy"tw~[0w9pOJj].Wpeacr7ռVϏOnT.uUy{agt-\LX̴͝.vn:2lnfǨ-ݴK87|yZF[vG 3˴cG.+ "<~Q6#6uF/dr(}ܥʞc!∎*qcsr\cBw5)Y|eZ͗MXYBio!M3<C'^0 }HgB=~_1h|U}WPU#'Ke@e6j5)(1l &z]Z[[^֒ܣ!^E*[V;=_ @@@@@gTT!}Vڛrejti˴9w>}r=9@@@@@3)s=g֔o_abȻMnEЍ       d\qo߿B偭'"`];Z&Oo qݟ6#      $O@ӻzk:yěLi.zZc H1`@)@@@@@@,`u [)@AEv5Ƞ@@@@@@@# G@@@@@@@EX8@@@@@@@A*@` pT@@@@@@@X Es@@@@@@@ G@@@@@@@EX8@@@@@@@A*@` pT@@@@@@@X Es@@@@@@@ G@@@@@@@EX8@@@@@@@A*@` pT@@@@@@@X Es@@@@@@@ G@@@@@@@EX8@@@@@@@A*@` pT@@@@@@@Xc97۽INZ@@@@@@@N .`@N    ٻ;O8`4˿egɗJ*ЪaQ&6ZAZLCl +5ָ~!h7PDp5F sgasw=s    L)       tN       $F8S        "@`@S(        0 1Δ       @-       @b H3        -t ;"       L)       tN       $F8S        "@`@S(        0 1Δ       @-       @b H3        -t ;"       L)       tN       $F_b@@@@@ ̩˕3$5P̡gj)]O[W\c:\SKS9e@@ =r*Py)) !u@@@@lU|8E{>{;#\/uP@X^e_WEϚi x>& @@#G (]YԲ%W^qvos%uhPz*G-yvtV>?T}pd(cШ0}ofK Q3L*ϊN5'g9 @@@@'P& 0 E>4jv◧IVG$20C@"uyl <t(IG3=#% #/Fk2t6_d1h$9ij_|+.}Ixnv[0rfW\z7nSm!Q|jM(Dxft9Z62#R(%CNgہ'ku!4^0u7ϢgJgi: 1z;lD@:(,SciWR 7rxG^*Ǭ/їh٭R׾VGah?z>cy9\қjعhT pO'eɻ;hjտh.M.Rٽro    =M~s4*n[5ͻ]ŪR),RcJߺ*&<2c9L3m{@wT꤫aSbTvGUdywg;ʱZzG>X53<,!LC{w|XvD`%ﶥZ07ygY"eeFUNGZu<-o2T^A%Oe+h[EyiM\5Y\_zeoO˱Rw&9sP ܳ<iKUԡ{8"^" r8xOBYoDbIU(Ozpq*{FU4o^Ҽa3)G@J5!YR}u4i]&oQɍsTl &h~_ߛlUF5/T2R#[o@@@@WF2s(Yز([},9SY)٘IY4'saz(Z#^F|kіd1n Ur=9[B_>f?.֚;s[uȥ⋋BTF,~&iF@XxMgyQ%,Uу<#(J<.l7ޥRE;Z^J "vf .-*-עDpo_yu^ ~DOrFy꿆%7Fԧ7M'C;:g!U0ix^>m}+ӒWdcK`P*%Oγ&Zy-dC-jv^Yragۤk̗9k_ ~UC}Ê!\VDZKقYSulq`P@%ZxERCmjRke1.K(r`A@@@@$i+ό2 YMϖÝ3&⿭vG n5 =qI9NHWd֖*oQΗ3#YJ%Q*gLdF%&equJw$bM. X# 7u-[!A ?|^z(gwUXP@uՌ5YNzsM/FJ( JOJujΝkZC8fwNY>fkh}^ rjɪEaAYcU",zӾvz~! 'GP>iUYL$?.P|i{b^rϿ(CuYf˵y=Xyz@j^.UbCgvQ2x/iͣ/    =Q~LJv29g2Ң?c9fiIeUr-U*}ۃWFYԮoRsJUo yJiԇ)'6t|UZUf.jf/3<3Rժ!ޟz s.ԿCT|SNICrTvymkSUhʹ /:}/u+t*ϚLWo+Ocb޶Z_SUi:]}Ry t^st͏kɛ"Q[ 6Go"m_qWV2Fؖs3V/i/yڈ~6_ZNx    @ x}0{doְb7TcSZtm؟OAE*{Z m|)F.P4rzчu225Б>29̳V+!Uy+U]3Rj!k9ʎa*$QV7P7~oq)Lpv+&;1;Oi#Yjދ;ɡԳR# Ȏ  lLĦj3_Tg'``r77~KޫVK*{Tl;ӯo?,7$GZnkIM`%s3Tp_蹳@@@@ Oվzܪ޵ɮy2Vy7hە*r b~ό":rNy?wr0S ˪fXPe*Uocb_ɦ󜶱SiI͌D6UAޞ7Ԍ3w&fѿnFHi|6Ԩ>c֖6I:V*PETZcFvF@NX V-|ݫT9mYӣZSF+W_;ۿٻ?HwHC=MM&R͌ly(ןޟ@@@@Wüt_VMZb˸х57.pF;>)y5^[TRoR򮜮ǩ(75Уլ{˥ö yֿZ7ޥO+~|p\2)Y*UçZ[̏8'izv&ei쐘F͖  @gz_`9Sǘ|-rzhE31SZn~)'s!T@=Tx(^@OE&)@@@@+-3C0 $%+uU*'ec5ۚ}}ݪ]?:0uM#EcxvxV܏%_0RĞѐH tK ?*eqV4`+8}m>W!~}W{e&jTpSk5cR֚֬W( ~|KW,Tq_uNG@++,˔B4N.SVȗVKsGs5vo.chE7 HMLRpK y;/c!    pBTh0]wʾF%5K ynO%9Tt]ޑ=ՙQ\;$^#~iY6SѶMT3QlG<`CM6SF' +&)5tR3?YTt[_Fpg=?E  *{YNW26/֖h!g蹅{ToQv^4nPVͪ#!$~'f9a<7):+b@@@@+zQ. ֿR]5E፶uU~M7qf!lf\;V5,wPSnijYS8ҭ瑦;AyQJC&ߔ؜˦ r2B%bݑ9Vsd5IhM`Uk[p;#soje_Ys(w袜[T32?HZ[f:@D$94vfVHsR3 $JP @}Iʝ۫:;J6IFO޹F)P+rm2_ -4sU=ZK>@@@@zGozrWs?fL}t {zt@oҼMϯ9?| yVi[Sj( B6Ϭig~RK\s`ø&oPU|9NޗO:XʱZUk9뤼Z]9:LX|EٿV*G^*>g?e V+WJx]jjʉ   @W .hGcegTr2id]L%b;/3]+Ua{e\]wYE@@@@.v(CwiZW5IY?Gr5x?PV޾Ll6 hMj-cf,K2}_Vk{jogKXiVo\-W}EZVmdןm6U7CjεQ_>v- 'd =XUju?]_Mg+u9C7Cy!-עjb@/K{}TfjKCyմ DcyB~ҿ c@@@@ kԇk}gG?yҺ`^xsD63@ќY*~j65y[nP?bDNepVvRM/Uzy6cֳ mCFTR0ˌ Zz c247i[gv6ESCFWxPEVhGgVܟ<u9>_5 sGI+O%ژsnO _i+Ux]]6&9YjNSKh=MЙEQ\ޥYVjnL>qj-,-b$gZvb1#0u.R[Tֵ9Q.o, fD og;ђ͔/5Czw#imE˥e͗o}[O/ern_Ѳ V9*Yrںܢ˕?&8XlA@@@@o+c)wrxo|&-RW7:˜wV*)DOʯ~vmcߛm*X](_os5j诜rKVCGm22L 0iNsŚ˵ZQ`,U>Yy̏)o傸 ڑic-mHujTzEkѓ˔MgZ9saCK,QVJ뽁9X Tu%]ZBž*-'%/@@sBGRRV9&i>\d(CI}[WnZDY\ZevTY%S&9J]Bs#OM7UsJh"~C/V{Eb̐    t3"-HYg~:1O\9OںB\D-ׂkfmz/]Yun6]%g2[M|ë $cå*{\ ndW2#D5ixֽJnU3,"y͟Ycɣ\QS'y󯗵E <y^#=*3]/yd֬\;{|-5TjYc]lݻTo/Qvd}|VFJ5R9{sV(1u|/(L  pBO(6H==&=I& eu8_XJ.6M|ڠ^Uv$ @@@@HUm#ΠykSigm2ϟ'S4Kz   ;Q] nyN>hTwܚ_&3U?WS@@zS ‹֞*7Ձ VzsvEs~DCr4Z|Q;+nL՛8@@@,`F ?V/BO|Zdoz?,&  #= ?]dnhÕ6b2S9Qޡ}\' Nӱk8J?=~Fipބ?&&/3\[vmZ[kع cG jYV>g:_Cn 9ƺQinxZn1󶧝аUp}9&1/m74o,g==wUo 8'S_s֐M=cd#~8lG3m߬{ε[zPc0jD/vbڡ5tS4wFD9Jg@ cc_|.y5*)s3kMI+}KqMqy{駁Z[\ޱIS_i2$1   pJ 4|N"o[ǿ[ͨyg6p!^!  _ۓAh_UX {ͅEf9S&nt~B; CIjg(_B:ݡW{|ZSΏhvUfFizk:sSih{9PeYq4fLzF\#lE{:Rf-$"6?~-̿bB.G4=1z]]Vمk Q՟{F?#g9Y:Mjn5'ڿ"$ơ/ѐ?\΍^5m}t+m\oxnS5bCXsuhvDNyreF{ ~)UD9{Y0UP@܈F2>2?9@hn+>)/}D2nеo8Nj5Z?g:;F}~\y1q|hڵnC_>[lQ*<" 勵j qD엦oyG;Ãr'47Y55Ƞ묭hߔ\(kZD^O ~fksO}MDPkbֻwh/,a#<  {8:4(KҊ_O:wrqA˪.WƸW {l}YFVÊݓ>u답%2(B6Ni+ltW45eYA%&pDF;yqe ؘ2qP5+   DpŪzw^ۭ?suV@@@ LG~^ ~ʎhrUܢo>qܰCEh4oܯUpwXo齯\u[~_uсQ[Mow{+@vﰓoӫ|̍ju3T=H-7\A޹#yFgFOӰ~ЌN;^W-iOJ ש{愬^nh{~kxxiPu]ȼ~Lo?g 2bAOV.P;y6`|pa`oqG?0u qnBuiN_4 |#%m&ULcCl m/kj;};h[G,78X-޿u:b)M4;ʃuM?u|Jڽ6rZf̏1N=`FưF8WowAi^b$ij5}a[n+r}8jVC-#mC:ƙ kѾ&?m36ԽG2W}J`*;/k\k$K?ȿ0j P\Zv- x30>-ZmVڒu:h="W-mLG~Y8;3"  R}|tLL#`-sŜӗ@@@ @1/^Б55m۲C{_Q X'>ZS|UӮ0cz?d sUiʌ`Pus/Է_ؠyEZZiKѥ&?e`P{Aoi@Y}c`;}iD6sm~|T 1st?%x;s q~Lo 51eH3{(PNsɫݗf!M2 VVN:`3M÷6[S+* {d}b)J ciA֎35L0Kt}W+=Wi?32Mɟ9szOдetB2=v`e6U{Wa]qes5A4 AVϭ@;ɑyc5,' <¼U)%׮_i[eKNݣO)P#۸-\?%V_~=ij5tW~/^cOh?}K `nWݬQ!QY;~f#Wu|vxG_i&yhwQg+gېŭ;BK9ذSu0XBvź'z?_htEy   p \>뮼Pm/pJq  gOՃNg; Z$]Xlg?Y3ޒ/6Zo~'p\0IX&^/}R_fkF-_Hq?zN!cazW U]Gkآ[ۿ,9N?􊽡|F\qs? |pyO{G線sT$MZz8 G]{-ejKF״ow{@rɌpbTE!BCb/z·{| 9p{ ʵ+}Z2M=2HrL]2$Xrcpǽ&@;z5dh3͘ӯo!I   -мI yL+= t޹/t8tIZOsf@yI:Q8 Cx=Ÿk\}s/c]frNm|7ovS({n3̿o|q4yWFk=aJig; 4._w]r_ɣէc*hFJi Mp&Nh; X̠$;gG h O gl \|Mx݈Mn,-Ϯ&HabK _j1S[gbo韖dl}.Tg;ƒK2ٺCD3r1tD@@@@ @g3 3@@h@:q7^1tSю{1<ȽT1[] 7_ZҎcGw|mШ6L}Gw|fc >`t ~pa|Q sSu>F ks`?4h:sR2_(ۊk`J.#ځP "tYϜf#aS25n?.s,S5bD9C|ec77ב/Ը?k1R&ߢhT{C@@@F  X6]97)"  Ѕm6uaY]{|NHKژ\Qܡ#;ףrTxLނzF6\Cr9:iou=Kn4ΙWkqgq05,PXL#{hPD VN==V:`Cp'O&(|Z䮲S.0zh('vGϽ&i |1#e;\fk[ڳ Z"׿#S< όW   @]:@@:^p@xg@Wu4ҟYu㯴!8Cw2];Z}MbSlғ?Nh:O#!# &b[U^ތRWZsNх3$rP   @7   pD43s';= ]1Avܼ?z(v߮aI{߿̻]C 7}m؇t˞3O3v,D*z [Fn֠v_f{ F~ $/{H+☒FD:6I{'skA̟j>1Se)XZׁ-   IUPGЛ uE@i@+KwUp.&S\ځ%QǟIȡ)+B1tӫL9rTMdBǠx{ 5 +@$N7z=2{6[35ى=e*2i+1.l-U7vv.8jG˗u11,Õގu E5y$C@@ D Q_3@@>ζv_%W6O?Lz{n&4_F_C{ 7Ym{}Od5dxVxd^J7lQîm::Qmn`P`{۾Z[z=o/x:l>OF1vwzsemo~}Nձ=>e6Z#hPrh@U&   "*(Gp\~@@ .0U[v/Gis^ڨ#!YY<ژxz-y!4hz[Mo5Gwk k:;I^b'ݻ n%+hrAѸwosm ke^7`>zBD+vse66 O3o/Js)ݔś+6Zy;ZA][3rC@@A=ezU  =UGy5ռhퟆu8p6~?}N L̕coבW1ZZvk?$GF 3s Lc5y6>|CЖf4Ҩ120x~|+zP=':X,}UBQywHiZTﮎ`ޢ#v}O-Ggo;rk6y e1MbÝC'@,wz+Ic~t}֍;  س m ᐝr/?ab~sٟBVwm~vm4_m@Ї2TXF9϶퐲_{_V(}B{eXx3~tu\xaOѫV@@@@dP"3}W  @聁SY]cWmF5L] >joεz#}n]vi.3sƣ/eQ__sy.z!7֏,^]PM]ڳ5Vo(]7|M۷n x>Ш}&튷x7|C֚@r=s*vj^`]ulJ}R[OA}[Ogk8ڡ/>Wgisox?E [@@@z_P淃jӺyu V0@@(#x;œ@iwkĊ/yiSm_ j[XM_qՙ<^O<.].l MczNNoke^ 4GI׮M!CG+S^)|GiHԷJa/iX}pOoK{Ҕ[#GNO`m&܂c"!2>i3u[wkG|i˴ ts-=Ys?x\oHQU:'hIMH l8s,N)@X墿~+m4`#uκq?Ir:[UV^[?zK159ڿcnW,ܗ Sk@hf$i/TkSE1[\c1Hn/a_n4u\cVO'ʽorm|LU*q+YZYwƨӣ#i.wnL+Wy74녛5,Z6QKf#   $5?e^IqBfzt{3 Ƀ{=Xϛ-ss:H^<0yu8Nџ'om6ϊgE `     $@ (u_֧@@@@@@@z@w1gP@@@@@@@b @@@@@@@z:QK@@@@@@@:%@`@8@@@@@@@!@`@N@@@@@@@N )6B@@@@@@@w;D@@@@@@@St@@@@@@@D-@@@@@@@b @@@@@@@z:QK@@@@@@@:%@`@8@@@@@@@!@`@N@@@@@@@N )6B@@@@@@@w;D@@@@@@@St@@@@@@@D-@@@@@@@b @@@@@@@z:QK@@@@@@@:%@`@8@@@@@@@!@`@N@@@@@@@N )6B@@@@@@@w;, s$;@@@-t]:k_xz@y  _QZE/;S5Y/_jx6 ^^NDvVܠKܭ37^o{Ңk ϟ爵ؗޜl /1:яKexVx5QοV(xT(9ZğV9U?9EZ8*ArFH ICb !g-{gNy$gf}ך׬-יּ{25?6 M   Po^%Po SQmo3֠YΜrn-W8}9Q kۙ>ް>%Vp{y" H*:Zv#   @ UׇH J2B6_ l#  n1^6|b6|q``٤Ğ4e_CtI#4Xt?ߴ\e& ,";i]pvlhS@@@=_uUNֺu퀾uy:koג>>e}   /@HiUhifq6K_?7S6Z,   $kK܎5|:4y`#6}r&<JߘL-   h׻:yόk[N 0w?Yp^."  Sf3W|y7)}9-7z-'@@.F ؼ3;kyRK̜%)_?>k>/n):g~[wS'Uq6J]4g*\(%]KLJ+IJTϳTwR5sDlzSq8~JT>O2Hg̖s\/;ufgi']W W{TcMljG8%th2 L,u4ekXv*OTfZ:YpiWJmGAycIⸯ|ΡXX> q=]![4Kz ĺU`_85/O?u~I0YmuWFˤ$}uTn=x7>cRY[e?~C*;Qf:hbؿ&F1(+%VݔhIߚQ1?v9,_aΜ8N=\>?ŚSgn#ݮ?hy7K   >*Oi/}Dw4-p_\`,@@hN:owsV2eݺ&h\utҽ:6ʼue+F#4tJ /3] {h<+^,3Qӕ^4޳c%x_ {V*kV3j-Y2[Wk~~^{Y`M:ώ5ʴ:: J=;|ֲ_}Vݳh/s)?t}wϺi4^_S3%5GzRsL{ tiO=Ϊgڃ6^V^UI؛i %[/]q's[;IZs9ȶ8ں&X+=x}Сcԯo]ƙhgWUuHl4Ƚe&fMU~H[iIcyd3 +Il>x4x31 E7U+jc9F<~6;=h܏hՋ]-X~1/F}X!K>ݠ @ )̱    } jG/ߤӗs_e澚,fXjvi}Qv+C1& `u?ӄFۯFpH9,'IV5S;,L ;mf0   @5Z^> (G!3G@@mJ[XaYsq==,3[༆<}cF;zIA>?c=9|Rytbҹ<'͓כ^Ԩ鞩 w铹fpveOzY=vR-a*zMJs]7S]ʱgyvXiwեS͠X0|r{pg'J\{)m4؞ߚ> fMsǪ5C[ѤaV6O\yR̫\V@իu1J{lCڵdIp_Â܊%J enKey5gJ6S1N2ϊy?:]^ ajvVM C,6ޮAY3oϒS滋j5˃uMX9vtߙkh?oշk YwJ}vz@cvHM3so:~$ lP毯Q'oe4=*)w#0E ?]*Ifzf\\y]u?]Xy`I)^a;Nv<0󔼦UºM-VbyUD6[Sn4(XCw]qjIqwZ+V߼+z- 7[7DMV83T,'{k   4Zcw]WayFZnKۏ{@@@ @VÕkU:v٠UTYӽY:8,5M8M vMN'W'kYc<9͙2߼=&N&X%S膷3 ˴?ԓVf^Wiś`iwԖ.ϺlEl]~q/յwfW@mWΣvEE ՔzJ~A%OAVXIkak) 1|6%40;n튾yrV |CY;5K{>AZ')Sc̫>$l 6ks;5 NLљsTn<\TG~e>c+Xͮ> [disιUN6I4$X[_TZYZ8ORUr?s_ީOff(rZ}496N1vn#۸;V۟W~kM|~=f^/$jm5{ϋ-ck|0FMVw/5zc\yYzOaK>{[滫0|ygϔIV)-Q+LrT Ok{lzBa;@@@Χo,_f~M|@@`b@݄o<c-gn/v9*|c6=VLI֒n 8y?13Bmf.nW 2w"3,d;{j7>{- 蒮y>h#_k$9>dK=ק–gy9L3ykcQ>gVrfXZCΪ _:I߹OѢcO oP~k> wNfz\b_̓jK]ݻJ^8t~8; Y{ҦsܳJ>E[τp4GD'OҕKͱZG^\П<5xվ8l=GWeL^P@@@GԮ~>_ o<  E(&~r"gWJf?Ym_}kj]Ir3M;qdO91z{]68OYB 'wWٵgaMR; ,ՈK.>ui3f[tE8A˅ؘ4#@H~IgT!du݋<7&]1fM NJ]y4NZ1˳C& 6pUsdwWIR1 >{iT|S3}&%sX\ڐj{kZ6    :RlMԷb@@<mNGi=)~OW"e5Yֻb|zuyuvuGo醯`BzWa8otF>=zT<Ľ'OdV:nhCMbc_GN_N;٥̠yO>631~{V5]VF~Wr4q u&`e޲E) *{]Qqx>=B3E8ڮf8JBnY}~ל dGPe{;}6CEGltcG8BOg6#{74tҴǵ3Éc5's,5ЉAM' {-.*x"   dkߤoAAIk"  @ỶFQvȞ gJ]Ҡ@iQ'd7 [K=S̠ixwmSj/ȳJiɗN}:]:Wg쓞 f} zK0}V Sx`7I Zeʶa2{%[ΆҴ~߸ڥ224IסgN9@@@$,)B-]"ڂ  p \`C?e*Lmym %ѭ0H߸iZ_ ӿ,GT4G|ϲ?KN $'RV:D@@ЇYI'ý{O8mkfT),kKW`   @k ԇ  p1 !)'U 7z8Y/h@S֞ݽc 0ywJY?3TCԘr]F d#a5%45ӕ~GMEl?.>p֛r}z%Y(J]ho5 |jziKq@@@.P+)`}) [@@h@k?5~e..oS[]vz if2ӯj|1=gޒPlQ6^W6h$PJhԔ7!JO @\t[Iѿ :{R*m uT$:Uӌ=Z'x]}x   @[I?؀  :;򶎆+}OkGƷ>ڛT`{;kf d\y4+vV_㵫6sgUGkNd!0K֚[7(ѵ_hoPvuͼM6[:'d귶 \]dp5x%Cy_mUZ!cϬ1t6&"h`6"   ^VRY#I8@@ \pS%K(~ xI"Ho#aI^S]GyTO|'c͓R3$*W6o[#Zhk*B&PDz(s{݃ej=s/+OI+l\A?o- gtp{p^k{7T<.ѢJ_ēZ[c,TKbmŏ铢rY2߸jORT>زB_d{_O,ާo춸~c_8^iߓG@@@L Xh^$41@@hU:z|̴~*;ppu[o{pv9*̂p`]$׀]mqiv}S_ɩ2I/o2=ꞝ@JW[FĂmsb#h?]kpϫ!3pf ؿF{gk.m}x^/ɬ\:F+2e)>2A>X:C81ﯼ[]x / {zjֹ5=Z=oO)tۃiSyʵ+fݧOB4"brMx(pl 6)U#G$NH;~djruSs-Jz_+)Qցֆty9Ә3=](گ(]Z]c"CxF̾] xڮC8ژn0xc/bX\AYn_NA-zv`)*QSWO{>sjtx}%/wyE]]Ofzp}3~sjKt4vh>`i=Au_J}N#K|D&Gi熬;r" ׇ"Nwc^7KmoOu"~wmtv56W&{   @K D'mtkhtY "  p1 |Z[̼>Jr F)6!NI,3m~^qΘiíİ4qOVmu,/2UB0]zk䊶ze]'Umb:wS+J7by*7[=7Qk\}(OEñݔh #0I/ 11M@@@@@@ <u-y"1%       @sĀ6*%       gS@@@@@@@ک3 i!       ` @?@@@@@@@@ Ў/.       @@@@@@@ڱrj       @@@@@@@@@ Ў/.       @@@@@@@ڱrj       @@@@@@@@@ Ў/.       @@@@@@@ڱrj       @@@@@@@@@ Ў/.       @@@@@@@ڱrj       @@@@@@@@@ Ў/.       @@@@@@@ڱ@ǖ>        BB@@@@@@@ @b@{       !H f@@@@@@@ڃ*r        1  @@@@@@@h$9        BĀ0lF@@@@@@@="       @B@@@@@@@ @b@{       !H f@@@@@@@ڃ*r        1  @@@@@@@h$9        BĀ0lF@@@@@@@="       @B@@@@@@@ @b@{       !H f@@@@@@@ڃ@pC(H53 bĦ6-pXyeHHQJ|6*ӶmSӰ~JQDLS댬X'(ՅΨNW+Sb|E*~%@@@@@'xq8 tyΛ4Mܭ{͜|}ppmlpi i[ݧ#)㦵*oSM;_LgmlP˪rjtܛy69ibMgӑf 3&sOZuuܗggbshJv+ɔ}rr+'@@@,P(V6ip   WEZ<ܩP󍪶 aD}]9Jt:} ĸݰU9䆕){>[}v_sNڒ4Q'|9Ǘ);L}l@@@@~zPV_l^i]Oe:1ZG(|K{iTsqJFޡoS{KyX緽J@@@M lzBn6R  #۠/kEK@4N&΢4wv|;U랒ٌK5fs:J'stlyC)J7Ldv#   #P]n}g   m0F }k=m✍oRĻ4p f3[άS?)[of7TۭrN@@@#P\/g^_G)n@@?co_%ER 7gW J;ϞѱuPbEݿcNO;ѹEY&VhbYU}#Y_Ůww.)}7E(,=凜BI~R3jM~ΜkT|⃕w*5uZC9{ Xr1q KQTm$TUz+Ea,=M"KM؁*7g-9>vĩbTxk5W2t?Pn9s-wVE_։C:m:i{t8{.M^!MX}tiIYGu4a77'̽zƽA217EQ1u$ ToM](u6?sMW^:sXR._+ 7Ċbϱ˞ػǙڳ*-OTj@?8E}z4Ywa>!  \2Wmu tuɺ֋SG@@izZ eo|J93}l͘y/WiAqmg+Fy ysك;wh sjLJ;4vڱ?ި{_R7=U9+'J`[/jUStz;j=cu.eR) (Ǖ`0gŤ\舆(ѼC|gWTUEJ噾 Q(}wZN?gVd85xz[ l|iӭ8wjڃ6NߡVt{\oJsӮ~`>B'E4f}BJo:V?ƎI: 4t9AQ>pSiԿ^nNwEV_<0! ejm4Ƚ9}9k _9xBWJ]J8]7|R߸K.w"7;<9 hc{I܉'GX~WYF*uF]X- !"n[G*WI}˳   p Tumњ_f@@h-Ń~]ab ꗔw|~ܿYfɉ{qJ|6K3pܩlVL'VL"έX%SN;}Wu3W$$DA*pd6z AJdw/);Ce ]_|<ٚ~e&\gc'wmO_J ,P%}8: )_ǔ_0~08 qf懕˔19[O[D8}=7)WZSUmwkw' mrL_ Wt'7|wd7peVH#.ցKXa7Dg($MdWRIRMV))?k`Z4TN4l@@@h#fyN彏0EWVV@@@)WnJf)v:PRu ^嬽OE(qR V>de9X'=~OK{o-qδf=eIjϵkw@IDAT|m(z3iO Lr?e}j^U~N:Oi[OU?}S[՜ܱީwa͓ݟ^`^ ߷u~2u[UzXO1IkXdf8VHztwOZ<sUE֙3%m2n$' XCk ^]+*ʚ^֫!K?n*yiO.-<KnVΜcR LͿE 5˃uM^}\G6L36~*ު/;Un]̗L 4fW335v[,5/Ud(I}Jݯ8>Y`%ܬ)YyϽkǴ_9ѥx kfN,ux+J1hw}zҖT >MeOf{)4Ogu:WD C4(X\3mޙBb_}EL2ۂEpo%>1z6}Ҧ.tO !L&뺙OS|mIz؈   pQ lz>ݩ[}Q:p  @1֭UͪZY xr:OOQ^3ޤ +>i.٤Zp8f*6g!ꓳMc&X/k_XIs͈d>/B=i>YGhfoRc aizkv{֭3l]l~82X7Iftd 1uƼn^ LrٳSO7)ăc.UۗjNS@'tЕDk{׭j̒ml]M}w<IVھM\|.[T&tQx{:z3Gqw,(Lwȧ_%D ٺ#~VV>Ht$`s_B'hC{>kЏ1 i&Uﮪl/X皟x(w[ŵ;|k9]Ӯ6vxS5{[v=L9в@@@(~Y yJ+1?#  @p3f내(#ϓ[+_4RuR{lStGrcZ`m>=_șJjy">`c^ʤ:m{>.ArFT3л޶L_{=j_P徇T|eo'usL|h>gV;Pcb󞭳 jDG7V5Ys33JI M@@@(P_s~>z?n8'@@"&b'N1ȩtn~WyRw0{pi SY5cau\c;´ɗa3Ϳ{II82[2}e:CL Ǫ~;! .9ʮ9ğ~uQȧ;|f8 "St7y"g}] 3I3J5ʷ=~g|߬:Kt&_jϨ}0>k7 b^$ ;s߉Hyz}vI?Y9v{KdBg=::uyh(wh$|>f@   HRo?  @TmќgzqQֻb,BvשGQWoh}6jQ[k]IV0!#bԍ8J{)A-6SGNiOgՠΧCFc:!6G{\(3h+:eJycVA~Wzy=E/5qP꠴mSNs}{}6۫q*z4N?Kā Eɚ)`<1:Ι><   p lzB>_\GVE8q@@V;|*h@%_ntuiYj''Tq?ݸ>Q䷖i5a\ǻKosW{Qtoť7M_ɞsZ Q`}"<íDӇ5!Oz_R2zgqU>)ԻzgCiZK W8@@@ P]n}>;铷*:y= \#  pq \`OxžQɶ GG }ϪQFϽUJ7.t3P#S%Dq&ȓH[i`Їϝi GyU[m 2Dڴ$4͏   )PW\}nu*UAi <]qB%qU'TZ]qJNnwY^F@wQ׉CQPESwExW|gzmfטr^#huU%45UIo2@]_W]տ̋cS| _}7A%ѷK,jI)4劷TNk%^/f h>A\@@@ nѽ[J9ږ^)읬E4Y"  V~?x3ai/)B Q~h]tRJKz fcCTb'$ZHCiHЎ.q=^w+#Դ`!V4q.uݺp5bv)*\ԊQ{CQ!ԜY;Gv6#  4Y&@@F4v U5smvoõqDp_Ja|P%FD)>XWČ3}M/woV޷:p@߄=wjeõif=]ǖ 5BT7=XPGoHƏ$Dݮd{r@'ףoPVR$4A   E&z+>?&gQ~Sn \d݆E@h`=U^w lr?Yj3c$Ns*={e:;AIIck9$*W>{XliB߆؇Uuf羧_[~:v[C[]53h\#tM>{Ps\؄ŭ-Q>gGAgbm1sWl[/>[ӡz@]XG}e>`۞ӗ9W_ bWi_Х|/R"WZ:"  Sx'Qf=>)ǬkpZ  -(&٬]묟7iڅ%f^u|zxgړ|PJ 1W:#Uؤo}^25@IܭXvn `WܧjW Plcޱ.2~jS߳sMU чӆFi}CLen\Yشe:"۞s^J>o|*rB8fbYd)Rݵ|h@҇?)AՋ#,'V=탛~ikSgwo|Sݭx ^ L l^0yI؃   ЬoC@@hg?1ͱ~+7ppqv}h^QǾحNgӼO.Nf%~Җ9o*ۊZsЮ''^/h뻫|x ʧϚI>9M~ofu{)Cw[~nDt_8sn[!j <$Lzgu?~HG9Oe"     4QV&i&=]{0>J]Yf~쪷zcZ$eko7z<~jyx;㋕m1!Z<;{ywK[W?\{y}^A kTc[w)c{0zc>mJPmo*[c`鹿3lؖ.gN?*da ~nZ<CwHQ- Ǭ~IGO ߕ%kYWCpgހ݉*Ƴ>@?~AC<x~)zk\ףu̕Qҙ{?;i7ïoP6+MSEêu~A%GVJ;ƴ1BvmJNd-f\.v~-O>3ǰ@@@F@@h@1@(odRGg,՘Cu~Ϻj">?Y<ΡQCIuPĦMяs^Q_(iMxcEF;pdT >yv\uo_Su!![)2g3s{xv:vq]({-9]e$%} B0'w9ߝ?^%ӲJ>C^i} 둏Y%ڦQi[i$X踀=G7W>QiddRP3\_Lܝe^-iX\fJfiqA-JT/Cޤ~0?.QѾL"WC}^cZ|t|k7p,Ux`}>nX_;&CoxsqJھuYjxkj6˼b Zӻձ]   !]'i{.@@.2OKsΝ[2+vy}@QG#;sB㻄 ֠sfe4q icbXqqLĮkx`flOHQ8ak5뱮O^~L¹D'JTMjyE?Ot_E٣}I7JURj.^cq&FBc4S=|xN@@@@@@>}(Ā"G       4@|%1MP@@@@@@@h$+C@@@@@@@h       Uꕡ]       4̀H@@@@@@@ڪm.@@@@@@@AĀf@$       mUĀzeh        @b@3 @@@@@@@*@b@[2 @@@@@@@f 1        @[ 1^څ       @3 @@@@@@@@ V B@@@@@@@H hDB        VH hWv!       $4"!@@@@@@@h$+C@@@@@@@h       Uꕡ]       4̀H@@@@@@@ڪm.@@@@@@@Ac3Ĩ3DLLLى       prp^ة@@@@@@@ 1u@@@@@@@"@byaR@@@@@@@ZGĀq@@@@@@@΋煝J@@@@@@@hZǙZ@@@@@@@8/$v*E@@@@@@@uH hgjA@@@@@@@p^ة@@@@@@@ 1u@@@@@@@"@byaR@@@@@@@ZGĀq@@@@@@@΋煝J@@@@@@@hZǙZ@@@@@@@8/$v*E@@@@@@@uH hgjA@@@@@@@p^ة@@@@@@@ 1u@@@@@@@"@byaR@@@@@@@ZGcTC-.pLG Nc7NSD7 h@MIӇS/jZf.]ii[lrOmXێ[U)"U%+Vݔnns$   -p*+ZK%ek_C@hN/lYߴ+u>5SY?ʹ曾QwVю6&v>I]֨MJnm;Vg5ek#Z}bMJT`aMfǫ7ϵޠf  |B@@@@:{J^wԵ[ӭWE9P   T*Vwtbj{ ;s Ү,݂+|[}CdTvO +S2`ySvCq@U|D@@@92tWdW]e7+?Hz"6#  P/A*pb{*Һ`C8].^Muݪč]#  n#9aY*)Vߖ(wp3Cާ/ p@  @m0@ D$״4p{j' gFld@@@f%Zn>oj.ĎG>9]Yo7;zb@@/N``pUپS~G^ԤZv*A@@lkf:sך'|%}9-7z-CL'@@+pgPїRtdԯz_ZwxR,'%vSMIE=Sغc:QW)TPTsti"RUib%X־2}yN|S\Os^z*.ؕ3OUCtʾ^QPrΜk5Xyl3mz9-OOqAVLkb`-*z8%th2 L,u4ekXv*OTfZ:YpiWJmGAyղb8 ҕ\Wu9"G! ш>`Zd`\ҽg>\Qj~doMTGc~C*;Qf?īܯݕ&F1(+%VݔhIߚQ1?v9,_aΜ8N=\>?d~4.yF,_a)   @pSʰL{aTCifր(+ "B† Kv D [BIXv*(J.PY&Zerޅ "*Em\a=! dٌǍBA3==33&߮ן.{K_n֒ kirGwT鈺d2vC$8O W*1[+Y^fq\w1yi?Yi酻DOBQzGՕ)>C0ęXGr]sVR͹]=ՠd7VWFKU%zʴuix Ȼ?wP|mڔK#/lq F9q~R ElS.2.WOko}="і s}^KeLƎԲ53;}Kpwaxfv0و_}{嫣b;ߺifO^jd%uꗢSZ<2'繿Xм?]l@u۱|q~Ulۑ\ݼ ?)xFby {Кǯ2͓Xq [dnd!+Tu'N?onoNіp˝h=T'KJ߭AJϻ%P@/ lgy>*ϛHqR7 L[cŃwfKʴG[(/WJ     jIptiFPP@LA>t$/ÙUS+˂1>ɱ*:s#}uj8ո.A2X{CJwNf 03dYO, \@]Ѧ(G7q>ޣ w (X?.qwr50ho3{2OB=Aи0 (oroAGr  4Ήy ??9>-AlH92-y3tJƅn42le/Q&$@$@$@$@$@$@$ީc(7xp@mR̮a7 t[M%܈$?(CgEuiېȾߗV}mD^epswgotS,o\ug7sӳz ^1}ØR*,}뒪f.Z32u|j1k8=ű>$ .c0պwUʪ}ePS+Pu-ހ!?3jeYɟs%}-5' qҋƒu9HV-vx -ۋ$h;쟢{h;XIͳZcD[8KM Ơn7æ5*?؄f Mcq.lo=E zjܻcQR7W#y*9i1?'\"+,sWkٸne~|m hҔ؀lJy6wus]-h/!UjLrlՒfgpPOK@vio.{s֢dً3ۄz?/c@?3)lގvc8S~10܇IPUɲM|uE6k^>{MqF]Rlm{i$c^ 1ʻkxoÛG?(1_PJ{a:$QC18^:w+cF5!ˣϗVߐL ϗQe      ;JGj%    /'00 4h&}epB6VaevlqjE \l{vشp-]ӝVϽ* lOk|ݓJHHHHHH H*Ė"v].ԕVKHZ&ˊrIS.{X|`mudxmʶ|K8dn)S{wcߐFp儚}adIn_]Lҽ0zLYT޾i2m̱Dq3{dYy^j<a؜Ep5hE%9}v'):OL3,ih%NDA?$=Aގmp݉?;#%d8|SnTK}Jv@!4nC 2.ϻ+V%b[R1`TiSޣ[@!K"6p0\ ZkGdZ(DւTd!w=VK֔Z6 XX^IHHHHHH` ;}A5HHHHtn_]*-PLjsCj*W @Vmc.Dbm|_yo`QsPBmчO?c<5Ine9'i TC4!N 1>mJiNsMudbH;V+Av7*Lq&6onM:}peXVՌ;BX.\GqyJ1p;)Ij,02ۇwdtDl>;j\ll #ml`Bn "@2-WH>=>xܐAޔyh̽aK7:u*Qˠ8dƨtWb7}gw )P~Ogq8 | uhp]sJ 8?DsyUy}J>muEq8I]ޑ=8ct%#N0\Tyo)wSpt}C xh >SHݾ=ۍ &ۂ8٭tP&P1rfN4pk'@It_6}ޯwREePOӒg+d n 0 ;n!0HHHHI& p35)s?98飻 #c;ё ~R$~LF1*_?8bd%)x֛?:t:O3mlS0 cD:#CD$ȶc\$-"O!        A\b` ؏: y}]10xE`p*vinq6}rﻮrPaL%m#$Vʓ/d"#S1 3#Q 1lx7 \تv$Dd18yȌOU[faJv ]6='7(ACPރHHHHHHH``qcd    $0'~pLuUWYhGa:TzMI N̐E[*d~I%H`uoɡ8<i4/!_+ȅfxnrHH{KGvz:Rq:Y $Yh34"EYBX>舄Y13GHHHHHHHK 0(`o $eu}'s΍OKѮ񉎏kF0[UWLW{oql~]X,%.ٷ=ـq^:XYIjt oBe^AuD /Gc|h\_mӄur΋-݅lyJHk1/B"iI      I (( ~ƠxHHHH!iQVzO8 rw{W3C>' =/NޜTCVϐ1V& ޢ]. g!Z,(ZJ[lwW#nD݋ (=)NWBp݁36]lh; =.b-UUOvn ,DA 8{54K'y?b8ܹZ}w ̴VWUO.a^n? 3Q={/\Y5mtfbaWuvJR=/ @J%()?ʑ xܐޭZsmW&Ѣ"4[D+ue'#!{it ~y!>܅ul H }lTRn4Wv7PiZ.>|5Dw.shC01 񵭪qҿQw|ڣ[-yYCˋVF6n9YStF[ es8deJݏV!Ȉ;{кvЫ|j8`~Zc8,ttKϋ0yw5q}{5(GY媼Nɻ&ZHG _kd Eh̨1]#3ː@~ X ( &+$@$@$@$@$017``zj \Eʧ1Y!|cz}^ףtůP*n{sNeqUZND̕>_yZc}KO q|=.]^-pmPA]U=[|ڻ|A %*$gRmC *<pzth{z+/ICUGO%G~RGo$=MpJOõv 酘[9>q+rC[Tڤ0ߊ@$@$@$@$@$@$@ $>[P>&)S orIDAT1mRd.C=R#P8~Fo EC zORaGXdzqܕ!n]L*q^RWa੍>'{6W:|)4:W`AQ| pY_w+MVgVpUu>CI֓XnebHHHHHH""ѯȽ|;rFf0Z    &pf nt ZVn |%;⫫v[nX_׀EH7""!}V_Ef =ĩvV;H }qJ6sGqtI1Aogrːnį Wl~aɳXҼ5Z)RHONbEZ *0Hf2mɹxSH5~,z LSRǨlAOoqz(64$&䞴k;m2KUMspQd?v7Y[Qx1 {ӟcwy sOXS[+M>E{gDf".G0of= x=;[=j|ϦWjl{vc-_lhM$@$@$@$@$@$@!`K{<~+2RyB$@$@$@$@$?rE&Ԝ9Ԅl\㴍Ì }V,1Iw3I8̢ѡ=ʙGV v~+WoCIY~UUPV!Lj6qv*-+n{2?=0*JTs,(a:zޢ$b6SiQ:&7 \u' ss$"0ב :$e:)hwʳ$F4g            S}}E6/E`$              2_T` Qb              ``Wly$@$@$@$@$@$@$@$@$@$@$@$@$@$@_-^8pfIENDB`graphql-ruby-2.5.19/guides/subscriptions/redis_dashboard_2.png000066400000000000000000005045371514115062600244770ustar00rootroot00000000000000PNG  IHDRqW iCCPICC ProfileHWXS[RIhH #E^%BBP# ]Qq-,6"u`CM N9g3fPefAĤdP(@DE(MHk֒X:_Eq@ N89  t@\ XM @%8]5$8U6~{@t%YtGQV ''ͅ>V99 V"Cl]LfaY.R!Eroa-C-YaLM *\8"?f l0!f|؞-B{4+ǩYh ;"\gy/dD16ia 3bd<3!eń}fEFMFl0h^ - 束,yQ\ 0X]rߒ(=V3v@T3{5lG(\rbep?X@ [*2sy6trͨGtD1 |Q=^}z1&4a07bqWǽ(hԸIVYe5EkVYjsƗ?86m=O{vNN&:L]oLuzz|z'`|X٬J֠~X~AAACazvA#=FS]377~obj`̤乩iii}3YYusy. "â%jdɷbmErXZݲYXX7X0mmlm^N033fO̻XձqJ'''$NH\x9I+ԒLJOޕ<45`ꆩ}L9tghȞqlLC)=)ّZPjHAg#כy=OH_ޟQ1W_egn|U5ݘCI9*Pd ҝ5gVwenInO[ކAapMcNLهk1wgqןd~" S/2\ToqKK^d[T^vibŏ DDXrk+VlZ[z̶JK?\ȪUV׬!kr&kZZ_톙.VLغQ2eѦ5>WeTݨnܬy[[xߪlmmmo5AQq~֮]_u=wwߣgu n;mo>}-ood65כr9GGJM=-I-GC淺6cV/>>rܓO=n~tg<v¹s?qBEG/\jtñtl|˵{R^WO]vz7"ntߌyִ[=ɾn{>P~PPa?s׿Q̣{9_<=W޳Gߋ%y18Jj7oN|>5]λ4?Oφg&}bk#9##l![z`Cx]= *^RAdE) gRq3J l`/9zzaE`/E7‡7:Z"22e'${y;D|\:;G'kꞍC pHYs%%IR$iTXtXML:com.adobe.xmp 996 2022 1 6<3@IDATx `ՙvpCHHHBBB !FYd+FVZ:)cЕʌ3LjmkVZ*RE $$$$,7yޛ|^s>yyn \quUxJgyHHF@߿=R,B$@7k4[L$@$@$@$@$@$@יuՑ "@a~\ 7;K$@$@$@$@$@$@$@$@$@$@$@$@$@$p PY "@a~\ 7;K$@$@$@$@$@$@$@$@$@$@$@$@$@$p PY "@a~\ 7;K$@$@$@$@$@$@$@$@$@$@$@$@$@$p PY "@a~\ 7;K$@$@$@$@$@$@$@$@$@$@$@$@$@$p PY "@a~\ 7;K$@$@$@$@$@$@$@$@$@$@$@$@$@$p PY "@a~\ 7;K$@$@$@$@$@$@$@$@$@$@$@$@$@$p PY "?zΒ 0{wp.Qop1ЫFo>>'Bw/(;'?CosCK$@$@$@$@$@$@$@$@$@$@$@$@q1$ ٛ0)T^#`Fsg!k_BH|,|D*/]Fst'ziHHHHHHHHHHHHF#@aFH)s _+,3>+]Ri9O I= AɷΡ(=L            qOnD,o@4zn'$,)ZE7>@?iHHHHHHHHHHH4 czx9[*~?uL!O4EeY,oNY~h1}G8- &@a~L/;Gc@w֔K"!4- J~:ʪD@>#xj4||AD]+'Սc佇           ||A 1C`@Ǖs)ߍKJQiY=Go!8>V֞3B? įމ^Txרj~]EaO#lVCGwi13R y5 n G̛R{qJ3\(Y~@||}0w H>Bb UЦ$`78@ssN$@$@$@$@$@$@$@$@$@$@$@$@ PwIng?$B?g}E@X(?4~t7\׷^BO~n'f82N ((:  (N= ?Eݻ0eɭ;8#ƻޓNa#_Axݟ{}tT [C~g^%_!:?럞Ĕb^y`f           w(̏!gI`G?OAD;Lu=&cU߈{6 ־ ^y_LŌgy0`<@$@$@$@$@$@$@$@$@$@$@$@$@y^$@c"w5?Cˡϙo?9󚰉HZLHl w$n=M-ٛ8^g& YCˎ8$ 岳8opgBIg}iFWm=+8oZ0!yf|盘|u6ʳ$@$@$@$@$@$@$@$@$@$@$@$@c8 g"AEt&bQ?y S:tpa'13a"o@A$@$@$@$@$@$@$@$@$@$@$@$0.PN8# 1Y֜|1$-qwDwԼGwq5xM4|ܴMO_`ˢP^#Ee(O?Cm_OE7$(SG׽-k˟kWMDxΗ~aLxOoSK$@$@$@$@$@$@$@$@$@$@$@$0 ԩ13vHDo/@W󂯄_&Lp@:S.Cwc3jH"wɚZĭ].μ \.ĶXeHHHHHHHHHHHHn oq`+H¯ O\Gz [t5\p-m87[?$|+FZP?Ы:&%           v(_;L$p@OS3>h>V,+>Qh?]?(4X_MR|G~~x&eπ69Ar aBTkCpL`"           (̏1fIEo|}EW<㢑Qw^1z͛_ͻbZDLz|e Ƅ8L> WjD XHHHHHHHHHHHHf!0|n^."" }om񊛅 +V{iOjr JjNwOSX+ \ ܼKG "B+/}vfty>~|O>~^kE$            \0!<|d&"Bq=}{j+Kw,<(o݅T#Çs7ᑗd ߈5 f"1&GCףV&YǦWE}H=#/hjn+Ыt D@׶sZ7QzzL> tu/wqJڬ3,85 '=p7RUhK~hpR$Z zt]M;XHHHHHHHHHHHH&0Ba~Owm5aHˋ= ,к5ˠSZ{6__XGbrQ#ҰBy~ /GYJ_łHj7ж4+" ]O4V`/k7j+1 b"+k78FƷ=`ęv"B<.#vQy+ZJ[HHHHHHHHHHHH.c)/_w>\CDϊX "2}~ ߊO坌,؂_|}garB[51io>"A .~r G!(&J֠2Ju{/Tw~*d=J_M X!|zX[?y=jKNJ'+ʧ 0yHSb .lwurڊlEGpa()c(eK`-òLh,o?mo6 EH}~ ]}Ĉ;!QaAhEKdY.RK z †hzou]>^N` \gNwcO>z\hؐn45c (1QU_^:j!/$ 1CƺnTt Bm \=`U{V?g-xe$3:|Od9YE߅sgPTRkܖ|ې3E85a_4%oh9t]7{MDrvŅϞ#؍1LeGzt;fb o̩KFhwa>\/ 7 ُP8|m]QCW%v=&SXx\kF{4E$@$@$@$@$@$@$@WKao6 Oܛ:؉/x([JӲ^)Lׇ:ܓYQ=孭{G896c ֧~'[ 4 u[7Q>hJ$|C{uq%?z -GLdn=6>wxc)s^ _჏0!1CԢ| Q f@މsRIMS m+ip"ތ4!\$碼m$`NMwpܹ(t\nJ b5kG:*,sML䪦ڦka'       83W,Y2/c{J4P W$?9 ӓb}xu]t]gI#Ќ]>vK S a"$}h4~t;w> }K9sM: (utgFl6< τɪ8Ǒ/(Yc qV<]l0 n[>Xm.%h]`\h=}¼kSG{fW`7 `KYE{)c=^$9 15`oT6o˺X-?! Ӑ`{~=lz*:A x{bwKoUhvhLVe4iA3j7l$o#gWb\i1a z:1x]?a/׵bXc_) "gQ\7N+|{q9__G-5o/Jhzo!wrgD]+)ap2T9@Hr>R,AN}B]]4P G~~{1wQ|}y7;uؽ;:PW]l,M6x +mjw֛OBh6LNLEN.O+MCіm㊥C߄0-kx |6d~盈Zr+mi~Ox{U먔uE֜/D7ǜ>kd dtu Uu ^Fa} 6PBͦ\P6t)p- 9юuhІׅ2Σm6Qj]5T go|ow;z#04R67Y/cc[~ "4$ߏζPY^]Pﮒs P{y4H vPb+~Z$B5J}O߅!AGbҕuy`]atmD&$@y >R=ΞŮ^[mLCXsvG[+ yYT^/?UH'Y{S 5SoY "*a"E fc$aMJ %/K0t+Mjڤd?s X {5hl1ݦM撵բl˜ "`'\vz}o]x7W3UĻ+fӰi | 8SLS E Ec~(-ާW;h~x՛_/3HC6;0P2䁯JD.^dMgO託Mif-!j,7=6;inQ^oML+oc*G.>9쌝CjجtLH;Hx33K*pz+w.y_ Brc.z.2Io}?.%\aqoQU4Ȅ-O* eP)db"pۜC^Ae(p\DZu[i)ȞӎÇPH,3=JmOvLd~SAi taԗ+=)E:3kmG"6zîBebS|E\_YB[n25Ac8Vn>WePыƒxG}K"^W= jSnyk 6t|!id,u' cL߁㇜1~*̝"É>de%CC;}\Љj(5fogQta}LԴ",1lُ%ԽE{@)@M&N8x^/md~$7ls--a~3D^jfƝR?,"L\7,҆bC2Gf`ɪ|D q;0+'- u'_]" ${Ƽ~S婖&(5w&a(} Hj%2&Y])ctxx^J% Pu̥+cJÄ 1=a\"4H[$ǒwLgdqVx9d6 cnnVºY;7}bc'~d5vV…*ӰQQO%޷>?rxs]')8)ÛlS^V%O +8yq_᭾{Xcw?RWfxnqNDy=xᱻQ36!k֌>dTLvqƹw>`xGg"7+bs~= 3?䥧{nK{>Q^۶;<)%ۗ~lW<;ىJm8"6cpK"x/g+-U>!Hs(~"M0?9)Ú߸"~ Pu'}AK לMMC }zƢA;^X5m"}DyB2Us:P&<:,#/O!Sڣ.Nl=+ڌowζ|{V ,z\Y=4c:²3pT)Oy-،S݌!c ҟ]J|s~2n){݇/>o_ E~Pp$=& bJWp_LnL!Z(%>N@qm>9) vUfH |cRO(E){h̘HxEH>ؙjBqi.i șM;U,Z U?cαh.}>TJg%}E'z튠 eo$Xۥ݅' k3M9k.ܔ,Lnj`q ~k>bMz|yۇ&8;lufW"3aiX1rXLA~G, ume %떹Hhg-/"P]zueVQ eoO4J:/)DiuqYB :qrq_9-Bm'8!–zCoO<,[]#6qJhcK;eSh9w\9ӣLؿ֣8=?qc7%VlWcu)Ebq[dҞ=kxYh](ڻ6byZꊱ[J^f"OAr|3g#t^(®#6҄Ov%g) D2g%ITm/N΋poY]U`/J\ܑ+"$%NE:v_Oe-0G^qG´YƉ r>)mOd/u-⽟`(9c&B(Qwj7NlRu4؞j3Tflv鎡*a RHX\Veggl#-epܶRezEdTʻ{ '<#1;#DW]6ׯy$⮆6u9A,1dKY6JM^խ5,5-Y3erOn})P ɦ"Mh ?UJT*=NARkV]LƪEf?Mb"uW{ن<$LMܗPٕg3AJEC#N_%%CT,7CF* XHEf< mhmߌlj֛92dTR*1H*[^& .C 綵º=!{ =?(4L(,?FHtc2M       qN6.Ը+ƎÐJ~긳Mw-<~ H͝'hw;˖׊0f׸ 몯/~Mx yüނl̏@?i}G$@:G Zpę(m؏OKmEpU}2!+PdH0ˬ`o]!b=%F^p@(o55(.B~;æf̚ DGmI2־IAߋ:j=-b9!ABrʤvņlA\JD]od b}0aęDyY^:!aeG]^NbX>`cxd Ö5|xV%&!%se=u;Q^=,I ve }]dW_R{c҆:!{x[Dk+kWכJ!sPl,$6J8v!OD$Q/:(omQ0C&5a@ pB-V`BL,ٕqݎzR2Lkثs)~ȹYT5$VFtspHHHHHHH`¼<:sGàoNhwdD._tʃؘDi19bL"q%'.N;9C:I3z>3:<6h_ x'(ϷW6y4q|^PzҳkHc^Si6.52@%as4e2οWtQHG# ǯ "oҟYWTT6s^- /5a4'S܊6$9>$ wظcFh .~Í#gmGyg}ޮj .¼2A!*{Q0Mkpm{q&?P)@kRC,s5>CP8"Lw%}a`uI0 9ZWsfx )*,z+޵ Ҡ+:]E8g#%>bxA]u뜴M5Ӂ# }W5UU:Ta,҅6D\/=36P₉6FiR ՝AXD @sXڸ3 !HՌӧNl=nW^ ClN^,QRUvuTrjG|G&|P C{/Zwg:$.ZlGwL"+"| %?z Tي (J} :}aįee-]Σfoh&]n@,NV4-:Ä*_mk[)B5hU~d#%OuLpqmc0m(BC VAȵ.5@1(J#bΑߋ63:ׯ1~/+̈SO;SpH{ T8W{l%v]f%>SQ׋/BL.Gxv:\ƑeäHʪǷ+ݱE $ īyY{*ϔ*AT~VEZ.8~H0G'qa0"RDuE&jp9f[dHqEii&Ǹ 7h/-i(Yf$ 5"ˏX3g'Ց2NzL0qU@vX~SnՎ= _ Kp*R& OGF:(Ȉ7:&x.F.﯏ϵǁ_>7d=M'D)Du L'Gﰧ5DCaѷ[>"+IhT3צqxοOC4rb#ET ѹqG֠j]{WFxgD|t$=3:MhdS³;OXљkM :ZP|hdNK ')yP8Bq^Dcm{ZCwU !A ?wqe"M6YhijܾIM(&,ޡNxG/c5QpD|} n:,{dd6aino+NtLGLFBxhO@;.{;z Ƴã>21|= L.]͘0S$@$@$@$@$@$@$@㏀f JBcRML*V=u^!ŋikEt 8{{joԮMAeHL0>F%\Y+֒[ܪFܝN,0"7GkGZW@o\6{Z-&Cb?9f>bVކ9?|1z] ;oX_^+% >![C>? .UA83հ2Ѡ#.Gy!MDv.zGK%[sU3;Иs qXd2۠xt]8Kڥɑy T4>;f\tP"`>ƱMțƏ2~+޼$}{!ݍ= H nl"Ոû<Iks#\0Fsvk_/#QYC6^4HsdYW"tC[[6pM pmhn4Mxp3ecnZ s ';PQz^ýGިXū[DyYV|ݽ=/k׌\{֌֭峼Ztz_eֱ'_%kV<4"K+4gaTtTTՑvIDeg?; "|u2'@+/dòdMo~"|q1vuRbǑ={F*%ʖ"xIhpynAOV_DZcX'Ydu]Ǡ=8_'GB(m-ӎ"7JJE;p 265-klm8@IDATB0+{ҟ^O ;lbXҠeދF5)&ʹ+ zrfaK\ |a~<$26.+0d"P/c b"$ߔ Pl ]t(ʛk3$}7Q74z/ujw/A bGU:p0#V[/ʐS%;y^rkoy-91wkHHHHHHHq)&s%垑z/k#R{v/n@*xg䔧^&MhMR٭QD%\Eԃ\ũ]E7uQmYԵ$$|xFgUtˁ(>ڝ}P։^6s6}""+«+ߋ/@f⫶e啌&<'^"D :RVw;L<1EI}]*xo@Hx9=o̧7t܀ދ-R.^垻=y" dQp)2g&Qx ܌M@(YR:xubYԇ3 HU]ΔbŃ ʵiL6ac9߅K%$r˵kqO@TVc@%Kܯn Y (; F[NU݃:?T-+֎1'Y~$>)"xVŠj¢XzfC/"$yvg{:HjJ\O_TRC}T?0иkWc~mB$@$@$@$@$@$@$@7գ ?z> yf7g VTywb[܍E鴸|''H%Q W[f:=܊y: 8wXk~M">ffm7T4zI8XHW#^[/*>hd z4E*Bk^YcÃȟy uS"Q,xߐ{ 8=`XW~PߠW_}}f\BZJw5H ՘z\ȯEwg, &mOKOؔtwV|P:r!KUTW #mddE}e2yL.4-2D'#rbcR:H.ErX`jLM 7vZZ\\.,tVYc>=ܣ[Hxj,h>[ƶV4+EIzWB|{jCӝ8w]cZetש{EIv.%zHf-Z_N4+ Mۿ-oqV2MjE};%ufvEblBrzN*:hnZk[#=阏>ˑ =U=~ IK3x\{ݪxRgvVw6dw'yK: ԦC]x4xQv9\!~bD_Jux55]kGfBEDW\+V5 Y)|b4Pi9Rxp77P+{"U߁ ףIQF'}(kqi`+mbRQXkHͱS0C5Oc4jzY&EYRH`#IS-vO\Nljr0Yk$$      i x&c'>7?u7l]XDG +kH2Ƙx.`H}7"WUGka4{D{f@X.^~WrjK&V l0wXn~Uּ2VGʍ%~h>)z.I(Fo^=b< JGEWQ{ C^ތZp*BBTO}CleU\rWs'r KY,6һ-)Y?܅j>YkSn[X'tlb'E"0(QVv|Ef!AԕxG( c#59SEEyBÙSvlb{t(o3ĹkqOA6o ̷Np\/╶O47ɁH>en$D|U*;n}ʭ5e2CuX,^6!!r",¼X$$ch)J9 ~Tĩ>4S`_.žŎJPݻ#3Lm*uג&?{uع"18OτN?i|E$@$@$@$@$@$@$@7+Ob o[BcG/y4l|m VI}xie=rf5zD܃oeWGs=10Oux<>gWXP)f]xbl?RU⪍تc/q-xn\&<cR0݄DgOY܈7n%V$mMly۶fɁڏ\E#G< ֦$ r\t2x(~ݓLPB+kkB#]COdXv ;4ɚ}׽¦hI\Hø"9o%ӚգIB!a5LZ>-vO)vrjDNƙv< H :W;=WtHPy_uΠP&}ܹ:UgP K$ 2V"/HdK(:{J](,!VVRhR>l@qq%_+TXfgLjCnDWn~qX0 1qfbRR45aaaujP"U~ץsPvh.KE-;)=# YkF9HZϗ|M^IsTrp]A&29#Q!"\ n"%,d'# m`Nf"B֌3QzθFR3mL8o#Eoe8URcdir^8ˋ42_L@bݙu[t ws /dra%{[W? c~m       1C@yaڎl⢇?ʹXb!¼8{Ͼoӯ`F#y>9la#Kܳ-@zFu̍"_Y=>?+_>">K : w[D ~٬(=z/BOsaMzs~= ЮC_&$`b4RfY3:PG,I |y9JQ#{ d1Ejo!_^_o nㇼ/ƕ?yJsU!va5KS2]Tʜw~; bz? /](,|'G!gA&J?4jD 뤉FL0ޤlSh& w&V'/G F n.7%׃3W?qHHHHHHH`BN1:/JDa76`ͣbxIh0aDQO[+aO.g1D_=/J&bxZ;jC^nƽ+\l't ՚=ɺlU ƛZztv^Dy3XXiX'4<A a+݄T 1PWi>ur{&0[?wyXuu,םpE GlܐFi_惟BU̒+` 0y^|'߇-NH(HH}ʕ2 ߉46ck}NOF: Z᣿"=?1a%K:w[M@WIf2eqۡ+_ꪐ>;@u%e$b8:SnX*{/-ȹe6'*j~Z'ēk 0-2!bWK:n Wh/t(s3 ə8}jѭQeayr=,܅ Iv`/$J"|'v53_aX5;|px/[dVb+ @r1hڣ1--Cb/ K}5hij"K?IYX:hԘ\9fҁ(,q݈imj/:bhQaM + wm Nd1Դ1[Fg`|V 32&s`021U&&I#4 cnk{$@$@$@$@$@$@$@㔀GNql>#\1ǏF#_lןE)<a9|#[ã۵ɹ%g4ZFeaO|貆wrGčg+֯FPjW+u'G]kQ&/8a{3EXZY] e["J>$y7 >+O5[ # &%]LՂ2iŤZle{}fΙ9g2gw 9s}뾮k+²G3v45k?'UԟV#>ms/hx>j fVO[.d^bp΁]~L p}\ FֆBxLm=/a</^ŻkHcю>Ow>.D!E|GDͣ;#Z?X o~ p' :-e@:ŏGHĄځ %Ώp`O>i?vߤwaH{R/2e47]؞p>~CRvƏ .Kq5qԿٲ(3];߅O@A$]u b{*I),$5$e-.81?~'Es7/ę{!|R[1.4qR8˯~#G>_-&#u_73AӼ>|xx͔ݾ#R#oFSp/qrMgFhxG8_[2^tX'L'b3q'BR^lۿ7`%XimNh$~>ܿUu_푃_^+bF_·n1Hd5g'k6e5DE/Z.NF,X2?|O||8J@=f?ޛb!(qA4._j>m7gi\Y ngO1sW&! SφL?v9L06Z'*?6Ky贂p;c>:.F<{~x'8&OD,w!XN.E Lo4NH/a+u3U|i_O/E^O_8O~J~wn|J~oS.Z':":" y1>       y'{t=&x0Gx|֯/2hbZRNK 9o$R΋b_22-F1Ư?KPPvgq,fVaƫ?jD_GBA$^}ޜ%&HJUyJf_3E5q>_y?Gh?D81~܉ 5{A'O _hhƿȂ 1}q+w/ِp^e(z5jov 4u)&.|#> s:g"y E__mg`7_!1?5߸Yt@v_FE$@$@$@$@$@$@@V|_kg/ʢ9}M؇'2/#w x/0 qt}1T (EU7@]7~8y(N/oM;>GA >·ez!8op>SļN$@$@$@$@$@$@$@$@$@$@$@$0a D0?aKnjx/6\νBg/sNEW*#:b?O`"Zf?no.ѐ)>>|4C?z~ ezݟa;,J[rL`̻qcet$@$@$?2F$@$@$@$@$@$@$@$  {F Xrk%WwoS%" ܟ+/~EL+uZĻb6}8xLGҗQXExs|s$N 3,XMp2HHl,r>bg`        (I&6_Oyi[DU8 \*Z/GLͿݮW=~+OΜw4&6hYk7"_R8k@`|G6 $lW1Әr'1 9HD1sW뿈;1i;O^z .@SZ߈?)|,:/+DT1>y5p^|˿a 4e&[ EvM-TH$@$'0^H]y= 3(BH ?Ο:3spao̘yL "͎>y8u2hHZ ፿G|~U,5wU+Nwh%) @%XGpI̾\{!K71 .oq'81{ot1U4 yD?U(>x _<"xZ| ݆Ngv?m;|Ex=            uܿvK!Hy6'SP>]=|M λ`*f\;W~ qO=/ã8-ᓆK3_ f?UGc=';7LHHHHHHHHHHHH G(HFC$&nAo^'OB3mqqyE9>V\zi'={?},1?e7| _݈6 8!9 ̟LH i/!yxo>onF/r.)~ ^zG.NxEx}6qꞏOk:וE1p`B>v* HHHHHHHHHHHH (!LFE$,Y_E`~OZ\zgQ>3>a"-{M}g3eomy\r1 ˽z4ͯd=?eHHHHHHHHHHHH!;ђ Սp45u(&b?/ppQ'Qpya{3PH=j& dG. 3@`ZLך-Q'yI<©?Y34i baHHHHHHHHHHHH @QO% x\0c:Qԏ::$Wu'/-{Y-7響:b#1c{&^#            CbI H`߷GWÇ-۷bg 0 ">K/MOz n1>            <`> @ky_gx,8#3 LvOv,? 8Iy'2n              IOI (w.&             ( 8Iy'2n              IOI (w.&             ( 8Iy'2n              IOI (w.&             ( 8Iy'2n              IOI  p2M$@$@$@$@$@$@$@$@$@$@$@$@$@$@5's$@$@$@$@$@$@$@$@$@$@$@$@$@$@`qLHHHHHHHHHHHHHH`2`~2>N$@$@$@$@$@$@$@$@$@$@$@$@$@$8 GHHHHHHHHHHHHHH&3 's$@$@$@$@$@$@$@$@$@$@$@$@$@$@`qLHHHHHHHHHHHHHH`2`~2>N$@$@$@$@$@$@$@$@$@$@$@$@$@$8 GHHHHHHHHHHHHHH&3 's$@$@$@$@$@$@$@$@$@$@$@$@$@$@`qLHHHHHHHHHHHHHH`2`~2>N$@$@$@$@$@$@$@$@$@$@$@$@$@$8 GHHHHHHHHHHHHHH&3 's$@$@$@$@$@$@$@$@$@$@$@$@$@$@`qLHHHHHHHHHHHHHH`2`~2>N$@$@$@$@$@$@$@$@$@$@$@$@$@$8 GHHHHHHHHHHHHHH&3 's$@$@$@$@$@$@$@$@$@$@$@$@$@$@`qLHHHHHHHHHHHHHH`2`~2>N$@$@$@$@$@$@$@$@$@$@$@$@$@$8 GHHHHHHHHHHHHHH&3 's$@$@$@$@$@$@$@$@$@$@$@$@$@$@`qLHHHHHHHHHHHHHH`2`~2>N$@$@$@$@$@$@$@$@$@$@$@$@$@$8 GHHHHHHHHHHHHHH&3 's$@$@$@$@$@$@$@$@$@$@$@$@$@$@`qLHHHHHHHHHHHHHH`2`~2>N$@$@$@$@$@$@$@$@$@$@$@$@$@$8 GHHHHHHHHHHHHHH&3 's$@$@$@$@$@$@$@$@$@$@$@$@$@$@`qLHHHHHHHHHHHHHH`2`~2>N$@$@$@$@$@$@$@$@$@$@$@$@$@$8 GHHHHHHHHHHHHHH&3 's$@$@$@$@$@$@$@$@$@$@$@$@$@$@`qLHHHHHHHHHHHHHH`2`~2>N$@$@$@$@$@$@$@$@$@$@$@$@$@$8 GHHHHHHHHHHHHHH&3 's$@$@$@$@$@$@$@$@$@$@$@$@$@$@`qLHHHHHHHHHHHHHH`2`~2>N$@$@$@$@$@$@$@$@$@$@$@$@$@$8 GHHHHHHHHHHHHHH&3 's$@$@$@$@$@$@$@$@$@$@$@$@$@$@x LHHHH802‰}'Ns дk%!         G 'p(БQ p ΀2ܮ Wf%Caa33 p$btO%_/4ӕVxVBc QjfH`RK[Q=BX6Lu=}?vM.C|^] WNF%6k:0>K\Lyf΢ࡽXt[#fy.+"T+2. { }6 qI(,}f1H.WJK<!SڷAH/mԁ2xRҶihoƌˤN 3Qs\/q"I3_=W|.~ϝ ah.]Y 4gP2V      ؜K$:mؼ|U ^U5a0<,nn ^8ٛ N w SŠxIh y;@a? W@QU jCNgq@ȏMd 4ne0kmZVmb m/lCsqF1ZNsho(g}I/غͷg 斡z8ڶ4]،e5XP!$;X^^ƾh4k^<<,Dϳ=ċ%YTB8ڌfT7660X]̌)w3Jn[9YDy=>mAkPYrNXĬLA@uEzu'3-*+J{kepo/:nFĈ<5siO9 uA zubVy}6wݾ(oסn`422ˆyjӛjM|gO2tJ`[+V4HFƽR/u`ۺ;܏5Ҩ؂T;8V9mϢ"cϤ9̞ӍX}罏qs䒗HHHHHH/GB&]6:Zɖ<"}jQǶ9"ݢ;>CSD=<%=|^ss|mTU;172YDp5&6a2I>=P>عkD0n(,^ kUh+o `~"c)<.,xF=c>- mh~eM[e+w)Z\Җڵ- {jƚ{*sn[Ge/x &I.֮K[  ]јў,jF(Ӻ.,1|wݑF^RЁ]UۯDnvcsp/nZ)й9]Sg\ce^Ix]S~e<B9LsM_l\1|;*8w{~Y?V=MQacֽ3'^ ֎m~pF݂gY|V.HoH37EKs~Gq1*H\R2 ηU>⨛Uf*foa5|U:˪Y 1Z;61-Dy4)zɳPʣێ!!.AhUW%taΜմ;5L(&c8,-Qn;KvP93ѾwSf&J_3xl=8!{cG=O֙ r)3U5u$Qwv0cbHՏD*#?m~ q:e3LNi,]P+?:\9UF /~oc,)c|3Vk_^%4Eַ3ͦBȷ;鋪,AzZEg&ލw kѮRQL2%|dwS04ΉbHHHHHHrA|TC ÚǂtzWcm_vu?Fz|2g+>DC]lolKgZ6"@Ek nEgZN am"MQۜįL Q}PtgS\";c)L.dWVdܜүm`Au },$ Q{1Ƥd䛤Ov'fb^xhGh]ڬo-^>DؤϩCMpQ o 2 6'?[8d)&Y.lƼUvڇ2궥sN``r[^VoEECR!Z[>N!?ߍ%+%"`_b8ҏE&ER]`S}Z`<^1[$$ގ;Dە'zyc7jGjޤ][#Y4U#Hj [н)j % } bu݋2w0<P,25'/=YɈ_Y-j#`p KƬ?Enji8~@鱧.1QyHHHHHH:b-M#I)&@,.xTJ. UfC 6Wl ѐS=hzMJ<2hRC&;Wy䦺Q{"Z|t/h"؏G1-ښ"|}26FY맮7 [uۆҼjzh:ܮixoE:E(xS;*8EbFЖuشIL6/2 aΥ(-!x}`r"BU>3āEY,P3ow-d'+Bv n-.\6q B"&[*[=(BVP7,7vA̓]ֿSm;qdh̸꽕^Gb`ʍ+ݤ-rIO XT%,r>^v5.|>Q|uU@IDATF"v4>}+3΁ %";~3kb*gΛ֎.RЇn1ǓǓIǞvV0P^&     HO)P!Da4B4% f.kwte'd">ЏBM~uc(mT_hS0uݷ}YҟdZ>liP[R%o=";DY miڹsX*J: ])}rFL6WP{O\++v߫3{,P>)S;_9]C=SmF`k^VI'1> |q-c7ۋ/SG嘻AN,$ wj)lųM^9{oeRJ*,3ʫ"ZзZ9@^r!;׭hU(D(IۙV>i%[`F2X:q\L%;(_֮C8!t6׷ϬGea.N#sj93ޱd``*$@$@$@$@$@$0 X̟E"Xw׮4I^<Ny'v}0wNH#4poìjt2#&ݧ&5NluCKzmhd]:^@-VUf~8lCQM.@|/NN?QMZ=g# 2Ifݕ&֖9WKJ9aʱփ{Dq$ torrsm.3~4dc.%8~N0{{NFy&G$@$@$@$@$@5]c%fB+Cla^]\y^ hEb8$ni9ȳ:8f]ps $n?6]0M]9z^+EC& ᨜xVEy+%bu%(^BB^r_)#A?NL|j.Z{s\p%S!1HLi' Z0dsS &Bt[ -WmKlL"sd/0JZ8]:XWaܗ7[9]<&W(R;C~aN<Ǵg1s=jWjLcﳹL_>1#46SPWHo ڦ9´nJjߠ^ _6'ύR'h( .+6Z_M: SX(揕4hcڨVYY|f$S]dzsè=-jBxup<ƙҮkH 96"c+u+ǡavZɖ6CGo?"m۶(2bIobLZ^uY[QsAj/=Vo/T#.Z㲶E0&4~tt4Y NRS"#47ټFryi,y?|gLW=(Vʷ iɿ$@$@$@$@$@$@ XΟr.JABѵoyxΎSc*+ffog +φ^.zP C8(u3h [(Be5ie F wcyĮZڲ,,^ meT[H.&|aXV#ܨT+HKW%cX,eLш瓵y%c-1谲K\k" C,9 k->ظ:;&fQqvȂm=wK \CKqRGp"ƶY^, n6 #*1wD}/{=Ȩ=>$qvXm[X"Ǫ@'Xj}$fZX==$%wbUZxQ3oҷm/e8NCk#Vo{Rr mںF~YĘzb|cOE-ځ͛|[7{kQ=7N-1ϐ dOd#)i3 '77juA48ScؑckYa2pdMJyiO+q2k`'V/5nI⌱Y*՟vJ=7h?1Өlh!?*LJ"N1[(?{Kc^hjm6X!MS9pT'?*>{zжkѓGpߋfmhKa ch~eL(61rTxTb=Aş#\f9@Uh3I;l/~@F{8>/k%wNDi׹A:ȯvN--7 ^D8[mP8|`dgӪ;@OfQ3cb2Z\+7׍ 9Un4=>$ *jEwn*X3h[3Päѿ5'85v&̷<5Y8;zN>oIYhĝ݉C1f$-,TIn]۠`Į[ ]ih%FH;/Q4GYuI c=F .9p0+vZ}ke~ݏ f}`=3:P'L\.+ #{{-nv gDjy{h2V F2f#vyhg*Jݴ ֕(lϷԩX|qZ [8ܞ{>yeX,^zlrq4eߤ_?Sz 3H;}Z Ne+b[%bldSChMZ}׈]O6DZ4k3,Sutl}k4Y5V{Ŝ39k)[0+]@ou5سņ_ JSE4Q]-j :)1ʋShVv|փ%74OQmoDY:|jb;!I;٭y!,1k2(%Le-ӹјJsyK }R絚&ryt}y)>4SD(yiP(O*5Ε󭥓zθ?mo0?亿Kxb /KմP 81Y%>&d$ŴJ]YhڭW ULV3iVɄK$S¦YBy5n &s+Pi1TY׳Gl7K6Ŏd܉W3I(ĭ@o*4moKMSs +'m0k}Fmf5h6s4e׌&Dl{Ji-+v\(d){NqGCNx߹b[nyW4 hr5Uh#]$% wL{ ZfYW+ixD(n* ,ٸ8fv^3/ `{KqJYY)9{+ϮV-ٶSN3߭Ak\5%sEcPJMfuE֣\&!M[L;< .^.S3{6.Bs*.` <3M'&2ЛP: h`GFW "PSbҗ̄^kכƖ wq]ADܐȘySb|8&.O1J !Rs bGZ3$+5w罝2&2sJhk 3X+)ΙN.PJ{1A:p  otEC}aUkX܅3]k(X:'%':,vZ3;X$k\-Xl;1`dm[,'CVc2l؍b^7f1nX:އwXnwcO'Ku+qI{n͜lY9wlqtۋrKkVm^c$"78kCرV'HS0:&Xv[q20=IBy-iT.nFpoA:Lw9'}Kn%}w&XN$*"03Gf=' h k֪"qٞ[_~6ʢ"OB%~=r.~]g5K]`f/ҏFN4m@O/g4ճE6oC̞7Y(MGd;s:uV4{VzrN9;ZG *MY;=X;W/}SUeh^~hlCU(3 [yg@[s;́>I͍vWjɂܶGajqCRVqI %3yTÙ;:~^~ZF{yqP^˜Xdί~Jrev{:ȰL&%b1s0b$     I`Ry+%( ֺooFYN̷ckE֟G "LUl66:'T/cKM (vmӏMSv5<_||>RQ.Η;mLd~J_IJdAS[SdO1s?tSm3ڻ z(+A1zםzIw}ɂbEHߥwZx1Hn|:cEMRAn2l> ݛUKY 72]k8hEjZPig*͛|o7"Xpuw&bISt^ <`pRj8}h"0Jgȿm}έh捊Łhځi%Bq[yh~mKɚ״R3Ε,Ǟ32*AK;eo'z׽Qp(2sM^~Ѝ_ǔCݎ3NjtC LF" FZQ}@D u) l0UלԳۢ,XQ1ÞѢYmrti孹7B:g֣>bUfUE?XL^lUX,[bU1u޲w_w7:&J* R[t{srC=U*++_ f q۶`id?ZzN֡ݦtMKKg2Wu&kCUPj j,fnMqO <-qx/XLUR# ϏOFX.}=h[jѺmGfZfn3Vğqtghjy7D}=b1fhnd<.h3{l6C[WUrȭ-܌QEj J'|Ui[+ lQuS+Ia=gΰщ)N<<,UOW}VѶQ_=&ϹXJ1,t89'{SSTAxZgZ@u,o/ `X7eok({U kΎCP, /֥dL-՗1lǞ2J-w&:Jލ@Ԝ LND~]/u9Ąx_@4/]DHg\Ӫ; = WR5\01=ԉC R\\~qdW0I@/8.4*qnfY<7.P3'm/?ۢ `>Mբ4MD0j4|)< vD*Ls3Oxjŗ(fE[ _+U冦aR#j6WƄ0&YE اh\ٍ߅h=[zExنq[W֡B6?ٟAlʸNzߟ"6XgòPNg B-1?:6t=݅Ֆ2>1U=avD>p#=$q~\ˠmF,qѼlqJ'v▧CڽJݑPGЄڛr*1[mq[ :hon^>)؏e 2돳q]/KܶU-[T]wF5h{1ZtwD89- 9[~t\c-j>ƞ32t]gO鿭{_Xc+$@$@$@$@$@$@*3<-k}}p?,Z9Ff^L|>">KJ|;)bށ!{‹).P2_wC_cr[n탊VgO MÃ-"f2/~Z2dE\&Bj8u_4 dϘehD<[jM{V-? VUD z#&4ݭtn2q"Zp̈z]^(Ó@K,aRpj2?B_& V,kze1bd!#vCG;qWLڭ?w%vǴm?:χj`0=Gim'z6ykJe r4|Ukcw>Pξ5"B4>MjOz,]\ w);D(,xs bqku|*="4ЦOFU孏TTNf̋3a&FɭuD* P^bcA[S͑26epysJIK,qL=mqN>4*FRA#sQǽDr{M7J/+{ѶM=+LƚGн^L:omS ,fGͬ-__5Ll"eQVE~U bf6bH[~6 T)30d*є߼ TZ0hS+-K{>>S]([W{U߃>YlB7jtdK-P,_g!Gr@,XXhU:^,,\}XyĪS* ny/iMǒaglVP3մ+^5/:dZ{_ ](7U^]i*\3KQj27HX )*Y6R;֞N{_<7,&34/8VV}0v|M(js"8*pK7a,mk޷ 2/(;-Ihi;݄GhJ;zp#bzGj.z+mf,[> G?k gĊƍrm#>n ڕko3f|_1sbEY|Llg~ے1(}okD<"     Ȟ@E"ǚw`Vm/ ւIir|jAvmմb_h,5$޿3:_~[ڌZﵢ)*Uj`` ro|͏'sr'ZnڒK31͞]` qwh*l&Em,F΅d3O0ϏiTJ&Ӧ#ñEG"mxE3"M{Kyoܥ E=s x9 {z Dٻ8h@[;v$׏g.T?rE y8QZܝdq܆̩4?tF. }tme))(S!l]H}ضغ@,w\bҷ?Ǯu*mI Yq)j3Ԝؗtw։; w-R|ϲr[G4"2FE?hjN#ᐅP"4d(&F>=)v#>}l$FI:g(o^ݫ3噄ﭭ<3łLV{[࿡n^ r~NInT4dqP=Vz*hM!w`Ǟ-h|X-*ץ󨸣oY~OЏqZb/FE%ɚ_rD۷ayFE 8G8]Rk& z:}m@ ;~N-F,D(ݰukS'z7nbm~(,'&Oǐorfbה?3ldܼHmQeAEU,Oj@FS.бЖlU̪UV}&Q(Uꩬr}v=(hۅι R&EIQ,<쉀eLj>`yujt%4Ȃ[1b{Ch4迒b=IoH$?a.;g»؏AT={ea߈6K|֊%^[[XaC;\W+FP4&oÎ"'<,sC)eucL14=x1MF1^ԇpH*YUMv㶘3P(_>1iYbuֿ$@M$.߳oMA1GXW-l_ Tzj7.U|~6 %;)k2q;zPc7*Oy? !,bnXc|2SV`դHHHHH& xZt^GqSLUk{|D[> 61ZRL3v`s=@A#ڎU- or\ߥ\ܻCIFY޳kz>;1S"xT e= MXS_A̧Cbvw~=> 0tX{ KѾ!P@KDeFK&3b~]#?[̄mY mo7Y޺ѥC&>PIm̅TFcd5fV<6^;|8Pސ` 7gkNXIla Bt@; ܳQ,pPfpr-iZq&O}SB8sL2rc%     oD0]68\`i`tf YӞgL.FX~|F~Yܚ*kE՜_ds$ޘ/wVRjdF lY 1Y75;Ъ&C/F0Է9*RB4ZY][uZ r}ԙΥ?(E3p뭰z6JrIcAizV]ݲrtgChAXO[ ZMв>~#IYGGFur,S`;shbvČtC.0ggFI1{#,풅.{P;&jn E09Y.śȂIhʘm2+ vKqm cI U\c*e/>2 %5\3+<[@ytjF[x7<]?ieu&/~łBǃB$a8W n9J%ճ+UrBdz{柑t\7\OG$@$@$@$@$@$@ L'ENY#)ءwX#]>yDhILۊiʒ擵<򫹧YLg XM|2%gN,jeID5X f2{`3P.[|nfMQ}{K ؊%F'+Rf$զ# 7P>8[v=[^ Mlքa'b"mC}bR\zskak9ݔ:l]×)TJOT|() D%Ij1ᎌb { 6f/ܵru93~gNs DZ@#%Io?цMºf"divw 嫱shʏ8*f+ߏ= -v>Jb #FvMWIm4fGI VO ϒ4Ÿl=(lf֜å;6]&^#     0%Y||Fdb-ˏH.aTyѺcZ ٭߮Ya)rC%[s=( kf;|'jTSp0,Z0$~J&ߎ:qޜ&\rE+ -dx2cSsA P};E3W[ -s }e/MBrLZz"XrM6L+0˥W|Yct,-VQ<#v)Wzh߹F%7%7*"C"UP9YԳnDTfm?􄫚iQj~g<@gnaεcV&%?ҷWq ˒9fѿY  ]R[la@.nlY +tTQoYOCͧP &bLJ43H^uv/ " D -B-(/L勎}o%_*y\ݳOJ ǡ=1E,l=b];/XJO*Q(ozOC ƵOA[&&ɏK\GO2Cd"Xݓ^EMt(׊H7FƷhPa|ss'm{aL#-g-˅3귵xHHHHHH̟zd =ӌcCğfs_W>n Yhb~jTpFkPsɕUcV<1xE7 ZtռDDf[976}=h,خ,"K=3{Ro+n[E`UnrĽۺP^n cXߒ$^;0Wb ':pcw'E1DY 8VJ~t59o9l%aV=+B / aSw :Ydcy]YpbZ FFeUM|`Tk~f-iAL݃kxaeՠy}o#1h,*m6 Z%GY!nq`ؼkHtYo;iBW&tnǚc,]̮̏5r'M>d;8V*qqg^% thOv'>&?z!ՒG%,- m%VB+}Jz!aWV,,X׆ZWbIEZ?gf9s$>ϓ̙3||q [R`ػ6r>L֋ެ U[G+h$p}ByRm`v}c˥vAԉ=tG@@HV`@'>3_n:qD:'ݍ+exV74HR9VgQ+KkA:|tИc6֜ӱ=7mx71}۲-mq÷at&:e9bo\ey=Y5b/ΧF+en}*+B,zx}3e9XKZ<@.Z2y~9X+aӴڶߴJkKJxIoٝ 0VY-*5oXc XI᭡58hܜ#9㥮%퐋'b?@Շ- ~/9-f cJh֍Whh\$n.`Nsj$/N/孎$rF3Y Zg?I$jeo5;]8b!3u^\5K^\|CK>Xjkm6m} cu9CNۡ~/֖;ྙ2sLbCB=ksک˖OY+cݑ zis[uU*{eR3ڦM"'s|Fmٵo}Q=˺.X-3]ܛk x2@|Q}R`ɳ۶,'){`ɯ'}ȳ+Yג\'z]-ڂ fDȓCK-!,5s @IDAT7 |pIQ3тooMP޻=a*.rKJ7Q_״N WFY#|oM8b h@oɆyl~X'y>`Sʂ((; e֠Q/n^&ϬǥL޺RbT޸۴)MO9C&bjV)852Ңh#5I7T1-:]Zm:\YFwUZNZ AarɝaтSHҒx=`|&2e%hitcSU-(\ZkẐb HEWHU"Mw1W 5xj );^ӔtmRjnxgú¡?VӷK FI׵VkΑO<+گvXҸ7K^oʘj~]ăbiqIڨX+wR@n\ @ǡHݲuEsKeBxqZkZ[tV0kx)^e JӲ`;2fyHPvOO` 5ָX d:~u[1훣%ѯu-ݿI}ak0vziP%ﲶ8pՖuH*p˥8%)BIi2_'?z/ΛX oW۠{GmˋN>ԑ+Oh߇Z@mFT ݇^7 [#nc5Z8#g\yB87~U2XH&ٰ{M!׿R͛5>|l.F;DA|Y9ϗD|{w\6t{&M ޜok-传_BaRE@@HJiZ[@zTɘǮx>rc|TISE1K_gR. 7j-."E*eZ>_צC;re* Jdr"CkrvDXWXѴ-)V̕")ϻNESy[ê'vB["X?V+ZcE.&&o!ݝ|4R/%~Kxp[r]T9N{VL`h!3tSliݬ"i91At"7Q/?kyT+'9h]%u÷xU0e"yS R(БydCz]2>H۸0O=﷜ɺ:?ÆDΙ@u{g-d^!WaNrnu.mRܯ[pH5YZ$GIzl(JˑW%7]Nըe{L&tnIj==9URz'x>4ҹ馸=KecZk4p{Nɔ!r@ E<mv['&In'H朕f22MrH ϖmڔ{xlz.6FoBOzVY_}sp!6oC=ߩ>d˕{/@,[FFbzN*4UG>CHˆ 3a,פkIUѢb=i_h=J¹ -Nhݧ:Ѣrhb)-#h^(ۺ\[7m]"MoKl +A@@Zή  _|jXSkEN7_S,Mvhsվ5?1͕S:dm 4{\j Ofɚ[(ZA!}ڦ5c?u]QZkSU6n:jg˩W~X*˘E=p·|"D9ͷ=j=KַJd~`;D|->LFlAy##$C<)nׄ\цǷF]hez`{aZ2HgfC+V"d =ou:b)x6T\"&k`~`κoDm8^_մs봬~}H==/(7Ȓ"/ۤ1fYݵV\,[s6Yco@/\- %3 c6ΒQ7}O}1:c0hT~3E7F=fJy!KޤޒOL`4ZyIj?\Tq.\TXZ)[wkChSҴ9sR\j>7cyF +%؂H8WzczsB|A:oVb9 nͼ.ʒqN)dHJ9v<#-l| +Ѡ#SJ]GJ7ۜ8?jp{H.*}.Ƶ2E Z%q\krkM%zO#StzuւxjɁ]ڏh; NmN J[dc[NT1_9G^]O ӸN}[ۍ9o(=u"7O{k=.sv-齎DOoq-;崞75Bg7z/שr]# Br[@@@_t?0/؈+)/0%ƺ@@@@@@@ $ݔ}wW       |m@@@@@@@:9+D@@@@@@8 8}l+       @ urV       q 0q궞?Yg@@@@@@r?lp ;y!C2͐NU1d@@@@@@ {@@@@@@@ Д}Z9I @@@@@@@y@@@@@@@Hr       vv!       Vi$1@@@@@@@C@@@@@@@ Ib       =x       @Z̧@@@@@@@ {@@@@@@@ O+'!       ` 0o       i 0VNC@@@@@@@.@`;@@@@@@@*@`>$       ]݃w       U|Z9I @@@@@@@y@@@@@@@Hr       vv!       Vi$1@@@@@@@C@@@@@@@ Ib       =x       @Z̧@@@@@@@ {@@@@@@@ O+'!       ` 0o       i 0VNC@@@@@@@.@`;@@@@@@@*@`>$       ]݃w       U|Z9I @@@@@@@y@@@@@@@Hr       vv!       VAIs-n'8Gdt+`rWb}%n]SWvx>p tXd8D@@@@@@&|yK7ܻ2~bܜ+1Wi2ݞ,fx_<ߖ#79%s       Q )#˪=:ז.eur[3!cu5U<2ɗ̫)w>A@@@@@@@yOG|i l~n);ho];~eI`e+8=#       t_ K( 9(%L q ͐ }rt)^\+& 7ɚW]eA@@@@@@@$P`ϥگRrԖKVӝR ejKM\?Nl+       >A@lV$ '3_֮,U:媔QבC"9125kd 9[ڑZsKZ/GȘqc$H+Jinok's6C .C[u??!KD_O|V;3cr^5ġ-9}BǨ7۔m}@@@@@@@rOu,j}AǞK7_, Y'e_]oM@zVd/3zR$RE&K*(@\ 9E"!ʥxde8RQ_I*fmI-R)+9!uu;n뒢)#      @='- CSkc'3L̗"),(u4?] y!A‚B) e2}|iEyc Jee η -In/y!Ay#֡zQ8u;dr9_<(9$Ay#1aֵ1       טxH*vy+R~)n*)'}ҿX/_c9z>&.+j rjcW׮5j/7 2@Ru3P)}b١Tlnyc6eMdWOH6z(7/:(w3n 5D_9ٟ- ¿}%*@l<֦,̑c[cI6em_!MٷX+j m>wn40+       @zטoP=N d&)\0(o"clT<T6.+V=<'1S-tp+".FF_"p&O t3>/gAi:Qn卥Yzد="f#+H-Mb֛J4tf e,pͣ?oE35fm{жjٲN6I;1a@@@@@@@aZ;-Gku+^|JC6P_f酲Sm0?j}m4G5Σ R*ɵ- 9&7vȳ;:-hFlUVvUٍ_l^ >?]_Pę       @ю@s쩬 2x IqSwҕt?5E dzz'{j 2'RiGvf?񁽧yc#DZ֙Hc :,eR@8WC&KQi_"ŹcRN@@@@@@@%Fc>}Y2!jNLMbaIG8D  2%N/C96il:$g>4T#lu$ߧm$?JYVƸs1       @:ӹ6ZwrORK^k#wM7@-<(Z/zU*O`*En(=0(Sj֛|qg@@@@@@@/zGpӧMK<4 Ƨɝ]$b "uѕywֿڜr,ݢχ:^7vjb W\-+п|1       @;[s!ymbe''#RW;>I>5'w[&c`{vL.,+yӻ#;$֟Nݯ/AZyr}(j@+'@@@@@@@?qL曫g-*mn$?fL֚os[G=q6>!~Q($79Vc'kK7l5&7e !       @O $ɛ[_lon=j\Ͳdz 9Oo{ﰨA&{ed)XO2Yi~M7mּwFjKnͿgѺYtjϖS%ϗX.-ZpdKsyeC`r7Yмy =RhɒqZ*&       @7rO˻6ϖj[qYaX\Z^Zmz6__"akS {k="K'GODk`}㨔XKmtuT뮖:ə]e%xA])1WX![c3祖{4Jj9ERI\"}0q0jH?xL7?D88󥩽Ij+K:kj_™W.G)U_P"/yKPޛ>/U/ ?}]| ~ )oon]v& 4yo|6ٙ       ) $Uc>t-[wcdxU229Ba5C^wryyPךfFY<}m7'41_-2^Ek@@@@@@' 6@@@@@@@H) #       O/B@@@@@@@.@@@@@@@~ŲY       }C|       @? 0OX6 @@@@@@@o@@@@@@@f!       }{        T|?b,@@@@@@@!@`o|@@@@@@@ _,       7\       Se@@@@@@@= @@@@@@@~*@`~l       @ 07r       O/B@@@@@@@.@@@@@@@~ŲY       }C|       @? 0OX6 @@@@@@@o@@@@@@@f!       }{        T|?b,@@@@@@@!0(l 0@,LdvA@@@@@@@_\tI.\ /_Ny{֘7C !(21 "      \Fv#fnS.iԔg@@@@@@@@,Нy<w-@@@@@@@N$}kXWpVUO^{H߿z_9S>su|o8 _('S']#^'Ӳ\<eće+Wʍ }w%ˌ"   УW,0>ϬJ[E ҵx0NF>|g:]&cwΔIQ3]sߒWn"[NUl)9Zy*c#dώ{qĨ 2[@NYKW ϯYWI7Zyq!YZFV i8[<䎐3x7*ee %uy×O?ISm\ȟkđK 0Z9wE~ 9w7o9>-HQT#9jmGȔ&}glC-|>.O; e8RܞG䵯ϖ_F0jew"z`}Jt;1B2FRLZzޗfRX2;*c1Jԏ}}fђW 73stFo;{\e5{>`kj`>6?q@+>uȤqb?^e%~PJګ"'[wt 3    qH1S:> I7gzIC2uuu<$EWj|DoՇV%O']ku`{YsuH͒.sY歖; P_n"FkoyZꃅRښerɸ^: wEeȵ@36lݚM N֫ۓQLݫe ֛ckl9k\N}1h~KNOεˍzm2y|E~ gkq]c+Z)~j;}m #COz/?!צr(S/Ki.*   |\c³oMAkE7# im^ ۊ<݇1v<qtS[Ե7k=D:wQKq8'= 8;OkvF&;SnlEڿ 9g;u[q:>Dv>!AyK1Z`/i{ش7>Ղ@͖z>swJ-_!oSΞdmʟ|uI}r{UEbH#_-M%3>'j cɽgуFt{s օ ďnTTnZҦf9=}p\i\3,Tu<j4藯F&+ hȆ^Z׿.w *y+b)fc3;ȏhU}S1 <=-ΉD ')vKU-`D_QRtF}&iܐbډnWw|Ϧw?eLqn4JzЗRhX%=mWI-@@@@bBtul#9I/i_yy,0zU \#VY|џ9Ƅ¥RoPx;@kp&3ӌFBycvXtУ_{@IDAT/O}w .{P v=Xο>%cՃ!6-1qiݧ44(?{Van=oZFv!|[r0zP޻mSh!Sxo:_ 2?vdzc +п ٽ9jj,.kvs߮M΋дn[7,k|_w-@p?kYexX(s7xDTysɊ-v%wؤrN64}CJ"`%n%#g,ż   I ƣ^$ݳ -=%^NɅ͚!1JscG˘ c孅:DF;CD[N̾GȧÃ-=(?j. -rC$RFt䈜om_ӁCOO~BwѺM6`1݆._^ !fҮƅlrNۜ<~D:(|}^S8)pρ}cq2?t]6N:̯uDsPi>Ĩuwْ5.$$~$sσcFg~tY[[eeEע>&:,ߵ9|D71vZwgx]:ǂ699V2gh0(s%IgGZ |6coPё -2MkYZF~{=X::lcdĞMƅ4=oɩό{4ε]&ɯU^<" |FFP~R~ :PCڴVv5r.j[@W/g{׏ zx[Nw]2j_;a SJcdM Bwh3;@mKރ,g7L]e >=A&yr`FɽI u_}uwN'D]u|P\?MkGiHr>-Ays5 )=.Ʊ4lDɊH< e2c].Y@7Anbh"crZH r =*Sͯ]]v{㜄279}`r2r݉qg'{u12_kLz]F~c|@ͮk12Hy>=X S=gl${Ieh_!}GlKܾZ<[k~\tFx'CkA3?X)]d}5оf{Y>dYo4W~4fpLgKU1]/Y `\8oY?-؈RlI@keӻ<`Y6ڝm=MurN4o#9嶭" oH?Y9 ƨMh~vb7>][l-\2Kk&wwoVWE<.,xE1yoY\j̘c´] sa!N02XcRy'Hn &_>]191fh& +WJNq~BNaȌ i!+%0vҲgL?ES~c)ڇkaKZh²%T,<ˬ\Q \# 50/(˝h`>u)gk^#Z&uL߼'ܥ5bg5ҹ$9]4ZE,`d;eRN}kԬAJCU #Iߜ*ߟL40osYh|c}' zy.9$kqS=W&tpI3 eriu54ާ^1ZF 8 JD{`3Lc[`̅qp_w4g鮎|K/kG Nw~8Ig_^+,V԰z9?\fc^TkwD2 \Q,d޶4ky0k!9/IsOkCSCwjph9/_Y$F,FH1m"o#mam7zis\#9۶kNv^i)7=Svgo'J]Z쮤juw7DO4m[C #_->__*cnRѾ|#e=Ck^84=XHB[<$+J_Xu%k]6IO5Ǫ11E`2^뽘Ays.Toy3?=L˵zmsѾ5_/H 3eܜ!)wd ʨ&Nϱg,tz]u!qȅb`;T,c!HpP|X.l~}:Ckv+6?#6z0Z$Y"6Y`sf2xMu:[ [>!rAyoZ[=[MЖ0nBGNCu (9v|S-<ħz!U_# MA܅rSiuFpoT = LZu |쵶;\n,olI5GL?].RL6([0-ӭ&8n\ !   Чy lMRC:96)9.RԯS+Od8+ʧUFMz=#'6u,DNu\]-(2rzA6N`Hڂle[e\m:yLM >5Oj;̓N?߇{j F qs$_۬AryZ۶LyZ+z-xKYdq0o"ZW!9.4@N˩y*A|1ߺW.qΐkYNj޼u|8bږeQurrģ};Hl1Q27i}M \~g[jG̿b BZCqbߍ&,]pJ# ²;thI57X2\,$o p97wr7BmrhPgfh嘁cڢEM0K?mV=89رX'^SvYñ׌ZXpq>xh+rqq Z. 8W*}a߰As>OBL>6={fYro(2GnmnMywphh3_ >fhߎA9}I:^b7/Ay[{Aq>\ C[ ~K޲cb>m߼O'Xmͧylq[uז?nXݦ&tG9;`)?Y.qcɲq /Pw I@ٰW}}7ʅw^גu9qxmom^ϙ]\ߴASڝM`8k=ݹo I[oSR`p|2¯- \{p,u3΀f+dHǵ9z00̟hݹֺ .ׂH/kKgg끎 wtȧiDt+F^gШAy$ֱ-ɢvi_>3Ia@@@@,:XK5rFμu*9ztMSm/IIuuIg?8K!~vthSޚ;*>GoGkwZmuRGimy=mHq]j_k4h0}pvK , (3Bjռv2/u[ɟ*}.oiڗ9G{x#-9fk NwJ#g3@N7yd`Ho]ye40jo~v 6)4L ^=M86mG fd'3]?,V~C^Rkv۷DV;3c^:6i:I#AfgaTa,5in W'\^zL'4޳Oyʌ5] L<3Ņy}=o)$]P''`hO9ϖ:轋5b)sEz>6/Uh4?6l]rSSƅWY2|yuh/qSLk7=/w f'Zmd ^7a8eJ[+O;z iT3Z j3nX6'#`3mt],7~'K{r:veo27)+5s eWc_Wޣ0%>CM!0C4}-CYc9Gz8۹zN_,.wT6O5tM?7Cݹo#/Zc>8\m|}@^{D›'I C,:bvyŦ>B`v(z.8t -çe32zoX"H**R5tjaiݴI\\6!P}/xD;5mtZ{k{^1km|C68W I'^${ȟu*!#   |L g{6A82Ȯ55@mnEAy;"PX{~{mZ/mҾ׭lIpd=(o4C['p& ^/r%(o$3/&x.^kӋ8vk2ikgoռO5\41{zq?"m)8aU v8䖦 8cy::Unq0(9,3j#$a04P^~rB$yy7io &W ǬIeLet%Ԛb4RìpLQ W=K~]|Yad0+𴐂8glAan- :N2#S\-/X0 Ňˌ~|ǵenK*=?+f{w59h-=~sJ71N"NIkAˍJ5} pȤKa?mkƔ%j[8W占oE RSM]z£-(?yXE223L!;9Jl8r_qaò~d$u ~cLlcO X2,>=#-\jڐ|SqPyQ=tT$J)$D+NAߏ \]Pڂ GgU*DV+P*RI 4.(Ӡ )CHܙ3L&`;wםs35[͗k XK[cek=}p %}O᭲odHP^G9(Xlw\Fm@NHPJɭ }>NM]hT6Q;î-\k^z+:҃ҰsѣAy9wl!L7<8/$8ر^&@ R[ǒ_VsG Խr0$    H]ώex&ݟڣ^6@ZR$޽~sLyܓ:auƇM^`P3=jA#kX~_7ۻ*73|E>QphZ>Jk 4[в{WKIWo(Z@ αo?03#sܚ?;iѼR)7CKa~Tc?([%^ɷng @̷ S-t}?Gf^~`П/Y5zFG)t$e`89?פTŒRΥ9GS:XFXFn mKۑ-impS2X npXi}-- ,Ü'PII7}i~^Fƛ#r\>7NQR;d#aנ%K*%w1?xѠw1 CD?{p0H':͎V'QhPhh]lQ|NQ1wƬ<7VYYZq3j$Q&ÞTps/ͥ6vW~LkrsiGɫ)qH?b6Lm(s_}YG|\w6qg7\cϺZv]yZ* D=V3˞ۭxC~v|2xOaձm9:jYh ͔vKPڸ/(ߩ %]Fkص"}v=xVr\*Kjt;L`48~=d̐쯛G{k{[yrLv&8#uj:e9QnV<    @OŽq@g响?NyE5p}(0x~@ G )z}}dyz 2ɩ~|2tV9:= ;I2$g0CAg։=k-=*tEٶh:u3/˄3^x:DnƲ"rTpcsv̵:ׯ5({ K,x-N Yz5Y9ty-8p@[2:I r잙ykktCF^ercȰ"G#$h-R_hpYKzל˕m2\t{_nA-嘎|ye:jE,olI-|%Xׂ 7܂荵{;ҥSJmБQ*ZZ<,坷;Xjܷz3o߿"oܣC[Jdׁ]   |ybjtn*y^~u|佱Z8~ݢsMR4޲Sڌ/?e!Qy=f/Y?|jEϪs9iڳq}8kXv =D$(c{W%f>+v*n1k\yS?dFxKL,Lièx-x<حc>F=jMm(Lԝ!C_Ρ2SG{@~-Y)e5Ϗ1zD9i 9G$/  LrCR!ygWʍ﯑#`OwZ~M@ɀ4~H#aߧNm$m:,|5}[%iNqm=Wd 7 '}l6֚LxP~R<aw=-'Y?y&e4}c6 +Xz`[ҢkhTc3ftJZIڍQlSR8qiϗ/e_[']whz;zP|]G,XzmPN2N GgZ'ح_m*e4r za=>\V>9OxKܵSŘf9*Z,}RfA)Ke ;w9Z>zJoh/qFrV/Ǜ+g1wꗓۍe3F=o{^1^ݠoi~58AG }@@@/@O?qDd“G9=Z[Gp[2)bOިK0ϒiˤyWKvZ1zuOw['#,;?ح\qH_*=Hot=PVn3/˸ޡYG~7˻|/~4v91#kfpm˱emXe3pXMzQ{'__pC3ae-_#7eH{Iu(xblP^s~yv^C[ldIiQ>L0q>(ܔ?t=k? `ҹo Eb)2N2׹Xpt2"B\!]H'H_+:ɋ)7>FDyX䯋զ<,$תچ*u(d8++ߐgL ]Y/Hぃ日?XM;O~?*I:XgJb2ߵiby6[[QL=)ctaNѣ~G0[@@@ 3N Z[{{<S\"*Sȴ0q4ɉk1n9N1RS 5), !Q/I?:6ɻ6Iћ:Ľ%᣽?@ sJX޲×icR=F _ab қ,]gm\/S:|k9ڬӡ-t5"n 3rݴCnU`ANgخK5oi~όI'K` ֻvԚ =`Yo%EF=Na $=+'K;F֫hyLײkU Gv oW-."! oXzN}eRN2eM·@ɯt~ l0)Wo.4|aO`"Z,]漤ZaD1chOq ]gMDe$u`扭yNE{auT֚D ʗVFm8Nc{, | N\Ǣw=f33%< ^aA2(/#ycm0C2$]Bm~n9ὢ$xFKOyú_x4."yjt۟,-QaC>KaAy#ALb;~㐁Z5NuqO6!f̛LΐZ$V#}^g?' R%MOIea=^z,fױ}/F|$#{Ar\Q4Boo3]#E7FŻOh~C_88(q/WWˑ#21Y;&K:l!|BG,I7"zԍͻ6خ16ha*/;.3[Lu.e#erUh69P&mҼ0Oʨuw;AB<Q=糔~׮YuE\#L5wK3޼uԂI}^{Rw cOt-/$xÝyQuvy 攟!?Y)wO{z4uڐ߹pLx+sF,O-Ŗ›f";Ķ&g&un/ ~g?X͠aTASX^諣J7YEPq \?55˥}1}Cٟ"/~bqp({3swm:}Cλ=iŸZFZ"QLJkCtN~   \a?=eqNw.ɖM C5K:y [;Gr%iA$'zEj;]-C~m9(س+k#὚tx_hOo9^tr6o{gˡfOy]2!~TW/r`V)Zsgb;kgۂg{̥{M; {n푛a\shF^0gVK"aW/YgUzr7,_z5 I ;f˧n:9:]w)kڸ$ !4`__@ʷ.I~ zrC[HxύQE^[tWea('ΐFy-ГU~p5sy; 1pjfٸ)i]1//_|'ڿs̷<.'3K,kٛDuooٯcڠ%XïHWu/nּu݁h}\{Chv ^pGsRs)lyXd3z~ھk߯ o5x'}|nG{6o^;[25yf˵Pۨ m(K3ꞖC>3tkOoamnRg^YZ}䟤;HJ^ J&,#7͓b~Zy齢usw#eҲt׆ϗIPlwḋ,י2V&w[/~*:_*תj ;R.=S|fs׮ fݾ9Xe27RYk}1wL񡶽qwm:} omعI7@@@葀S2v!2ɮWP[4[O Z6mfЦa4/'UKL:^xq_:N{zNaKt6ئohO{{h7kP} k1}E輕&9l|-R8q\Y2on&{**%om@Bu];^S}vA>o3:(t.Z(UP晧ǻ( &ً&JhxI>_fȢ50 =fAR6=#m<vw1p> >,4@\Aqq. }I ?jTƻx-~gn|!=S%k ^*7HC2 9wKO?v8|R&Y< }] C>)ȗ'Q_G->ybrfZf]/ gaf>6l#g÷1sO,{@&3Cw]4?wm]gC4+]W&͖^0q,S{i(+kqDO=SiiPylt h!:#F>S+ gВo ds#JkGxg+-VhL?F߭ZRh WvU]!GH; MFè}+WIU=!KeOS%Y_<K~g{_m5RD>Wt)=1޿S|=+b>f/o\n̠v_܉׉C}:teObK#^+\#ཛྷ\E}Z?AȐiu$$^llAy3ZjIZ=_ң>rzcKb߂q``է/˱ߖ"{ű-Wt.ƓbܟMKz}nn]a,uOM!e>l-{^ή;*&OHe`>"7o#mHQaD뭥Aafh#g1qw?ݧ3ɻvF YR:+G Z3Kݗ8``Y:%Ss~4bۏF\& q~?JF4?dNe~.R-Uligk[;pwf!K*Ko)eⳖmc:@x^/kɔ9ìJJ1H@ce;˹JZY?#m2(`\I:N-'Խ&nq`MF crt0Z{l[y[ջD Whe\Q*&-mt4;#+7ۊ$윷jsB7-R.3 Y{`qelk^XVw}DKel^F 4,JҺhAEeH{{ ;a +L gPbo;M/[eo?;׵"FCGhT7I}kĒҼ1 ?ZCM 3{}2xHJԸF^/_,[~tG=5&%mXR2J4\yE[IWGYYz[.@Gx. Z]ݞ{ŞƬv]lzNXl@@@@ XGaMkƗlf:oy ~$wnd\(B^˱~:k 0ed䍙-o gHvM?]ȃ*14[jyL= f2#Ww{9":ɝN4[.DžS\ n }̊;leƔd/Z Eoi @?.|['.:u"߰!h#)wHhZG:!u k Sk߿d=\?7 ?}F:/uOʍ?2:@IDATtu2|hpDs%8GoiC_ &(oI)Qvvc]-LR׈u0xln>{eF&5,W.p"˸dV ko,M+u=2_YIד1ҍRn)UcL^'#_3/ߕi1z~~GFl_α1J~W71(oQ/_hhJwmjR Si,3jui|KAy!e՚:ϱ=_F ^lxf2L&4kH1"~_ 1IޕG3˕!5Mtݷ&TtO%{[ߒّ<]]1>YvG?>Ck7u-,qFVo{oL׷dD4_c+#aCʌdp o{Ds״+wmsYFz-(x]Z<"   @oXz9ڛwoO&]{p};s՞m92KE##yyCc)J;3us%7+Ru>(7{pk]!Hsdj;_}ǔ*:7m^hp#6Fn52AZ'j?(Fʠ|H>XtW'9:qeCe8Rt1]/ݐy# {Qy[{{|k.}9"1ug뀁97xĥ | ]ϟi.Yz=n&q1>sQ?"F988/V;^dwʐTl>ni7ᆳ^Ý=ζ/N{F gJ%{L;emskd\ߴ׸Щ]D-+4Y}k"A׷Ϥ׽->ש$pMS뽼VILH'-תJJ,w3[k^O+ٞu;6Xo量.ؕE][:W;@@@K\#|\Dщd#XXZt\}yC;6JSZIlIjpnQz[Jx0*L HB i9yJbJGE[z #/AOzI Ҡ%U!*O K@}K{c -4D*؞k}zqčxK<;ꓦ{*GR9߷Rh~&{LZ0MM˵*޿Ҙ_Y}} k.ESj   % ' -*6l_(   Tm4?G@@@ 7=+kD>v̙)>,3na2tM; c@.ȉIۣeWbdߎ6У`g@@ \rUUVZ{ϐkKU%h   @񕲿nu{rbͷdbpX@ye.82ɝ#";]E >c @@@7**=2ib~oB   \6r;VmssscgNoL iQ}\~rV        滺 G       E'NKQ"       z;KOp       _#Vn̍yDv4 x<$%        Xe@@@@@@@ 0$@@@@@@@HF|2ZE@@@@@@@ II@@@@@@@d'EZ@@@@@@@ 0$@@@@@@@HF|2ZE@@@@@@@ II@@@@@@@d'EZ@@@@@@@ 0$@@@@@@@HF|2ZE@@@@@@@ II@@@@@@@d'EZ@@@@@@@ 0$@@@@@@@HF|2ZE@@@@@@@ II@@@@@@@d'EZ@@@@@@@ 0$@@@@@@@HF|2ZE@@@@@@@ II@@@@@@@d'EZ@@@@@@@ 0$@@@@@@@HF|2ZE@@@@@@@ II@@@@@@@d'EZ@@@@@@@=K\.w4 &ִq= M2@@@@@@@K^ v`KZ(7){O.~$Ky"i腀?]ʿ1> {FrG       @IZ}5]U['Ƶݍ{dw{{A˚$}ry9%*v49!      L v`~(X_lC%W{w*&s:RBn֌Gs@:2%@@@@@@@|*?ٽ5= yJwM[ʁ_z\-g2B@@@@@@@ !yGDYjsuLw^~fސPeH       M >2 7.1xDJM3K>cr:>!W -F#Vxe:       @߁\F}se۾?| Egd/|*>ddu!;O+dZy GRYT/2wC;=Y;orwPG;GU6Kf6Y+67[]]V-aj%{{Lyge·Z2qd dd[cuj͒¨g-uVɜ"g 5~:[Uu6#      $#w({of7j_u+o݇垑U|yLV%Y5JjbyXWmqTCJU-w˶ WHZmH] QJAy=rKNe7]MFo^ko+u")J"9ڃF-u&Kg{-}f@@@@@@@_ $׾{V ,k;;|枟%ilv:]}}Gȁ⟟KҬC775?C߇.Ry]J,.Wl׺Ի\.YZ~l<I/Taxw\ߨF:_Ґll}@fN^ff!Z/ͧ\RW׏eV+ki  K@@@@@@@ q±̴7:!5XXTXRo9g{L9|0#[%ml7W5|tp.Y:{p#'>Qb4g0J:?{92*3@ְA{2 q}b}—FJ4P_h++       )$&LW䟌ʞ.lσ.(]%wM^$5:b"aY)1,_%uh;?Ǻ\|7~U.9o{Zy\S>%p/++o%C.rt5GV egn{ר")RsǼ[ҧwꀆHݾU:z?UzR^^z%SGZd@@@@@@@ YK9]ʴ5 ^á>%uvg0WKV3(_!/[EJgdKzPO1~/Og1l6iuQ^m]**@U5g!7TPɟȥ8R\V7U_Yyi,z7>=SS#V@@@@@@@JzKa_|޿{3eTxO?#( 'Ç++fP^+#u/%6ߵƭ~wПX&&p: {e;vJ!cXz% [?2b@@@@@@@@JyY%}yl4]'޻>`|3<qȩ{,G]*978lZz|5Ruko)M`Gl^+KY*~cIy%{(/8+@Ƭ        Z`^RkKTn+m6C˾/Qخ77J:S8v[VX׶OE/) H^W⏼xj-K' ,9C7R7< Vm[ɮ#Zr#d#sǻt(V9`6Dok<9~Xv%{_Eɗ       @?H10/2zkLh%1kpU7:Tgnqjnmϑr ʉqHwVsYrv}$u[JgI@7 UkZVqWٸPf=a+\X6^ 4GX/\=oʪ[YakdQ|JkdERz?ؠZNj34j3|O#0`k|68uKCy+K7`Q:.\ i,yo$Y_uʽAii4pVߚ\%Yg s2_*'       @T{̋%@L rUa} */I)1_+"sSPlܜĥ_eV=Cy`kݫ| IO Kez9uKWjw׏zE VzXu؀       r.}.@;]84جúaxA]O{h40 ~mLp(#Gr!       3"      dT|F) @@@@@@@ ogE@@@@@@ȨrS       @ 08Nj       Q0@@@@@@@&@`q@@@@@@@ 3Ma       M|;/       @Fg@@@@@@@v9^@@@@@@@ (7!      7s        0Qn C@@@@@@ox@@@@@@@2*@`>       3"      dT|F) @@@@@@@ ogE@@@@@@ȨrS       @geeIvv 0 A@@@@@@@K!%ݝ1osrrʧL̎       с݈T{=Y@@@@@@@= 3|}~kq       @Obq#       @ScO@@@@@@@ KD@@@@@@@R 0{"       W|\"        @@@@@@@@@@@@@@̧nǞ        0       .@`>u;D@@@@@@@ D$@@@@@@@@ u۱'       qM†8#CR t_<+AF@@@@@@@@609.d}v*|]yLgW?dFsH^/_:#d}Q90۝Ҿ2 m}($3~4ISbHy!9tˊȡʄ&zPVn,gĢ[&? f)3YxAμיz. zno?(I?mt`       U?SCD8 ,}C-Es:fx0 ,q AQ{VbU)39$DK2dNbP FeCƒ![?ͮ-ߜ=2$abfv `Y:~@>_ia 92J&&̌G@@@@@@Wf4}wҶ%Xd3/ȤRW ] ;*/Ńe[W}QJCŰm64ٺ+C׏1LeȻzii<       ]Ono=.݁z̖$] \[3,[i,/fN;j_q;p%awq $n0'(rB NѰ>.9f9ˤxۇd        yFP=ܧg^Y1QSQ$C'7i7}vc1ŭ\g^k[%6P"WTJ[<(E\2kb[gz٣,r#Mг2Y6̶%fUm)y      M"ck:rur, #hw|d\S8$|QvϹ3IIi?7c%mO12LuH/kkQ_'7O)l~ٕe($"$/z|2CȒIpGu fc[rHn1X@@@@@@@X ?c],lpq):Mi\Xʤ2:ɡW_H晵)S2EOn=j 4Uvȭã|x>XnƤ>/Ce "-8ʻZfC        D e&g%+!nV040 ~^l۽=-#U7yWm-ߺ9ZF oO1%\XȤ#J?]»d_1T<9ɽ?z`=|m?vٖ>۵!PG;FFݵ"zG/       @1x`I69pPa^Xy(s{AR52`H+ZӞ,γwsY#E7sm󍛥!NF=>H0I^ILx1et޽vx樂o_ #|8KQsX7ٹrbI|R^ZB/mg'4~2IZ@@@@@@rg?=<^p\[1~obJ~V<Βd)i^%c7/mqypx*4džDaero;?SjCm))/g$zrdZdJ۰ ǥپO>MI֛       qxI{g 3g&/-= 9'T%.oHwo} z"4YsV:)lIpPN}Ci-dl[Ro!t nO3@A       p do&ݟZB{v{T]6})]!yU$HN(F_#Gn:ycuZY`[Vkgec->Cy~^BYd/Er͜y2: 2G9       '̟O6v3Ad_PLF&}"@ TwLgY 4?ҫ'Ƒ]IwɾkdRwCzg[,c#c߫̋X)[L {3ҹ7%k f@@@@@@@S-{"Mzity^/)PN>K*f x-rʝo/2WxtZh : @@@@@@. u$<"'/YIiپJ\+u8{]:>*kޒI.Ŏ#@}{ q&j/ƺIWcU2n\Qb5xRytm$-cLJ2-K|o-/>Yst#Ad)zs\[&}84      }/A),GT#'W #P^.J=KҝlKɛ:D}G~ⱦ[FnaoRto"Z52|#       P̵ҿYhJw݇妾2Dtu O7]+m&Bٵ/Ȕ %r}T>#a}\G@@@@@@KV 3=CZ`gyxÌ!êoxb^==%L-2Y|/svruzL={Ӓ7QE՚*)m/@@@@@@@߇]`=@M_'gȳߑ+-!ґi,ANn%J):Iƕ&iYdF*A=,aB㒰J@@@@@@.gXs^/,2 |h}T͹[Z4[.\X&=T%}D).?I 9|@5Eұl%/3s\<0Sqծ-c-Gf6ςVCfKqI       @ wۤ<;j ˹rAr3/ȾWuΚW\]X>ٻ(-@:F#w08$EID45nO!V0O$`(6)xOR )S -/}~:s3s \^s]~֠/ĖA{.&>fז/-1t?]] @ @ @ 3jS!:0}Mf">~Z|gx̽87.*_;3~/|G_3)zձy~L<3 @ @ @C8#Ou\WM>YJIs=k6ƁpK/ɅYGUJ/8Θ7}4.qa+ @ @ @ p :]Le[㯭J8.L\IRLx@ @ @ @L8m#KQ4]tsbrӬh0}),pkGZN bҴ ;z  @ @ @Z᎘?#Jk @ @ @ @v&OTߢ @ @ @ @` H̏g @ @ @ 0$GU @ @ @y#1 @ @ @ t @ @ @F{fzL @ @ @#H@b~=,]%@ @ @ @' 1? @ @ @ @AKW  @ @ @ @` H̏g @ @ @ 0$GU @ @ @y#1 @ @ @ t @ @ @F{fzL @ @ @#H@b~=,]%@ @ @ @'0hbرc# @ @ @@7;41ȑUU @ @ @G@IDAT @'Fr&?7yzL @ @ @'W͙gs `L&3P1 @ @ @ @@#Kb @ @ @ @5 H& @ @ @" 1_X @ @ @ P|`  @ @ @ @@-h%@ @ @ @5 H& @ @ @" 1_X @ @ @ P|`  @ @ @ @@-h%@ @ @ @5 H& @ @ @" 1_X @ @ @ P|`  @ @ @ @@-h%@ @ @ @5 H& @ @ @" 1_X @ @ @ P|`  @ @ @ @@-h%@ @ @ @5 H& @ @ @" 1_X @ @ @ P|`  @ @ @ @@-h%@ @ @ @5 H& @ @ @" 1_X @ @ @ P|`  @ @ @ @@-h%@ @ @ @5 1,6ݴ:6|jAԗUS~t}{lz`AXWgǺwƏf5tt)1{k @ @ @T3ji亻z.e䮧cU:~ziFd~^xZg>YjdM8/M3@k;G3ѵc{=LNK7\sf2}ʥÞGܵTWKYZ4LԍNO3ψ *4n\tT`aOWw\iXĸ꣮ddUJIt{1eZiL~mdcO;cg2ufz e}+bػ=nUvƒߩybљjz ǔK,ԟֱN7]'?zyi).Cz^}z^=ߧa'@ @ @ @(IVTJN]fukqc5*%ccgJt63F;REsYtqRlmAcۿH,ݿem{N{5әseo6=wk+J,U]K?QŚi,%-|WOMk+?qKcsO;}Pl-bU#Uձ cZKX֘㿈W5'Ģ1ls|=7h}eęDz>T?paZ-fQ^jmi{=<\K79TݛrY4 o獼y @ @ @NS}9| 4ۚ4/_̟hn]KO-lG.pJxIʷE/%Zk)ʖ~IAGjon|k˂bd7ӽ,aoʀ1@K4Ig￴?8^V?6_9,)ߔYPfّ>pOGؽ/uZcť?y]#q˓Ok>Zfx-eIw2G'^ 9%团=+ez[nW @ @ @8'K&Fi7!?T|cN> P޺+]ݔXT(B\j߭G5~!^_]wkPccJ;MMu[gtvfJFH롗֞v=&+^ٶ%Vߑ( y1<5S+3^i'r>Q]=_Rl:)䋯[[|LoݫήؔXvƆ AnK)־IS,_ jI|Lߣ;c.yStJg/{r<;xᙕݞNdlnbsյ);7{, n}Җ](a @ @ @'PSbzz,>J-zpK}A4F+nZ[$W\cW~'$.Hkgso߲6^ĺiN~`F,ԚXsSԥu{kHnbEiMvҵtt}|S2zMذmS,z׌SWbmtֶX5ߡƒĺҴ.fԧYҲ%,A?+m?X]^hpH;yǶƒMQ_\pK<6w}/?8XOz~~gQlx4ɾsek,Ko]kb9o.!ÉyXتXXċ?  @ @ @N@M{Y[3#?n|Q5dX񞧢$aٖϕ\L{4&Kkw~/}:yzgղgbFzRo5ܴ(Ӛ)'_ybUD5>-mc1cj|o/W?^}Mo;JGևt~*-o0@+TXwۢ%$ͼULLƂݗDUv(}PĢܒ|Jļ\'-4f @ @ @3B|=p^LBe_=qGݷǞd[r-,ES%3Ȯe"K?2\мMz/;ܳ/4^ Q=S]Syo.]clܒO qOß>:(yý||0A c fĒ?MӹPeh%Z4]VX !MaNÒuˉzϟ YnZ>3ȿ  @ @ @'^` h૱gjZc[KF_tlߑY=;s-LAҩ'Ѹ %ӷvM#Ѧ lFLwwt{v;wƋ[IA@@.ru\<]C.hSrfąPqSL(> U ~4l{h{⽻EC#߁\ҳc]ӎ(.F;mSSlM bƴE}NМF? @ @ @g@Mq5?AL)UEfL88]?͵iq4ܰ~tV;6Kۯߝ_XRI=Nװ]|ik~zO>`rnp_ٮO|X>ٿ=zb}_26nj qS#KN[ @ @ @<[~r,>NJ* uOi>}ohWbCӔψ]㾻8~oyNmn(ϤoS<+b}scUjz=-]_k۳nMz͜h|n%`c,6SΫ @ @ @j'Ӵj]KΟ:u3ٱpoe.W-.nue\JR'l]4] " ݯaۚ*3-=2V+WEkW[ Yn Ew[oo^t|}M}<=V @ @ @ 8Z[߷:"e?Od?-$bIJkf ŏ+$YbFK.1=%n[ZwoYUH7ݱ&{AWQ̼t%WiÆ nkK?\u=Wf/w? L<ڟXKֶ/ͱ8,QteOOםj @ @ @ho细R"ҘWMwĖ+}[r /|OUyȏ_zR>Dǐ;=P@=w4U>(S1ձ(Gg+st?h9@R>սus՟{ҩ~+9 @ @ @Nue^tOP@›}o4 ^ycrZ}=/1־fZȟG]cTk\.d~!>w_~ƀUP<ŗ57ƃ>pͱ MsGẄo#6nҭ‡LOU*9O{oQ @ @ @~XJx7,hha)"X|"V_X}MŎoFUiC _禮-UGbmcc{`+Gl$^h{黚c폫|>JX yS٭Xp42ű4%~2Cmĝ7Xx:mqtW @ @ @.0|mϴjߘ5k7l`wtgߒm |̻PN/ݰ<%w2dkX[x~h{ߴ>uO)[yܸcwwO DEs}S.lv{\=kxgoQi~v=tdڸqV6{oݙ>/r瞾w/D*#}ޕhD&=lZ.j淭UtFhԦX0@(}r#Bgz7ϥ;xc,X`'3@K'҉y_X%xTD^} @ @ @ .0gIl}fJp\h+u=)]?䦴yǦ*ݴ2nLXV'c=E7~aqlBZeYlhr9{zsc3+=5'}pOSl?L^wO]b+) ^/<soZs9sM-O*~+|o |]S|>Y=_g} y;[ߩچt:s"qSOWܷG @ @ @iڈ\7~gaJouip*w-ۺX/|me=XVjh^xCߔ'%-x[̨0X091^y:rskjkSjGM-KbͳigE5}/ŹϯioW~>U bErzxQW!1X.]А{[i|mS3o\q}ZwGijscMz?W߽X~Ud{kaq%q{OF>(֍_8*S gK;nBөҦ[i  @ @ @85j*M}_Ŕߚ1Аט\]8.H5lӺєlHI!Wbv}{pvsݸ U2eqNmeDwߓ$55wԡHAީ!M@ο.=|F|OZ @ @ @@`*ʥ4}(!@ @ @ @g@MSٟ7o @ @ @ @L?> @ @ @ @Y#pF$K @ @ @ @(p$ϏQ= @ @ @ @$]]]z$Zsza @ @ @ pRNoJ~RaUN @ @ @gT @ @ @8{$g @ @ @ @ ?. @ @ @ @+ 1>[wF @ @ @gt @ @ @^ٺ3 @ @ @8$π  @ @ @ p H̟֝ @ @ @ @ 1<] @ @ @ @W@b} @ @ @3! @ @ @gug @ @ @ pH̟A @ @ @ @?{;#@ @ @ @3@@b x@ @ @ @gl @ @ @gC @ @ @8{$g @ @ @ @ ?. @ @ @ @+ 1>[wF @ @ @gaԨQ1v=zP @ @ @ @Bرcqȑ8~g٤q$M  @ @ @Tl<;6hHy @ @ @ux#Au~; @ @ @dH|1?v% t|\~nq>8ԑ=3gјt?v񝷯' O(L{W1+xB~m'G;xt[g*:'&.9k OV)8zl}ty/Hcsߋy#xvqGok8>6 @ @ @ 0|S4EvmΊGzٓ^xh僕*n_wOR>[9x]d6?D|(OKo^\듸=ToR>pjgFßcGNʓ\xU3 @ @ @_S[sqUѸ+:wkZdy ޾}@6[֪wⷒogĆb]zͮxp+⭍3KQ޵ rKŻ֓-&^jp׊8^X~v_g/ƕ (K |O>-Va{8+x 3FYG+[ Xj6~Pt1ys=ehYoyŤ|~3]swQ>?[wRL|W*K{t[Wbrޣc|T7g?0[I1y~z#@ @ @ @L$ Yv$Sr4<4Ҹw;'._1̎N# {Fo4vŖvsDc1yI#,D]ؙMKV޺jRġ}<6J9ńe y-o]+'V>ToطX8w&<_8p.x'K?obx[OW7H}qbCg~IHl5%q֚qOљ?O>(s5NB?p켷ڇZ>]m/IzWĿt<Z)|7{ύϬ8EL.'?3+[Vro  @ @ @@I*{Ho(~R޻B u&Ņ% )IZo.xqgS%||=$?">EMN}kcޏǗ~8^177{VJ#sỸ{?QLj_4Og8lhw3c|*K?WJtn٤|SHQmy6Λ.;GcGgv"h(D׸|eK\sLt--z{o6'58Py)~~}řq_d"'NkɫHN0tso-& I3@ijw.O'oh7ū/=?OIhGv( -v-%KFWJk;'jV+:G @ @ @41?pW_=ɯ^O|2MhTsDlKj|]+ u˾X%kTK)񭔘?s9r>Dt>;$㪸=Z)ή^?ۯҔw璺ωv'矌;Ŵ*S7oWPOS:9M”Z;p*P{0vN156Lxa/7.>s0=ޑuO}.榌npY?$f xo) s-,G-CxƮ gm;76d /Z.»Si @ @ @Q']G*4zz%Į&w{&6L/MnZ<>eYoƱO?X:xˎϔ'U49-!?6>-PH..Ɨā_HԾoK ~鉒|M-'ΒJQ}1[Hj"~kb3Y<ݻi!w԰od-_-1n/ƞWar5b$)`Tü|mC8{ @ @ @x=8\r`h9ȁ|jgKϗ@S!}~ Ӎl=0=7DmI>1iz}ہ ##E> ?8IN7WYOY]:oZq{4VbTCKYB}VwߜfT{xX]! @ @ @8IC$$jB4e|09b͗ @Am};^*TV|6  ;4PX.97U+}څwb]ǗϏ _U>[?4^Mts̯=v8b kpD @ @ @I>I ڱesblgb/Ʈ3}?($;ZqA~Fuw $󇙨>]2D{Y?xJI\2I=h{wrlK>bxN_> @ @ @ pFDbndWJ9#姱9wO`桯gɖ-y1x.51!GQyA4\lu %f{/Yw}8p|oJ?"8Zz02Ϩ NcwE @ @ @d |B}]RH}`N\r{}/wow>@l޺_^$^)p&N=m_vD'!US?g{MQ?ŠQ#_=1hBxusqxYX {'y _i9C`T{9#@ @ @ @t :!?z4~?+:)-%\k{1~Mcg\RLo}.qhPߤ|S𛦐/$ͻNA3+JFo\h <󷋧6>/ueEo-vyc*'~#bmfot YW{Ie @ @ @FHםcsVfcqqO>x}Ċt3d}xxiSFx$ѵ.ys+]8=.+|bce}]u_٩AO.Kwҙ fsuoCK voɈ[>'~b?wC{ @ @ @$$ړ/|EaɗCM1eyBޝ#7DOߙ{ON/ߧ@u?|.~5SqIC%?~C|d=/ @ @ @#K M=l4xӋMǾkeiLJ6L|*ѻEϊwšޘqYѹ4^zJw6>?_qdϮ881'f+gVyǛZ,L-uu<4'1$;K@oϵD4uύw|/~_t_C|'MAJ1QJwqHPoص>GE1~ܫ'3'9W7ol=-qo2}2O'hV?_}Wk1.gb)`ۣaeј2.Q%ŏS%nz[󣘳3QZo(7=ŘyiLnPxL^ܷXxV_ٸbv&{GoZ\axeްxy1y1 %%4ܣƔC]W|Go,M}vҳX9qz9\Jj:'ZүkCy%v  @ @ @8uaGֵ:{_ibrQx=3Ǧsj7{uG;'c)[TIgń.~<~B N"9ǒs-_1~›cʢR;OncNjb2ʖ]~?'?z̸4qnPC @ @ @8thbqx#G8 @ @ @8#l<;6f( d2!@ @ @ @J1_k @ @ @Q@bF0 @ @ @ @Z @ @ @ @$kN @ @ @jEK, @ @ @Q@bF0 @ @ @ @Z @ @ @ @$kN @ @ @jEK, @ @ @Q@bF0 @ @ @ @Z @ @ @ @$kN @ @ @jEK, @ @ @Q@bF0 @ @ @ @Z @ @ @ @$kN @ @ @jEK, @ @ @Q@bF0 @ @ @ @Z @ @ @ @$kN @ @ @jEK, @ @ @Q@bF0 @ @ @ @Z @ @ @ @$kN @ @ @jEK, @ @ @Q`LeᙃݝWq8uꣾ>Nшz\?QW\NS =b:3:3aJ4xE{ @ @ @HfvH^<6ndK]71ujtu׿H}am墳ĊO7ŀEd:be-1ղ{ĭV߳ŽggW]4pwL @ @ @\3;6ƍScՒYXuhnlGt|'?<.R=)֧cGZ⢆?@FŔ)s)i @ @ @>b~Ƹ7N|k4_63pcCcզްώ'F[sCI9CU|BWƭ[̜:%Fċo[3 >bU-'*^hN ]9;ov򕙜%@ @ @ @X7u74F[.tǚxS LOGmr.:eDST{vk5mղݱqIvԼmS#;!41}i  @ @ @ @l]_HMcIl M׷Ǧ㨳XW{w_͹e_{||6>ZZ+[r)!@ @ @ @*07B'ݽpHӕ7ݰ,tψlf?WGGBw2}0byiAzݽ{{t'jƤ)]Pmyo8㢾>#r~OHu(}ġ4TȒM1ۣgSti_޻'2p6ϡwO"][ɨ;SsW{Bm:oʔogDÐASݱ}[gܕ#ۧŴgƌPx?z}P @ @ @N )lWR"zwK׬y Cޘqc5*%ccg6wgW]4H??7E>%֥$-ӹ9~|aZjĂWw,JwygϭH>_[_OcI=U+_7b7Ax}ؐ((0!%Շ5{I92֘l[lWNJ)z/zpSzcMuse b?Zszw:?3#VAK|n[k>('=w|myܾ25V>xƴ*?>% @ @ @ @d >їcK6o$yL*v/RgdhIȍG+˓M-iucH+m5Ko׸*>ו?]ksoR>E4NG]}Qee!潿5ZUF (h雔[@&8g&团:XYCm1؞H_qOIʷ=v\R>[ؓ_J'  @ @ @ p]C/6ήm4|߄ҽ98/Qke:b-lUr[c>(%Iy[LfSrg*c cA4@XSg(^xly[*NUߩo%i,Ͷh+6}aI45 5[u]vun*-{c[)S6dG.Ne_\i^ /VB /'_k?JS8ݝfLXfLXgD~~!d?(Kiy(K @ @ @7 0ʛ..9tCv(\vFbR>{.rWb۪N%b^ƌ|} }݌\xk޽szvăzro~bG!=)M>OV;#]~[i)b%G]vܽFWrC[bӟ. T=$XWφOs[>DZ?G/>_Z`Ql\ |6q^{k,+/^Y|lUz(%c#Ku @ @ @ pjJךfN^H,lGr僚7ĢQo]~ru޻})o_ekpVqօZ_;;HZy3kM̍6DkkNk:Fi"ڮ1ڿ3~P\Ȧee!ϝiڲh$l?!@ @ @ @@5%k~y_C-#n\t_U]}a 0_}SV -߿Z&y.GXpWt>!VޑrIXӱ(A(uƴU?Z5+;-H#ڦ qu4ĒkŒŢ;i+9E @ @ @$pr<_ '"=%&Dr}H RI(VP"z0X`EMR  &@ T d _0{왽疙m8ξZg'^kՠ*ꭘs]諽rƛle{/лĄkͽٷa&}n/?txC0w n)X6NpOe}^*sxiVp'nÿrg0A݊vrM2Fx\`!G),w/uzl{){4LOGI5s{VN~f:eKM|t\(@ P(@ P(@ P@z@K:z"Kt:"ɼu)놧# ˭sg/+fߚ}Qυޑi+oHt_MAc22@ ̇U2o8(@ P(@ P(@ P@ 4ev^ ~a >SJԁC{grg>{@]g?wXM axY(/W| X9 4}-<sKD0>!bşи,[ۂ^~3#g^j^a8`&`[ѷ]YHh]?I*&^Z;p=m P(@ P(@ P(@ P+p?37-'!i.~J\Y[-S[ȃΝՈ! x)2|9v6|eB_3[a:Z5z{/1=(@ P(@ P(@ P(@#0WeHЯٿ}Iݶxr\;wS^%wk~Mmߙ$Koec~||;ymX!ԕens^6n .O#XS2̾Li6y ,ʒCoy nZf>!#U܆n<$VwYz9깓Qv}p!UݞtSَ|~fIP~كH"棠D Q{*4}COeD}6#MoO ? +P-r'[W2fPO?{sr{~v+VgQrضE(PgG)@ P(@ P(@ P(@ 4I<$=Sm{>E~8WwND }_w\Gڌe2ՎM犔9 v>2)K¤2| b6;3~ۅ4w'Q__s헅xjX,ҟV7Y"0- w# ~+~Y7gVszq` G^"[%>n~s~4 Jye(/^Cw錌W "I(gNt+W_9Hp¬e[FCf(@ P(@ P(@ PhmC灳h=f=P^WOw݄RF(SIl[7H4^ 8?uU۳./LJCk?wcij e=2;}za&mq[zqҽmCH|ֳǑvTb!?r3ӑ鬿kaFMR0hr(@ P(@ P(@ P@S1o?K^5>;JRg}}q -ݎ>uq~r-؎Kk ramcj/|t#pY#<&aGຫ~_2t:i.[ #| [Pw ۿn-O_DDklqo*Q5mko[6cݏlwB/k5wOaJTẇ&~2? P(@ P(@ P(@ P-%pɓ'7es2|x#ѹ3GJ VW!1||BtDϪ G^}l;;CDuq m~NsN)RξKe(~[9RF#eftҾP' szVJ[z6!k6T@ pLXGP^)Ⱥnz*G`& =֣dsP$`gKGq(v(<u(@ P(@ P(Zgst,ɀ u $ʃ?hL3VZ6R.0N{6ycK.>_V;.c}xlK'$q)q-ytyng ]gOgk+È_9}V5Rs4a&^-<\rKq 0fOݖ^ʻG>|f'1 hP+WNT,<݌xn} ye܍ݟėھw$Upݳ"mT "/wՒ`ڵu3](PV:j6:]b 8O}i5wvXdݎC!W =-,T&"`*؃J҃h؃9m'j}O2Lx?y9 F1 'L}Cˣ'>^ٚB;CP_'cx_LUثvFA?X`"&mR<+bjk 7~KK0|I 0 X:p>imJ3?(@ P(@ P(T'2 O'M[mۄ`PUuG;ޱ[[?z~N#*q<2MGcYY+Ƃb-@ :o^o0 :o7tt6޳1qqUf?j. 11rEF$tֈݬy,QwއEb͙4 w8{_XOo_|ьVzvͻj/ԓ(@ P(@ P@ 4f?_`6XPg+Qoc VD,`3, 7ǤԎI[H:*Y/ :[䤣TǼj .5$AGDE5pʺ{{b}Vyx 9O+זB," q~Ie[{,u a rw XXs9^H=Z/Y;!={'[_g6*~Ի o>^J}A &ʫA@W\ԩFrO\%1۹ZJl+cx 0'ş@[^ Bkw ȡ*ᤄ$yS0^ÿ CdE}B=16;W}eMO&?{qJ:kc%9sH={I8i쟜Cs_n3sk)ؾ% CΧXt?o ,$Jej P(@ P(@ P 4O>0,عi>*ڊf?v@2|n)=?U5&!g"KYVf8_JTuU(6͙g0Ϟ}P[f۩-ʱ^SQQerVV ('`H ~0UN5GasYH.k*ϫf[i\0In_3֡0Q<(q-dbIF#zH䜕b5(>ogܐxpTkFV^kUӞ^)"FC0K}\%wkvV+6fUQ䙩,U J4uҀGⷓch_(G uOOo]KtMys{yn bG k5%!(r5Qcft<|S3wBAcet2"Qvp(b^waX/Y~M@'׭\٭+ UW1*7nq 0'a@%bȢ|s+d#Ic׳Tk{b,|c![]gGd%l$tk:nk*Wvtlxd{Dv>R/QuB&,SmFXQ?>F3\D9`>1؃ioi[)ozf7wvw]Q oo9WӤ5S10-YV=kSq<=IǞ7rQ󪦾[[E/k6Qtf!EsxST`wz,V~un.kјrńҿmņ(\űϨh˭ {tR#>,~$sJ2C:K(@ P(@ PBC%Ѷ2_QC}TVU102'Jl=<3{%9"Czbkxխ*&#b|l%fjvVTċ]w;-bRV*87UM *q>(Cb*+{b`L?HIxШܾum?K$^bl\rŮuxtW1%+k׾[9!9l`MMڹZU: 7P[y@GQ|Y~M_"G^<jU *b:]7܏iYȚ Ur0/wiMshtw sf{kku[nt3~p v ;8 yf3ѿKLۃTfyUm }aqco=<(rN^m.+xőTYDo/A v=; UO{ y฼6@ v~-G\+u(XfBV+K[{]2"ePG's%ZǚW';|{=b,ǩjY5]hW$0.P-xT"4KAy%IQv:2zCd/9qPYWנ>yވk(>%͇e0^HEѣE2/@9"w~D P(@ P(@ P@# &'AyyZ"aFt>VJ[P^[Tsvf)?Ϡ2K{P^~kPYYjǚ`z8 hנ3_%{M2,?oMݪ{PX7EPz᩾L݋6@&ShԎoAyMK}IjY`w-Dz۹.Ac:lxQ"rp$ Y/?]އǞgk#הU7y9XOiR9=9i4Zf ؒ,r4^YVx ;'ߩ@CFdh2lZV=CP>,=GQ}?QNuMkPޙysډ!]oӅXjK|\RuՍ~|_kև= .wd+ڼv?ܾtW մ'k#q&u"v0wez@ P(@ P(@ P% y9=g7f.23"6U6F؂={u5u02^3Ad] vܼ҇ 2RѯW,"d ;7úP[;ünjĒG}P]1,+%wiAQ~rN~~QbG"{ˋx*MG~d,\.,[>>RYP4dD)-gb=<^ʱM;;IWG/ܢ:{|URS{18! \ՙ*_rfMUXP̽Y ֽRggbrJ=BXak)F3ŀ%)QCwVpF?M<a.Aj5>8~&ϭ s&|E|goFo{1hj­֝QpWo'1֠<*KEyzFHivʜjfޥH`>=0&0P(nrS <{T [>!ǡ3QMOōC%0fy |O$P#4Zo5bj 3=?USТ}'b J`QӮZ**@ *ǾSQxzNsWbCmU.Be'ʖAf :>)=e뻄_?CSc1ѱ=$czڍ2rAigqg(@ P(@ P@ Cm]:T Y#Xo0D :&jG +CkodeW!*,Ifm̿6Qшyɕ׽uD/ju=b8JeK`^ǖs&#C}QF+罂#b-[SbXW}L^F9=ʭΎxY1DD S+ [b,<(lLJ@^]"b6y6^,.)[Ҽw^&=*3_}[)49\%nDZ{P^)KzT9; Ԩ6P#} qwgu9@k]$z&!ˆ2G:wJ֝w.ʽ\]lqse%qyUX$pۥ

ď;1w5&M %@IDAT?1v7CWlt ߧA<(@ P(@ P%$:Ck8usRWfHƒw97Sz~ob;+6 d6:G!|&(6'Iꆼ߶[ ʫU蛆pd+6r ԼnĎ@.2k4y TDܘyv^G4h]c6(7cdњG`h?_u=Q٣KPkxG*t[]߹ ]TzJ.x f}rKP޽@=g>:z!3-ȃ %r=zEr]#^嵥{+Gk.Qw>N] ոQFk]<8XoOAyk݌cj&y_4F@X] i>n3ߊʚFü'|U[| NՠI;{ڑY8^Rf!k~>QKhdh'z-3AsFy!g]߂^8ss(@ P(@ PB&0cy:8#XCR_ݢ;)~_SY;}b l^¤F<|pW>n}8B4tQZ]d#%p#53}=9ӡĬk@13tj_nV*g9pt,E u_ьFWRl2y ~|gOy]ptMi 7{4"n}fh˴l#,{5F 4@@ R(կ:t\>}ǁtͺ#/ g9ڵ6`Pwl 2)zM ~v䱮b1s&"Si~7Ɋ sN)@ Q\\[\uuպow2", sd2LafK}x l]p$`d$m7~Ma P(@ P(@ P>M,92DdYΙ^A^X3C>^Ό-5 W(p].<}F`;e~}TԏCSt5g E{ۧuJc|72:1ERW&Ǽ(RgB&;0*G%׷:u5 >JnՌ=X@2gޒC1=|ߔ{KP/%x)8b1sˡ|Dqk\8^ʩp?ה@Y=><^J. fL Ŗ9F+]ן>֓/]?/d.Px]NqNӠolɨ 7[I2[_K2ȫl]ݥMYMܿm+%/ݵGwl8_S 4|[A;5䚣`Tfd)%2l? TaC P(@ P(@ @%8̪͝mm["G5' EmK9O;5kZ(@u!}r{5'x0W_f{yW'`+~J;f+98XT}ZnGa! (|J]W+| ?lae~tb6Ri_%?mcmKzE<vs6_3DdXÛGNYghYc@;U}*z_T AwK8z?Wӵx%XzR,h bZ>$ʦ+N|y[wVN[7~!ukz~8jKKKQ5P(@ P(@ P-$pe TqcYH9K~sp{Az"R:6K;ՠ5=ӻz{ӆCn~V?{ ^ nd8S]79a+z5xWW tqܶRmRB+(TLVhz46]cFMGX$fݐ#uR۴QnZ6w[T|i^5ҥZ2ZG]5f.ԗF4cI .z1nΏ 8{N܌ Wy˱oGq]a\(@ P(@ Pڽ@ӉI0S.b' eXJ aªE#w0؏.[nTjMqqor;2_%#W[ W ǥ Tonƶ?^~2C/H\utNVjpdÇjG ĘCmW2DX;ҸGcXt5Q{{FD" gs}ŮoϢ/®r ѠȞɺ.ނf$<jGQ3{K TpOת3YXΩ[2UCS qٻ)%H^alb᳘t~ssL5p(P {񦿗n^xΪmqyZuI_ | 4Tk?7u~Ln쓎}ŷݡG=*_mcFNg? fwUZ<(@ P(@ P(u5nDD#ΙXL](m)"MyBYۚ4 ݍ`a J y&ۧ)暄4啠@͡iҵꕺޭ;Q q ]e^s;T֌("wylAHeyc_2싲4TcmQ I zJ=ݼ-ꑀᣆ[_q1;N?Ҩ a٢-%Zú<=k?=!E0b{yΌcRѯn/yYcO P(@ P(@ k סx:*E:xVRS uٴm)Vrk:O#5ԡtkq%,Pl9T`}ƽ0i# bkR:6y"c/M,ǔ4 _]^ `&"{:#1}Df$f=5*URhV;ek{Ƹ#{~ba/Q~~)s>96co]6|Z-ovJAfYGWt6}Ww֠1c3z [G\ئI_3U-m>#X9=g=& h}Xٙcooi!r(@ P(@ P@.aFӷLx#o놮Q2-F@=#T3Hʋy+5\GDH'*t!kY-c1ũ(ɔZ $E{|4`)[QkFG\3_谹#1Ҹ[Izm>uRvlL^ u.bØ,|8+I GqmI{y#"9# :vy)qԶ{g3]f1ҽ>aC9cpq ^] ׌G'(3M5Kva"S^R~GTz[ H4Wcg$Ha},A\emt\?eDu}6=aJ>(,-Lo].意ĭDۘ nu1S᧽;F6@t\${4n\A8|]"*ғѠ.Z}w"o|5zc1==}qBudyUyAy| > Jԏ2Вu[*#!er?տS2QtZF#٬ܳ֍r4 I*v>rvd7,,Q=|:ؐձrstb;ɿ7\i[xڋoTw5F}4Li+5םno3~szrQv ˁkRLPR(@ P(@ P^ DzZ$(Zye|ܔgꆳWxAd&oN )=+</Ò1䚭lRlDxe*,MKAKSí37ۊxi}4AoggJuWF'ȯ }]'i^JޭOe`|HZO٧I䌜8H?~8BGCK;g]zK-%Aѓ9/f_F̣H5t; cWzcK>(wsiǩZ{G/偍p޽~cqgkU9.y{L$nzI܂r0nlǯ$f!芰m(tMP^v?`K8{g-s`~u>(/bz LҔUm_;!s,|MZꯄ] pB\wQ7ByOw>(̛=r7:>ץV}ᬿL_Yt r,H'Āw_zxB*Vy,,u[H(@ P(@ P(@1 cts0HoG0W/$+Gw{IcfbGKc3-ܖL}d=6iCwIJ50Ay4w;3lzY#ևaTL&?p{ S)@״?>N9j1 g2 ;ӹmg-{7 ʈ蓆ш!#ƽv{ 3i)oKiX"l褽~ϥKۥSmRKޱkDDC Dp)K.A.|ݿہOz 0ߘu1j3.! 7΁uh}5~c]C{(&ԗ!axv+,},0WvDdp7s47 :w| Rկ)ȃ+?nF챚.2ay;p׋m~1+<Q7`cA󐑧vv׷wKF[:'6KbCWoeo/])$zyWzEv:Gkn#&<.e#~`9$I}bapMy0&ϘWbӪ%;؟i}f1vwKUE"6͹Sz˛f}`1j?1%>$ٜv\(@ P(@ PtNmPBN *(.8y5ۢW lvj)[y WPrݔ,*F^IGڳ5^Ȍ`POEJ9+[Z7 wf}5Q]%ߵB~0Rkݎt/Bc_:j5?}Ȧ;t0>4;ꏀI3Y,{@/g}PC78. P(@ P(@ P Bc(aU(@ PtyHM?:'I1]D\@P%+gά ^-/rVk(@ P(@ P@``]_^6(t^B$M9+ӑDU ,i)jP%~hf:&ĩ+C?L*'Y>R(@ P(@ PR olTO <(@27UιB2ז㥻_? SR_{c}3fDShW2<@fWM6^](@ P(@ P*11l7(@ P c?U)kfˏU~(1I[irb +5M/ij_/P?q(@ P(@ P(p 00_v߂ l (@KD fv..?FBёd.kCսfM(xuRk#{kAN P(@ P(@v#ͥdC(@ P$`6Fc+@`Z=Cm(@ P(@ P@ p(8(@ P] 0(߮.g{k 튲=(@ P(@ Phʾ}\G(@ P(@ P(@ PBT0(@ P(@ P(@ P(@ l(@ \N24Z[Py휑ш2[lr/lO-r)@ P(@ P(@ P@`y(p UCI%TvPU?_Ŝs9Z4,)Tf8r%{(\ 6(@ P(@ P@``}__@h\<ũ69 iXG9ŌD!c/c1Io@=+@Ns5K;=K$WёVjJ=}ƍS^jWe`H]wd|LLFiLO}9H s[z+_<B0`->l8nIPws;< P(@ P(@ P腻䪭;q6+L?S6"_OL[b:s5@@? ; (Yyf]n嘪]QZjV~~1JfzolBiLQ\F蟬9',>!\iFr(@ P(@ P@`vwICA 5cudnja+'12]Ghϟ)m?ectGC8~ 0YVEȟ-Wv[0mߥYtPk6Zg* !MP2}|Oa(:%rAkyz6|2L 6bq}w`d={,*ŗ{cuCP,4& C4"ڲCHឃVdp6\Њ/5~/J+ց(@ P(@ P(@& 00TBm&XM ֪S[Զx˯QeD+;ranFtnlH;>*l=.M-K)G{w unNb-F\*-{X1Sk{?>S-N=}v^ a{.e5yQN[neiw[DƑXf.bGA9`_>qRFdwՀhLw~]~, /-7;އa]uxcwlW(@ P(@ P(p *$Vz}PQ蟌 w"tL jӨgak|14Acf`31v0xBq O(yuz°kՊ(f=eJ@x9#\Ue/gaA:G-Oב$+4籥>~lGMq!%`g\U˰,;Oo/g.{3,C{]4u4Blbѕ%stEkի^Z~Ҍ{㥥Y٬Ӑ^p=ic=V`FfmHI/oz ssvS{-BknM/L8(?qbogTK?Y3zw*u ; /?SMkgX~LF8K.'6/vfE,@?o߉J9铤̵6 Ɂv@nv?6>WJ0`)@F89r]@6d&AM}lsW.;E-)8+@P U*`/)؁x,,<2$8 P"pfkH ̜{99 9wzcם|'u{>s?<wܻyםꉏ'/r%ͽq}fO듑 7߈w/ڝiLfӅM7Şs˾9cߜ?[1m2 @ @ @D`˗UXfM÷ԱџtGm3.DةȖ/MɫFNFˆ2%ܶOm;W\r$Ƣs:sqU~c}ؕԳ3]Oy]1ֳb`]Uw}/%mXvEvU0 hCQb˘OEKޅhq߁d|boh.Yvm7qz%I?:y)S_,xF{-y}S͎ qxVO4t? P~Pڪ}1]sSn}uyOդND{`DHTj2c(3bO%kjnx<س){X53\6J=qG?_doךkOtǏ qwf8^C*&nx{lVΗޙZW=x<6B*cλ>Jy~q~[sOA @ @9 q$g㤖7/{bG M^~t-O-SUѮ;7OhHNJ}Ks6k<SZ)9ͱ땁)'v-ړ_щ 軨]w5MP>߈#/{Vm_;-ՃF?#U^w\&(oֻ-s _noMlXӟ_wOնCo%ʧ+'RA֗ZgXok!v PI~wً|R|SĻIO׃uw/}>hlYv6tgz!ڞޟ}#c//Fbp/7P>@1ym7cI`HgHˇ7K2.fS]qaxl}#oFq3zAL2xp0Gbdx0 ϧZo4l`Rnlܹb\KNzrܭ$ٝL^Xs )Kڗϔþ:7 Q'\ΒvgR3'bR̨FNGރ}78bS݅,ko&k$M6&/S&Wzb]w:Tb8?r-zWWwAC?ឪi??ꍖmſϞ #7k߃}a*.~jW~f$KG".P?O1֖;sf3|σ'::?W18zh|Ol^v8 [>T>ܛ,mTDS2 /AssQ,FG$ojذ-Nylhj!֯MXMIqˢGS{[oXllCa_tp'gT˳o&˳z3ı]i=[&m_JǙ+*꺭֍x3 vr}-ݩ2{J 8O^_9߄sߦ;NtE˺L4M?Gg_ ۏHAsRLSxzg4ɺj޿ڗ)QN~5O=~aBr3ov[fߛ-q}LSBs<55u1}>K>/_Gə﫣1}O.ݼɻߓti;׭+6{ϝM3~߻'o<'n;moٚ7훣cS\OpBZr?+ZQamQ @ @n]3jI~'\TSo{+DVw}xk5yg2i|vKfμxmWjsljÑjJǶ9=Px-*J<⓶VJ_3 q>og=P\|2M_ ۗL[\HvX^_?J?ܞr0ڎxpJ=SfyHAH]Bs?4>8ܓT4"k]ggAa!ߺǝ'3ոi5̱xn n{jCr.@T{6Ǧ(F @ @( նP[.tБb\ҳhVNI978>a/T;}Y+ŷ4Dj,NݔP>Ma˙OSOӮ-{$@0m!o(Ӑʰ}K= Ẫ;㵉173wu-hvfAWRYm{'f?|ft%X;m,sdqj+)̦{I=PpR0S8jjwY.h=Ga4> wƧ>8H|$~IE>UF wfcuM96GޱCWNG @ @fsCG'1,I\~Yb$?jpa4z{j.J*d؝M7т;&ҽG|zqpaZ,lX-uJg,9y Cc4݂dpG/F|xv~r sPe/MTxglX{ۍ;KnlĻ8q|Lc0R?O~LR[|65d=QMK%@ @ @ X~: pO9z}tJnIJymz.ƣ EZ<%pJj4N<9zXC1م7ҽ:x]gu[kL4~pNB-=U~{k3q ܸ:wH>ju*sgJRۋ\~2Vo_?HL1V+mnq[lؔ~яc_&6?c߼Ss[Q1-\AY`V27)VH @ @ @lk-<=OnT>&3G#[1</'S,7)9{lؐ/ӣ/LY|u(|icWڃӡCqgf`Wo}h‘Z>_4a|8kcݍGν%.?(=HGMɥo|/o+ѕfͱdG"| @ @ @` O7 [t)b[1T ΍Eǣ-/J*l}1hsGqx{T4Mmh!ܣŽ/<*ڞff~¤>-z Qub5P\Toܸ#5P\Du%Kf}8M^0:::2S2WnՀVN:u&S)={l{%'?Gۣ;ܥ kc_l2sDsPNs2E>O8܏E܏IV8 sbk{m/)t39~޻K;\؟/OO BW%O$0@*OcoǝoM>2˱G?o2zM| @ @Mu׌-슎ޏd[t?4uSK9^,5:?:{#=IIdiw*:™H2;#[w[u`z&-nbest c4~ EuMYtݎg4ŞEvyPלN۳It1~nd)m}Oղh{p:oݏWߡnPFݏ?F,޿H6O6s2+ۘZ^<v Roҋ{G{gx@*P?a݃q[d$2qB>/lOGOc!~=ۮ|#5{)=3q7n^$@ @ @iY8nt9]-\2n"@7yB Y~/"kK]{opd+,pOGt~4mJ]Ҟ.D{wEs \Ė'NwEgIz^+vUv5dص6y(G/:Ti#y`։əWݟ̴|r,iWbZ]eNP /̮ZgەҼ{j0Yxb햒Pmr_I~,gT-p8lol}23w%+r;fGS<Չ߉CqGqg[pocqW㝱a]!̛1~BKt~U][ @ @T`˗U˼f͚joc7-K"|8 fVHB%w%~\ƒi!zfIfol0bUJ7$dPͬ;O2IC3_U/)3v%q9)e gu[s:Ϣ{t~5o_ZS5gP$?@oSCSy]Ᾰg[Qc .@?[M^ZoXM3y]^ūs}:[Ģp;?;;Џ:c헒WW|/N=VSt NO o @ @,)0?'6 @`qFH} gz/ L~.y0ga>y7Ag|8~2}_س/nqs^~y>/ KV_  @ @ Pk`RC);ɢVɬu2qۓ˽H~N$x/뻅 @ @T@`NF @g[K)#qM\Ǔޖ,fjѶg' "I @ @} @@#hbwW3(j2I*ⓤ+AyýE~->]h]1Ψ?+ Hm&Xrbo @ @XHK/  @& dbwT3%S LaP>Eo:(p à|~X7 :# Ĩ`= @ @3KـhTX߾p#87`4%\i//Y{zZŽqm|hI?qL;2 @ @ @f Xf @撅3S멒 @ @ PF`KٛcU.cIPiKLLs  @ @ 4,e4M  @ @ @ @`/L @ @ @XKsܴ @ @ @($@ @ @ @) 04ǭb~x(VX11tbֹ:-cǃ;bG-[=J @ @ @ @Xqkf͚j'cůž8;[o^I`SF%-Nϫq  @ @ @ @`TsbB=?싩{TL&ڒ$@ @ @ @RR%Y9{7T{ @ @ @ X%ٰ3|pMO,3_"@ @ @ @ cp lɸ'1z5 0Wjڪ& @ @ @ qW7DC-5_-Zʪ @ @ @[@@u)_/?3>w98F鯎F[=qs;'Ѳ=gr[ @ @ @N\&?W-?rFϝl{{.W> @ @ @$ 0_If):6m+3ᡷmS @ @ @yV9M =FGbddo,F~fdЏz$(ɸ02׮]kLs] @ @ @ܲO}oWfeWI Y{j_QýަBs5E-"y @ @ @@ -,CT(7@  @ @ @ p gdj΍͖zxi @ @ @ @}}{SggJO$@ @ @ @ ky:H䮦6%  @ @ @ @`^[:YYi @ @ @! 0?Rtdt+f{"@ @ @ @<灷l{Ru)q8- @ @ @" 0_V9vǙGS۩ՁsGj$ @ @ @L' 0?-paC6~>bOL:G1I @ @ @ @wWr1n$Ğ>>Ni{vmNsƾ/}Sy$  @ @ @ @`F3bL#V|jE_I5wWxH'3}ݏ;essk9Wt @ @ @P@`~P-CC3oaLֆ{ƙg[}-#qÒ @ @ @ 0E@`~ X[y*o~"c]qbۺX߼ybw~ @ @ @TXqkUǚ5kvl9 $hhh445Efr> @ @ @ @`3Ϝ)ܚ!\ 뚮o  @ @ @$`)9)D @ @ @f& 0?3' @ @ @ @Ħ @ @ @̜"@ @ @ @sB @ @ @ @`f3s @ @ @I@`~Nl  @ @ @ @ I. @ @ @ 0'9)D @ @ @f& 0?3' @ @ @ @Ħ @ @ @ʙelrgmq\ՙ)sW.F) @ @ @ @r_n#Z'}#+'`Pn @ @ @,QK/сf_8 @ @ @zD&v:cHr,|6RC @ @ @ X~o3$/]~g6'5 @ @ @/`c=$@ @ @ @(`MįS_MZx5wrdV&sEY @ @ @X~f/1m&-;;@JypGt̒8IcŧVĊUbկ%w~{ǃɑj]A3 Wؑ϶ow`&@ @ @ @@"תIYan@''b3?cNBѷE]/HB'ZVEԮjM\"*e~+6~ۓ6Tox$}ޖoũN$Ƣs8c]'vFC @ @ @ p ϩfω|%oJᵮ:i>=@Td6}:c݋2sqw&S>>%wb_?P&(ݭRh|Ģ6  @ @ @X&Kr ɽ'ϟ.Iᣩ};c۔)hX_<}126~y-ŵ8s`$y-߯|qغ[-jڵdߧ]N菶 3y]@Q6 @ @ @ p]@`~^ V<>lwT4HQGu˳Iک؟nh7fbCcEeG,|+'Ճ=6D4}5N +P;po! @ @ @ O5Y{Vo=Ozn_|ڞLFG&"Gۦ)§r&GGjgO`jΓW/F#oOںd칧EӶ8lja=1A< @ @ @m_=V8Ao;Wb[4 _(Q=U.g#±S2~=;.6%  @ @ @ 0ChgxFj&ٴ#&uިɮD6֧FS/hOJ'sLd .DZ&].}ݱ~KC^-3~e&r?+i.WZC\ @ @ @] ]s;DHt댽˳n2F֦*'2mONW%<4E=+S}#.( @ @ @,e_Ne k޾3s83ܹIKW.۠|edt`f.+efϥ"e @ @ @̘_}ط;荎ښ316xՈL8m5 @ @ @:A]2ሣn뉮"}0^xPS˳\i?`ɻ .J*y>;gs92Z>V>({53~FuƉ7XxzK&@ @ @ @@kNz*\>ڿZ8Ѷظeb}{h63HQ`?*ힼPkOڟ' 9\tٶhmʁű]V!E @ @ @ ߰.j>QsK6YGTph}PhKVLd37ZhX'e4v{4=[ؑ<,ƒ¾I? IrC1Ͻ1^  @ @ @J`˗U+f͚j\HSgwc\52?[cWIн% mGӋwqrwsΡؼI`k􍝌lI[Gy!hqM-Ѻv0OW>˱Ƣs%%5_y=⥾=J*n- @ @ @:hP&Zx6x٧; s5DNjOkǧ[:{7elWO>펆{:kwIs=EѮ.'JML`K-?8̡8r3+'r&@ @ @ @gU׹~5-%{*lkk}qxwl{ۃq/ƾ'S旛D> X[l&v8g^_m-O<燯ϭܥBUeJlWޮ@K@T~f1IC @ @ @X~.j˽L.CF!yz)VI&m`ŪX: I3'u&%K**6Au¦ @ @ @_`K ^  @ @ @ p 50o)[E @ @ @y7ޙ  @ @ @ @Y  @ @ @ @ <{g&@ @ @ @[@@`d]$@ @ @ @' 0읙 @ @ @n[`u @ @ @nͳwf @ @ @oAE @ @ @y7ޙ  @ @ @ @Xy q]?>gժhX>5̻^ @ @ @ @=SsSGOu}1- @ @ @H X>1іm}?M)y  @ @ @ @Xqkf͚joc3?r1ϥ}#"k|E @ @ @N`xxxN}[]%[_:'<-A @ @ @O`yKZ>r i @ @ @ P/X"-/N~?'9$ @ @ @ @ % 0˜M2i,dRHJ @ @ @ @|ZC @ @ @X@`Ơ#@ @ @ @i4 @ @ @|-@Wעu @ @ @ @rJ6 @ @ @ @,-UHQ$ @ @ @xd6F g7ehԊ8Cq⭡9֬ @ @ @,'/_VCk֬v?޿X8y{ @ @ @ @`I ϩfωFK]\{  @ @ @ @XyK:wn#:t*Z룡!VM$@ @ @ @[U@`~#{TJ2= @ @ @ @ Rs .Jn?) @ @ @mP`BZ @ @ @SsN^sI  @ @ @ @` /= @ @ @7  @ @ @ @` /= @ @ @7  @ @ @ @` /= @ @ @s JnhLaK @ @ @L OR"q?>3YydZ @ @ @3H~kstn{mjK @ @ @VR^wF6~Fo4M @ @ @̘Ʌpu4U.(gL!@ @ @ @)`}$+-Ehh $@ @ @ @R/_V3f͚4 @ @ @%oKωM! @ @ @ 039E @ @ @$ 0?'6 @ @ @ @g$ @ @ @9ZB-ZkGg"S?-\^ @ @ @@݄szUc%m @ @ @ @!PKǾͱݑ[ZI @ @ @&0Y5NfzW]2C8hQm @ @ @ @ ת5t͚5/رw/c=;#`gT1 @ @ @,0<<\`#u3cMtěO̜/d @ @ @Թ@n[w7.z|C @ @ @@]#*8JK @ @ @ @!P߁a @ @ @ @|E @ @ @ @o @ @ @ PQ` #VV @ @ @ @.;05mv<ޖ&@ @ @ @/Pׁgё2lP vH @ @ @ @:|dcK7B󛲑=WĎGšo#?8[缚G @ @ @˗/_f͚j7]9~lg~,z:"3 @ @ @,j.]g&;7-A @ @ @R`e]*ըkcsOwŞXߐU͖/H @ @ @ @@ w`>/we$56ͱvejS @ @ @,:G 7F|C @ @ @@]s ol] {G)E @ @ @@AϽ"V5GvSDRl-;m @ @ @ @SĶR  @ @ @ @ O0#x*.\h @ @ @,)/_Vk֬v1 @ @ @ pK ϩu;c~NQ @ @ @ԙ|  @ @ @ @_^7 @ @ @ Pgu6 C @ @ @K@`~y @ @ @ @@ ـh @ @ @,/5zC @ @ @u& 0_g9 @ @ @ x  @ @ @ԙ|  @ @ @ @_^7 @ @ @ Pgu6 C @ @ @K@`~y @ @ @ @@ ـh @ @ @,/5zC @ @ @u& 0_g9 @ @ @ x  @ @ @ԙ|  @ @ @ @_^7 @ @ @ Pgu6 C @ @ @K@`~y @ @ @ @@ ـh @ @ @,/5zC @ @ @u& 0_g9 @ @ @ x  @ @ @ԙ|  @ @ @ @_^7 @ @ @ Pgu6 C @ @ @K`t. @ @ @ @@3+M @ @ @j! 0_ Eu @ @ @ @ `&@ @ @ @: @ @ @ @@ 0v @ @ @ @ZBQ @ @ @ @|  @ @ @ @@-k @ @ @ PA@` @ @ @ @PT @ @ @ 0_n @ @ @ P Z( @ @ @Tc7 @ @ @|-A @ @ @*W @ @ @BI[<^ IENDB`graphql-ruby-2.5.19/guides/subscriptions/subscription_classes.md000066400000000000000000000255621514115062600252120ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Subscriptions title: Subscription Classes desc: Subscription resolvers for pushing updates to clients index: 1 --- You can extend {{ "GraphQL::Schema::Subscription" | api_doc }} to create fields that can be subscribed to. These classes support several behaviors: - [Authorizing](#check-permissions-with-authorized) (or rejecting) initial subscription requests and subsequent updates - Returning values for [initial subscription requests](#initial-subscription-with-subscribe) - [Unsubscribing](#terminating-the-subscription-with-unsubscribe) from the server - Implicitly [scoping updates](#scope), to direct data to the right subscriber - [Skipping updates](#subsequent-updates-with-update) for certain clients (eg, don't send updates to the person who triggered the event) Continue reading to set up subscription classes. ## Add a base class First, add a base class for your application. You can hook up your base classes there: ```ruby # app/graphql/subscriptions/base_subscription.rb class Subscriptions::BaseSubscription < GraphQL::Schema::Subscription # Hook up base classes object_class Types::BaseObject field_class Types::BaseField argument_class Types::BaseArgument end ``` (This base class is a lot like the {% internal_link "mutation base class", "/mutations/mutation_classes" %}. They're both subclasses of {{ "GraphQL::Schema::Resolver" | api_doc }}.) ## Extend the base class and hook it up Define a class for each subscribable event in your system. For example, if you run a chat room, you might publish events whenever messages are posted in a room: ```ruby # app/graphql/subscriptions/message_was_posted.rb class Subscriptions::MessageWasPosted < Subscriptions::BaseSubscription end ``` Then, hook up the new class to the {% internal_link "Subscription root type", "subscriptions/subscription_type" %} with the `subscription:` option: ```ruby class Types::SubscriptionType < Types::BaseObject field :message_was_posted, subscription: Subscriptions::MessageWasPosted end ``` Now, it will be accessible as: ```graphql subscription { messageWasPosted(roomId: "abcd") { # ... } } ``` ## Arguments Subscription fields take {% internal_link "arguments", "/fields/arguments" %} just like normal fields. They also accept a {% internal_link "`loads:` option", "/mutations/mutation_classes#auto-loading-arguments" %} just like mutations. For example: ```ruby class Subscriptions::MessageWasPosted < Subscriptions::BaseSubscription # `room_id` loads a `room` argument :room_id, ID, loads: Types::RoomType # It's passed to other methods as `room` def subscribe(room:) # ... end def update(room:) # ... end end ``` This can be invoked as ```graphql subscription($roomId: ID!) { messageWasPosted(roomId: $roomId) { # ... } } ``` If the ID doesn't find an object, then the subscription will be unsubscribed (with `#unsubscribe`, see below). ## Fields Like mutations, you can use a generated return type for subscriptions. When you add `field(...)`s to a subscription, they'll be added to the subscription's generated return type. For example: ```ruby class Subscriptions::MessageWasPosted < Subscriptions::BaseSubscription field :room, Types::RoomType, null: false field :message, Types::MessageType, null: false end ``` will generate: ```graphql type MessageWasPostedPayload { room: Room! message: Message! } ``` Which you can use in queries like: ```graphql subscription($roomId: ID!) { messageWasPosted(roomId: $roomId) { room { name } message { author { handle } body postedAt } } } ``` If you remove `null: false`, then you can return different data in the initial subscription and the subsequent updates. (See lifecycle methods below.) Instead of a generated type, you can provide an already-configured type with `payload_type`: ```ruby # Just return a message payload_type Types::MessageType ``` (In that case, don't return a hash from `#subscribe` or `#update`, return a `message` object instead.) ## Scope Usually, GraphQL-Ruby uses explicitly-passed arguments to determine when a {% internal_link "trigger", "subscriptions/triggers" %} applies to an active subscription. But, you can use `subscription_scope` to configure _implicit_ conditions on updates. When `subscription_scope` is configured, only triggers with a matching `scope:` value will cause clients to receive updates. `subscription_scope` accepts a symbol and the given symbol will be looked up in `context` to find a scope value. For example, this subscription will use `context[:current_organization_id]` as a scope: ```ruby class Subscriptions::EmployeeHired < Subscriptions::BaseSubscription # ... subscription_scope :current_organization_id end ``` Clients subscribe _without_ any arguments: ```graphql subscription { employeeHired { hireDate employee { name department } } } ``` But `.trigger`s are routed using `scope:`. So, if the subscriber's context includes `current_organization_id: 100`, then the trigger must include the same `scope:` value: ```ruby MyAppSchema.subscriptions.trigger( # Field name :employee_hired, # Arguments {}, # Object { hire_date: Time.now, employee: new_employee }, # This corresponds to `context[:current_organization_id]` # in the original subscription: scope: 100 ) ``` Scope is also used for determining whether subscribers can receive the same {% internal_link "broadcast", "subscriptions/implementation#broadcast" %}. ## Check Permissions with #authorized? Suppose a client is subscribing to messages in a chat room: ```graphql subscription($roomId: ID!) { messageWasPosted(roomId: $roomId) { message { author { handle } body postedAt } } } ``` You can implement `#authorized?` to check that the user has permission to subscribe to these arguments (and receive updates for these arguments), for example: ```ruby def authorized?(room:) super && context[:viewer].can_read_messages?(room) end ``` The method may return `false` or raise a `GraphQL::ExecutionError` to halt execution. This method is called _before_ `#subscribe` and `#update`, described below. This way, if a user's permissions have changed since they subscribed, they won't receive updates unauthorized updates. Also, if this method fails before calling `#update`, then the client will be automatically unsubscribed (with `#unsubscribe`). ## Initial Subscription with #subscribe `def subscribe(**args)` is called when a client _first_ sends a `subscription { ... }` request. In this method, you can do a few things: - Raise `GraphQL::ExecutionError` to halt and return an error - Return a value to give the client an initial response - Return `:no_response` to skip the initial response - Return `super` to fall back to the default behavior (which is `:no_response`). You can define this method to add initial responses or perform other logic before subscribing. ### Adding an Initial Response By default, GraphQL-Ruby returns _nothing_ (`:no_response`) on an initial subscription. But, you may choose to override this and return a value in `def subscribe`. For example: ```ruby class Subscriptions::MessageWasPosted < Subscriptions::BaseSubscription # ... field :room, Types::RoomType def subscribe(room:) # authorize, etc ... # Return the room in the initial response { room: room } end end ``` Now, a client can get some initial data with: ```graphql subscription($roomId: ID!) { messageWasPosted(roomId: $roomId) { room { name messages(last: 40) { # ... } } } } ``` ## Subsequent Updates with #update After a client has registered a subscription, the application may trigger subscription updates with `MySchema.subscriptions.trigger(...)` (see the {% internal_link "Triggers guide", "/subscriptions/triggers" %} for more). Then, `def update` will be called for each client's subscription. In this method you can: - Unsubscribe the client with `unsubscribe` - Return a value with `super` (which returns `object`) or by returning a different value. - Return `NO_UPDATE` to skip this update ### Skipping subscription updates Perhaps you don't want to send updates to a certain subscriber. For example, if someone leaves a comment, you might want to push the new comment to _other_ subscribers, but not the commenter, who already has that comment data. You can accomplish this by returning `NO_UPDATE`. ```ruby class Subscriptions::CommentWasAdded < Subscriptions::BaseSubscription def update(post_id:) comment = object # # if comment.author == context[:viewer] NO_UPDATE else # Continue updating this client, since it's not the commenter super end end end ``` ### Returning a different object for subscription updates By default, whatever object you pass to `.trigger(event_name, args, object)` will be used for responding to subscription fields. But, you can return a different object from `#update` to override this: ```ruby field :queue, Types::QueueType, null: false # eg, `MySchema.subscriptions.trigger("queueWasUpdated", {name: "low-priority"}, :low_priority)` def update(name:) # Make a Queue object which _represents_ the queue with this name queue = JobQueue.new(name) # This object was passed to `.trigger`, but we're ignoring it: object # => :low_priority # return the queue instead: { queue: queue } end ``` ## Terminating the subscription with #unsubscribe Within a subscription method, you may call `unsubscribe` to terminate the client's subscription, for example: ```ruby def update(room:) if room.archived? # Don't let anyone subscribe to messages on an archived room unsubscribe else super end end ``` `#unsubscribe` has the following effects: - The subscription is unregistered from the backend (this is backend-specific) - The client is told to unsubscribe (this is transport-specific) Arguments with `loads:` configurations will call `unsubscribe` if they are `required: true` (which is the default) and their ID doesn't return a value. (It's assumed that the subscribed object was deleted.) You can provide a final update value with `unsubscribe` by passing a value to the method: ```ruby def update(room:) if room.archived? # Don't let anyone subscribe to messages on an archived room unsubscribe({message: "This room has been archived"}) else super end end ``` ## Extras Subscription methods can access query-related metadata by configuring `extras [...]` in the class definition. For example, to use a `lookahead` and the `ast_node`: ```ruby class Subscriptions::JobFinished < GraphQL::Schema::Subscription # ... extras [:lookahead, :ast_node] def subscribe(lookahead:, ast_node:) # ... end def update(lookahead:, ast_node:) # ... end end ``` See the {% internal_link "Extra Field Metadata", "/fields/introduction#extra-field-metadata" %} for more information about available metadata. graphql-ruby-2.5.19/guides/subscriptions/subscription_type.md000066400000000000000000000030451514115062600245260ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Subscriptions title: Subscription Type desc: The root type for subscriptions index: 1 --- `Subscription` is the entry point for all subscriptions in a GraphQL system. Each field corresponds to an event which may be subscribed to: ```graphql type Subscription { # Triggered whenever a post is added postWasPublished: Post # Triggered whenever a comment is added; # to watch a certain post, provide a `postId` commentWasPublished(postId: ID): Comment } ``` This type is the root for `subscription` operations, for example: ```graphql subscription { postWasPublished { # This data will be delivered whenever `postWasPublished` # is triggered by the server: title author { name } } } ``` To add subscriptions to your system, define an `ObjectType` named `Subscription`: ```ruby # app/graphql/types/subscription_type.rb class Types::SubscriptionType < GraphQL::Schema::Object field :post_was_published, subscription: Subscriptions::PostWasPublished # ... end ``` Then, add it as the subscription root with `subscription(...)`: ```ruby # app/graphql/my_schema.rb class MySchema < GraphQL::Schema query(Types::QueryType) # ... # Add Subscription to subscription(Types::SubscriptionType) end ``` See {% internal_link "Implementing Subscriptions","subscriptions/implementation" %} for more about actually delivering updates. See {% internal_link "Subscription Classes", "subscriptions/subscription_classes" %} for more about implementing subscription root fields. graphql-ruby-2.5.19/guides/subscriptions/triggers.md000066400000000000000000000054311514115062600225700ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Subscriptions title: Triggers desc: Sending updates from your application to GraphQL index: 2 --- From your application, you can push updates to GraphQL clients with `.trigger`. Events are triggered _by name_, and the name must match fields on your {% internal_link "Subscription Type","subscriptions/subscription_type" %} ```ruby # Update the system with the new blog post: MySchema.subscriptions.trigger(:post_added, {}, new_post) ``` The arguments are: - `name`, which corresponds to the field on subscription type - `arguments`, which corresponds to the arguments on subscription type (for example, if you subscribe to comments on a certain post, the arguments would be `{post_id: comment.post_id}`.) - `object`, which will be the root object of the subscription update - `scope:` (shown below) for implicitly scoping the clients who will receive updates. ## Scope To send updates to _certain clients only_, you can use `scope:` to narrow the trigger's reach. Scopes are based on query context: a value in `context:` is used as the scope; an equivalent value must be passed with `.trigger(... scope:)` to update that client. (The value is serialized with {{ "GraphQL::Subscriptions::Serialize" | api_doc }}) To specify that a topic is scoped, add a `subscription_scope` option to the Subscription class: ```ruby class Subscriptions::CommentAdded < Subscription::BaseSubscription description "A comment was added to one of the viewer's posts" # For a given viewer, this will be triggered # whenever one of their posts gets a new comment subscription_scope :current_user_id # ... end ``` (Read more in the {% internal_link "Subscription Classes guide", "subscriptions/subscription_classes#scope" %}.) Then, subscription operations should have a `context: { current_user_id: ... }` value, for example: ```ruby # current_user_id will be the scope for some subscriptions: MySchema.execute(query_string, context: { current_user_id: current_user.id }) ``` Finally, when events happen in your app, you should provide the scoping value as `scope:`, for example: ```ruby # A new comment is added comment = post.comments.create!(attrs) # notify the author author_id = post.author.id MySchema.subscriptions.trigger(:comment_added, {}, comment, scope: author_id) ``` Since this trigger has a `scope:`, only subscribers with a matching scope value will be updated. ## Validation By default, subscriptions are re-validated when a trigger causes them to send updates. To disable this, you can pass `validate_update: false` when hooking up subscriptions to your schema. For example: ```ruby use SomeSubscriptions, validate_update: false ``` If you're sure you won't be releasing breaking changes to your schema, this setting can reduce overhead in evaluating updates. graphql-ruby-2.5.19/guides/testing/000077500000000000000000000000001514115062600171635ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/testing/helpers.md000066400000000000000000000050671514115062600211570ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Testing title: Helpers desc: Running GraphQL fields in isolation index: 3 --- GraphQL-Ruby ships with a test helper method, `run_graphql_field`, that can execute a GraphQL field in isolation. To use it in your test suite, include the module with your schema class: ```ruby # Mix in `run_graphql_field(...)` to run on `MySchema` include GraphQL::Testing::Helpers.for(MySchema) ``` Then, you can run fields using {{ "Testing::Helpers#run_graphql_field" | api_doc }}: ```ruby post = Post.first graphql_post_title = run_graphql_field("Post.title", post) assert_equal "100 Great Ideas", graphql_post_title ``` `run_graphql_field` accepts two required arguments: - Field _path_, in `Type.field` format - Runtime object: some non-`nil` object to resolve the field on. Additionally, it accepts some keyword arguments: - `arguments:`, GraphQL arguments to the field, in Ruby-style (underscore, symbol) or GraphQL-style (camel-case, string) - `context:`, the GraphQL context to use for this query `run_graphql_field` performs several GraphQL-related steps: - Checks `.visible?` on the named Object Type, raising an error if it isn't visible - Wraps the given runtime object in the GraphQL Object Type - Checks `.authorized?` on the type, calling {{ "Schema.unauthorized_object" | api_doc }} if authorization fails - Prepares arguments for field resolution - Checks `#visible?` on the field, raising an error if the field isn't visible - Checks `#authorized?` on the field, calling {{ "Schema.unauthorized_field" | api_doc }} if it fails - Calls any {% internal_link "field extensions", "/type_definitions/field_extensions" %} - Runs {% internal_link "Dataloader", "/dataloader/overview" %} and/or GraphQL-Batch, as needed ## Resolving fields on the same object You can use {{ "Testing::Helpers#with_resolution_context" | api_doc }} to use the same type, runtime object, and GraphQL context for multiple field resolutions. For example: ```ruby # Assuming `include GraphQL::Testing::Helpers.for(MySchema)` # was used above ... with_resolution_context(type: "Post", object: example_post, context: { current_user: author }) do |rc| assert_equal "100 Great Ideas", rc.run_graphql_field("title") assert_equal true, rc.run_graphql_field("viewerIsAuthor") assert_equal 5, rc.run_graphql_field("commentsCount") # Optionally, pass `arguments:` for the field: assert_equal 9, rc.run_graphql_field("commentsCount", arguments: { include_unmoderated: true }) end ``` The method yields a resolution context (`rc`, above) which responds to `run_graphql_field`. graphql-ruby-2.5.19/guides/testing/integration_tests.md000066400000000000000000000135351514115062600232610ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Testing title: Integration Tests desc: Run the whole GraphQL stack in tests index: 2 --- Besides testing {% internal_link "schema structure", "/testing/schema_structure" %}, you should also test your GraphQL system's behavior. There are really a few levels to this: - __Application-level__ behaviors, like business logic, permissions, and persistence. These behaviors may be shared by your API and user interface. - __Interface-level__ behaviors, like GraphQL fields, mutations, error scenarios, and HTTP-specific behaviors. These are unique to your GraphQL system. - __Transport-level__ behaviors, like HTTP headers, parameters and status codes ## Testing Application-Level Behaviors When it comes to _how your application behaves_, you should lean on _unit tests_ which exercise application primitives directly. For example, if postings require a title and a body, you should write a test for `Post` which asserts that invalid `Post`s fail to save. Several other components of the application may be tested this way: - Permissions: test your authorization system using example resources and actors. Dedicated, high-level frameworks like [Pundit](https://github.com/varvet/pundit) make it easy to test authorization in isolation. - Business logic: What are the _operations_ that a user can perform in your system? For example, on a blog, they might be: drafting and publishing posts, moderating comments, filtering posts by category, or blocking users. Test these operations in isolation so you can be confident that the core code is correct. - Persistence (and other external services): how does your app interact with the "outside world", like databases, files, and third-party APIs? These interactions also deserve specific tests. By testing these (and other) application-level behaviors _without_ GraphQL, you can reduce the overhead of your test suite and simplify your testing scenarios. ## Testing Interface-Level Behaviors After building your application, you give it an interface so that people (or other software) can interact with it. Sometimes the interface is a website, other times it's a GraphQL API. The interface has transport-specific primitives that map to your application's primitives. For example, a React app might have components that correspond to `Post`, `Comment`, and a `Moderation` operation. (These components might even be context-specific, like `ThreadComment` or `DraftPost`.) Similarly, a GraphQL interface has types and fields that correspond to the underlying application primitives (like `Post` and `Comment` types, a `Post.isDraft` field, or a `ModerateComment` mutation). The best way to test a GraphQL interface is with _integration tests_ which run the whole GraphQL system (using `MySchema.execute(...)`). By using an integration test, you can be sure that all of GraphQL-Ruby's internal systems are engaged (validation, analysis, authorization, data loading, response type-checking, etc.). A basic integration test might look like: ```ruby it "loads posts by ID" do query_string = <<-GRAPHQL query($id: ID!){ node(id: $id) { ... on Post { title id isDraft comments(first: 5) { nodes { body } } } } } GRAPHQL post = create(:post_with_comments, title: "My Cool Thoughts") post_id = MySchema.id_from_object(post, Types::Post, {}) result = MySchema.execute(query_string, variables: { id: post_id }) post_result = result["data"]["node"] # Make sure the query worked assert_equal post_id, post_result["id"] assert_equal "My Cool Thoughts", post_result["title"] end ``` To make sure that different parts of the system are properly engaged, you can add integration tests for specific scenarios, too. For example, you could add a test to make sure that data is hidden from some users: ```ruby it "doesn't show draft posts to anyone except their author" do author = create(:user) non_author = create(:non_user) draft_post = create(:post, draft: true, author: author) query_string = <<-GRAPHQL query($id: ID!) { node(id: $id) { ... on Post { isDraft } } } GRAPHQL post_id = MySchema.id_from_object(draft_post, Types::Post, {}) # Authors can see their drafts: author_result = MySchema.execute(query_string, context: { viewer: author }, variables: { id: post_id }) assert_equal true, author_result["data"]["node"]["isDraft"] # Other users can't see others' drafts non_author_result = MySchema.execute(query_string, context: { viewer: non_author }, variables: { id: post_id }) assert_nil author_result["data"]["node"] end ``` This test engages the underlying authorization and business logic, and provides a sanity check at the GraphQL interface layer. ## Testing Transport-Level Behaviors GraphQL is usually served over HTTP. You probably want tests that make sure that HTTP inputs are correctly prepared for GraphQL. For example, you might test that: - POST data is correctly turned into query variables - Authentication headers are used to load a `context[:viewer]` In Rails, you might use a [functional test](https://guides.rubyonrails.org/testing.html#functional-testing-for-controllers) for this, for example: ```ruby it "loads user token into the viewer" do query_string = "{ viewer { username } }" post graphql_path, params: { query: query_string } json_response = JSON.parse(@response.body) assert_nil json_response["data"]["viewer"], "Unauthenticated requests have no viewer" # This time, add some authentication to the HTTP request user = create(:user) post graphql_path, params: { query: query_string }, headers: { "Authorization" => "Bearer #{user.auth_token}" } json_response = JSON.parse(@response.body) assert_equal user.username, json_response["data"]["viewer"]["username"], "Authenticated requests load the viewer" end ``` graphql-ruby-2.5.19/guides/testing/overview.md000066400000000000000000000015231514115062600213540ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Testing title: Overview desc: Testing a GraphQL system index: 0 redirect_from: - /schema/testing --- So, you've spiked a GraphQL API, and now you're ready to tighten things up and add some proper tests. These guides will help you think about how to ensure stability and compatibility for your GraphQL system. - {% internal_link "Structure testing", "/testing/schema_structure" %} verifies that schema changes are backwards-compatible. This way, you don't break existing clients. - {% internal_link "Integration testing", "/testing/integration_tests" %} exercises the various behaviors of the GraphQL system, making sure that it returns the right data to the right clients. - {% internal_link "Testing helpers", "/testing/helpers" %} for running GraphQL fields without writing a whole query graphql-ruby-2.5.19/guides/testing/profiling.md000066400000000000000000000104121514115062600214740ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Testing title: Profiling desc: Profiling the performance of GraphQL-Ruby index: 4 --- If you want to know more about how time is spent during GraphQL queries, including GraphQL-Ruby internals, you can use Ruby profiling tools to take a closer look. If you want to investigate GraphQL-Ruby performance together, prepare a runtime profile and memory profile as described below and {% open_an_issue "Performance investigation" %} on GitHub, including those files. ## StackProf [StackProf](https://github.com/tmm1/stackprof) is a Ruby library for figuring out where an operation's time is spent. To capture a profile, surround a block with `StackProf.run { ... }`. ```ruby require "stackprof" # Prepare any GraphQL-related data or context: query_string = "{ someGraphQL ... }" context = { ... } # This will dump a profile in `tmp/graphql-prof.dump` StackProf.run(mode: :wall, interval: 10, out: "tmp/graphql-prof.dump") do # Execute the query inside the block: MySchema.execute(query_string, context: context) end ``` The `out:` option tells StackProf to create a "dump" at the given location. Then, anyone who has that file can investigate the profile using the `stackprof` command, for example: ``` $ stackprof tmp/graphql-prof.dump ================================== Mode: wall(1) Samples: 2492 (58.06% miss rate) GC: 0 (0.00%) ================================== TOTAL (pct) SAMPLES (pct) FRAME 902 (36.2%) 94 (3.8%) GraphQL::Execution::Interpreter::Runtime#evaluate_selection_with_resolved_keyword_args 1283 (51.5%) 87 (3.5%) GraphQL::Execution::Interpreter::Runtime#continue_field 274 (11.0%) 78 (3.1%) GraphQL::Schema::Field#resolve 1068 (42.9%) 73 (2.9%) GraphQL::Execution::Interpreter::Runtime#evaluate_selection # ... ``` Additionally, `stackprof` accepts a `--method` argument which provides details about the performance and usage of a specific method, for example: ``` $ stackprof tmp/small.dump --method #gather_selections GraphQL::Execution::Interpreter::Runtime#gather_selections (/Users/rmosolgo/code/graphql-ruby/lib/graphql/execution/interpreter/runtime.rb:305) samples: 17 self (0.7%) / 17 total (0.7%) callers: 16 ( 94.1%) GraphQL::Execution::Interpreter::Runtime#continue_field 6 ( 35.3%) Array#each 1 ( 5.9%) GraphQL::Execution::Interpreter::Runtime#run_eager callees (0 total): 6 ( Inf%) Array#each code: 1 (0.0%) / 1 (0.0%) | 305 | when :lookahead 6 (0.2%) / 6 (0.2%) | 306 | if !field_ast_nodes 3 (0.1%) / 3 (0.1%) | 307 | field_ast_nodes = [ast_node] | 308 | end ``` Anyone with the `.dump` file can perform this analysis -- it's a really useful file! If you want to investigate GraphQL-Ruby performance together, please share a runtime profile. ## MemoryProfiler [MemoryProfiler](https://github.com/SamSaffron/memory_profiler) provides insight into where an operation interacts with system memory and the Ruby heap. This is helpful because memory usage problems cause code to run slowly; fixing them can make code run fast. To produce a report, wrap a block in `MemoryProfiler.report { ... }` and then call `.pretty_print` on the result. For example, to create a report on a GraphQL query: ```ruby require 'memory_profiler' # Prepare any GraphQL-related data or context: query_string = "{ someGraphQL ... }" context = { ... } report = MemoryProfiler.report do # Execute the query inside the block: MySchema.execute(query_string, context: context) end # Write the result to a file report.pretty_print(to_file: "tmp/graphql-memory.txt") ``` The report will include many interesting sections including: - Total memory and objects allocated - Objects allocated by location and by class - String allocations, including the number of times a string with the same value was allocated All of these can indicate "hot spots" in the code and inform refactors to reduce memory use. In turn, this reduces time spent in Ruby GC. If you want to investigate GraphQL-Ruby performance together, please share a memory profile. graphql-ruby-2.5.19/guides/testing/schema_structure.md000066400000000000000000000053121514115062600230660ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Testing title: Schema Structure desc: Make sure that your schema changes are backwards-compatible index: 1 --- Structural changes to a GraphQL schema come in two categories: - __Breaking__ changes may cause previously-valid queries to become invalid. For example, if you remove the `title` field, anyone who tries to query that field will get a validation error instead of response data. - __Non-breaking__ changes _add_ options to a schema without breaking previously-valid queries. Making a _breaking_ change can be bad news for your API clients, since their applications may break. But, sometimes they're required. _Non-breaking_ changes don't affect existing queries, since they just _add_ new parts to the schema. Here are few tips for managing schema structure changes. ## Maintain a `.graphql` schema dump Make structure changes part of the normal code review process by adding a `schema.graphql` artifact to your project. This way, any changes to schema structure will show up clearly in a pull request as a diff to that file. You can read about this approach in ["Tracking Schema Changes with GraphQL-Ruby"](https://rmosolgo.github.io/ruby/graphql/2017/03/16/tracking-schema-changes-with-graphql-ruby) or the built-in {{ "GraphQL::RakeTask" | api_doc }} for generating schema dumps. ## Automatically check for breaking changes You can use [GraphQL::SchemaComparator](https://github.com/xuorig/graphql-schema_comparator) to check for breaking changes during development or CI. If you maintain a dump of queries that typically run against your server, you may also utilize `GraphQL::StaticValidation` to validate these queries directly. A Rake task such as the one below can be used to identify changes that are incompatible with existing queries. ```ruby namespace :graphql do namespace :queries do desc 'Validates GraphQL queries against the current schema' task validate: [:environment] do queries_file = 'test/fixtures/files/queries.json' queries = Oj.load(File.read(queries_file)) Validate.run_validate(queries, MySchema) end module Validate def self.run_validate(queries, schema) puts '⏳ Validating queries...' puts "\n" results = queries.map { |query| schema.validate(query) } errors = results.flatten if errors.empty? puts '✅ All queries are valid' else print_errors(errors) end end def self.print_errors(errors) puts 'Detected the following errors:' puts "\n" errors.each do |error| path = error.path.join(', ') puts "❌ #{path}: #{error.message}" end end end end end ``` graphql-ruby-2.5.19/guides/type_definitions/000077500000000000000000000000001514115062600210625ustar00rootroot00000000000000graphql-ruby-2.5.19/guides/type_definitions/directives.md000066400000000000000000000113231514115062600235450ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Type Definitions title: Directives desc: Special instructions for the GraphQL runtime index: 10 --- Directives are system-defined keywords with two kinds of uses: - [runtime directives](#runtime-directives) _modify execution_, so that when they are present, the GraphQL runtime does something different; - [schema directives](#schema-directives) _annotate schema definitions_, indicating different configurations or metadata about schemas and types. ## Runtime Directives Runtime directives are server-defined keywords that modify GraphQL execution. All GraphQL systems have at least _two_ directives, `@skip` and `@include`. For example: ```ruby query ProfileView($renderingDetailedProfile: Boolean!){ viewer { handle # These fields will be included only if the check passes: ... @include(if: $renderingDetailedProfile) { location homepageUrl } } } ``` Here's how the two built-in directives work: - `@skip(if: ...)` skips the selection if the `if: ...` value is truthy ({{ "GraphQL::Schema::Directive::Skip" | api_doc }}) - `@include(if: ...)` includes the selection if the `if: ...` value is truthy ({{ "GraphQL::Schema::Directive::Include" | api_doc }}) ### Custom Runtime Directives Custom directives extend {{ "GraphQL::Schema::Directive" | api_doc }}: ```ruby # app/graphql/directives/my_directive.rb class Directives::MyDirective < GraphQL::Schema::Directive description "A nice runtime customization" location FIELD end ``` Then, they're hooked up to the schema using `directive(...)`: ```ruby class MySchema < GraphQL::Schema # Attach the custom directive to the schema directive(Directives::MyDirective) end ``` And you can reference them in the query with `@myDirective(...)`: ```ruby query { field @myDirective { id } } ``` {{ "GraphQL::Schema::Directive::Feature" | api_doc }} and {{ "GraphQL::Schema::Directive::Transform" | api_doc }} are included in the library as examples. ### Runtime hooks Directive classes may implement the following class methods to interact with the runtime: - `def self.include?(obj, args, ctx)`: If this hook returns `false`, the nodes flagged by this directive will be skipped at runtime. - `def self.resolve(obj, args, ctx)`: Wraps the resolution of flagged nodes. Resolution is passed as a __block__, so `yield` will continue resolution. Looking for a runtime hook that isn't listed here? {% open_an_issue "New directive hook: @something", " " %} to start the conversation! ## Schema Directives Schema directives are used in GraphQL's interface definition language (IDL). For example, `@deprecated` is built in to GraphQL-Ruby: ```ruby type User { firstName @deprecated(reason: "Use `name` instead") lastName @deprecated(reason: "Use `name` instead") name } ``` In the schema definition, directives express metadata about types, fields, and arguments. ### Custom Schema Directives To make a custom schema directive, extend {{ "GraphQL::Schema::Directive" | api_doc }}: ```ruby # app/graphql/directives/permission.rb class Directives::Permission < GraphQL::Schema::Directive argument :level, String locations FIELD_DEFINITION, OBJECT end ``` Then, attach it to parts of your schema with `directive(...)`: ```ruby class Types::JobPosting < Types::BaseObject directive Directives::Permission, level: "manager" end ``` Arguments and fields also accept a `directives:` keyword: ```ruby field :salary, Integer, null: false, directives: { Directives::Permission => { level: "manager" } } ``` After that: - the configured object's `.directives` method will return an array containing an instance of the specified directive - IDL dumps (from {{ "Schema.to_definition" | api_doc }}) will include the configured directives Similarly, {{ "Schema.from_definition" | api_doc }} parses directives from IDL strings. For a couple of built-in examples, check out: - {{ "GraphQL::Schema::Directive::Deprecated" | api_doc }} which implements `deprecation_reason` (via {{ "GraphQL::Schema::Member::HasDeprecationReason" | api_doc}}) - {{ "GraphQL::Schema::Directive::Flagged" | api_doc }}, which is an example of using schema directives to implement {% internal_link "visibility", "/authorization/visibility" %} ## Custom Name By default, the directive's name is taken from the class name. You can override this with `graphql_name`, for example: ```ruby class Directives::IsPrivate < GraphQL::Schema::Directive graphql_name "someOtherName" end ``` ## Arguments Like fields, directives may have {% internal_link "arguments", "/fields/arguments" %} : ```ruby argument :if, Boolean, description: "Skips the selection if this condition is true" ``` graphql-ruby-2.5.19/guides/type_definitions/enums.md000066400000000000000000000053651514115062600225440ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Type Definitions title: Enums desc: Enums are sets of discrete values index: 2 --- Enum types are sets of discrete values. An enum field must return one of the possible values of the enum. In the [GraphQL Schema Definition Language](https://graphql.org/learn/schema/#type-language) (SDL), enums are described like this: ```ruby enum MediaCategory { AUDIO IMAGE TEXT VIDEO } ``` So, a `MediaCategory` value is one of: `AUDIO`, `IMAGE`, `TEXT`, or `VIDEO`. This is similar to [ActiveRecord enums](https://api.rubyonrails.org/classes/ActiveRecord/Enum.html). In a GraphQL query, enums are written as identifiers (not strings), for example: ```ruby search(term: "puppies", mediaType: IMAGE) { ... } ``` (Notice that `IMAGE` doesn't have quotes.) But, when GraphQL responses or variables are transported using JSON, enum values are expressed as strings, for example: ```ruby # in a graphql controller: params["variables"] # { "mediaType" => "IMAGE" } ``` ## Defining Enum Types In your application, enums extend {{ "GraphQL::Schema::Enum" | api_doc }} and define values with the `value(...)` method: ```ruby # First, a base class # app/graphql/types/base_enum.rb class Types::BaseEnum < GraphQL::Schema::Enum end # app/graphql/types/media_category.rb class Types::MediaCategory < Types::BaseEnum value "AUDIO", "An audio file, such as music or spoken word" value "IMAGE", "A still image, such as a photo or graphic" value "TEXT", "Written words" value "VIDEO", "Motion picture, may have audio" end ``` Each value may have: - A description (as the second argument or `description:` keyword) - A comment (as a `comment:` keyword) - A deprecation reason (as `deprecation_reason:`), marking this value as deprecated - A corresponding Ruby value (as `value:`), see below By default, Ruby strings correspond to GraphQL enum values. But, you can provide `value:` options to specify a different mapping. For example, if you use symbols instead of strings, you can say: ```ruby value "AUDIO", value: :audio ``` Then, GraphQL inputs of `AUDIO` will be converted to `:audio` and Ruby values of `:audio` will be converted to `"AUDIO"` in GraphQL responses. Enum classes are never instantiated and their methods are never called. You can get the GraphQL name of the enum value using the method matching its downcased name: ```ruby Types::MediaCategory.audio # => "AUDIO" ``` You can pass a `value_method:` to override the value of the generated method: ```ruby value "AUDIO", value: :audio, value_method: :lo_fi_audio # ... Types::MediaCategory.lo_fi_audio # => "AUDIO" ``` Also, you can completely skip the method generation by setting `value_method` to `false` ```ruby value "AUDIO", value: :audio, value_method: false ``` graphql-ruby-2.5.19/guides/type_definitions/extensions.md000066400000000000000000000135521514115062600236110ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Type Definitions title: Extending the GraphQL-Ruby Type Definition System desc: Adding metadata and custom helpers to the DSL index: 8 redirect_from: - /schema/extending_the_dsl/ --- While integrating GraphQL into your app, you can customize the definition DSL. For example, you might: - Assign "area of responsibility" to different types and fields - DRY up shared logic between types and fields - Attach metadata for use during authorization This guide describes various options for extending the class-based definition API. Keep in mind that these approaches may change as the API matures. If you're having trouble, consider opening an issue on GitHub to get help. **Note**: This document describes best practice with GraphQL-Ruby 1.10+. For customizing schemas on older versions, use GitHub to browse older versions of this page. ## Customization Overview In general, the schema definition process goes like this: - The application defines lots of classes for the GraphQL types - Starting from root types (`query`, `mutation`, and `subscription`) and any defined `orphan_types`, the schema discovers all types, fields, arguments, enum values, and directives in the schema - Non-type objects (fields, arguments, enum values) are initialized when they're attached to the classes or instances they belong to. ## Customizing type definitions In your custom classes, you can add class-level instance variables that hold configuration. For example: ```ruby class Types::BaseObject < GraphQL::Schema::Object # Call this method in an Object class to get or set the permission level: def self.required_permission(permission_level = nil) if permission_level.nil? # return the configured value @required_permission else @required_permission = permission_level end end end # Then, in concrete classes class Dossier < BaseObject # The Dossier object type will have `.metadata[:required_permission] # => :admin` required_permission :admin end # Now, the type responds to that method: Dossier.required_permission # => :admin ``` Now, any runtime code which calls `type.required_permission` will get the configured value. ### Customizing fields Fields are generated in a different way. Instead of using classes, they are generated with instances of `GraphQL::Schema::Field` (or a subclass). In short, the definition process works like this: ```ruby # This is what happens under the hood, roughly: # In an object class: field :name, String, null: false # ... # Leads to: field_config = GraphQL::Schema::Field.new(name: :name, type: String, null: false) ``` So, you can customize this process by: - creating a custom class which extends `GraphQL::Schema::Field` - overriding `#initialize` on that class (instance methods) - registering that class as the `field_class` on Objects and Interfaces which should use the customized field. For example, you can create a custom class which accepts a new parameter to `initialize`: ```ruby class Types::BaseField < GraphQL::Schema::Field # Override #initialize to take a new argument: def initialize(*args, required_permission: nil, **kwargs, &block) @required_permission = required_permission # Pass on the default args: super(*args, **kwargs, &block) end attr_reader :required_permission end ``` Then, pass the field class as `field_class(...)` wherever it should be used: ```ruby class Types::BaseObject < GraphQL::Schema::Object # Use this class for defining fields field_class BaseField end # And.... class Types::BaseInterface < GraphQL::Schema::Interface field_class BaseField end class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation field_class BaseField end ``` Now, `BaseField.new(*args, &block)` will be used to create `GraphQL::Schema::Field`s on those types. At runtime `field.required_permission` will return the configured value. ### Customizing Connections Connections may be customized in a similar way to Fields. - Create a new class extending 'GraphQL::Types::Relay::BaseConnection' - Assign it to your object/interface type with `connection_type_class(MyCustomConnection)` For example, you can create a custom connection: ```ruby class Types::MyCustomConnection < GraphQL::Types::Relay::BaseConnection # BaseConnection has these nullable configurations # and the nodes field by default, but you can change # these options if you want edges_nullable(true) edge_nullable(true) node_nullable(true) has_nodes_field(true) field :total_count, Integer, null: false def total_count object.items.size end end ``` Then, pass the field class as `connection_type_class(...)` wherever it should be used: ```ruby module Types class Types::BaseObject < GraphQL::Schema::Object # Use this class for defining connections connection_type_class MyCustomConnection end end ``` Now, all type classes that extend `BaseObject` will have a connection_type with the additional field `totalCount`. ### Customizing Edges Edges may be customized in a similar way to Connections. - Create a new class extending 'GraphQL::Types::Relay::BaseEdge' - Assign it to your object/interface type with `edge_type_class(MyCustomEdge)` ### Customizing Arguments Arguments may be customized in a similar way to Fields. - Create a new class extending `GraphQL::Schema::Argument` - Use `argument_class(MyArgClass)` to assign it to your base field class, base resolver class, and base mutation class Then, in your custom argument class, you can use `#initialize(name, type, desc = nil, **kwargs)` to take input from the DSL. ### Customizing Enum Values Enum values may be customized in a similar way to Fields. - Create a new class extending `GraphQL::Schema::EnumValue` - Assign it to your base `Enum` class with `enum_value_class(MyEnumValueClass)` Then, in your custom enum class, you can use `#initialize(name, desc = nil, **kwargs)` to take input from the DSL. graphql-ruby-2.5.19/guides/type_definitions/field_extensions.md000066400000000000000000000143161514115062600247530ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Type Definitions title: Field Extensions desc: Programmatically modify field configuration and resolution index: 10 --- {{ "GraphQL::Schema::FieldExtension" | api_doc }} provides a way to modify user-defined fields in a programmatic way. For example, Relay connections are implemented as a field extension ({{ "GraphQL::Schema::Field::ConnectionExtension" | api_doc }}). ## Making a new extension Field extensions are subclasses of {{ "GraphQL::Schema::FieldExtension" | api_doc }}: ```ruby class MyExtension < GraphQL::Schema::FieldExtension end ``` ## Using an extension Defined extensions can be added to fields using the `extensions: [...]` option or the `extension(...)` method: ```ruby field :name, String, null: false, extensions: [UpcaseExtension] # or: field :description, String, null: false do extension(UpcaseExtension) end ``` See below for how extensions may modify fields. ## Modifying field configuration When extensions are attached, they are initialized with a `field:` and `options:`. Then, `#apply` is called, when they may extend the field they're attached to. For example: ```ruby class SearchableExtension < GraphQL::Schema::FieldExtension def apply # add an argument to this field: field.argument(:query, String, required: false, description: "A search query") end end ``` This way, an extension can encapsulate a behavior requiring several configuration options. ## Adding default argument configurations Extensions may provide _default_ argument configurations which are applied if the field doesn't define the argument for itself. The configuration is passed to {{ "Schema::FieldExtension.default_argument" | api_doc }}. For example, to define a `:query` argument if the field doesn't already have one: ```ruby class SearchableExtension < GraphQL::Schema::FieldExtension # Any field which uses this extension and _doesn't_ define # its own `:query` argument will get an argument configured with this: default_argument(:query, String, required: false, description: "A search query") end ``` Additionally, extensions may implement `def after_define` which is called _after_ the field's `do .. . end` block. This is helpful when an extension should provide _default_ configurations without overriding anything in the field definition. (When extensions are added by calling `field.extension(...)` on an already-defined field `def after_define` is called immediately.) ## Modifying field execution Extensions have two hooks that wrap field resolution. Since GraphQL-Ruby supports deferred execution, these hooks _might not_ be called back-to-back. First, {{ "GraphQL::Schema::FieldExtension#resolve" | api_doc }} is called. `resolve` should `yield(object, arguments)` to continue execution. If it doesn't `yield`, then the underlying field won't be called. Whatever `#resolve` returns will be used for continuing execution. After resolution and _after_ syncing lazy values (like `Promise`s from `graphql-batch`), {{ "GraphQL::Schema::FieldExtension#after_resolve" | api_doc }} is called. Whatever that method returns will be used as the field's return value. See the linked API docs for the parameters of those methods. ### Execution "memo" One parameter to `after_resolve` deserves special attention: `memo:`. `resolve` _may_ yield a third value. For example: ```ruby def resolve(object:, arguments:, **rest) # yield the current time as `memo` yield(object, arguments, Time.now.to_i) end ``` If a third value is yielded, it will be passed to `after_resolve` as `memo:`, for example: ```ruby def after_resolve(value:, memo:, **rest) puts "Elapsed: #{Time.now.to_i - memo}" # Return the original value value end ``` This allows the `resolve` hook to pass data to `after_resolve`. Instance variables may not be used because, in a given GraphQL query, the same field may be resolved several times concurrently, and that would result in overriding the instance variable in an unpredictable way. (In fact, extensions are frozen to prevent instance variable writes.) ## Extension options The `extension(...)` method takes an optional second argument, for example: ```ruby extension(LimitExtension, limit: 20) ``` In this case, `{limit: 20}` will be passed as `options:` to `#initialize` and `options[:limit]` will be `20`. For example, options can be used for modifying execution: ```ruby def after_resolve(value:, **rest) # Apply the limit from the options, a readable attribute on the class value.limit(options[:limit]) end ``` If you use the `extensions: [...]` option, you can pass options using a hash: ```ruby field :name, String, null: false, extensions: [LimitExtension => { limit: 20 }] ``` ## Using `extras` Extensions can have the same `extras` as fields (see {% internal_link "Extra Field Metadata", "fields/introduction#extra-field-metadata" %}). Add them by calling `extras` in the class definition: ```ruby class MyExtension < GraphQL::Schema::FieldExtension extras [:ast_node, :errors, ...] end ``` Any configured `extras` will be present in the given `arguments`, but removed before the field is resolved. (However, `extras` from _any_ extension will be present in `arguments` for _all_ extensions.) ## Adding an extension by default If you want to apply an extension to _all_ your fields, you can do this in your {% internal_link "BaseField", "/type_definitions/extensions.html#customizing-fields" %}'s `def initialize`, for example: ```ruby class Types::BaseField < GraphQL::Schema::Field def initialize(*args, **kwargs, &block) super # Add this to all fields based on this class: extension(MyDefaultExtension) end end ``` You can also _conditionally_ apply extensions in `def initialize` by adding keywords to the method definition, for example: ```ruby class Types::BaseField < GraphQL::Schema::Field # @param custom_extension [Boolean] if false, `MyCustomExtension` won't be added # @example skipping `MyCustomExtension` # field :no_extension, String, custom_extension: false def initialize(*args, custom_extension: true, **kwargs, &block) super(*args, **kwargs, &block) # Don't apply this extension if the field is configured with `custom_extension: false`: if custom_extension extension(MyCustomExtensions) end end end ``` graphql-ruby-2.5.19/guides/type_definitions/input_objects.md000066400000000000000000000126071514115062600242620ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Type Definitions title: Input Objects desc: Input objects are sets of key-value pairs which can be used as field arguments. index: 3 --- Input object types are complex inputs for GraphQL operations. They're great for fields that need a lot of structured input, like mutations or search fields. In a GraphQL request, it might look like this: ```ruby mutation { createPost(attributes: { title: "Hello World", fullText: "This is my first post", categories: [GENERAL] }) { # ^ Here is the input object ..................................................... ^ } } ``` Like a Ruby `Hash`, an input object consists of keys and values. Unlike a Hash, its keys and value types must be defined statically, as part of the GraphQL system. For example, here's an input object, expressed in the [GraphQL Schema Definition Language](https://graphql.org/learn/schema/#type-language) (SDL): ```ruby input PostAttributes { title: String! fullText: String! categories: [PostCategory!] } ``` This input object has three possible keys: - `title` is required (denoted by `!`), and must be a `String` - `fullText` is also a required String - `categories` is optional (it doesn't have `!`), and if present, it must be a list of `PostCategory` values. ## Defining Input Object Types Input object types extend {{ "GraphQL::Schema::InputObject" | api_doc }} and define key-value pairs with the `argument(...)` method. For example: ```ruby # app/graphql/types/base_input_object.rb # Add a base class class Types::BaseInputObject < GraphQL::Schema::InputObject end class Types::PostAttributes < Types::BaseInputObject description "Attributes for creating or updating a blog post" argument :title, String, "Header for the post" argument :full_text, String, "Full body of the post" argument :categories, [Types::PostCategory], required: false end ``` For a full description of the `argument(...)` method, see the {% internal_link "argument section of the Objects guide","/fields/arguments.html" %}. ## Using Input Objects Input objects are passed to field methods as an instance of their definition class. So, inside the field method, you can access any key of the object by: - calling its method, corresponding to the name (underscore-cased) - calling `#[]` with the _camel-cased_ name of the argument (this is for compatibility with previous GraphQL-Ruby versions) ```ruby class Types::MutationType < GraphQL::Schema::Object # This field takes an argument called `attributes` # which will be an instance of `PostAttributes` field :create_post, Types::Post, null: false do argument :attributes, Types::PostAttributes end def create_post(attributes:) puts attributes.class.name # => "Types::PostAttributes" # Access a value by method (underscore-cased): puts attributes.full_text # => "This is my first post" # Or by hash-style lookup (camel-cased, for compatibility): puts attributes[:fullText] # => "This is my first post" end end ``` ## Customizing Input Objects You can customize the `GraphQL::Schema::Argument` class which is used for input objects: ```ruby class Types::BaseArgument < GraphQL::Schema::Argument # your customization here ... end class Types::BaseInputObject < GraphQL::Schema::InputObject # Hook up the customized argument class argument_class(Types::BaseArgument) end ``` You can also add or override methods on input object classes to customize them. They have two instance variables by default: - `@arguments`: A {{ "GraphQL::Execution::Interpreter::Arguments" | api_doc }} instance - `@context`: The current {{ "GraphQL::Query::Context" | api_doc }} Any extra methods you define on the class can be used for field resolution, as demonstrated above. ## Converting to Other Ruby Objects Your input objects can be automatically converted to other Ruby types before they're passed to your application code. This is an easy way to use `Range`'s in your schema: ```ruby class Types::DateRangeInput < Types::BaseInputObject description "Range of dates" argument :min, Types::Date, "Minimum value of the range" argument :max, Types::Date, "Maximum value of the range" def prepare min..max end end class Types::CalendarType < Types::BaseObject field :appointments, [Types::Appointment], "Appointments on your calendar", null: false do argument :during, Types::DateRangeInput, "Only show appointments within this range" end def appointments(during:) # during will be an instance of Range object.appointments.select { |appointment| during.cover?(appointment.date) } end end ``` ## `@oneOf` You can make input objects that require _exactly one_ field to be provided using `one_of`: ```ruby class FindUserInput < Types::BaseInput one_of # Either `{ id: ... }` or `{ username: ... }` may be given, # but not both -- and one of them _must_ be given. argument :id, ID, required: false argument :username, String, required: false end ``` An input object with `one_of` will require exactly one given argument and it will require that the given argument's value is not `nil`. With `one_of`, arguments must have `required: false`, since any _individual_ argument is not required. When you use `one_of`, it will appear in schema print-outs with `input ... @oneOf` and you can query it using `{ __type(name: $typename) { isOneOf } }`. This behavior was adopted to the September 2025 GraphQL specification. graphql-ruby-2.5.19/guides/type_definitions/interfaces.md000066400000000000000000000202731514115062600235330ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Type Definitions title: Interfaces desc: Interfaces are lists of fields which objects may implement index: 4 redirect_from: - /types/abstract_types/ --- Interfaces are lists of fields which may be implemented by object types. An interface has fields, but it's never actually instantiated. Instead, objects may _implement_ interfaces, which makes them a _member_ of that interface. Also, fields may _return_ interface types. When this happens, the returned object may be any member of that interface. For example, let's say a `Customer` (interface) may be either an `Individual` (object) or a `Company` (object). Here's the structure in the [GraphQL Schema Definition Language](https://graphql.org/learn/schema/#type-language) (SDL): ```graphql interface Customer { name: String! outstandingBalance: Int! } type Company implements Customer { employees: [Individual!]! name: String! outstandingBalance: Int! } type Individual implements Customer { company: Company name: String! outstandingBalance: Int! } ``` Notice that the `Customer` interface requires two fields, `name: String!` and `outstandingBalance: Int!`. Both `Company` and `Individual` implement those fields, so they can implement `Customer`. Their implementation of `Customer` is made explicit by `implements Customer` in their definition. When querying, you can get the fields on an interface: ```graphql { customers(first: 5) { name outstandingBalance } } ``` Whether the objects are `Company` or `Individual`, it doesn't matter -- you still get their `name` and `outstandingBalance`. If you want some object-specific fields, you can query them with an _inline fragment_, for example: ```graphql { customers(first: 5) { name ... on Individual { company { name } } } } ``` This means, "if the customer is an `Individual`, also get the customer's company name". Interfaces are a good choice whenever a set of objects are used interchangeably, and they have several significant fields in common. When they don't have fields in common, use a {% internal_link "Union", "/type_definitions/unions" %} instead. ## Defining Interface Types Interfaces are Ruby modules which include {{ "GraphQL::Schema::Interface" | api_doc }}. First, make a base module: ```ruby module Types::BaseInterface include GraphQL::Schema::Interface end ``` Then, include that into each interface: ```ruby module Types::RetailItem include Types::BaseInterface comment "TODO comment in the RetailItem interface" description "Something that can be bought" field :price, Types::Price, "How much this item costs", null: false def price # Optional: provide a special implementation of `price` here end # Optional, see below definition_methods do # Optional: if this method is defined, it overrides `Schema.resolve_type` def resolve_type(object, context) # ... end end end ``` Interface classes are never instantiated. At runtime, only their `.resolve_type` methods are called (if they're defined). ### Implementing Interfaces To define object types that implement this interface use the `implements` method: ```ruby class Types::Car < Types::BaseObject implements Types::RetailItem # ... additional fields end class Types::Purse < Types::BaseObject implements Types::RetailItem # ... additional fields end ``` Those object types will _inherit_ field definitions from those interfaces. If you add an object type which implements an interface, but that object type doesn't appear in your schema as a field return type, a union member, or a root type, then you need to add that object to the interfaces's `orphan_types`. ### Implementing Fields Interfaces may provide field implementations along with the signatures. For example: ```ruby field :price, Types::Price, "How much this item costs", null: false # Implement this field to return a `::Price` object def price ::Price.from_cents(@object.price_in_cents) end ``` This method will be called by objects who implement the interface. To override this implementation, object classes can override the `#price` method. Read more in the {% internal_link "Fields guide", "/fields/introduction" %}. ### Definition Methods You can use `definition_methods do ... end` to add helper methods to interface modules. By adding methods to `definition_methods`: - Those methods will be available as class methods in the interface itself - These class methods will _also_ be added to interfaces that `include` this interface. This way, class methods are inherited when interfaces `include` other interfaces. (`definition_methods` is like `ActiveSupport::Concern`'s `class_methods` in this regard, but it has a different name to avoid naming conflicts). For example, you can add definition helpers to your base interface, then use them in concrete interfaces later: ```ruby # First, add a helper method to `BaseInterface`'s definition methods module Types::BaseInterface include GraphQL::Schema::Interface definition_methods do # Use this to add a price field + default implementation def price_field field(:price, ::Types::Price, null: false) define_method(:price) do ::Price.from_cents(@object.price_in_cents) end end end end # Then call it later module Types::ForSale include Types::BaseInterface # This calls `price_field` from definition methods price_field end ``` The type definition DSL uses this mechanism, too, so you can override those methods here also. Note: Under the hood, `definition_methods` causes a module to be `extend`ed by the interface. Any calls to `extend` or `implement` may override methods from `definition_methods`. ### Resolve Type When a field's return type is an interface, GraphQL has to figure out what _specific_ object type to use for the return value. In the example above, each `customer` must be categorized as an `Individual` or `Company`. You can do this by: - Providing a top-level `Schema.resolve_type` method; _OR_ - Providing an interface-level `.resolve_type` method in `definition_methods`. This method will be called whenever an object must be disambiguated. For example: ```ruby module Types::RetailItem include Types::BaseInterface definition_methods do # Determine what object type to use for `object` def resolve_type(object, context) if object.is_a?(::Car) || object.is_a?(::Truck) Types::Car elsif object.is_a?(::Purse) Types::Purse else raise "Unexpected RetailItem: #{object.inspect}" end end end end ``` You can also optionally return a "resolved" object in addition the resolved type by returning an array: ```ruby module Types::Claim include Types::BaseInterface definition_methods do def resolve_type(object, context) type = case object.value when Success Types::Approved when Error Types::Rejected else raise "Unexpected Claim: #{object.inspect}" end [type, object.value] end end end ``` The returned array must be a tuple of `[Type, object]`. This is useful for interface or union types which are backed by a domain object which should be unwrapped before resolving the next field. ## Orphan Types If you add an object type which implements an interface, but that object type doesn't properly appear in your schema, then you need to add that object to the interfaces's `orphan_types`, for example: ```ruby module Types::RetailItem include Types::BaseInterface # ... orphan_types Types::Car end ``` Alternatively you can add the object types to the schema's `orphan_types`: ```ruby class MySchema < GraphQL::Schema orphan_types Types::Car end ``` This is required because a schema finds it types by traversing its fields, starting with `query`, `mutation` and `subscription`. If an object is _never_ the return type of a field, but only connected via an interface, then it must be explicitly connected to the schema via `orphan_types`. For example, given this schema: ```graphql type Query { node(id: ID!): Node } interface Node { id: ID! } type Comment implements Node { id: ID! } ``` `Comment` must be added via `orphan_types` since it's never used as the return type of a field. (Only `Node` and `ID` are used as return types.) graphql-ruby-2.5.19/guides/type_definitions/lists.md000066400000000000000000000111461514115062600225450ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Type Definitions title: Lists desc: Ordered lists containing other types index: 6 --- GraphQL has _list types_ which are ordered lists containing items of other types. The following examples use the [GraphQL Schema Definition Language](https://graphql.org/learn/schema/#type-language) (SDL). Fields may return a single scalar value (eg `String`), or a _list_ of scalar values (eg, `[String]`, a list of strings): ```ruby type Spy { # This spy's real name realName: String! # Any other names that this spy goes by aliases: [String!] } ``` Fields may also return lists of other types as well: ```ruby enum PostCategory { SOFTWARE UPHOLSTERY MAGIC_THE_GATHERING } type BlogPost { # Zero or more categories this post belongs to categories: [PostCategory!] # Other posts related to this one relatedPosts: [BlogPost!] } ``` Inputs may also be lists. Arguments can accept list types, for example: ```ruby type Query { # Return the latest posts, filtered by `categories` posts(categories: [PostCategory!]): [BlogPost!] } ``` When GraphQL is sent and received with JSON, GraphQL lists are expressed in JSON arrays. ## List Types in Ruby To define a list type in Ruby use `[...]` (a Ruby array with one member, the inner type). For example: ```ruby # A field returning a list type: # Equivalent to `aliases: [String!]` above field :aliases, [String] # An argument which accepts a list type: argument :categories, [Types::PostCategory], required: false ``` For input, GraphQL lists are converted to Ruby arrays. For fields that return list types, any object responding to `#each` may be returned. It will be enumerated as a GraphQL list. To define lists where `nil` may be a member of the list, use `null: true` in the definition array, for example: ```ruby # Equivalent to `previousEmployers: [Employer]!` field :previous_employers, [Types::Employer, null: true], "Previous employers; `null` represents a period of self-employment or unemployment" null: false ``` ## Lists, Nullable Lists, and Lists of Nulls Combining list types and non-null types can be a bit tricky. There are four possible combinations, based on two parameters: - Nullability of the field: can this field return `null`, or does it always return a list? - Nullability of the list items: when a list is present, may it include `null`? Here's how those combinations play out:   | nullable field | non-null field ------|------|------ nullable items | [Integer, null: true], null: true
# => [Int] | [Integer, null: true], null: false
# => [Int]! non-null items | [Integer]
# => [Int!] | [Integer], null: false
# => [Int!]! (The first line is GraphQL-Ruby code. The second line, beginning with `# =>`, is the corresponding GraphQL SDL code.) Let's look at some examples. #### Non-null lists with non-null items Here's an example field: ```ruby field :scores, [Integer], null: false # In GraphQL, # scores: [Int!]! ``` In this example, `scores` may not return `null`. It must _always_ return a list. Additionally, the list may _never_ contain `null` -- it may only contain `Int`s. (It may be empty, but it cannot have `null` in it.) Here are values the field may return: | Valid | Invalid | | ------ | ------ | | `[]` | `null` | | `[1, 2, ...]` | `[null]` | | | `[1, null, 2, ...]` | ### Non-null lists with nullable items Here's an example field: ```ruby field :scores, [Integer, null: true], null: false # In GraphQL, # scores: [Int]! ``` In this example, `scores` may not return `null`. It must _always_ return a list. However, the list _may_ contain `null`s and/or `Int`s. Here are values the field may return: Valid | Invalid ------|------ `[]` | `null` `[1, 2, ...]`| `[null]` | `[1, null, 2, ...]` | ### Nullable lists with nullable items Here's an example field: ```ruby field :scores, [Integer, null: true] # In GraphQL, # scores: [Int] ``` In this example, `scores` return `null` _or_ a list. Additionally, the list _may_ contain `null`s and/or `Int`s. Here are values the field may return: Valid | Invalid ------|------ `null` | `[]` | `[1, 2, ...]`| `[null]` | `[1, null, 2, ...]` | ### Nullable lists with non-null items Here's an example field: ```ruby field :scores, [Integer] # In GraphQL, # scores: [Int!] ``` In this example, `scores` return `null` _or_ a list. However, if a list is present, it may _not_ contain `null` -- only `Int`s. Here are values the field may return: Valid | Invalid ------|------ `null` | `[null]` `[]` | `[1, null, 2, ...]` `[1, 2, ...]` | graphql-ruby-2.5.19/guides/type_definitions/non_nulls.md000066400000000000000000000040431514115062600234140ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Type Definitions title: Non-Null Types desc: Values which must be present index: 7 --- GraphQL's concept of _non-null_ is expressed in the [Schema Definition Language](https://graphql.org/learn/schema/#type-language) (SDL) with `!`, for example: ```graphql type User { # This field _always_ returns a String, never returns `null` handle: String! # `since:` _must_ be passed a `DateTime` value, it can never be omitted or passed `null` followers(since: DateTime!): [User!]! } ``` In Ruby, this concept is expressed with `null:` for fields and `required:` for arguments. ## Non-null return types When `!` is used for field return types (like `handle: String!` above), it means that the field will _never_ (and may never) return `nil`. To make a field non-null in Ruby, use `null: false` in the field definition: ```ruby # equivalent to `handle: String!` above field :handle, String, null: false ``` This means that the field will _never_ be `nil` (and if it is, it will be removed from the response, as described below). ### Non-null error propagation If a non-null field ever returns `nil`, then the entire selection will be removed from the response and replaced with `nil`. If this removal would result in _another_ invalid `nil`, then it cascades upward, until it reaches the root `"data"` key. This is to support clients in strongly-typed languages. Any non-null field will _never_ return `null`, and client developers can depend on that. ## Non-null argument types When `!` is used for arguments (like `followers(since: DateTime!)` above), it means that the argument is _required_ for the query to execute. Any query which doesn't have a value for that argument will be rejected immediately. Arguments are non-null by default. You can use `required: false` to mark arguments as optional: ```ruby # This will be `since: DateTime` instead of `since: DateTime!` argument :since, Types::DateTime, required: false ``` Without `required: false`, any query _without_ a value for `since:` will be rejected. graphql-ruby-2.5.19/guides/type_definitions/objects.md000066400000000000000000000071311514115062600230370ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Type Definitions title: Objects desc: Objects expose data and link to other objects index: 0 --- GraphQL object types are the bread and butter of GraphQL APIs. Each object has _fields_ which expose data and may be queried by name. For example, we can query a `User` like this: ```ruby user { handle email } ``` And get back values like this: ```ruby { "user" => { "handle" => "rmosolgo", "email" => nil, } } ``` Generally speaking, GraphQL object types correspond to models in your application, like `User`, `Product`, or `Comment`. Sometimes, object types are described using the [GraphQL Schema Definition Language](https://graphql.org/learn/schema/#type-language) (SDL): ```ruby type User { email: String handle: String! friends: [User!]! } ``` This means that `User` objects have three fields: - `email`, which may return a `String` _or_ `nil`. - `handle`, which returns a `String` but _never_ `nil` (`!` means the field never returns `nil`) - `friends`, which returns a list of other `User`s (`[...]` means the field returns a list of values; `User!` means the list contains `User` objects, and never contains `nil`.) The same object can be defined using Ruby: ```ruby class Types::User < GraphQL::Schema::Object field :email, String field :handle, String, null: false field :friends, [User], null: false end ``` The rest of this guide will describe how to define GraphQL object types in Ruby. To learn more about GraphQL object types in general, see the [GraphQL docs](https://graphql.org/learn/schema/#object-types-and-fields). ## Object classes Classes extending {{ "GraphQL::Schema::Object" | api_doc }} describe [Object types](https://graphql.org/learn/schema/#object-types-and-fields) and customize their behavior. Object fields can be created with the `field(...)` class method, [described in detail below](#fields) Field and argument names should be underscored as a convention. They will be converted to camelCase in the underlying GraphQL type and be camelCase in the schema itself. ```ruby # first, somewhere, a base class: class Types::BaseObject < GraphQL::Schema::Object end # then... class Types::TodoList < Types::BaseObject comment "Comment of the TodoList type" description "A list of items which may be completed" field :name, String, "The unique name of this list", null: false field :is_completed, String, "Completed status depending on all tasks being done.", null: false # Related Object: field :owner, Types::User, "The creator of this list", null: false # List field: field :viewers, [Types::User], "Users who can see this list", null: false # Connection: field :items, Types::TodoItem.connection_type, "Tasks on this list", null: false do argument :status, TodoStatus, "Restrict items to this status", required: false end end ``` ## Fields Object fields expose data about that object or connect the object to other objects. You can add fields to your object types with the `field(...)` class method. See the {% internal_link "Fields guide", "/fields/introduction" %} for details about object fields. ## Implementing interfaces If an object implements any interfaces, they can be added with `implements`, for example: ```ruby # This object implements some interfaces: implements GraphQL::Types::Relay::Node implements Types::UserAssignableType ``` When an object `implements` interfaces, it: - inherits the GraphQL field definitions from that object - includes that module into the object definition Read more about interfaces in the {% internal_link "Interfaces guide", "/type_definitions/interfaces" %} graphql-ruby-2.5.19/guides/type_definitions/scalars.md000066400000000000000000000070651514115062600230440ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Type Definitions title: Scalars desc: Scalars are "simple" data types like integers and strings index: 1 --- Scalars are "leaf" values in GraphQL. There are several built-in scalars, and you can define custom scalars, too. ({% internal_link "Enums", "/type_definitions/enums" %} are also leaf values.) The built-in scalars are: - `String`, like a JSON or Ruby string - `Int`, like a JSON or Ruby integer - `Float`, like a JSON or Ruby floating point decimal - `Boolean`, like a JSON or Ruby boolean (`true` or `false`) - `ID`, which a specialized `String` for representing unique object identifiers - `ISO8601DateTime`, an ISO 8601-encoded datetime - `ISO8601Date`, an ISO 8601-encoded date - `ISO8601Duration`, an ISO 8601-encoded duration. ⚠ This requires `ActiveSupport::Duration` to be loaded and will raise {{ "GraphQL::Error" | api_doc }} if it's `.coerce_*` methods are called when it is not defined. - `JSON`, ⚠ This returns arbitrary JSON (Ruby hashes, arrays, strings, integers, floats, booleans and nils). Take care: by using this type, you completely lose all GraphQL type safety. Consider building object types for your data instead. - `BigInt`, a numeric value which may exceed the size of a 32-bit integer Fields can return built-in scalars by referencing them by name: ```ruby # String field: field :name, String, # Integer field: field :top_score, Int, null: false # or: field :top_score, Integer, null: false # Float field field :avg_points_per_game, Float, null: false # Boolean field field :is_top_ranked, Boolean, null: false # ID field field :id, ID, null: false # ISO8601DateTime field field :created_at, GraphQL::Types::ISO8601DateTime, null: false # ISO8601Date field field :birthday, GraphQL::Types::ISO8601Date, null: false # ISO8601Duration field field :age, GraphQL::Types::ISO8601Duration, null: false # JSON field ⚠ field :parameters, GraphQL::Types::JSON, null: false # BigInt field field :sales, GraphQL::Types::BigInt, null: false ``` Custom scalars (see below) can also be used by name: ```ruby # `homepage: Url` field :homepage, Types::Url ``` In the [Schema Definition Language](https://graphql.org/learn/schema/#type-language) (SDL), scalars are simply named: ```ruby scalar DateTime ``` ## Custom Scalars You can implement your own scalars by extending {{ "GraphQL::Schema::Scalar" | api_doc }}. For example: ```ruby # app/graphql/types/base_scalar.rb # Make a base class: class Types::BaseScalar < GraphQL::Schema::Scalar end # app/graphql/types/url.rb class Types::Url < Types::BaseScalar comment "TODO comment of the scalar" description "A valid URL, transported as a string" def self.coerce_input(input_value, context) # Parse the incoming object into a `URI` url = URI.parse(input_value) if url.is_a?(URI::HTTP) || url.is_a?(URI::HTTPS) # It's valid, return the URI object url else raise GraphQL::CoercionError, "#{input_value.inspect} is not a valid URL" end end def self.coerce_result(ruby_value, context) # It's transported as a string, so stringify it ruby_value.to_s end end ``` Your class must define two class methods: - `self.coerce_input` takes a GraphQL input and converts it into a Ruby value - `self.coerce_result` takes the return value of a field and prepares it for the GraphQL response JSON When incoming data is incorrect, the method may raise {{ "GraphQL::CoercionError" | api_doc }}, which will be returned to the client in the `"errors"` key. Scalar classes are never initialized; only their `.coerce_*` methods are called at runtime. graphql-ruby-2.5.19/guides/type_definitions/unions.md000066400000000000000000000043501514115062600227210ustar00rootroot00000000000000--- layout: guide doc_stub: false search: true section: Type Definitions title: Unions desc: Unions are sets of types which may appear in the same place (but don't share fields). index: 5 --- A union type is a set of object types which may appear in the same spot. Here's a union, expressed in [GraphQL Schema Definition Language](https://graphql.org/learn/schema/#type-language) (SDL): ```ruby union MediaItem = AudioClip | VideoClip | Image | TextSnippet ``` This might be used on a search field, for example: ```ruby searchMedia(term: "puppies") { ... on AudioClip { duration } ... on VideoClip { previewURL resolution } ... on Image { thumbnailURL } ... on TextSnippet { teaserText } } ``` Here, the `searchMedia` field returns `[MediaItem!]`, a list where each member is part of the `MediaItem` union. So, for each member, we want to select different fields depending on which kind of object that member is. {% internal_link "Interfaces", "/type_definitions/interfaces" %} are a similar concept, but in an interface, all types must share some common fields. Unions are a good choice when the object types don't have any significant fields in common. Since union members share _no_ fields, selections are _always_ made with typed fragments (`... on SomeType`, as seen above). ## Defining Union Types Unions extend `GraphQL::Schema::Union`. First, make a base class: ```ruby class Types::BaseUnion < GraphQL::Schema::Union end ``` Then, extend that one for each union in your schema: ```ruby class Types::CommentSubject < Types::BaseUnion comment "TODO comment on the union" description "Objects which may be commented on" possible_types Types::Post, Types::Image # Optional: if this method is defined, it will override `Schema.resolve_type` def self.resolve_type(object, context) if object.is_a?(BlogPost) Types::Post else Types::Image end end end ``` The `possible_types(*types)` method accepts one or more types which belong to this union. Union classes are never instantiated; At runtime, only their `.resolve_type` methods may be called (if defined). For information about `.resolve_type`, see the {% internal_link "Interfaces guide", "/type_definitions/interfaces#resolve-type" %}. graphql-ruby-2.5.19/javascript_client/000077500000000000000000000000001514115062600177325ustar00rootroot00000000000000graphql-ruby-2.5.19/javascript_client/.npmignore000066400000000000000000000001171514115062600217300ustar00rootroot00000000000000node_modules/ OperationStoreClient.js yarn.lock src/ **/__tests__/* coverage/* graphql-ruby-2.5.19/javascript_client/CHANGELOG.md000066400000000000000000000177121514115062600215530ustar00rootroot00000000000000# graphql-ruby-client # 1.14.9 (12 Dec 2025) - Update `glob` #5472 # 1.14.8 (17 Jun 2025) - Update some dependencies #5387 # 1.14.7 (6 Jun 2025) - `AblyLink`: Support calling `.unsubscribe()` from application code #5374 # 1.14.6 (25 Mar 2025) - `ActionCableLink`: accept ActionCable subscription callbacks #5288 # 1.14.5 (8 Nov 2024) - `sync`: Fix `--dump-payload` with `--outfile` #5152 # 1.14.4 (8 Nov 2024) - ActionCable: prevent unsubscribe being called twice with Relay and Urql #5150 # 1.14.3 (5 Nov 2024) - `createActionCableHandler`: Make sure `unsubscribe` is only called once #5109 # 1.14.2 (4 Nov 2024) - `sync`: Add a `--dump-payload` option for printing out the HTTP Post data #5143 # 1.14.1 (30 Sept 2024) - `AblyLink`: don't set up an Ably subscription when no Subscription header is present #5113 # 1.14.0 (3 Jul 2024) - Subscriptions: with Relay and ActionCable, don't send an empty query string (`""`) when using persisted operations #5008 # 1.13.3 (20 Mar 2024) - Subscriptions: Support `urql` + ActionCable #4886 # 1.13.2 (28 Feb 2024) - Update `glob` to v10+ to eliminate dependency on `inflight` #4859 # 1.13.1 (23 Feb 2024) - createAblyHandler: add typing for `onError` handler #4845 # 1.13.0 (23 Jan 2024) - Sync: add support for `generate-persisted-query-manifest` files #4798 - createActionCableHandler: remove needless `perform("send", ...)` call #4793 # 1.12.1 (29 Dec 2023) - GraphiQL: support custom `channelName` and `url` in ActionCable fetcher #4756 # 1.12.0 (7 Dec 2023) - Add GraphiQL support for subscriptions #4724 # 1.11.10 (17 Nov 2023) - `createRelaySubscriptionHandler`: Support Relay persisted queries with ActionCable #4705 # 1.11.9 (1 Sept 2023) - `createRelaySubscriptionHandler`: fix error handling in handler functions #4603 # 1.11.8 (9 May 2023) - ActionCable: accept a custom `channelName` for `createActionCableHandler` and `addGraphQLSubscriptions` #4463 # 1.11.7 (24 February 2023) - ActionCableLink: fix race condition #4359 # 1.11.6 (14 February 2023) - Sync: fix `--changeset-version` #4328 - Improve verbose logging #4328 # 1.11.5 (27 January 2023) - Sync: add a `--changeset-version` for use with Changesets #4304 - Sync: fix handling of `--header` with a single header # 1.11.4 (4 January 2023) - PusherLink: pass initial response along to the client #4282 # 1.11.3 (13 October 2022) - `createAblySubscriptions`: don't use `Error.captureStackTrace` which isn't supported in all JS runtimes #4223 - `createAblySubscriptions`: properly handle empty initial response from the interpreter (`{}`) #4226 # 1.11.2 (26 August 2022) - Sync: Add a `--header` option for custom headers #4171 # 1.11.1 (19 July 2022) - Subscriptions: ActionCableLink: only forward the result if `data` or `errors` is present #4114 # 1.11.0 (4 July 2022) - Subscriptions: Add `urql` support for Pusher #4129 # 1.10.7 (29 Mar 2022) - Dependencies: loosen apollo client and graphql version requirements to accept newer versions #4008 # 1.10.6 (10 Jan 2022) - Pusher Link: Don't pass along the `complete` handler because Apollo unsubscribes if you do #3830 # 1.10.5 (17 Dec 2021) - Dependencies: replace `actioncable` with `@rails/actioncable` #3773 # 1.10.4 (19 Nov 2021) - Sync: Also make sure documents are valid after removing `@client` fields #3715 # 1.10.3 (18 Nov 2021) - Sync: Remove any fields with `@client` before sending operations to the server #3712 # 1.10.2 (25 Oct 2021) - Pusher Link: Properly forward network errors to subscribers #3638 # 1.10.1 (22 Sept 2021) - Sync: Add `--apollo-codegen-json-output=...` option #3616 # 1.10.0 (25 Aug 2021) - Remove direct dependency on `request` #3594 - Update `createRelaySubscriptionHandler` to support Relay 11. Use `createLegacyRelaySubscriptionHandler` to get the old behavior. #3594 # 1.9.3 (31 Mar 2021) - Move `graphql` and `@apollo/client` to `peerDeps` for more flexible versions #3395 # 1.9.2 (19 Feb 2021) - Remove dependency on React by changing imports to `@apollo/client/core` #3349 # 1.9.1 (11 Feb 2021) - Support graphql 15.x in dependencies #3334 ## 1.9.0 (10 Feb 2021) - Move "compiled" `.js` files into the root directory of the published NPM package (instead of `dist/`). To upgrade, remove `dist/` from your import paths. (These files will be treated as public APIs in their own right, exposed independently to support smaller bundle sizes.) #2768 - Upgrade dependency from `apollo-link` to `@apollo/client` #3270 ## 1.8.2 (2 Feb 2021) - Pusher: Accept a `decompress:` function for handling compressed payloads #3311 ## 1.8.1 (16 Nov 2020) - Sync: When `--url` is omitted, generate an outfile without syncing with a server ## 1.8.0 (10 Nov 2020) - Ably: Support server-side `cipher_base:` config in the client ## 1.7.12 (3 Nov 2020) - Ably: Add `rewind:` config so messages aren't lost between subscribe and registration of listener. #3210 - Ably: Fix race condition where error was raised before the channel was available. #3210 ## 1.7.11 (15 June 2020) - Ably: Improve channel state handling in case the initial subscription result contains errors #2993 ## 1.7.10 (13 June 2020) - Ably: Improve error handling and channel cleanup #2991 ## 1.7.9 (15 May 2020) - Ably: _completely_ unsubscribe when subscriptions are done #2944 - Ably: propagate errors from subscriptions #2944 ## 1.7.8 (1 May 2020) - `sync`: Add support for Apollo-Android's `OperationOutput.json` #2914 ## 1.7.7 (15 Apr 2020) - Ably handler: dispatch initial response #2866 - Ably handler: catch any error in initial HTTP call #2877 ## 1.7.6 (3 Apr 2020) - Fix ActionCableLink sending unsubcribe to ActionCable #2842 ## 1.7.5 (4 Mar 2020) - Add missing dependency declarations ## 1.7.4 (18 Feb 2020) - Move all exports to top level - Fix sync body handling: wait for all chunks, improve verbose output ## 1.7.3 (17 Feb 2020) - Fix CLI for TypeScript ## 1.7.2 (17 Feb 2020) - Convert outfile generators to TypeScript and include them in published package ## 1.7.1 (17 Feb 2020) - Fix `bin` configuration in package.json ## 1.7.0 (17 Feb 2020) - Rewrite in TypeScript ## 1.6.8 (18 Sept 2019) - Properly send `Content-Type: application/json` when posting persisted operations ## 1.6.7 (18 Sept 2019) - Add post data to `--verbose` output of `sync` ## 1.6.6 (6 Aug 2019) - Add `--relay-persisted-output` for working with Relay Compiler's new `--persist-output` option #2415 ## 1.6.5 (17 July 2019) - Update dependencies #2335 ## 1.6.4 (11 May 2019) - Add `--verbose` option to `sync` #2075 - Support Relay 2.0.0 #2121 - ActionCableLink: support subscriber when there are errors but no data #2176 ## 1.6.3 (11 Jan 2019) - Fix `.unsubscribe()` for PusherLink #2042 ## 1.6.2 (14 Dec 2018) - Support identified Ably client #2003 ## 1.6.1 (30 Nov 2018) - Support `ably:` option for Relay subscriptions ## 1.6.0 (19 Nov 2018) - Fix unused requires #1943 - Add `generateClient` function to generate code _without_ the HTTP call #1941 ## 1.5.0 (27 October 2018) - Fix `export` usage in PusherLink, use `require` and `module.exports` instead #1889 - Add `AblyLink` #1925 ## 1.4.1 (19 Sept 2018) - Add `connectionOptions` to ActionCableLink #1857 ## 1.4.0 (12 Apr 2018) - Add `PusherLink` for Apollo 2 Subscriptions on Pusher - Add `OperationStoreLink` for Apollo 2 persisted queries ## 1.3.0 (30 Nov 2017) - Support HTTPS, basic auth, query string and port in `sync` #1053 - Add Apollo 2 support for ActionCable subscriptions #1120 - Add `--outfile-type=json` for stored operation manifest #1142 ## 1.2.0 (15 Nov 2017) - Support Apollo batching middleware #1092 ## 1.1.3 (11 Oct 2017) - Fix Apollo + ActionCable unsubscribe function #1019 ## 1.1.2 (9 Oct 2017) - Add channel IDs to ActionCable subscriptions #1004 ## 1.1.1 (21 Sept 2017) - Add `--add-typename` option to `sync` #967 ## 1.1.0 (18 Sept 2017) - Add subscription clients for Apollo and Relay Modern ## 1.0.2 (22 Aug 2017) - Remove debug output ## 1.0.1 (21 Aug 2017) - Rename from `graphql-pro-js` to `graphql-ruby-client` ## 1.0.0 (31 Jul 2017) - Add `sync` task graphql-ruby-2.5.19/javascript_client/LICENSE.md000066400000000000000000000007341514115062600213420ustar00rootroot00000000000000Copyright (c) Minimum Viable Software `graphql-ruby-client` is an Open Source project licensed under the terms of the LGPLv3 license. Please see https://www.gnu.org/licenses/lgpl-3.0.html for license text. `GraphQL::Pro` customers are granted a commercial-friendly license allowing private forks and modifications of `graphql-ruby-client`. Please see https://graphql.pro/ for more detail. You can find the commercial license terms at https://graphql.pro/COMM-LICENSE.html. graphql-ruby-2.5.19/javascript_client/jest.config.js000066400000000000000000000002611514115062600225000ustar00rootroot00000000000000module.exports = { roots: [ "/src" ], verbose: true, testMatch: [ "**/__tests__/**/[^.]+Test.ts", ], transform: { "^.+\\.ts$": "ts-jest" }, } graphql-ruby-2.5.19/javascript_client/package-lock.json000066400000000000000000006054411514115062600231600ustar00rootroot00000000000000{ "name": "graphql-ruby-client", "version": "1.14.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "graphql-ruby-client", "version": "1.14.9", "license": "LGPL-3.0", "dependencies": { "glob": "^10.5.0", "minimist": "^1.2.0" }, "bin": { "graphql-ruby-client": "cli.js" }, "devDependencies": { "@apollo/client": ">=3.3.13", "@rails/actioncable": "^7.0.0", "@types/glob": "^7.1.1", "@types/jest": "^25.1.2", "@types/minimist": "^1.2.0", "@types/node": "^18.0.0", "@types/pako": "^1.0.1", "@types/pusher-js": "^4.2.2", "@types/rails__actioncable": "^6.1.6", "@types/react": "^17.0.0", "@types/relay-runtime": "^14.0.0", "@types/zen-observable": "^0.8.2", "ably": "1.2.50", "graphql": ">=15.0.0", "jest": "^29.0.0", "nock": "^11.0.0", "pako": "^2.0.3", "prettier": "^1.19.1", "pusher-js": "^7.0.3", "relay-runtime": "11.0.2", "ts-jest": "^29.0.0", "typescript": "5.3.3", "urql": "^2.2.2" }, "engines": { "node": ">=10" }, "peerDependencies": { "@apollo/client": ">=3.3.6", "graphql": ">=14.3.1" } }, "node_modules/@ably/msgpack-js": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@ably/msgpack-js/-/msgpack-js-0.4.0.tgz", "integrity": "sha512-IPt/BoiQwCWubqoNik1aw/6M/DleMdrxJOUpSja6xmMRbT2p1TA8oqKWgfZabqzrq8emRNeSl/+4XABPNnW5pQ==", "dev": true, "dependencies": { "bops": "^1.0.1" } }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@apollo/client": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.8.1.tgz", "integrity": "sha512-JGGj/9bdoLEqzatRikDeN8etseY5qeFAY0vSAx/Pd0ePNsaflKzHx6V2NZ0NsGkInq+9IXXX3RLVDf0EotizMA==", "dev": true, "license": "MIT", "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@wry/context": "^0.7.3", "@wry/equality": "^0.5.6", "@wry/trie": "^0.4.3", "graphql-tag": "^2.12.6", "hoist-non-react-statics": "^3.3.2", "optimism": "^0.17.5", "prop-types": "^15.7.2", "response-iterator": "^0.2.6", "symbol-observable": "^4.0.0", "ts-invariant": "^0.10.3", "tslib": "^2.3.0", "zen-observable-ts": "^1.2.5" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0", "graphql-ws": "^5.5.5", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" }, "peerDependenciesMeta": { "graphql-ws": { "optional": true }, "react": { "optional": true }, "react-dom": { "optional": true }, "subscriptions-transport-ws": { "optional": true } } }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { "version": "7.22.9", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { "version": "7.22.11", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.11.tgz", "integrity": "sha512-lh7RJrtPdhibbxndr6/xx0w8+CVlY5FJZiaSz908Fpy+G0xkBFTvwLcKJFF4PJxVfGhVWNebikpWGnOoC71juQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.10", "@babel/generator": "^7.22.10", "@babel/helper-compilation-targets": "^7.22.10", "@babel/helper-module-transforms": "^7.22.9", "@babel/helpers": "^7.22.11", "@babel/parser": "^7.22.11", "@babel/template": "^7.22.5", "@babel/traverse": "^7.22.11", "@babel/types": "^7.22.11", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/babel" } }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { "version": "7.24.5", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", "dev": true, "dependencies": { "@babel/types": "^7.24.5", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { "version": "7.22.10", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz", "integrity": "sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.22.9", "@babel/helper-validator-option": "^7.22.5", "browserslist": "^4.21.9", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" }, "node_modules/@babel/helper-environment-visitor": { "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { "version": "7.23.0", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { "@babel/template": "^7.22.15", "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { "version": "7.22.9", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-module-imports": "^7.22.5", "@babel/helper-simple-access": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "@babel/helper-validator-identifier": "^7.22.5" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-plugin-utils": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-simple-access": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { "version": "7.24.5", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", "dev": true, "dependencies": { "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { "version": "7.27.6", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { "version": "7.27.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.27.3" }, "bin": { "parser": "bin/babel-parser.js" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-bigint": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-class-properties": { "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-import-meta": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-json-strings": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-jsx": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-logical-assignment-operators": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-numeric-separator": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-object-rest-spread": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-optional-catch-binding": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-optional-chaining": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-top-level-await": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-typescript": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/runtime": { "version": "7.27.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { "version": "7.24.5", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.24.2", "@babel/generator": "^7.24.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.24.5", "@babel/parser": "^7.24.5", "@babel/types": "^7.24.5", "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { "version": "7.27.6", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true, "license": "MIT" }, "node_modules/@graphql-typed-document-node/core": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", "dev": true, "license": "MIT", "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "license": "ISC", "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" }, "engines": { "node": ">=12" } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" }, "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, "license": "ISC", "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" }, "engines": { "node": ">=8" } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jest/console": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.6.4.tgz", "integrity": "sha512-wNK6gC0Ha9QeEPSkeJedQuTQqxZYnDPuDcDhVuVatRvMkL4D0VTvFVZj+Yuh6caG2aOfzkUZ36KtCmLNtR02hw==", "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "jest-message-util": "^29.6.3", "jest-util": "^29.6.3", "slash": "^3.0.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/console/node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", "dev": true, "license": "MIT" }, "node_modules/@jest/core": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.6.4.tgz", "integrity": "sha512-U/vq5ccNTSVgYH7mHnodHmCffGWHJnz/E1BEWlLuK5pM4FZmGfBn/nrJGLjUsSmyx3otCeqc1T31F4y08AMDLg==", "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.6.4", "@jest/reporters": "^29.6.4", "@jest/test-result": "^29.6.4", "@jest/transform": "^29.6.4", "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-changed-files": "^29.6.3", "jest-config": "^29.6.4", "jest-haste-map": "^29.6.4", "jest-message-util": "^29.6.3", "jest-regex-util": "^29.6.3", "jest-resolve": "^29.6.4", "jest-resolve-dependencies": "^29.6.4", "jest-runner": "^29.6.4", "jest-runtime": "^29.6.4", "jest-snapshot": "^29.6.4", "jest-util": "^29.6.3", "jest-validate": "^29.6.3", "jest-watcher": "^29.6.4", "micromatch": "^4.0.4", "pretty-format": "^29.6.3", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "peerDependenciesMeta": { "node-notifier": { "optional": true } } }, "node_modules/@jest/core/node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", "dev": true, "license": "MIT" }, "node_modules/@jest/environment": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.6.4.tgz", "integrity": "sha512-sQ0SULEjA1XUTHmkBRl7A1dyITM9yb1yb3ZNKPX3KlTd6IG7mWUe3e2yfExtC2Zz1Q+mMckOLHmL/qLiuQJrBQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/fake-timers": "^29.6.4", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/environment/node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", "dev": true, "license": "MIT" }, "node_modules/@jest/expect": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.6.4.tgz", "integrity": "sha512-Warhsa7d23+3X5bLbrbYvaehcgX5TLYhI03JKoedTiI8uJU4IhqYBWF7OSSgUyz4IgLpUYPkK0AehA5/fRclAA==", "dev": true, "license": "MIT", "dependencies": { "expect": "^29.6.4", "jest-snapshot": "^29.6.4" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect-utils": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.6.4.tgz", "integrity": "sha512-FEhkJhqtvBwgSpiTrocquJCdXPsyvNKcl/n7A3u7X4pVoF4bswm11c9d4AV+kfq2Gpv/mM8x7E7DsRvH+djkrg==", "dev": true, "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/fake-timers": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.6.4.tgz", "integrity": "sha512-6UkCwzoBK60edXIIWb0/KWkuj7R7Qq91vVInOe3De6DSpaEiqjKcJw4F7XUet24Wupahj9J6PlR09JqJ5ySDHw==", "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.6.3", "jest-mock": "^29.6.3", "jest-util": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/fake-timers/node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", "dev": true, "license": "MIT" }, "node_modules/@jest/globals": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.6.4.tgz", "integrity": "sha512-wVIn5bdtjlChhXAzVXavcY/3PEjf4VqM174BM3eGL5kMxLiZD5CLnbmkEyA1Dwh9q8XjP6E8RwjBsY/iCWrWsA==", "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.6.4", "@jest/expect": "^29.6.4", "@jest/types": "^29.6.3", "jest-mock": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/reporters": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.6.4.tgz", "integrity": "sha512-sxUjWxm7QdchdrD3NfWKrL8FBsortZeibSJv4XLjESOOjSUOkjQcb0ZHJwfhEGIvBvTluTzfG2yZWZhkrXJu8g==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "^29.6.4", "@jest/test-result": "^29.6.4", "@jest/transform": "^29.6.4", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", "exit": "^0.1.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", "jest-message-util": "^29.6.3", "jest-util": "^29.6.3", "jest-worker": "^29.6.4", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", "v8-to-istanbul": "^9.0.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "peerDependenciesMeta": { "node-notifier": { "optional": true } } }, "node_modules/@jest/reporters/node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", "dev": true, "license": "MIT" }, "node_modules/@jest/reporters/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, "engines": { "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/source-map": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", "graceful-fs": "^4.2.9" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/test-result": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.6.4.tgz", "integrity": "sha512-uQ1C0AUEN90/dsyEirgMLlouROgSY+Wc/JanVVk0OiUKa5UFh7sJpMEM3aoUBAz2BRNvUJ8j3d294WFuRxSyOQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.6.4", "@jest/types": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/test-sequencer": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.6.4.tgz", "integrity": "sha512-E84M6LbpcRq3fT4ckfKs9ryVanwkaIB0Ws9bw3/yP4seRLg/VaCZ/LgW0MCq5wwk4/iP/qnilD41aj2fsw2RMg==", "dev": true, "license": "MIT", "dependencies": { "@jest/test-result": "^29.6.4", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.6.4", "slash": "^3.0.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/transform": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.6.4.tgz", "integrity": "sha512-8thgRSiXUqtr/pPGY/OsyHuMjGyhVnWrFAwoxmIemlBuiMyU1WFs0tXoNxzcr4A4uErs/ABre76SGmrr5ab/AA==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.6.4", "jest-regex-util": "^29.6.3", "jest-util": "^29.6.3", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", "write-file-atomic": "^4.0.2" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/transform/node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, "node_modules/@jest/types": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/types/node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "license": "MIT", "optional": true, "engines": { "node": ">=14" } }, "node_modules/@rails/actioncable": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@rails/actioncable/-/actioncable-7.1.2.tgz", "integrity": "sha512-KGziTZfbmGm8/fHOpj515xupbYU+49hsp4etfdpoDJ/CEY2bRZR0cyFcJkpK6n0t/sxOHNWY6bo9vSgXZvT7Mg==", "dev": true, "license": "MIT" }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true, "license": "MIT" }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "dev": true, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sindresorhus/is?sponsor=1" } }, "node_modules/@sinonjs/commons": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" } }, "node_modules/@sinonjs/fake-timers": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", "dev": true, "dependencies": { "defer-to-connect": "^2.0.0" }, "engines": { "node": ">=10" } }, "node_modules/@types/babel__core": { "version": "7.20.1", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", "integrity": "sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==", "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "node_modules/@types/babel__generator": { "version": "7.6.4", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__template": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__traverse": { "version": "7.20.1", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.1.tgz", "integrity": "sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg==", "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.20.7" } }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", "dev": true, "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "node_modules/@types/express-serve-static-core": { "version": "4.17.28", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*" } }, "node_modules/@types/express-serve-static-core/node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", "dev": true, "license": "MIT" }, "node_modules/@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", "dev": true, "license": "MIT", "dependencies": { "@types/minimatch": "*", "@types/node": "*" } }, "node_modules/@types/glob/node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", "dev": true, "license": "MIT" }, "node_modules/@types/graceful-fs": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/graceful-fs/node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", "dev": true, "license": "MIT" }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", "dev": true }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", "dev": true, "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "node_modules/@types/istanbul-reports": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", "dev": true, "license": "MIT", "dependencies": { "@types/istanbul-lib-report": "*" } }, "node_modules/@types/jest": { "version": "25.2.3", "resolved": "https://registry.npmjs.org/@types/jest/-/jest-25.2.3.tgz", "integrity": "sha512-JXc1nK/tXHiDhV55dvfzqtmP4S3sy3T3ouV2tkViZgxY/zeUkcpQcQPGRlgF4KmWzWW5oiWYSZwtCB+2RsE4Fw==", "dev": true, "license": "MIT", "dependencies": { "jest-diff": "^25.2.1", "pretty-format": "^25.2.1" } }, "node_modules/@types/jest/node_modules/@jest/types": { "version": "25.5.0", "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", "dev": true, "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^1.1.1", "@types/yargs": "^15.0.0", "chalk": "^3.0.0" }, "engines": { "node": ">= 8.3" } }, "node_modules/@types/jest/node_modules/@types/istanbul-reports": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", "dev": true, "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "*", "@types/istanbul-lib-report": "*" } }, "node_modules/@types/jest/node_modules/@types/yargs": { "version": "15.0.15", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.15.tgz", "integrity": "sha512-IziEYMU9XoVj8hWg7k+UJrXALkGFjWJhn5QFEv9q4p+v40oZhSuC135M38st8XPjICL7Ey4TV64ferBGUoJhBg==", "dev": true, "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } }, "node_modules/@types/jest/node_modules/chalk": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" }, "engines": { "node": ">=8" } }, "node_modules/@types/jest/node_modules/diff-sequences": { "version": "25.2.6", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", "dev": true, "license": "MIT", "engines": { "node": ">= 8.3" } }, "node_modules/@types/jest/node_modules/jest-diff": { "version": "25.5.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^3.0.0", "diff-sequences": "^25.2.6", "jest-get-type": "^25.2.6", "pretty-format": "^25.5.0" }, "engines": { "node": ">= 8.3" } }, "node_modules/@types/jest/node_modules/jest-get-type": { "version": "25.2.6", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", "dev": true, "license": "MIT", "engines": { "node": ">= 8.3" } }, "node_modules/@types/jest/node_modules/pretty-format": { "version": "25.5.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^25.5.0", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^16.12.0" }, "engines": { "node": ">= 8.3" } }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/minimatch": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", "dev": true, "license": "MIT" }, "node_modules/@types/minimist": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { "version": "18.19.130", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/pako": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@types/pako/-/pako-1.0.4.tgz", "integrity": "sha512-Z+5bJSm28EXBSUJEgx29ioWeEEHUh6TiMkZHDhLwjc9wVFH+ressbkmX6waUZc5R3Gobn4Qu5llGxaoflZ+yhA==", "dev": true, "license": "MIT" }, "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", "dev": true, "license": "MIT" }, "node_modules/@types/pusher-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@types/pusher-js/-/pusher-js-4.2.2.tgz", "integrity": "sha512-LP9isBRAFlNzQohQtySJxJjzmy4zQCcv5xGZD2G3rsDnTWfpEkFKyLw3x9711pFAXwwUl9ZivxKkcnFr8umSAQ==", "dev": true, "license": "MIT" }, "node_modules/@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", "dev": true, "license": "MIT" }, "node_modules/@types/rails__actioncable": { "version": "6.1.10", "resolved": "https://registry.npmjs.org/@types/rails__actioncable/-/rails__actioncable-6.1.10.tgz", "integrity": "sha512-Dr6A/+OTsoTgvnj2ynysjuMvmk2XDI4J71ljPANcODZjMafKeSau2/IJ+B4IuJ6x9bsG/DQpBxNxH6RqmyFzpw==", "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "17.0.65", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.65.tgz", "integrity": "sha512-oxur785xZYHvnI7TRS61dXbkIhDPnGfsXKv0cNXR/0ml4SipRIFpSMzA7HMEfOywFwJ5AOnPrXYTEiTRUQeGlQ==", "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "node_modules/@types/relay-runtime": { "version": "14.1.23", "resolved": "https://registry.npmjs.org/@types/relay-runtime/-/relay-runtime-14.1.23.tgz", "integrity": "sha512-tP2l6YLI2HJ11UzEB7j4IWeADyiPIKTehdeyHsyOzNBu7WvKsyf4kAZDmsB2NPaXp9Lud+KEJbRi/VW+jEDYCA==", "dev": true, "license": "MIT" }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", "dev": true, "license": "MIT" }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true, "license": "MIT" }, "node_modules/@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", "dev": true, "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } }, "node_modules/@types/yargs-parser": { "version": "21.0.0", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "dev": true, "license": "MIT" }, "node_modules/@types/zen-observable": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.3.tgz", "integrity": "sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw==", "dev": true, "license": "MIT" }, "node_modules/@urql/core": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@urql/core/-/core-2.6.1.tgz", "integrity": "sha512-gYrEHy3tViJhwIhauK6MIf2Qp09QTsgNHZRd0n71rS+hF6gdwjspf1oKljl4m25+272cJF7fPjBUGmjaiEr7Kg==", "dev": true, "license": "MIT", "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "wonka": "^4.0.14" }, "peerDependencies": { "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, "node_modules/@wry/context": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.3.tgz", "integrity": "sha512-Nl8WTesHp89RF803Se9X3IiHjdmLBrIvPMaJkl+rKVJAYyPsz1TEUbu89943HpvujtSJgDUx9W4vZw3K1Mr3sA==", "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { "node": ">=8" } }, "node_modules/@wry/equality": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.6.tgz", "integrity": "sha512-D46sfMTngaYlrH+OspKf8mIJETntFnf6Hsjb0V41jAXJ7Bx2kB8Rv8RCUujuVWYttFtHkUNp7g+FwxNQAr6mXA==", "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { "node": ">=8" } }, "node_modules/@wry/trie": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.4.3.tgz", "integrity": "sha512-I6bHwH0fSf6RqQcnnXLJKhkSXG45MFral3GxPaY4uAl0LYDZM+YDVDAiU9bYwjTuysy1S0IeecWtmq1SZA3M1w==", "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { "node": ">=8" } }, "node_modules/ably": { "version": "1.2.50", "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.50.tgz", "integrity": "sha512-9uC5lE7wFBR7nOJdltArHU1UDaBu6CTMbMuie+brUuH884fM1mQuFiMzZetxVacygyUG38dp5MFOyOPF9h0RsQ==", "dev": true, "dependencies": { "@ably/msgpack-js": "^0.4.0", "got": "^11.8.5", "ws": "^8.14.2" }, "engines": { "node": ">=5.10.x" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" }, "peerDependenciesMeta": { "react": { "optional": true }, "react-dom": { "optional": true } } }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, "license": "MIT", "dependencies": { "type-fest": "^0.21.3" }, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" }, "engines": { "node": ">= 8" } }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true, "license": "MIT" }, "node_modules/babel-jest": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.4.tgz", "integrity": "sha512-meLj23UlSLddj6PC+YTOFRgDAtjnZom8w/ACsrx0gtPtv5cJZk0A5Unk5bV4wixD7XaPCN1fQvpww8czkZURmw==", "dev": true, "license": "MIT", "dependencies": { "@jest/transform": "^29.6.4", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "node_modules/babel-plugin-istanbul": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" }, "engines": { "node": ">=8" } }, "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" }, "engines": { "node": ">=8" } }, "node_modules/babel-plugin-istanbul/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/babel-plugin-jest-hoist": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", "@types/babel__core": "^7.1.14", "@types/babel__traverse": "^7.0.6" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/babel-preset-current-node-syntax": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.8.3", "@babel/plugin-syntax-import-meta": "^7.8.3", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.8.3", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-top-level-await": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "node_modules/babel-preset-jest": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", "dev": true, "license": "MIT", "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, "node_modules/base64-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.0.2.tgz", "integrity": "sha512-ZXBDPMt/v/8fsIqn+Z5VwrhdR6jVka0bYobHdGia0Nxi7BJ9i/Uvml3AocHIBtIIBhZjBw5MR0aR4ROs/8+SNg==", "dev": true, "engines": { "node": ">= 0.4" } }, "node_modules/bops": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/bops/-/bops-1.0.1.tgz", "integrity": "sha512-qCMBuZKP36tELrrgXpAfM+gHzqa0nLsWZ+L37ncsb8txYlnAoxOPpVp+g7fK0sGkMXfA0wl8uQkESqw3v4HNag==", "dev": true, "dependencies": { "base64-js": "1.0.2", "to-utf8": "0.0.1" } }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/browserslist": { "version": "4.21.10", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", "dev": true, "funding": [ { "type": "opencollective", "url": "https://opencollective.com/browserslist" }, { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" }, { "type": "github", "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "dependencies": { "caniuse-lite": "^1.0.30001517", "electron-to-chromium": "^1.4.477", "node-releases": "^2.0.13", "update-browserslist-db": "^1.0.11" }, "bin": { "browserslist": "cli.js" }, "engines": { "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, "node_modules/bs-logger": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", "dev": true, "license": "MIT", "dependencies": { "fast-json-stable-stringify": "2.x" }, "engines": { "node": ">= 6" } }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "node-int64": "^0.4.0" } }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, "license": "MIT" }, "node_modules/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", "dev": true, "engines": { "node": ">=10.6.0" } }, "node_modules/cacheable-request": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", "dev": true, "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" }, "engines": { "node": ">=8" } }, "node_modules/cacheable-request/node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, "dependencies": { "pump": "^3.0.0" }, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "license": "MIT", "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/caniuse-lite": { "version": "1.0.30001524", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001524.tgz", "integrity": "sha512-Jj917pJtYg9HSJBF95HVX3Cdr89JUyLT4IZ8SvM5aDRni95swKgYi3TgYLH5hnGfPE/U1dg6IfZ50UsIlLkwSA==", "dev": true, "funding": [ { "type": "opencollective", "url": "https://opencollective.com/browserslist" }, { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" }, { "type": "github", "url": "https://github.com/sponsors/ai" } ], "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true, "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/ci-info": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/sibiraj-s" } ], "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/cjs-module-lexer": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true, "license": "MIT" }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" }, "engines": { "node": ">=12" } }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/clone-response": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", "dev": true, "dependencies": { "mimic-response": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", "dev": true, "license": "MIT", "engines": { "iojs": ">= 1.0.0", "node": ">= 0.12.0" } }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true, "license": "MIT" }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, "engines": { "node": ">=7.0.0" } }, "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true, "license": "MIT" }, "node_modules/cross-fetch": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", "dev": true, "license": "MIT", "dependencies": { "node-fetch": "^2.6.12" } }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" }, "engines": { "node": ">= 8" } }, "node_modules/csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", "dev": true, "license": "MIT" }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "license": "MIT", "dependencies": { "ms": "2.1.2" }, "engines": { "node": ">=6.0" }, "peerDependenciesMeta": { "supports-color": { "optional": true } } }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, "dependencies": { "mimic-response": "^3.1.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/decompress-response/node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/dedent": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", "dev": true, "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "peerDependenciesMeta": { "babel-plugin-macros": { "optional": true } } }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", "dev": true, "engines": { "node": ">=10" } }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, "node_modules/electron-to-chromium": { "version": "1.4.503", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.503.tgz", "integrity": "sha512-LF2IQit4B0VrUHFeQkWhZm97KuJSGF2WJqq1InpY+ECpFRkXd8yTIaTtJxsO0OKDmiBYwWqcrNaXOurn2T2wiA==", "dev": true, "license": "ISC" }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "dev": true, "license": "MIT", "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/sindresorhus/emittery?sponsor=1" } }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, "dependencies": { "once": "^1.4.0" } }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/escape-string-regexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" }, "engines": { "node": ">=4" } }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", "dev": true, "engines": { "node": ">= 0.8.0" } }, "node_modules/expect": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/expect/-/expect-29.6.4.tgz", "integrity": "sha512-F2W2UyQ8XYyftHT57dtfg8Ue3X5qLgm2sSug0ivvLRH/VKNRL/pDxg/TH7zVzbQB0tu80clNFy6LU7OS/VSEKA==", "dev": true, "license": "MIT", "dependencies": { "@jest/expect-utils": "^29.6.4", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.6.4", "jest-message-util": "^29.6.3", "jest-util": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", "dev": true, "license": "Apache-2.0", "dependencies": { "bser": "2.1.1" } }, "node_modules/fbjs": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz", "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==", "dev": true, "license": "MIT", "dependencies": { "cross-fetch": "^3.1.5", "fbjs-css-vars": "^1.0.0", "loose-envify": "^1.0.0", "object-assign": "^4.1.0", "promise": "^7.1.1", "setimmediate": "^1.0.5", "ua-parser-js": "^1.0.35" } }, "node_modules/fbjs-css-vars": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==", "dev": true, "license": "MIT" }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, "engines": { "node": ">=8" } }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" }, "engines": { "node": ">=8" } }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" }, "engines": { "node": ">=14" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/foreground-child/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "license": "ISC", "engines": { "node": ">=14" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true, "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true, "license": "MIT" }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, "license": "MIT", "engines": { "node": ">=8.0.0" } }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, "license": "MIT", "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/glob/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/glob/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/got": { "version": "11.8.6", "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", "dev": true, "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" }, "engines": { "node": ">=10.19.0" }, "funding": { "url": "https://github.com/sindresorhus/got?sponsor=1" } }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, "license": "ISC" }, "node_modules/graphql": { "version": "16.11.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", "dev": true, "license": "MIT", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, "node_modules/graphql-tag": { "version": "2.12.6", "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.1.0" }, "engines": { "node": ">=10" }, "peerDependencies": { "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.1" }, "engines": { "node": ">= 0.4.0" } }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "react-is": "^16.7.0" } }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, "license": "MIT" }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", "dev": true, "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" }, "engines": { "node": ">=10.19.0" } }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.17.0" } }, "node_modules/import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", "dev": true, "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" }, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" } }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, "license": "ISC" }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.0.0" } }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, "license": "MIT" }, "node_modules/is-core-module": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", "dev": true, "license": "MIT", "dependencies": { "has": "^1.0.3" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/is-generator-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "engines": { "node": ">=0.12.0" } }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, "license": "MIT", "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=8" } }, "node_modules/istanbul-lib-instrument": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.0.tgz", "integrity": "sha512-x58orMzEVfzPUKqlbLd1hXCnySCxKdDKa6Rjg97CwuLLRI4g3FHTdnExu1OqffVFay6zeMW+T6/DowFLndWnIw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^7.5.4" }, "engines": { "node": ">=10" } }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" }, "engines": { "node": ">=10" } }, "node_modules/istanbul-lib-source-maps": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", "source-map": "^0.6.1" }, "engines": { "node": ">=10" } }, "node_modules/istanbul-reports": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" }, "engines": { "node": ">=8" } }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, "funding": { "url": "https://github.com/sponsors/isaacs" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "node_modules/jest": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/jest/-/jest-29.6.4.tgz", "integrity": "sha512-tEFhVQFF/bzoYV1YuGyzLPZ6vlPrdfvDmmAxudA1dLEuiztqg2Rkx20vkKY32xiDROcD2KXlgZ7Cu8RPeEHRKw==", "dev": true, "license": "MIT", "dependencies": { "@jest/core": "^29.6.4", "@jest/types": "^29.6.3", "import-local": "^3.0.2", "jest-cli": "^29.6.4" }, "bin": { "jest": "bin/jest.js" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "peerDependenciesMeta": { "node-notifier": { "optional": true } } }, "node_modules/jest-changed-files": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.6.3.tgz", "integrity": "sha512-G5wDnElqLa4/c66ma5PG9eRjE342lIbF6SUnTJi26C3J28Fv2TVY2rOyKB9YGbSA5ogwevgmxc4j4aVjrEK6Yg==", "dev": true, "license": "MIT", "dependencies": { "execa": "^5.0.0", "jest-util": "^29.6.3", "p-limit": "^3.1.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-circus": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.6.4.tgz", "integrity": "sha512-YXNrRyntVUgDfZbjXWBMPslX1mQ8MrSG0oM/Y06j9EYubODIyHWP8hMUbjbZ19M3M+zamqEur7O80HODwACoJw==", "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.6.4", "@jest/expect": "^29.6.4", "@jest/test-result": "^29.6.4", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", "dedent": "^1.0.0", "is-generator-fn": "^2.0.0", "jest-each": "^29.6.3", "jest-matcher-utils": "^29.6.4", "jest-message-util": "^29.6.3", "jest-runtime": "^29.6.4", "jest-snapshot": "^29.6.4", "jest-util": "^29.6.3", "p-limit": "^3.1.0", "pretty-format": "^29.6.3", "pure-rand": "^6.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-circus/node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", "dev": true, "license": "MIT" }, "node_modules/jest-cli": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.6.4.tgz", "integrity": "sha512-+uMCQ7oizMmh8ZwRfZzKIEszFY9ksjjEQnTEMTaL7fYiL3Kw4XhqT9bYh+A4DQKUb67hZn2KbtEnDuHvcgK4pQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/core": "^29.6.4", "@jest/test-result": "^29.6.4", "@jest/types": "^29.6.3", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "import-local": "^3.0.2", "jest-config": "^29.6.4", "jest-util": "^29.6.3", "jest-validate": "^29.6.3", "prompts": "^2.0.1", "yargs": "^17.3.1" }, "bin": { "jest": "bin/jest.js" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "peerDependenciesMeta": { "node-notifier": { "optional": true } } }, "node_modules/jest-config": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.6.4.tgz", "integrity": "sha512-JWohr3i9m2cVpBumQFv2akMEnFEPVOh+9L2xIBJhJ0zOaci2ZXuKJj0tgMKQCBZAKA09H049IR4HVS/43Qb19A==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", "@jest/test-sequencer": "^29.6.4", "@jest/types": "^29.6.3", "babel-jest": "^29.6.4", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "jest-circus": "^29.6.4", "jest-environment-node": "^29.6.4", "jest-get-type": "^29.6.3", "jest-regex-util": "^29.6.3", "jest-resolve": "^29.6.4", "jest-runner": "^29.6.4", "jest-util": "^29.6.3", "jest-validate": "^29.6.3", "micromatch": "^4.0.4", "parse-json": "^5.2.0", "pretty-format": "^29.6.3", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "@types/node": "*", "ts-node": ">=9.0.0" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, "ts-node": { "optional": true } } }, "node_modules/jest-config/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, "engines": { "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/jest-diff": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.6.4.tgz", "integrity": "sha512-9F48UxR9e4XOEZvoUXEHSWY4qC4zERJaOfrbBg9JpbJOO43R1vN76REt/aMGZoY6GD5g84nnJiBIVlscegefpw==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-docblock": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.6.3.tgz", "integrity": "sha512-2+H+GOTQBEm2+qFSQ7Ma+BvyV+waiIFxmZF5LdpBsAEjWX8QYjSCa4FrkIYtbfXUJJJnFCYrOtt6TZ+IAiTjBQ==", "dev": true, "license": "MIT", "dependencies": { "detect-newline": "^3.0.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-each": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.6.3.tgz", "integrity": "sha512-KoXfJ42k8cqbkfshW7sSHcdfnv5agDdHCPA87ZBdmHP+zJstTJc0ttQaJ/x7zK6noAL76hOuTIJ6ZkQRS5dcyg==", "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "jest-util": "^29.6.3", "pretty-format": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-environment-node": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.6.4.tgz", "integrity": "sha512-i7SbpH2dEIFGNmxGCpSc2w9cA4qVD+wfvg2ZnfQ7XVrKL0NA5uDVBIiGH8SR4F0dKEv/0qI5r+aDomDf04DpEQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.6.4", "@jest/fake-timers": "^29.6.4", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.6.3", "jest-util": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-environment-node/node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", "dev": true, "license": "MIT" }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-haste-map": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.6.4.tgz", "integrity": "sha512-12Ad+VNTDHxKf7k+M65sviyynRoZYuL1/GTuhEVb8RYsNSNln71nANRb/faSyWvx0j+gHcivChXHIoMJrGYjog==", "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.6.3", "jest-util": "^29.6.3", "jest-worker": "^29.6.4", "micromatch": "^4.0.4", "walker": "^1.0.8" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "optionalDependencies": { "fsevents": "^2.3.2" } }, "node_modules/jest-haste-map/node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", "dev": true, "license": "MIT" }, "node_modules/jest-leak-detector": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.6.3.tgz", "integrity": "sha512-0kfbESIHXYdhAdpLsW7xdwmYhLf1BRu4AA118/OxFm0Ho1b2RcTmO4oF6aAMaxpxdxnJ3zve2rgwzNBD4Zbm7Q==", "dev": true, "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3", "pretty-format": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-matcher-utils": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.6.4.tgz", "integrity": "sha512-KSzwyzGvK4HcfnserYqJHYi7sZVqdREJ9DMPAKVbS98JsIAvumihaNUbjrWw0St7p9IY7A9UskCW5MYlGmBQFQ==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.6.4", "jest-get-type": "^29.6.3", "pretty-format": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-message-util": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.6.3.tgz", "integrity": "sha512-FtzaEEHzjDpQp51HX4UMkPZjy46ati4T5pEMyM6Ik48ztu4T9LQplZ6OsimHx7EuM9dfEh5HJa6D3trEftu3dA==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.6.3", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-mock": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.6.3.tgz", "integrity": "sha512-Z7Gs/mOyTSR4yPsaZ72a/MtuK6RnC3JYqWONe48oLaoEcYwEDxqvbXz85G4SJrm2Z5Ar9zp6MiHF4AlFlRM4Pg==", "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-mock/node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", "dev": true, "license": "MIT" }, "node_modules/jest-pnp-resolver": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "dev": true, "license": "MIT", "engines": { "node": ">=6" }, "peerDependencies": { "jest-resolve": "*" }, "peerDependenciesMeta": { "jest-resolve": { "optional": true } } }, "node_modules/jest-regex-util": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-resolve": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.6.4.tgz", "integrity": "sha512-fPRq+0vcxsuGlG0O3gyoqGTAxasagOxEuyoxHeyxaZbc9QNek0AmJWSkhjlMG+mTsj+8knc/mWb3fXlRNVih7Q==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.6.4", "jest-pnp-resolver": "^1.2.2", "jest-util": "^29.6.3", "jest-validate": "^29.6.3", "resolve": "^1.20.0", "resolve.exports": "^2.0.0", "slash": "^3.0.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-resolve-dependencies": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.6.4.tgz", "integrity": "sha512-7+6eAmr1ZBF3vOAJVsfLj1QdqeXG+WYhidfLHBRZqGN24MFRIiKG20ItpLw2qRAsW/D2ZUUmCNf6irUr/v6KHA==", "dev": true, "license": "MIT", "dependencies": { "jest-regex-util": "^29.6.3", "jest-snapshot": "^29.6.4" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-runner": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.6.4.tgz", "integrity": "sha512-SDaLrMmtVlQYDuG0iSPYLycG8P9jLI+fRm8AF/xPKhYDB2g6xDWjXBrR5M8gEWsK6KVFlebpZ4QsrxdyIX1Jaw==", "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.6.4", "@jest/environment": "^29.6.4", "@jest/test-result": "^29.6.4", "@jest/transform": "^29.6.4", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.13.1", "graceful-fs": "^4.2.9", "jest-docblock": "^29.6.3", "jest-environment-node": "^29.6.4", "jest-haste-map": "^29.6.4", "jest-leak-detector": "^29.6.3", "jest-message-util": "^29.6.3", "jest-resolve": "^29.6.4", "jest-runtime": "^29.6.4", "jest-util": "^29.6.3", "jest-watcher": "^29.6.4", "jest-worker": "^29.6.4", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-runner/node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", "dev": true, "license": "MIT" }, "node_modules/jest-runtime": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.6.4.tgz", "integrity": "sha512-s/QxMBLvmwLdchKEjcLfwzP7h+jsHvNEtxGP5P+Fl1FMaJX2jMiIqe4rJw4tFprzCwuSvVUo9bn0uj4gNRXsbA==", "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.6.4", "@jest/fake-timers": "^29.6.4", "@jest/globals": "^29.6.4", "@jest/source-map": "^29.6.3", "@jest/test-result": "^29.6.4", "@jest/transform": "^29.6.4", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.6.4", "jest-message-util": "^29.6.3", "jest-mock": "^29.6.3", "jest-regex-util": "^29.6.3", "jest-resolve": "^29.6.4", "jest-snapshot": "^29.6.4", "jest-util": "^29.6.3", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-runtime/node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", "dev": true, "license": "MIT" }, "node_modules/jest-runtime/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, "engines": { "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/jest-snapshot": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.6.4.tgz", "integrity": "sha512-VC1N8ED7+4uboUKGIDsbvNAZb6LakgIPgAF4RSpF13dN6YaMokfRqO+BaqK4zIh6X3JffgwbzuGqDEjHm/MrvA==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/types": "^7.3.3", "@jest/expect-utils": "^29.6.4", "@jest/transform": "^29.6.4", "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", "expect": "^29.6.4", "graceful-fs": "^4.2.9", "jest-diff": "^29.6.4", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.6.4", "jest-message-util": "^29.6.3", "jest-util": "^29.6.3", "natural-compare": "^1.4.0", "pretty-format": "^29.6.3", "semver": "^7.5.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-util": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.6.3.tgz", "integrity": "sha512-QUjna/xSy4B32fzcKTSz1w7YYzgiHrjjJjevdRf61HYk998R5vVMMNmrHESYZVDS5DSWs+1srPLPKxXPkeSDOA==", "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-util/node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", "dev": true, "license": "MIT" }, "node_modules/jest-validate": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.6.3.tgz", "integrity": "sha512-e7KWZcAIX+2W1o3cHfnqpGajdCs1jSM3DkXjGeLSNmCazv1EeI1ggTeK5wdZhF+7N+g44JI2Od3veojoaumlfg==", "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-watcher": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.6.4.tgz", "integrity": "sha512-oqUWvx6+On04ShsT00Ir9T4/FvBeEh2M9PTubgITPxDa739p4hoQweWPRGyYeaojgT0xTpZKF0Y/rSY1UgMxvQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/test-result": "^29.6.4", "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.13.1", "jest-util": "^29.6.3", "string-length": "^4.0.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-watcher/node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", "dev": true, "license": "MIT" }, "node_modules/jest-worker": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.6.4.tgz", "integrity": "sha512-6dpvFV4WjcWbDVGgHTWo/aupl8/LbBx2NSKfiwqf79xC/yeJjKHT1+StcKy/2KTmW16hE68ccKVOtXf+WZGz7Q==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "jest-util": "^29.6.3", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-worker/node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", "dev": true, "license": "MIT" }, "node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, "license": "MIT" }, "node_modules/js-yaml": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, "engines": { "node": ">=4" } }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true, "license": "MIT" }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true, "license": "ISC" }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" }, "engines": { "node": ">=6" } }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "dependencies": { "json-buffer": "3.0.1" } }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true, "license": "MIT" }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, "engines": { "node": ">=8" } }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "dev": true, "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, "engines": { "node": ">=10" } }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "license": "MIT", "dependencies": { "semver": "^7.5.3" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true, "license": "ISC" }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "tmpl": "1.0.5" } }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true, "license": "MIT" }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" } }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, "engines": { "node": "*" } }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true, "license": "MIT" }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, "node_modules/nock": { "version": "11.9.1", "resolved": "https://registry.npmjs.org/nock/-/nock-11.9.1.tgz", "integrity": "sha512-U5wPctaY4/ar2JJ5Jg4wJxlbBfayxgKbiAeGh+a1kk6Pwnc2ZEuKviLyDSG6t0uXl56q7AALIxoM6FJrBSsVXA==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.0", "json-stringify-safe": "^5.0.1", "lodash": "^4.17.13", "mkdirp": "^0.5.0", "propagate": "^2.0.0" }, "engines": { "node": ">= 8.0" } }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dev": true, "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" }, "engines": { "node": "4.x || >=6.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "peerDependenciesMeta": { "encoding": { "optional": true } } }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true, "license": "MIT" }, "node_modules/node-releases": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", "dev": true, "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "dev": true, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.0.0" }, "engines": { "node": ">=8" } }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" } }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" }, "engines": { "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/optimism": { "version": "0.17.5", "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.17.5.tgz", "integrity": "sha512-TEcp8ZwK1RczmvMnvktxHSF2tKgMWjJ71xEFGX5ApLh67VsMSTy1ZUlipJw8W+KaqgOmQ+4pqwkeivY89j+4Vw==", "dev": true, "license": "MIT", "dependencies": { "@wry/context": "^0.7.0", "@wry/trie": "^0.4.3", "tslib": "^2.3.0" } }, "node_modules/p-cancelable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-locate": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, "engines": { "node": ">=8" } }, "node_modules/p-locate/node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, "engines": { "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, "node_modules/pako": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", "dev": true, "license": "(MIT AND Zlib)" }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" }, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true, "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", "engines": { "node": ">=8.6" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true, "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, "license": "MIT", "dependencies": { "find-up": "^4.0.0" }, "engines": { "node": ">=8" } }, "node_modules/prettier": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", "dev": true, "license": "MIT", "bin": { "prettier": "bin-prettier.js" }, "engines": { "node": ">=4" } }, "node_modules/pretty-format": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz", "integrity": "sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==", "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/pretty-format/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/pretty-format/node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true, "license": "MIT" }, "node_modules/promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", "dev": true, "license": "MIT", "dependencies": { "asap": "~2.0.3" } }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", "dev": true, "license": "MIT", "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" }, "engines": { "node": ">= 6" } }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "node_modules/propagate": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", "dev": true, "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "node_modules/pure-rand": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz", "integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==", "dev": true, "funding": [ { "type": "individual", "url": "https://github.com/sponsors/dubzzz" }, { "type": "opencollective", "url": "https://opencollective.com/fast-check" } ], "license": "MIT" }, "node_modules/pusher-js": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-7.6.0.tgz", "integrity": "sha512-5CJ7YN5ZdC24E0ETraCU5VYFv0IY5ziXhrS0gS5+9Qrro1E4M1lcZhtr9H1H+6jNSLj1LKKAgcLeE1EH9GxMlw==", "dev": true, "license": "MIT", "dependencies": { "@types/express-serve-static-core": "4.17.28", "@types/node": "^14.14.31", "tweetnacl": "^1.0.3" } }, "node_modules/pusher-js/node_modules/@types/node": { "version": "14.18.56", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.56.tgz", "integrity": "sha512-+k+57NVS9opgrEn5l9c0gvD1r6C+PtyhVE4BTnMMRwiEA8ZO8uFcs6Yy2sXIy0eC95ZurBtRSvhZiHXBysbl6w==", "dev": true, "license": "MIT" }, "node_modules/pusher-js/node_modules/tweetnacl": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", "dev": true, "license": "Unlicense" }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true, "license": "MIT" }, "node_modules/relay-runtime": { "version": "11.0.2", "resolved": "https://registry.npmjs.org/relay-runtime/-/relay-runtime-11.0.2.tgz", "integrity": "sha512-xxZkIRnL8kNE1cxmwDXX8P+wSeWLR+0ACFyAiAhvfWWAyjXb+bhjJ2FSsRGlNYfkqaTNEuDqpnodQV1/fF7Idw==", "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.0.0", "fbjs": "^3.0.0", "invariant": "^2.2.4" } }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/resolve": { "version": "1.22.4", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "dev": true }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" }, "engines": { "node": ">=8" } }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", "dev": true, "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/response-iterator": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/response-iterator/-/response-iterator-0.2.6.tgz", "integrity": "sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw==", "dev": true, "license": "MIT", "engines": { "node": ">=0.8" } }, "node_modules/responselike": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", "dev": true, "dependencies": { "lowercase-keys": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" }, "engines": { "node": ">=10" } }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true, "license": "MIT" }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, "engines": { "node": ">=8" } }, "node_modules/shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, "license": "ISC" }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "dev": true, "license": "MIT" }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, "license": "MIT", "dependencies": { "escape-string-regexp": "^2.0.0" }, "engines": { "node": ">=10" } }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", "dev": true, "license": "MIT", "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" }, "engines": { "node": ">=10" } }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" }, "engines": { "node": ">=8" } }, "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" }, "engines": { "node": ">=8" } }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" } }, "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" } }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, "engines": { "node": ">=8" } }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10" } }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" }, "engines": { "node": ">=8" } }, "node_modules/test-exclude/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, "engines": { "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "dependencies": { "is-number": "^7.0.0" }, "engines": { "node": ">=8.0" } }, "node_modules/to-utf8": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz", "integrity": "sha512-zks18/TWT1iHO3v0vFp5qLKOG27m67ycq/Y7a7cTiRuUNlc4gf3HGnkRgMv0NyhnfTamtkYBJl+YeD1/j07gBQ==", "dev": true }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true, "license": "MIT" }, "node_modules/ts-invariant": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", "integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==", "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.1.0" }, "engines": { "node": ">=8" } }, "node_modules/ts-jest": { "version": "29.1.1", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", "integrity": "sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==", "dev": true, "license": "MIT", "dependencies": { "bs-logger": "0.x", "fast-json-stable-stringify": "2.x", "jest-util": "^29.0.0", "json5": "^2.2.3", "lodash.memoize": "4.x", "make-error": "1.x", "semver": "^7.5.3", "yargs-parser": "^21.0.1" }, "bin": { "ts-jest": "cli.js" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@jest/types": "^29.0.0", "babel-jest": "^29.0.0", "jest": "^29.0.0", "typescript": ">=4.3 <6" }, "peerDependenciesMeta": { "@babel/core": { "optional": true }, "@jest/types": { "optional": true }, "babel-jest": { "optional": true }, "esbuild": { "optional": true } } }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true, "license": "0BSD" }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/type-fest": { "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { "node": ">=14.17" } }, "node_modules/ua-parser-js": { "version": "1.0.35", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz", "integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==", "dev": true, "funding": [ { "type": "opencollective", "url": "https://opencollective.com/ua-parser-js" }, { "type": "paypal", "url": "https://paypal.me/faisalman" } ], "license": "MIT", "engines": { "node": "*" } }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true, "license": "MIT" }, "node_modules/update-browserslist-db": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", "dev": true, "funding": [ { "type": "opencollective", "url": "https://opencollective.com/browserslist" }, { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" }, { "type": "github", "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "dependencies": { "escalade": "^3.1.1", "picocolors": "^1.0.0" }, "bin": { "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" } }, "node_modules/urql": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/urql/-/urql-2.2.3.tgz", "integrity": "sha512-XMkSYJKW9s4ZlbSuxcUz3fTBIykOn0sGileRXQeyZpaRBXJPVz5saSY05k7jdefNxShZtTI+/nr7PYUWQertfg==", "dev": true, "license": "MIT", "dependencies": { "@urql/core": "^2.6.1", "wonka": "^4.0.14" }, "peerDependencies": { "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "react": ">= 16.8.0" } }, "node_modules/v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", "dev": true, "license": "ISC", "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^1.6.0" }, "engines": { "node": ">=10.12.0" } }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "makeerror": "1.0.12" } }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dev": true, "license": "MIT", "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" }, "engines": { "node": ">= 8" } }, "node_modules/wonka": { "version": "4.0.15", "resolved": "https://registry.npmjs.org/wonka/-/wonka-4.0.15.tgz", "integrity": "sha512-U0IUQHKXXn6PFo9nqsHphVCE5m3IntqZNB9Jjn7EB1lrR7YTDY3YWgFvEvwniTzXSvOH/XMzAZaIfJF/LvHYXg==", "dev": true, "license": "MIT" }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" }, "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, "node_modules/wrap-ansi/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" }, "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" }, "engines": { "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/ws": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { "optional": true }, "utf-8-validate": { "optional": true } } }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, "license": "ISC" }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" }, "engines": { "node": ">=12" } }, "node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { "node": ">=12" } }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/zen-observable": { "version": "0.8.15", "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==", "dev": true, "license": "MIT" }, "node_modules/zen-observable-ts": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz", "integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==", "dev": true, "license": "MIT", "dependencies": { "zen-observable": "0.8.15" } } } } graphql-ruby-2.5.19/javascript_client/package.json000066400000000000000000000025071514115062600222240ustar00rootroot00000000000000{ "name": "graphql-ruby-client", "version": "1.14.9", "description": "JavaScript client for graphql-ruby", "main": "index.js", "types": "index.d.ts", "repository": "https://github.com/rmosolgo/graphql-ruby", "author": "Robert Mosolgo", "license": "LGPL-3.0", "bin": "./cli.js", "devDependencies": { "@apollo/client": ">=3.3.13", "@rails/actioncable": "^7.0.0", "@types/glob": "^7.1.1", "@types/jest": "^25.1.2", "@types/minimist": "^1.2.0", "@types/node": "^18.0.0", "@types/pako": "^1.0.1", "@types/pusher-js": "^4.2.2", "@types/rails__actioncable": "^6.1.6", "@types/react": "^17.0.0", "@types/relay-runtime": "^14.0.0", "@types/zen-observable": "^0.8.2", "ably": "1.2.50", "graphql": ">=15.0.0", "jest": "^29.0.0", "nock": "^11.0.0", "pako": "^2.0.3", "prettier": "^1.19.1", "pusher-js": "^7.0.3", "relay-runtime": "11.0.2", "ts-jest": "^29.0.0", "typescript": "5.3.3", "urql": "^2.2.2" }, "scripts": { "test": "tsc && jest", "prepublishOnly": "tsc" }, "dependencies": { "glob": "^10.5.0", "minimist": "^1.2.0" }, "peerDependencies": { "@apollo/client": ">=3.3.6", "graphql": ">=14.3.1" }, "prettier": { "semi": false, "trailingComma": "none" }, "engines": { "node": ">=10" } } graphql-ruby-2.5.19/javascript_client/readme.md000066400000000000000000000011211514115062600215040ustar00rootroot00000000000000Find the `graphql-ruby-client` docs on the [GraphQL-Ruby website](https://graphql-ruby.org/javascript_client/overview). ## License `graphql-ruby-client` is available under the LGPLv3 license; [graphql-pro](https://graphql.pro) customers are granted a special commercial license. ## Development - With GraphQL-Ruby: - Install the dependencies with `rake js:install` - Run the tests with `rake js:test` - Stand-alone: - Install dependencies `yarn install` - Run the tests `yarn run test` - Run the TypeScript compiler: `yarn tsc -w` - Install for local development with `npm link .` graphql-ruby-2.5.19/javascript_client/src/000077500000000000000000000000001514115062600205215ustar00rootroot00000000000000graphql-ruby-2.5.19/javascript_client/src/__generated__/000077500000000000000000000000001514115062600232535ustar00rootroot00000000000000graphql-ruby-2.5.19/javascript_client/src/__generated__/Card_card.graphql.js000066400000000000000000000014471514115062600271160ustar00rootroot00000000000000/** * @flow */ /* eslint-disable */ 'use strict'; /*:: import type {ConcreteFragment} from 'relay-runtime'; export type Card_card = {| +id: string; +name: string; +imageUrl: string; |}; */ const fragment /*: ConcreteFragment*/ = { "argumentDefinitions": [], "kind": "Fragment", "metadata": null, "name": "Card_card", "selections": [ { "kind": "ScalarField", "alias": null, "args": null, "name": "id", "storageKey": null }, { "kind": "ScalarField", "alias": null, "args": null, "name": "name", "storageKey": null }, { "kind": "ScalarField", "alias": "imageUrl", "args": null, "name": "image_url", "storageKey": null } ], "type": "Card" }; module.exports = fragment; graphql-ruby-2.5.19/javascript_client/src/__generated__/GetStuff.graphql.js000066400000000000000000000113141514115062600267750ustar00rootroot00000000000000/** * This file was generated by: * relay-compiler * * @providesModule AppFeedQuery.graphql * @generated SignedSource<> * @relayHash 353e010cb78d082b29cb63ee7e9027b3 * @flow * @nogrep */ 'use strict'; /*:: import type {ConcreteBatch} from 'relay-runtime'; */ /* eslint-disable comma-dangle, quotes */ /* query AppFeedQuery { feed(type: NEW, limit: 5) { ...Feed } } fragment Feed on Entry { repository { owner { login } name } ...FeedEntry } fragment FeedEntry on Entry { repository { owner { login } name stargazers_count } postedBy { login } } */ const batch /*: ConcreteBatch*/ = { "fragment": { "argumentDefinitions": [], "kind": "Fragment", "metadata": null, "name": "AppFeedQuery", "selections": [ { "kind": "LinkedField", "alias": null, "args": [ { "kind": "Literal", "name": "limit", "value": 5, "type": "Int" }, { "kind": "Literal", "name": "type", "value": "NEW", "type": "FeedType!" } ], "concreteType": "Entry", "name": "feed", "plural": true, "selections": [ { "kind": "FragmentSpread", "name": "Feed", "args": null } ], "storageKey": "feed{\"limit\":5,\"type\":\"NEW\"}" } ], "type": "Query" }, "id": null, "kind": "Batch", "metadata": {}, "name": "AppFeedQuery", "query": { "argumentDefinitions": [], "kind": "Root", "name": "AppFeedQuery", "operation": "query", "selections": [ { "kind": "LinkedField", "alias": null, "args": [ { "kind": "Literal", "name": "limit", "value": 5, "type": "Int" }, { "kind": "Literal", "name": "type", "value": "NEW", "type": "FeedType!" } ], "concreteType": "Entry", "name": "feed", "plural": true, "selections": [ { "kind": "InlineFragment", "type": "Entry", "selections": [ { "kind": "LinkedField", "alias": null, "args": null, "concreteType": "Repository", "name": "repository", "plural": false, "selections": [ { "kind": "LinkedField", "alias": null, "args": null, "concreteType": "User", "name": "owner", "plural": false, "selections": [ { "kind": "ScalarField", "alias": null, "args": null, "name": "login", "storageKey": null } ], "storageKey": null }, { "kind": "ScalarField", "alias": null, "args": null, "name": "name", "storageKey": null }, { "kind": "ScalarField", "alias": null, "args": null, "name": "stargazers_count", "storageKey": null } ], "storageKey": null }, { "kind": "LinkedField", "alias": null, "args": null, "concreteType": "User", "name": "postedBy", "plural": false, "selections": [ { "kind": "ScalarField", "alias": null, "args": null, "name": "login", "storageKey": null } ], "storageKey": null } ] } ], "storageKey": "feed{\"limit\":5,\"type\":\"NEW\"}" } ] }, "text": "query AppFeedQuery {\n feed(type: NEW, limit: 5) {\n ...Feed\n }\n}\n\nfragment Feed on Entry {\n repository {\n owner {\n login\n }\n name\n }\n ...FeedEntry\n}\n\nfragment FeedEntry on Entry {\n repository {\n owner {\n login\n }\n name\n stargazers_count\n }\n postedBy {\n login\n }\n}\n" }; module.exports = batch; graphql-ruby-2.5.19/javascript_client/src/__tests__/000077500000000000000000000000001514115062600224575ustar00rootroot00000000000000graphql-ruby-2.5.19/javascript_client/src/__tests__/__snapshots__/000077500000000000000000000000001514115062600252755ustar00rootroot00000000000000graphql-ruby-2.5.19/javascript_client/src/__tests__/__snapshots__/syncTest.ts.snap000066400000000000000000000541051514115062600304260ustar00rootroot00000000000000// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`sync operations Input files Merges fragments and operations across files 1`] = ` [ { "alias": "4568c28d403794e011363caf815ec827", "body": "fragment Frag1 on Query { moreStuff } query GetStuff { ...Frag1 }", "name": "GetStuff", }, { "alias": "faf462be033e16dd2a56130d56a9192f", "body": "fragment Frag1 on Query { moreStuff } fragment Frag2 on Query { ...Frag3 } fragment Frag3 on Query { evenMoreStuff } query GetStuff2 { stuff ...Frag1 ...Frag2 }", "name": "GetStuff2", }, { "alias": "aab385a1685772ad520fc70d468030fa", "body": "fragment Frag2 on Query { ...Frag3 } fragment Frag3 on Query { evenMoreStuff } fragment Frag4 on Query { evenMoreStuff { stuffInside } } query GetStuff3 { stuff { withStuffInside } ...Frag2 ...Frag4 }", "name": "GetStuff3", }, { "alias": "b2cb0b317d071f9f38905fba21d73258", "body": "query GetStuffIsolated { ...FragIsolated things { existHere } } fragment FragIsolated on Query { evenMoreStuff { stuffInside } }", "name": "GetStuffIsolated", }, { "alias": "6cdae165fd6dc5dc5900e5a2bba90cc2", "body": "query GetStuffIsolated2 { things { existHere } }", "name": "GetStuffIsolated2", }, ] `; exports[`sync operations Input files Uses mode: file to process each file separately 1`] = ` [ { "alias": "664225b943e29ea8c6aae40bbde8923a", "body": "fragment Frag1 on Query { moreStuff }", "name": "", }, { "alias": "269bbe8bbe7f6a0b9dae7f98b45a9675", "body": "fragment Frag2 on Query { ...Frag3 }", "name": "", }, { "alias": "d12578840c6518c746b125ae2e7a8ab1", "body": "fragment Frag3 on Query { evenMoreStuff }", "name": "", }, { "alias": "1a1b6154fb1db8bc6652edfbd7d9ac8a", "body": "fragment Frag4 on Query { evenMoreStuff { stuffInside } }", "name": "", }, { "alias": "8ab1711fcbb7befc98d06ef7d155fd81", "body": "query GetStuff { ...Frag1 }", "name": "GetStuff", }, { "alias": "cf517696fbd9ec204cd402f48c831090", "body": "query GetStuff2 { stuff ...Frag1 ...Frag2 }", "name": "GetStuff2", }, { "alias": "1e5290206d87a4da749118d84f7e2c65", "body": "query GetStuff3 { stuff { withStuffInside } ...Frag2 ...Frag4 }", "name": "GetStuff3", }, { "alias": "b2cb0b317d071f9f38905fba21d73258", "body": "query GetStuffIsolated { ...FragIsolated things { existHere } } fragment FragIsolated on Query { evenMoreStuff { stuffInside } }", "name": "GetStuffIsolated", }, { "alias": "6cdae165fd6dc5dc5900e5a2bba90cc2", "body": "query GetStuffIsolated2 { things { existHere } }", "name": "GetStuffIsolated2", }, ] `; exports[`sync operations Logging Can be quieted with quiet: true 1`] = `[]`; exports[`sync operations Logging Logs progress 1`] = ` [ [ "Syncing 5 operations to bogus...", ], [ "Generating client module in src/OperationStoreClient.js...", ], [ "✓ Done!", ], ] `; exports[`sync operations Printing the result prints failure and sends the message to the promise 1`] = ` [ [ "Syncing 5 operations to http://example.com/stored_operations/sync...", ], [ "[Sync] 2 Headers:", ], [ "[Sync] Content-Type: application/json", ], [ "[Sync] Content-Length: 1132", ], [ "[Sync] Data:", "{"operations":[{"name":"GetStuff","body":"fragment Frag1 on Query {\\n moreStuff\\n}\\n\\nquery GetStuff {\\n ...Frag1\\n}","alias":"4568c28d403794e011363caf815ec827"},{"name":"GetStuff2","body":"fragment Frag1 on Query {\\n moreStuff\\n}\\n\\nfragment Frag2 on Query {\\n ...Frag3\\n}\\n\\nfragment Frag3 on Query {\\n evenMoreStuff\\n}\\n\\nquery GetStuff2 {\\n stuff\\n ...Frag1\\n ...Frag2\\n}","alias":"faf462be033e16dd2a56130d56a9192f"},{"name":"GetStuff3","body":"fragment Frag2 on Query {\\n ...Frag3\\n}\\n\\nfragment Frag3 on Query {\\n evenMoreStuff\\n}\\n\\nfragment Frag4 on Query {\\n evenMoreStuff {\\n stuffInside\\n }\\n}\\n\\nquery GetStuff3 {\\n stuff {\\n withStuffInside\\n }\\n ...Frag2\\n ...Frag4\\n}","alias":"aab385a1685772ad520fc70d468030fa"},{"name":"GetStuffIsolated","body":"query GetStuffIsolated {\\n ...FragIsolated\\n things {\\n existHere\\n }\\n}\\n\\nfragment FragIsolated on Query {\\n evenMoreStuff {\\n stuffInside\\n }\\n}","alias":"b2cb0b317d071f9f38905fba21d73258"},{"name":"GetStuffIsolated2","body":"query GetStuffIsolated2 {\\n things {\\n existHere\\n }\\n}","alias":"6cdae165fd6dc5dc5900e5a2bba90cc2"}]}", ], [ "[Sync] Response Headers: ", "{"content-type":"application/json"}", ], [ "[Sync] Response Body: ", "{"errors":{"4568c28d403794e011363caf815ec827":["something"]},"failed":["4568c28d403794e011363caf815ec827"],"added":["defg"],"not_modified":[]}", ], [ " 0 added", ], [ " 0 not modified", ], [ " 1 failed", ], ] `; exports[`sync operations Printing the result prints failure and sends the message to the promise 2`] = ` [ [ "Sync failed, errors:", ], [ " GetStuff:", ], [ " ✘ something", ], ] `; exports[`sync operations Printing the result prints success 1`] = ` [ [ "Syncing 5 operations to http://example.com/stored_operations/sync...", ], [ "[Sync] 2 Headers:", ], [ "[Sync] Content-Type: application/json", ], [ "[Sync] Content-Length: 1132", ], [ "[Sync] Data:", "{"operations":[{"name":"GetStuff","body":"fragment Frag1 on Query {\\n moreStuff\\n}\\n\\nquery GetStuff {\\n ...Frag1\\n}","alias":"4568c28d403794e011363caf815ec827"},{"name":"GetStuff2","body":"fragment Frag1 on Query {\\n moreStuff\\n}\\n\\nfragment Frag2 on Query {\\n ...Frag3\\n}\\n\\nfragment Frag3 on Query {\\n evenMoreStuff\\n}\\n\\nquery GetStuff2 {\\n stuff\\n ...Frag1\\n ...Frag2\\n}","alias":"faf462be033e16dd2a56130d56a9192f"},{"name":"GetStuff3","body":"fragment Frag2 on Query {\\n ...Frag3\\n}\\n\\nfragment Frag3 on Query {\\n evenMoreStuff\\n}\\n\\nfragment Frag4 on Query {\\n evenMoreStuff {\\n stuffInside\\n }\\n}\\n\\nquery GetStuff3 {\\n stuff {\\n withStuffInside\\n }\\n ...Frag2\\n ...Frag4\\n}","alias":"aab385a1685772ad520fc70d468030fa"},{"name":"GetStuffIsolated","body":"query GetStuffIsolated {\\n ...FragIsolated\\n things {\\n existHere\\n }\\n}\\n\\nfragment FragIsolated on Query {\\n evenMoreStuff {\\n stuffInside\\n }\\n}","alias":"b2cb0b317d071f9f38905fba21d73258"},{"name":"GetStuffIsolated2","body":"query GetStuffIsolated2 {\\n things {\\n existHere\\n }\\n}","alias":"6cdae165fd6dc5dc5900e5a2bba90cc2"}]}", ], ] `; exports[`sync operations Printing the result prints success 2`] = ` [ [ "[Sync] Response Headers: ", "{"content-type":"application/json"}", ], [ "[Sync] Response Body: ", "{"errors":{},"failed":[],"added":["defg"],"not_modified":["xyz","123"]}", ], [ " 1 added", ], [ " 2 not modified", ], [ " 0 failed", ], [ "Generating client module in src/OperationStoreClient.js...", ], [ "✓ Done!", ], ] `; exports[`sync operations Printing the result prints success 3`] = `[]`; exports[`sync operations Relay support Uses Apollo Android OperationOutput JSON files 1`] = ` [ { "alias": "aba626ea9bdf465954e89e5590eb2c1a", "body": "mutation RemoveTodoMutation($input: RemoveTodoInput!) { removeTodo(input: $input) { deletedTodoId user { completedCount totalCount id } } }", }, { "alias": "67c2bc8aa3185a209d6651b4feb63c04", "body": "query appQuery( $userId: String ) { user(id: $userId) { ...TodoApp_user id } } fragment TodoApp_user on User { id userId totalCount ...TodoListFooter_user ...TodoList_user } fragment TodoListFooter_user on User { id userId completedCount todos(first: 2147483647) { edges { node { id complete __typename } cursor } pageInfo { endCursor hasNextPage } } totalCount } fragment TodoList_user on User { todos(first: 2147483647) { edges { node { id complete ...Todo_todo __typename } cursor } pageInfo { endCursor hasNextPage } } id userId totalCount completedCount ...Todo_user } fragment Todo_todo on Todo { complete id text } fragment Todo_user on User { id userId totalCount completedCount } ", }, { "alias": "db9904c31d91416f21d45fe3d153884c", "body": "mutation MarkAllTodosMutation( $input: MarkAllTodosInput! ) { markAllTodos(input: $input) { changedTodos { id complete } user { id completedCount } } } ", }, { "alias": "2eb8c9941fdb3117fdbc08d15fab62d0", "body": "mutation AddTodoMutation( $input: AddTodoInput! ) { addTodo(input: $input) { todoEdge { __typename cursor node { complete id text } } user { id totalCount } } } ", }, { "alias": "d970fd7dbf118794415dec7324d463e3", "body": "mutation RenameTodoMutation( $input: RenameTodoInput! ) { renameTodo(input: $input) { todo { id text } } } ", }, { "alias": "a49217db31a8be3f4107763b957d5fca", "body": "mutation RemoveCompletedTodosMutation( $input: RemoveCompletedTodosInput! ) { removeCompletedTodos(input: $input) { deletedTodoIds user { completedCount totalCount id } } } ", }, { "alias": "d7dda774dcfa32fe0d9661e01cac9a4a", "body": "mutation ChangeTodoStatusMutation( $input: ChangeTodoStatusInput! ) { changeTodoStatus(input: $input) { todo { id complete } user { id completedCount } } } ", }, ] `; exports[`sync operations Relay support Uses Apollo Codegen JSON files 1`] = ` [ { "alias": "22cc98c61c1402c92b230b7c515e07eb793a5152c388b015e86df4652ec58156", "body": "mutation UpdateSomething($name: String!) { updateSomething(name: $name) { __typename name } }", "name": "UpdateSomething", }, { "alias": "688df2ea182541c70a34c55ca056dc249014bf9f33c64eee527120c714e936fc", "body": "query getHelloWorld { helloWorld ...MoreFields } fragment MoreFields on Query { __typename }", "name": "getHelloWorld", }, ] `; exports[`sync operations Relay support Uses Relay generated .js files 1`] = ` [ { "alias": "353e010cb78d082b29cb63ee7e9027b3", "body": "query AppFeedQuery { feed(type: NEW, limit: 5) { ...Feed } } fragment Feed on Entry { repository { owner { login } name } ...FeedEntry } fragment FeedEntry on Entry { repository { owner { login } name stargazers_count } postedBy { login } } ", "name": "AppFeedQuery", }, ] `; exports[`sync operations Relay support Uses relay --persist-output JSON files 1`] = ` [ { "alias": "aba626ea9bdf465954e89e5590eb2c1a", "body": "mutation RemoveTodoMutation( $input: RemoveTodoInput! ) { removeTodo(input: $input) { deletedTodoId user { completedCount totalCount id } } } ", }, { "alias": "67c2bc8aa3185a209d6651b4feb63c04", "body": "query appQuery( $userId: String ) { user(id: $userId) { ...TodoApp_user id } } fragment TodoApp_user on User { id userId totalCount ...TodoListFooter_user ...TodoList_user } fragment TodoListFooter_user on User { id userId completedCount todos(first: 2147483647) { edges { node { id complete __typename } cursor } pageInfo { endCursor hasNextPage } } totalCount } fragment TodoList_user on User { todos(first: 2147483647) { edges { node { id complete ...Todo_todo __typename } cursor } pageInfo { endCursor hasNextPage } } id userId totalCount completedCount ...Todo_user } fragment Todo_todo on Todo { complete id text } fragment Todo_user on User { id userId totalCount completedCount } ", }, { "alias": "db9904c31d91416f21d45fe3d153884c", "body": "mutation MarkAllTodosMutation( $input: MarkAllTodosInput! ) { markAllTodos(input: $input) { changedTodos { id complete } user { id completedCount } } } ", }, { "alias": "2eb8c9941fdb3117fdbc08d15fab62d0", "body": "mutation AddTodoMutation( $input: AddTodoInput! ) { addTodo(input: $input) { todoEdge { __typename cursor node { complete id text } } user { id totalCount } } } ", }, { "alias": "d970fd7dbf118794415dec7324d463e3", "body": "mutation RenameTodoMutation( $input: RenameTodoInput! ) { renameTodo(input: $input) { todo { id text } } } ", }, { "alias": "a49217db31a8be3f4107763b957d5fca", "body": "mutation RemoveCompletedTodosMutation( $input: RemoveCompletedTodosInput! ) { removeCompletedTodos(input: $input) { deletedTodoIds user { completedCount totalCount id } } } ", }, { "alias": "d7dda774dcfa32fe0d9661e01cac9a4a", "body": "mutation ChangeTodoStatusMutation( $input: ChangeTodoStatusInput! ) { changeTodoStatus(input: $input) { todo { id complete } user { id completedCount } } } ", }, ] `; exports[`sync operations Sync output Can dump payload and outfile at the same time 1`] = ` "{ "operations": [ { "name": "GetStuff", "body": "fragment Frag1 on Query {\\n moreStuff\\n}\\n\\nquery GetStuff {\\n ...Frag1\\n}", "alias": "4568c28d403794e011363caf815ec827" }, { "name": "GetStuff2", "body": "fragment Frag1 on Query {\\n moreStuff\\n}\\n\\nfragment Frag2 on Query {\\n ...Frag3\\n}\\n\\nfragment Frag3 on Query {\\n evenMoreStuff\\n}\\n\\nquery GetStuff2 {\\n stuff\\n ...Frag1\\n ...Frag2\\n}", "alias": "faf462be033e16dd2a56130d56a9192f" }, { "name": "GetStuff3", "body": "fragment Frag2 on Query {\\n ...Frag3\\n}\\n\\nfragment Frag3 on Query {\\n evenMoreStuff\\n}\\n\\nfragment Frag4 on Query {\\n evenMoreStuff {\\n stuffInside\\n }\\n}\\n\\nquery GetStuff3 {\\n stuff {\\n withStuffInside\\n }\\n ...Frag2\\n ...Frag4\\n}", "alias": "aab385a1685772ad520fc70d468030fa" }, { "name": "GetStuffIsolated", "body": "query GetStuffIsolated {\\n ...FragIsolated\\n things {\\n existHere\\n }\\n}\\n\\nfragment FragIsolated on Query {\\n evenMoreStuff {\\n stuffInside\\n }\\n}", "alias": "b2cb0b317d071f9f38905fba21d73258" }, { "name": "GetStuffIsolated2", "body": "query GetStuffIsolated2 {\\n things {\\n existHere\\n }\\n}", "alias": "6cdae165fd6dc5dc5900e5a2bba90cc2" } ] } " `; exports[`sync operations custom file processing options Adds .graphql to the glob if needed 1`] = ` [ { "alias": "b8086942c2fbb6ac69b97cbade848033", "body": "query GetStuff { stuff }", "name": "GetStuff", }, ] `; exports[`sync operations custom file processing options Adds .graphql to the glob if needed 2`] = ` [ { "alias": "b8086942c2fbb6ac69b97cbade848033", "body": "query GetStuff { stuff }", "name": "GetStuff", }, ] `; exports[`sync operations custom file processing options Uses a custom hash function if provided 1`] = ` [ { "alias": "GETSTUFF", "body": "query GetStuff { stuff }", "name": "GetStuff", }, ] `; exports[`sync operations generating artifacts without syncing works with persisted query manifest 1`] = ` " /** * Generated by graphql-ruby-client * */ /** * Map local operation names to persisted keys on the server * @return {Object} * @private */ var _aliases = { "TestQuery1": "4a29162b05ee4d82ad02e8f50af4bf112f47181ec558a7100a", "TestQuery2": "xyz-123" } /** * The client who synced these operations with the server * @return {String} * @private */ var _client = "test-1" var OperationStoreClient = { /** * Build a string for \`params[:operationId]\` * @param {String} operationName * @return {String} stored operation ID */ getOperationId: function(operationName) { return _client + "/" + OperationStoreClient.getPersistedQueryAlias(operationName) }, /** * Fetch a persisted alias from a local operation name * @param {String} operationName * @return {String} persisted alias */ getPersistedQueryAlias: function(operationName) { var persistedAlias = _aliases[operationName] if (!persistedAlias) { throw new Error("Failed to find persisted alias for operation name: " + operationName) } else { return persistedAlias } }, /** * Satisfy the Apollo Link API. * This link checks for an operation name, and if it's present, * sets the HTTP context to _not_ include the query, * and instead, include \`extensions.operationId\`. * (This is inspired by apollo-link-persisted-queries.) */ apolloLink: function(operation, forward) { if (operation.operationName) { const operationId = OperationStoreClient.getOperationId(operation.operationName) operation.setContext({ http: { includeQuery: false, includeExtensions: true, } }) operation.extensions.operationId = operationId } return forward(operation) }, /** * Satisfy the Apollo middleware API. * Replace the query with an operationId */ apolloMiddleware: { applyBatchMiddleware: function(options, next) { options.requests.forEach(function(req) { // Fetch the persisted alias for this operation req.operationId = OperationStoreClient.getOperationId(req.operationName) // Remove the now-unused query string delete req.query return req }) // Continue the request next() }, applyMiddleware: function(options, next) { var req = options.request // Fetch the persisted alias for this operation req.operationId = OperationStoreClient.getOperationId(req.operationName) // Remove the now-unused query string delete req.query // Continue the request next() } } } module.exports = OperationStoreClient " `; exports[`sync operations generating artifacts without syncing works without a URL 1`] = ` " /** * Generated by graphql-ruby-client * */ /** * Map local operation names to persisted keys on the server * @return {Object} * @private */ var _aliases = { "GetStuff": "b8086942c2fbb6ac69b97cbade848033" } /** * The client who synced these operations with the server * @return {String} * @private */ var _client = "test-1" var OperationStoreClient = { /** * Build a string for \`params[:operationId]\` * @param {String} operationName * @return {String} stored operation ID */ getOperationId: function(operationName) { return _client + "/" + OperationStoreClient.getPersistedQueryAlias(operationName) }, /** * Fetch a persisted alias from a local operation name * @param {String} operationName * @return {String} persisted alias */ getPersistedQueryAlias: function(operationName) { var persistedAlias = _aliases[operationName] if (!persistedAlias) { throw new Error("Failed to find persisted alias for operation name: " + operationName) } else { return persistedAlias } }, /** * Satisfy the Apollo Link API. * This link checks for an operation name, and if it's present, * sets the HTTP context to _not_ include the query, * and instead, include \`extensions.operationId\`. * (This is inspired by apollo-link-persisted-queries.) */ apolloLink: function(operation, forward) { if (operation.operationName) { const operationId = OperationStoreClient.getOperationId(operation.operationName) operation.setContext({ http: { includeQuery: false, includeExtensions: true, } }) operation.extensions.operationId = operationId } return forward(operation) }, /** * Satisfy the Apollo middleware API. * Replace the query with an operationId */ apolloMiddleware: { applyBatchMiddleware: function(options, next) { options.requests.forEach(function(req) { // Fetch the persisted alias for this operation req.operationId = OperationStoreClient.getOperationId(req.operationName) // Remove the now-unused query string delete req.query return req }) // Continue the request next() }, applyMiddleware: function(options, next) { var req = options.request // Fetch the persisted alias for this operation req.operationId = OperationStoreClient.getOperationId(req.operationName) // Remove the now-unused query string delete req.query // Continue the request next() } } } module.exports = OperationStoreClient " `; exports[`sync operations verbose Adds debug output 1`] = ` [ [ "[Sync] glob: ", "./src/__tests__/documents**/*.graphql*", ], [ "[Sync] 1 files:", ], [ "[Sync] - src/__tests__/documents/doc1.graphql", ], [ "Syncing 1 operations to bogus...", ], [ "Verbose!", ], [ "Generating client module in src/OperationStoreClient.js...", ], [ "✓ Done!", ], ] `; graphql-ruby-2.5.19/javascript_client/src/__tests__/apolloExample/000077500000000000000000000000001514115062600252615ustar00rootroot00000000000000graphql-ruby-2.5.19/javascript_client/src/__tests__/apolloExample/apollo.config.js000066400000000000000000000003751514115062600303560ustar00rootroot00000000000000// apollo client:codegen gen/output.json --target json module.exports = { client: { service: { name: "testSchema", localSchemaFile: "./schema.graphql", }, includes: ["./*.ts"], mergeInFieldsFromFragmentSpreads: true, } } graphql-ruby-2.5.19/javascript_client/src/__tests__/apolloExample/fragment.ts000066400000000000000000000001651514115062600274360ustar00rootroot00000000000000import { gql } from '@apollo/client'; export const MORE_FIELDS = gql` fragment MoreFields on Query { __typename } ` graphql-ruby-2.5.19/javascript_client/src/__tests__/apolloExample/gen/000077500000000000000000000000001514115062600260325ustar00rootroot00000000000000graphql-ruby-2.5.19/javascript_client/src/__tests__/apolloExample/gen/output.json000066400000000000000000000056271514115062600302770ustar00rootroot00000000000000{ "operations": [ { "filePath": "file:///Users/rmosolgo/code/graphql-ruby/javascript_client/src/sync/__tests__/apolloExample/mutation.ts", "operationName": "UpdateSomething", "operationType": "mutation", "rootType": "Mutation", "variables": [ { "name": "name", "type": "String!" } ], "source": "mutation UpdateSomething($name: String!) {\n updateSomething(name: $name) {\n __typename\n name\n somethingElse @client\n }\n}", "fields": [ { "responseName": "updateSomething", "fieldName": "updateSomething", "type": "UpdateSomethingPayload", "args": [ { "name": "name", "value": { "kind": "Variable", "variableName": "name" }, "type": "String!" } ], "isConditional": false, "isDeprecated": false, "fields": [ { "responseName": "__typename", "fieldName": "__typename", "type": "String!", "isConditional": false }, { "responseName": "name", "fieldName": "name", "type": "String!", "isConditional": false, "isDeprecated": false } ], "fragmentSpreads": [], "inlineFragments": [] } ], "fragmentSpreads": [], "inlineFragments": [], "fragmentsReferenced": [], "sourceWithFragments": "mutation UpdateSomething($name: String!) {\n updateSomething(name: $name) {\n __typename\n name\n }\n}", "operationId": "22cc98c61c1402c92b230b7c515e07eb793a5152c388b015e86df4652ec58156" }, { "filePath": "file:///Users/rmosolgo/code/graphql-ruby/javascript_client/src/sync/__tests__/apolloExample/query.ts", "operationName": "getHelloWorld", "operationType": "query", "rootType": "Query", "variables": [], "source": "query getHelloWorld {\n helloWorld\n ...MoreFields\n}", "fields": [ { "responseName": "helloWorld", "fieldName": "helloWorld", "type": "String!", "isConditional": false, "isDeprecated": false } ], "fragmentSpreads": [ "MoreFields" ], "inlineFragments": [], "fragmentsReferenced": [ "MoreFields" ], "sourceWithFragments": "query getHelloWorld {\n helloWorld\n ...MoreFields\n}\nfragment MoreFields on Query {\n __typename\n}", "operationId": "688df2ea182541c70a34c55ca056dc249014bf9f33c64eee527120c714e936fc" } ], "fragments": [ { "typeCondition": "Query", "possibleTypes": [ "Query" ], "fragmentName": "MoreFields", "filePath": "file:///Users/rmosolgo/code/graphql-ruby/javascript_client/src/sync/__tests__/apolloExample/fragment.ts", "source": "fragment MoreFields on Query {\n __typename\n}", "fields": [ { "responseName": "__typename", "fieldName": "__typename", "type": "String!", "isConditional": false } ], "fragmentSpreads": [], "inlineFragments": [] } ], "typesUsed": [], "unionTypes": [], "interfaceTypes": [] } graphql-ruby-2.5.19/javascript_client/src/__tests__/apolloExample/mutation.ts000066400000000000000000000002431514115062600274700ustar00rootroot00000000000000import { gql } from '@apollo/client'; export const UPDATE_SOMETHING = gql` mutation UpdateSomething($name: String!) { updateSomething(name: $name) { name } } ` graphql-ruby-2.5.19/javascript_client/src/__tests__/apolloExample/query.ts000066400000000000000000000002741514115062600270010ustar00rootroot00000000000000import { gql } from '@apollo/client'; import { MORE_FIELDS } from './fragment'; export const GET_HELLO_WORLD = gql` query getHelloWorld { helloWorld ... MoreFields } ${MORE_FIELDS} ` graphql-ruby-2.5.19/javascript_client/src/__tests__/apolloExample/schema.graphql000066400000000000000000000002421514115062600300770ustar00rootroot00000000000000type Query { helloWorld: String! } type Mutation { updateSomething(name: String!): UpdateSomethingPayload } type UpdateSomethingPayload { name: String! } graphql-ruby-2.5.19/javascript_client/src/__tests__/cliTest.ts000066400000000000000000000045331514115062600244430ustar00rootroot00000000000000var childProcess = require("child_process") let fs = require('fs') describe("CLI", () => { it("exits 1 on error", () => { expect(() => { childProcess.execSync("node ./cli.js sync", {stdio: "pipe"}) }).toThrow("Client name must be provided for sync") }) it("exits 0 on OK", () => { childProcess.execSync("node ./cli.js sync -h", {stdio: "pipe"}) }) it("runs with some options", () => { var buffer = childProcess.execSync("node ./cli.js sync --client=something --header=Abcd:efgh --header=\"Abc: 123 45\" --changeset-version=2023-01-01 --mode=file --path=\"**/doc1.graphql\" --verbose", {stdio: "pipe"}) var response = buffer.toString().replace(/\033\[[0-9;]*m/g, "") expect(response).toEqual("No URL; Generating artifacts without syncing them\n[Sync] glob: **/doc1.graphql\n[Sync] 1 files:\n[Sync] - src/__tests__/documents/doc1.graphql\nGenerating client module in src/OperationStoreClient.js...\n✓ Done!\n") }) it("runs with just one header", () => { var buffer = childProcess.execSync("node ./cli.js sync --client=something --header=Ab-cd:ef-gh --mode=file --path=\"**/doc1.graphql\"", {stdio: "pipe"}) var response = buffer.toString().replace(/\033\[[0-9;]*m/g, "") expect(response).toEqual("No URL; Generating artifacts without syncing them\nGenerating client module in src/OperationStoreClient.js...\n✓ Done!\n") }) it("writes to a dump file", () => { let buffer = childProcess.execSync("node ./cli.js sync --client=something --header=Ab-cd:ef-gh --dump-payload=./DumpPayloadExample.json --path=\"**/doc1.graphql\"", {stdio: "pipe"}) console.log(buffer.toString()) let dumpedJSON = fs.readFileSync("./DumpPayloadExample.json", 'utf8') expect(dumpedJSON).toEqual(`{ "operations": [ { "name": "GetStuff", "body": "query GetStuff {\\n stuff\\n}", "alias": "b8086942c2fbb6ac69b97cbade848033" } ] } `) }) it("writes to stdout", () => { let buffer = childProcess.execSync("node ./cli.js sync --client=something --header=Ab-cd:ef-gh --dump-payload --path=\"**/doc1.graphql\"", {stdio: "pipe"}) let dumpedJSON = buffer.toString().replace(/\033\[[0-9;]*m/g, "") expect(dumpedJSON).toEqual(`{ "operations": [ { "name": "GetStuff", "body": "query GetStuff {\\n stuff\\n}", "alias": "b8086942c2fbb6ac69b97cbade848033" } ] } `) }) }) graphql-ruby-2.5.19/javascript_client/src/__tests__/documents/000077500000000000000000000000001514115062600244605ustar00rootroot00000000000000graphql-ruby-2.5.19/javascript_client/src/__tests__/documents/doc1.graphql000066400000000000000000000000331514115062600266620ustar00rootroot00000000000000query GetStuff { stuff } graphql-ruby-2.5.19/javascript_client/src/__tests__/example-apollo-android-operation-output.json000066400000000000000000000055541514115062600332540ustar00rootroot00000000000000{ "aba626ea9bdf465954e89e5590eb2c1a": { "name": "RemoveTodoMutation", "source": "mutation RemoveTodoMutation(\n $input: RemoveTodoInput!\n) {\n removeTodo(input: $input) {\n deletedTodoId\n user {\n completedCount\n totalCount\n thing @client\n id\n }\n }\n}\n" }, "67c2bc8aa3185a209d6651b4feb63c04": { "name": "appQuery", "source": "query appQuery(\n $userId: String\n) {\n user(id: $userId) {\n ...TodoApp_user\n id\n }\n}\n\nfragment TodoApp_user on User {\n id\n userId\n totalCount\n ...TodoListFooter_user\n ...TodoList_user\n}\n\nfragment TodoListFooter_user on User {\n id\n userId\n completedCount\n todos(first: 2147483647) {\n edges {\n node {\n id\n complete\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n totalCount\n}\n\nfragment TodoList_user on User {\n todos(first: 2147483647) {\n edges {\n node {\n id\n complete\n ...Todo_todo\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n userId\n totalCount\n completedCount\n ...Todo_user\n}\n\nfragment Todo_todo on Todo {\n complete\n id\n text\n}\n\nfragment Todo_user on User {\n id\n userId\n totalCount\n completedCount\n}\n" }, "db9904c31d91416f21d45fe3d153884c": { "name": "MarkAllTodosMutation", "source": "mutation MarkAllTodosMutation(\n $input: MarkAllTodosInput!\n) {\n markAllTodos(input: $input) {\n changedTodos {\n id\n complete\n }\n user {\n id\n completedCount\n }\n }\n}\n" }, "2eb8c9941fdb3117fdbc08d15fab62d0": { "name": "AddTodoMutation", "source": "mutation AddTodoMutation(\n $input: AddTodoInput!\n) {\n addTodo(input: $input) {\n todoEdge {\n __typename\n cursor\n node {\n complete\n id\n text\n }\n }\n user {\n id\n totalCount\n }\n }\n}\n" }, "d970fd7dbf118794415dec7324d463e3": { "name": "RenameTodoMutation", "source": "mutation RenameTodoMutation(\n $input: RenameTodoInput!\n) {\n renameTodo(input: $input) {\n todo {\n id\n text\n }\n }\n}\n" }, "a49217db31a8be3f4107763b957d5fca": { "name": "RemoveCompletedTodosMutation", "source": "mutation RemoveCompletedTodosMutation(\n $input: RemoveCompletedTodosInput!\n) {\n removeCompletedTodos(input: $input) {\n deletedTodoIds\n user {\n completedCount\n totalCount\n id\n }\n }\n}\n" }, "d7dda774dcfa32fe0d9661e01cac9a4a": { "name": "ChangeTodoStatusMutation", "source": "mutation ChangeTodoStatusMutation(\n $input: ChangeTodoStatusInput!\n) {\n changeTodoStatus(input: $input) {\n todo {\n id\n complete\n }\n user {\n id\n completedCount\n }\n }\n}\n" } } graphql-ruby-2.5.19/javascript_client/src/__tests__/example-relay-persisted-queries.json000066400000000000000000000047301514115062600315760ustar00rootroot00000000000000{ "aba626ea9bdf465954e89e5590eb2c1a": "mutation RemoveTodoMutation(\n $input: RemoveTodoInput!\n) {\n removeTodo(input: $input) {\n deletedTodoId\n user {\n completedCount\n totalCount\n id\n }\n }\n}\n", "67c2bc8aa3185a209d6651b4feb63c04": "query appQuery(\n $userId: String\n) {\n user(id: $userId) {\n ...TodoApp_user\n id\n }\n}\n\nfragment TodoApp_user on User {\n id\n userId\n totalCount\n ...TodoListFooter_user\n ...TodoList_user\n}\n\nfragment TodoListFooter_user on User {\n id\n userId\n completedCount\n todos(first: 2147483647) {\n edges {\n node {\n id\n complete\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n totalCount\n}\n\nfragment TodoList_user on User {\n todos(first: 2147483647) {\n edges {\n node {\n id\n complete\n ...Todo_todo\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n userId\n totalCount\n completedCount\n ...Todo_user\n}\n\nfragment Todo_todo on Todo {\n complete\n id\n text\n}\n\nfragment Todo_user on User {\n id\n userId\n totalCount\n completedCount\n}\n", "db9904c31d91416f21d45fe3d153884c": "mutation MarkAllTodosMutation(\n $input: MarkAllTodosInput!\n) {\n markAllTodos(input: $input) {\n changedTodos {\n id\n complete\n }\n user {\n id\n completedCount\n }\n }\n}\n", "2eb8c9941fdb3117fdbc08d15fab62d0": "mutation AddTodoMutation(\n $input: AddTodoInput!\n) {\n addTodo(input: $input) {\n todoEdge {\n __typename\n cursor\n node {\n complete\n id\n text\n }\n }\n user {\n id\n totalCount\n }\n }\n}\n", "d970fd7dbf118794415dec7324d463e3": "mutation RenameTodoMutation(\n $input: RenameTodoInput!\n) {\n renameTodo(input: $input) {\n todo {\n id\n text\n }\n }\n}\n", "a49217db31a8be3f4107763b957d5fca": "mutation RemoveCompletedTodosMutation(\n $input: RemoveCompletedTodosInput!\n) {\n removeCompletedTodos(input: $input) {\n deletedTodoIds\n user {\n completedCount\n totalCount\n id\n }\n }\n}\n", "d7dda774dcfa32fe0d9661e01cac9a4a": "mutation ChangeTodoStatusMutation(\n $input: ChangeTodoStatusInput!\n) {\n changeTodoStatus(input: $input) {\n todo {\n id\n complete\n }\n user {\n id\n completedCount\n }\n }\n}\n" } graphql-ruby-2.5.19/javascript_client/src/__tests__/indexTest.ts000066400000000000000000000014761514115062600250060ustar00rootroot00000000000000import {sync} from "../index" import childProcess from "child_process" describe("root module", () => { it("exports the sync function", () => { expect(sync).toBeInstanceOf(Function) }) it("exports things at root level", () => { // Make sure that the compiled JavaScript // has all the expected exports. var testScript = "var client = require('./index'); console.log(JSON.stringify({ keys: Object.keys(client).sort() }))" var output = childProcess.execSync("node -e \"" + testScript + "\"") var outputData = JSON.parse(output.toString()) var expectedKeys = [ "AblyLink", "ActionCableLink", "PusherLink", "addGraphQLSubscriptions", "createRelaySubscriptionHandler", "generateClient", "sync" ] expect(outputData.keys).toEqual(expectedKeys) }) }) graphql-ruby-2.5.19/javascript_client/src/__tests__/project/000077500000000000000000000000001514115062600241255ustar00rootroot00000000000000graphql-ruby-2.5.19/javascript_client/src/__tests__/project/frag_1.graphql000066400000000000000000000000501514115062600266370ustar00rootroot00000000000000fragment Frag1 on Query { moreStuff } graphql-ruby-2.5.19/javascript_client/src/__tests__/project/frag_2.graphql000066400000000000000000000000471514115062600266460ustar00rootroot00000000000000fragment Frag2 on Query { ...Frag3 } graphql-ruby-2.5.19/javascript_client/src/__tests__/project/frag_3.graphql000066400000000000000000000000541514115062600266450ustar00rootroot00000000000000fragment Frag3 on Query { evenMoreStuff } graphql-ruby-2.5.19/javascript_client/src/__tests__/project/frag_4.graphql000066400000000000000000000001021514115062600266400ustar00rootroot00000000000000fragment Frag4 on Query { evenMoreStuff { stuffInside } } graphql-ruby-2.5.19/javascript_client/src/__tests__/project/op_1.graphql000066400000000000000000000000361514115062600263420ustar00rootroot00000000000000query GetStuff { ...Frag1 } graphql-ruby-2.5.19/javascript_client/src/__tests__/project/op_2.graphql000066400000000000000000000000621514115062600263420ustar00rootroot00000000000000query GetStuff2 { stuff ...Frag1 ...Frag2 } graphql-ruby-2.5.19/javascript_client/src/__tests__/project/op_3.graphql000066400000000000000000000001141514115062600263410ustar00rootroot00000000000000query GetStuff3 { stuff { withStuffInside } ...Frag2 ...Frag4 } graphql-ruby-2.5.19/javascript_client/src/__tests__/project/op_isolated_1.graphql000066400000000000000000000002241514115062600302250ustar00rootroot00000000000000query GetStuffIsolated { ...FragIsolated things { existHere } } fragment FragIsolated on Query { evenMoreStuff { stuffInside } } graphql-ruby-2.5.19/javascript_client/src/__tests__/project/op_isolated_2.graphql000066400000000000000000000000721514115062600302270ustar00rootroot00000000000000query GetStuffIsolated2 { things { existHere } } graphql-ruby-2.5.19/javascript_client/src/__tests__/syncTest.ts000066400000000000000000000336161514115062600246540ustar00rootroot00000000000000import sync from "../sync" import Logger from "../sync/logger" var fs = require("fs") var nock = require("nock") interface MockOperation { alias: string, } interface MockPayload { operations: MockOperation[], generatedCode: string, } interface MockedObject { mock: { calls: object } } describe("sync operations", () => { beforeEach(() => { global.console.error = jest.fn() global.console.log = jest.fn() if (fs.existsSync("./src/OperationStoreClient.js")) { fs.unlinkSync("./src/OperationStoreClient.js") } }) afterEach(() => { jest.clearAllMocks(); }) describe("generating artifacts without syncing", () => { it("works without a URL", () => { var options = { client: "test-1", path: "./src/__tests__/documents", } return sync(options).then(function() { var generatedCode = fs.readFileSync("./src/OperationStoreClient.js", "utf8") expect(generatedCode).toMatch('"GetStuff": "b8086942c2fbb6ac69b97cbade848033"') expect(generatedCode).toMatchSnapshot() }) }) it("works with persisted query manifest", () => { var options = { client: "test-1", outfile: "./src/OperationStoreClient.js", apolloPersistedQueryManifest: "./src/sync/__tests__/generate-persisted-query-manifest.json", } return sync(options).then(function() { var generatedCode = fs.readFileSync("./src/OperationStoreClient.js", "utf8") expect(generatedCode).toMatch('"TestQuery2": "xyz-123"') expect(generatedCode).toMatchSnapshot() }) }) }) describe("custom HTTP options", () => { it("uses the provided `send` option & provided URL", () => { var url: string var options = { client: "test-1", path: "./src/__tests__/documents", url: "bogus", headers: { "X-Something-Special": "🎂", }, changesetVersion: "2023-05-05", quiet: true, send: (_sendPayload: object, options: { url: string, headers: {[key: string]: string}, changesetVersion: string }) => { url = options.url Object.keys(options.headers).forEach((h) => { url += "?" + h + "=" + options.headers[h] }) url += "&changesetVersion=" + options.changesetVersion }, } return sync(options).then(function() { expect(url).toEqual("bogus?X-Something-Special=🎂&changesetVersion=2023-05-05") }) }) }) describe("verbose", () => { it("Adds debug output", () => { var spy = (console.log as unknown) as MockedObject var options = { client: "test-1", path: "./src/__tests__/documents", url: "bogus", verbose: true, send: (_sendPayload: string, opts: { logger: Logger }) => { opts.logger.log("Verbose!") }, } return sync(options).then(function() { expect(spy.mock.calls).toMatchSnapshot() }) }) }) describe("custom file processing options", () => { it("Adds .graphql to the glob if needed", () => { var payload: MockPayload var options = { client: "test-1", path: "./src/__tests__/documents", url: "bogus", quiet: true, send: (sendPayload: MockPayload, _opts: object) => { payload = sendPayload }, } return sync(options).then(function() { expect(payload.operations).toMatchSnapshot() var optionsWithExt = {...options, glob: "./**/*.graphql"} return sync(optionsWithExt).then(function() { // Get the same result, even when the glob already has a file extension expect(payload.operations).toMatchSnapshot() }) }) }) it("Uses a custom hash function if provided", () => { var payload: MockPayload var options = { client: "test-1", path: "./src/__tests__/documents", url: "bogus", quiet: true, hash: (graphQLBody: string) => { // This is a bad hack to get the operation name var opName = graphQLBody.match(/query ([A-Za-z]+) \{/) return opName ? opName[1].toUpperCase() : null }, send: (sendPayload: MockPayload, _opts: object) => { payload = sendPayload }, } return sync(options).then(function() { expect(payload.operations).toMatchSnapshot() }) }) }) describe("Relay support", () => { it("Uses Relay generated .js files", () => { var payload: MockPayload var options = { client: "test-1", quiet: true, path: "./src/__generated__", url: "bogus", send: (sendPayload: MockPayload, _opts: object) => { payload = sendPayload }, } return sync(options).then(function () { expect(payload.operations).toMatchSnapshot() }) }) it("Uses relay --persist-output JSON files", () => { var payload: MockPayload var options = { client: "test-1", quiet: true, relayPersistedOutput: "./src/__tests__/example-relay-persisted-queries.json", url: "bogus", send: (sendPayload: MockPayload, _opts: object) => { payload = sendPayload }, } return sync(options).then(function () { return expect(payload.operations).toMatchSnapshot() }) }) it("Uses Apollo Android OperationOutput JSON files", () => { var payload: MockPayload var options = { client: "test-1", quiet: true, apolloAndroidOperationOutput: "./src/__tests__/example-apollo-android-operation-output.json", url: "bogus", send: (sendPayload: MockPayload, _opts: object) => { payload = sendPayload }, } return sync(options).then(function () { expect(payload.operations[0].alias).toEqual("aba626ea9bdf465954e89e5590eb2c1a") return expect(payload.operations).toMatchSnapshot() }) }) it("Uses Apollo Codegen JSON files", () => { var payload: MockPayload var options = { client: "test-1", quiet: true, apolloCodegenJsonOutput: "./src/__tests__/apolloExample/gen/output.json", url: "bogus", send: (sendPayload: MockPayload, _opts: object) => { payload = sendPayload }, } return sync(options).then(function () { expect(payload.operations[0].alias).toEqual("22cc98c61c1402c92b230b7c515e07eb793a5152c388b015e86df4652ec58156") return expect(payload.operations).toMatchSnapshot() }) }) }) describe("Input files", () => { it("Merges fragments and operations across files", () => { var payload: MockPayload var options = { client: "test-1", quiet: true, path: "./src/__tests__/project/", url: "bogus", // mode: "project" is the default send: (sendPayload: MockPayload, _opts: object) => { payload = sendPayload }, } return sync(options).then(function () { expect(payload.operations).toMatchSnapshot() }) }) it("Uses mode: file to process each file separately", () => { var payload: MockPayload var options = { client: "test-1", quiet: true, path: "./src/__tests__/project", url: "bogus", mode: "file", send: (sendPayload: MockPayload, _opts: object) => { payload = sendPayload }, } return sync(options).then(function() { expect(payload.operations).toMatchSnapshot() }) }) }) describe("Promise result", () => { it("Yields the payload and generated code", () => { var options = { client: "test-1", path: "./src/__tests__/project", url: "bogus", quiet: true, send: () => { }, } return sync(options).then(function(ppayload: unknown) { var payload = ppayload as MockPayload expect(payload.operations.length).toEqual(5) var generatedCode = fs.readFileSync("./src/OperationStoreClient.js", "utf8") expect(payload.generatedCode).toEqual(generatedCode) fs.unlinkSync("./src/OperationStoreClient.js") }) }) }) describe("Sync output", () => { it("Generates a usable artifact for middleware", () => { var options = { client: "test-1", path: "./src/__tests__/project", url: "bogus", quiet: true, send: () => { }, } return sync(options).then(function() { var generatedCode = fs.readFileSync("./src/OperationStoreClient.js", "utf8") expect(generatedCode).toMatch('"GetStuff": "4568c28d403794e011363caf815ec827"') expect(generatedCode).toMatch('module.exports = OperationStoreClient') expect(generatedCode).toMatch('var _client = "test-1"') fs.unlinkSync("./src/OperationStoreClient.js") }) }) it("Takes an outfile option", () => { var options = { client: "test-2", path: "./src/__tests__/project", url: "bogus", quiet: true, outfile: "__crazy_outfile.js", send: () => { }, } return sync(options).then(function() { var generatedCode = fs.readFileSync("./__crazy_outfile.js", "utf8") expect(generatedCode).toMatch('"GetStuff": "4568c28d403794e011363caf815ec827"') expect(generatedCode).toMatch('module.exports = OperationStoreClient') expect(generatedCode).toMatch('var _client = "test-2"') fs.unlinkSync("./__crazy_outfile.js") }) }) it("Can dump payload and outfile at the same time", () => { var options = { client: "test-2", path: "./src/__tests__/project", quiet: true, outfile: "customOutfile.js", dumpPayload: "customDumpPayload.js" } return sync(options).then(function() { var generatedCode = fs.readFileSync("./customOutfile.js", "utf8") expect(generatedCode).toMatch('"GetStuff": "4568c28d403794e011363caf815ec827"') expect(generatedCode).toMatch('module.exports = OperationStoreClient') expect(generatedCode).toMatch('var _client = "test-2"') var generatedPayload = fs.readFileSync("./customDumpPayload.js", "utf8") expect(generatedPayload).toMatchSnapshot() fs.unlinkSync("./customOutfile.js") fs.unlinkSync("./customDumpPayload.js") }) }) it("Skips outfile generation when using --persist-output artifact", () => { var options = { client: "test-2", relayPersistedOutput: "./src/__tests__/example-relay-persisted-queries.json", url: "bogus", quiet: true, send: () => { }, } return sync(options).then(function() { // This is the default outfile: var wasWritten = fs.existsSync("./src/OperationStoreClient.js") expect(wasWritten).toBe(false) }) }) it("Skips outfile generation when using --apollo-android-operation-output artifact", () => { var options = { client: "test-2", apolloAndroidOperationOutput: "./src/__tests__/example-apollo-android-operation-output.json", url: "bogus", quiet: true, send: () => { }, } return sync(options).then(function() { // This is the default outfile: var wasWritten = fs.existsSync("./src/OperationStoreClient.js") expect(wasWritten).toBe(false) }) }) }) describe("Logging", () => { it("Logs progress", () => { var spy = (console.log as unknown) as MockedObject var options = { client: "test-1", path: "./src/__tests__/project", url: "bogus", send: () => { }, } return sync(options).then(function() { expect(spy.mock.calls).toMatchSnapshot() }) }) it("Can be quieted with quiet: true", () => { var spy = (console.log as unknown) as MockedObject var options = { client: "test-1", path: "./src/__tests__/project", url: "bogus", quiet: true, send: () => { }, } return sync(options).then(function() { expect(spy.mock.calls).toMatchSnapshot() }) }) }) describe("Printing the result", () => { function buildMockRespondingWith(status: number, data: object) { return nock("http://example.com").post("/stored_operations/sync").reply(status, data) } it("prints failure and sends the message to the promise", () => { var spyConsoleLog = (console.log as unknown) as MockedObject var spyConsoleError = (console.error as unknown) as MockedObject buildMockRespondingWith(422, { errors: { "4568c28d403794e011363caf815ec827": ["something"] }, failed: ["4568c28d403794e011363caf815ec827"], added: ["defg"], not_modified: [], }) var options = { client: "test-1", path: "./src/__tests__/project", url: "http://example.com/stored_operations/sync", quiet: false, } var syncPromise = sync(options) return syncPromise.catch((errmsg: string) => { expect(errmsg).toEqual("Sync failed: GetStuff: something") expect(spyConsoleLog.mock.calls).toMatchSnapshot() expect(spyConsoleError.mock.calls).toMatchSnapshot() jest.clearAllMocks(); }) }) it("prints success", () => { var spyConsoleLog = (console.log as unknown) as MockedObject var spyConsoleError = (console.error as unknown) as MockedObject buildMockRespondingWith(422, { errors: {}, failed: [], added: ["defg"], not_modified: ["xyz", "123"], }) var options = { client: "test-1", path: "./src/__tests__/project", url: "http://example.com/stored_operations/sync", quiet: false, } var syncPromise = sync(options) expect(spyConsoleLog.mock.calls).toMatchSnapshot() jest.clearAllMocks(); return syncPromise.then(() => { expect(spyConsoleLog.mock.calls).toMatchSnapshot() expect(spyConsoleError.mock.calls).toMatchSnapshot() jest.clearAllMocks(); }) }) }) }) graphql-ruby-2.5.19/javascript_client/src/cli.ts000077500000000000000000000107451514115062600216520ustar00rootroot00000000000000#!/usr/bin/env node import parseArgs from "minimist" import sync, { SyncOptions } from "./sync/index" var argv = parseArgs(process.argv.slice(2)) if (argv.help || argv.h) { console.log(`usage: graphql-ruby-client sync Read .graphql files and push the contained operations to a GraphQL::Pro::OperationStore required arguments: --url= URL where data should be POSTed --client= Identifier for this client application optional arguments: --path= Path to .graphql files (default is "./**/*.graphql") --outfile= Target file for generated code --outfile-type= Target type for generated code (default is "js") --secret= HMAC authentication key --relay-persisted-output= Path to a .json file from "relay-compiler ... --persist-output" (Outfile generation is skipped by default.) --apollo-codegen-json-output= Path to a .json file from "apollo client:codegen ... --target json" (Outfile generation is skipped by default.) --apollo-android-operation-output= Path to a .json file from Apollo-Android's "generateOperationOutput" feature. (Outfile generation is skipped by default.) --apollo-persisted-query-manifest= Path to a .json file from Apollo's "generate-persisted-query-manifest" tool. (Outfile generation is skipped by default.) --mode= Treat files like a certain kind of project: relay: treat files like relay-compiler output project: treat files like a cohesive project (fragments are shared, names must be unique) file: treat each file like a stand-alone operation By default, this flag is set to: - "relay" if "__generated__" in the path - otherwise, "project" --header=
: Add a header to the outgoing HTTP request (may be repeated) --changeset-version= Populates \`context[:changeset_version]\` for this sync (for the GraphQL-Enterprise "Changesets" feature) --add-typename Automatically adds the "__typename" field to your queries --dump-payload= Print the HTTP Post data to this file, or to stdout if no filename is given --quiet Suppress status logging --verbose Print debug output --help Print this message `) } else { var commandName = argv._[0] if (commandName !== "sync") { console.log("Only `graphql-ruby-client sync` is supported") } else { var parsedHeaders: {[key: string]: string} = {} if (argv.header) { if (typeof(argv.header) === "string") { var headerParts = argv.header.split(":") parsedHeaders[headerParts[0]] = headerParts[1] } else { argv.header.forEach((h: string) => { var headerParts = h.split(":") parsedHeaders[headerParts[0]] = headerParts[1] }) } } let syncOptions: SyncOptions = { path: argv.path, relayPersistedOutput: argv["relay-persisted-output"], apolloCodegenJsonOutput: argv["apollo-codegen-json-output"], apolloAndroidOperationOutput: argv["apollo-android-operation-output"], apolloPersistedQueryManifest: argv["apollo-persisted-query-manifest"], url: argv.url, client: argv.client, outfile: argv.outfile, outfileType: argv["outfile-type"], secret: argv.secret, mode: argv.mode, headers: parsedHeaders, addTypename: argv["add-typename"], quiet: argv.hasOwnProperty("quiet"), verbose: argv.hasOwnProperty("verbose"), changesetVersion: argv["changeset-version"], } if ("dump-payload" in argv) { syncOptions.dumpPayload = argv["dump-payload"] } var result = sync(syncOptions) result.then(function() { process.exit(0) }).catch(function() { // The error is logged by the function process.exit(1) }) } } graphql-ruby-2.5.19/javascript_client/src/index.ts000066400000000000000000000010551514115062600222010ustar00rootroot00000000000000import sync from "./sync" import { generateClient } from "./sync/generateClient" import ActionCableLink from "./subscriptions/ActionCableLink" import PusherLink from "./subscriptions/PusherLink" import AblyLink from "./subscriptions/AblyLink" import addGraphQLSubscriptions from "./subscriptions/addGraphQLSubscriptions" import createRelaySubscriptionHandler from "./subscriptions/createRelaySubscriptionHandler" export { sync, generateClient, ActionCableLink, PusherLink, AblyLink, addGraphQLSubscriptions, createRelaySubscriptionHandler, } graphql-ruby-2.5.19/javascript_client/src/subscriptions/000077500000000000000000000000001514115062600234305ustar00rootroot00000000000000graphql-ruby-2.5.19/javascript_client/src/subscriptions/AblyLink.ts000066400000000000000000000160571514115062600255160ustar00rootroot00000000000000// An Apollo Link for using graphql-pro's Ably subscriptions // // @example Adding subscriptions to a HttpLink // // Load Ably and create a client // var Ably = require('ably') // // Be sure to create an API key with "Subscribe" and "Presence" permissions only, // // and use that limited API key here: // var ablyClient = new Ably.Realtime({ key: "yourapp.key:secret" }) // // // Build a combined link, initialize the client: // const ablyLink = new AblyLink({ably: ablyClient}) // const link = ApolloLink.from([authLink, ablyLink, httpLink]) // const client = new ApolloClient(link: link, ...) // // @example Building a subscription, then subscribing to it // subscription = client.subscribe({ // variables: { room: roomName}, // query: gql` // subscription MessageAdded($room: String!) { // messageWasAdded(room: $room) { // room { // messages { // id // body // author { // screenname // } // } // } // } // } // ` // }) // // subscription.subscribe({ next: ({data, errors}) => { // // Do something with `data` and/or `errors` // }}) // import { ApolloLink, Observable, FetchResult, NextLink, Operation, Observer } from "@apollo/client/core" import { Realtime, Types } from "ably" type RequestResult = FetchResult< { [key: string]: any }, Record, Record > type Subscription = { closed: boolean unsubscribe(): void } class AblyLink extends ApolloLink { ably: Realtime constructor(options: { ably: Realtime }) { super() // Retain a handle to the Ably client this.ably = options.ably } request(operation: Operation, forward: NextLink): Observable { const subscribeObservable = new Observable(_observer => {}) // Capture the super method const prevSubscribe = subscribeObservable.subscribe.bind( subscribeObservable ) // Override subscribe to return an `unsubscribe` object, see // https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/client.ts#L182-L212 subscribeObservable.subscribe = ( observerOrNext: | Observer | ((value: RequestResult) => void), onError?: (error: any) => void, onComplete?: () => void ): Subscription => { // Call super if (typeof observerOrNext == "function") { prevSubscribe(observerOrNext, onError, onComplete) } else { prevSubscribe(observerOrNext) } const observer = getObserver(observerOrNext, onError, onComplete) let ablyChannel: Types.RealtimeChannelCallbacks | null = null let subscriptionChannelId: string | null = null // Check the result of the operation const resultObservable = forward(operation) // When the operation is done, try to get the subscription ID from the server const resultSubscription = resultObservable.subscribe({ next: (data: any) => { // If the operation has the subscription header, it's a subscription const subscriptionChannelConfig = this._getSubscriptionChannel( operation ) if (subscriptionChannelConfig.channel) { subscriptionChannelId = subscriptionChannelConfig.channel // This will keep pushing to `.next` ablyChannel = this._createSubscription( subscriptionChannelConfig, observer ) } else { // This isn't a subscription, // So pass the data along and close the observer. if (data) { observer.next(data) } observer.complete() } }, error: observer.error // complete: observer.complete Don't pass this because Apollo unsubscribes if you do }) // Return an object that will unsubscribe _if_ the query was a subscription. return { closed: false, unsubscribe: () => { if (ablyChannel && subscriptionChannelId) { const ablyClientId = this.ably.auth.clientId if (ablyClientId) { ablyChannel.presence.leave() } else { ablyChannel.presence.leaveClient("graphql-subscriber") } ablyChannel.unsubscribe() resultSubscription.unsubscribe() } } } } return subscribeObservable } _getSubscriptionChannel(operation: Operation) { const response = operation.getContext().response // Check to see if the response has the header const subscriptionChannel = response.headers.get("X-Subscription-ID") // The server returns this header when encryption is enabled. const cipherKey = response.headers.get("X-Subscription-Key") return { channel: subscriptionChannel, key: cipherKey } } _createSubscription( subscriptionChannelConfig: { channel: string; key: string }, observer: { next: Function; complete: Function } ) { const subscriptionChannel = subscriptionChannelConfig["channel"] const subscriptionKey = subscriptionChannelConfig["key"] const ablyOptions = subscriptionKey ? { cipher: { key: subscriptionKey } } : {} const ablyChannel = this.ably.channels.get(subscriptionChannel, ablyOptions) const ablyClientId = this.ably.auth.clientId // Register presence, so that we can detect empty channels and clean them up server-side if (ablyClientId) { ablyChannel.presence.enter() } else { ablyChannel.presence.enterClient("graphql-subscriber", "subscribed") } // Subscribe for more update ablyChannel.subscribe("update", function(message) { var payload = message.data const result = payload.result if (result) { // Send the new response to listeners observer.next(result) } if (!payload.more) { // This is the end, the server says to unsubscribe if (ablyClientId) { ablyChannel.presence.leave() } else { ablyChannel.presence.leaveClient("graphql-subscriber") } ablyChannel.unsubscribe() observer.complete() } }) return ablyChannel } } // Turn `subscribe` arguments into an observer-like thing, see getObserver // https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/client.ts#L347-L361 function getObserver( observerOrNext: Function | Observer, onError?: (e: Error) => void, onComplete?: () => void ) { if (typeof observerOrNext === "function") { // Duck-type an observer return { next: (v: T) => observerOrNext(v), error: (e: Error) => onError && onError(e), complete: () => onComplete && onComplete() } } else { // Make an object that calls to the given object, with safety checks return { next: (v: T) => observerOrNext.next && observerOrNext.next(v), error: (e: Error) => observerOrNext.error && observerOrNext.error(e), complete: () => observerOrNext.complete && observerOrNext.complete() } } } export default AblyLink graphql-ruby-2.5.19/javascript_client/src/subscriptions/ActionCableLink.ts000066400000000000000000000054651514115062600267740ustar00rootroot00000000000000import { ApolloLink, Observable, FetchResult, Operation, NextLink } from "@apollo/client/core" import type { Consumer } from "@rails/actioncable" import { print } from "graphql" type RequestResult = FetchResult<{ [key: string]: any; }, Record, Record> type ConnectionParams = object | ((operation: Operation) => object) type SubscriptionCallbacks = { connected?: (args?: { reconnected: boolean }) => void; disconnected?: () => void; received?: (payload: any) => void; }; class ActionCableLink extends ApolloLink { cable: Consumer channelName: string actionName: string connectionParams: ConnectionParams callbacks: SubscriptionCallbacks constructor(options: { cable: Consumer, channelName?: string, actionName?: string, connectionParams?: ConnectionParams, callbacks?: SubscriptionCallbacks }) { super() this.cable = options.cable this.channelName = options.channelName || "GraphqlChannel" this.actionName = options.actionName || "execute" this.connectionParams = options.connectionParams || {} this.callbacks = options.callbacks || {} } // Interestingly, this link does _not_ call through to `next` because // instead, it sends the request to ActionCable. request(operation: Operation, _next: NextLink): Observable { return new Observable((observer) => { var channelId = Math.round(Date.now() + Math.random() * 100000).toString(16) var actionName = this.actionName var connectionParams = (typeof this.connectionParams === "function") ? this.connectionParams(operation) : this.connectionParams var callbacks = this.callbacks var channel = this.cable.subscriptions.create(Object.assign({},{ channel: this.channelName, channelId: channelId }, connectionParams), { connected: function(args?: any) { this.perform( actionName, { query: operation.query ? print(operation.query) : null, variables: operation.variables, // This is added for persisted operation support: operationId: (operation as {operationId?: string}).operationId, operationName: operation.operationName } ) callbacks.connected?.(args) }, received: function(payload) { if (payload?.result?.data || payload?.result?.errors) { observer.next(payload.result) } if (!payload.more) { observer.complete() } callbacks.received?.(payload) }, disconnected: function() { callbacks.disconnected?.() } }) // Make the ActionCable subscription behave like an Apollo subscription return Object.assign(channel, {closed: false}) }) } } export default ActionCableLink graphql-ruby-2.5.19/javascript_client/src/subscriptions/ActionCableSubscriber.ts000066400000000000000000000052161514115062600301740ustar00rootroot00000000000000import printer from "graphql/language/printer" import registry from "./registry" import type { Consumer } from "@rails/actioncable" interface ApolloNetworkInterface { applyMiddlewares: Function query: (req: object) => Promise _opts: any } class ActionCableSubscriber { _cable: Consumer _networkInterface: ApolloNetworkInterface _channelName: string constructor(cable: Consumer, networkInterface: ApolloNetworkInterface, channelName?: string) { this._cable = cable this._networkInterface = networkInterface this._channelName = channelName || "GraphqlChannel" } /** * Send `request` over ActionCable (`registry._cable`), * calling `handler` with any incoming data. * Return the subscription so that the registry can unsubscribe it later. * @param {Object} registry * @param {Object} request * @param {Function} handler * @return {ID} An ID for unsubscribing */ subscribe(request: any, handler: any) { var networkInterface = this._networkInterface // unique-ish var channelId = Math.round(Date.now() + Math.random() * 100000).toString(16) var channel = this._cable.subscriptions.create({ channel: this._channelName, channelId: channelId, }, { // After connecting, send the data over ActionCable connected: function() { // applyMiddlewares code is inspired by networkInterface internals var opts = Object.assign({}, networkInterface._opts) networkInterface .applyMiddlewares({request: request, options: opts}) .then(function() { var queryString = request.query ? printer.print(request.query) : null var operationName = request.operationName var operationId = request.operationId var variables = JSON.stringify(request.variables) var channelParams = Object.assign({}, request, { query: queryString, variables: variables, operationId: operationId, operationName: operationName, }) channel.perform("execute", channelParams) }) }, // Payload from ActionCable should have at least two keys: // - more: true if this channel should stay open // - result: the GraphQL response for this result received: function(payload) { var result = payload.result if (result) { handler(result.errors, result.data) } if (!payload.more) { registry.unsubscribe(id) } }, }) var id = registry.add(channel) return id } unsubscribe(id: number) { registry.unsubscribe(id) } } export default ActionCableSubscriber graphql-ruby-2.5.19/javascript_client/src/subscriptions/PusherLink.ts000066400000000000000000000136751514115062600261000ustar00rootroot00000000000000// An Apollo Link for using graphql-pro's Pusher subscriptions // // @example Adding subscriptions to a HttpLink // // Load Pusher and create a client // import Pusher from "pusher-js" // var pusherClient = new Pusher("your-app-key", { cluster: "us2" }) // // // Build a combined link, initialize the client: // const pusherLink = new PusherLink({pusher: pusherClient}) // const link = ApolloLink.from([authLink, pusherLink, httpLink]) // const client = new ApolloClient(link: link, ...) // // @example Building a subscription, then subscribing to it // subscription = client.subscribe({ // variables: { room: roomName}, // query: gql` // subscription MessageAdded($room: String!) { // messageWasAdded(room: $room) { // room { // messages { // id // body // author { // screenname // } // } // } // } // } // ` // }) // // subscription.subscribe({ next: ({data, errors}) => { // // Do something with `data` and/or `errors` // }}) // import { ApolloLink, Observable, Observer, Operation, NextLink, FetchResult } from "@apollo/client/core" import Pusher from "pusher-js" type RequestResult = FetchResult<{ [key: string]: any; }, Record, Record> type Subscription = { closed: boolean; unsubscribe(): void; } class PusherLink extends ApolloLink { pusher: Pusher decompress: (result: string) => any constructor(options: { pusher: Pusher, decompress?: (result: string) => any}) { super() // Retain a handle to the Pusher client this.pusher = options.pusher if (options.decompress) { this.decompress = options.decompress } else { this.decompress = function(_result: string) { throw new Error("Received compressed_result but PusherLink wasn't configured with `decompress: (result: string) => any`. Add this configuration.") } } } request(operation: Operation, forward: NextLink): Observable { const subscribeObservable = new Observable((_observer: any) => { }) // Capture the super method const prevSubscribe = subscribeObservable.subscribe.bind(subscribeObservable) // Override subscribe to return an `unsubscribe` object, see // https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/client.ts#L182-L212 subscribeObservable.subscribe = ( observerOrNext: Observer | ((value: RequestResult) => void), onError?: (error: any) => void, onComplete?: () => void ): Subscription => { // Call super if (typeof(observerOrNext) == "function") { prevSubscribe(observerOrNext, onError, onComplete) } else { prevSubscribe(observerOrNext) } const observer = getObserver(observerOrNext, onError, onComplete) var subscriptionChannel: string // Check the result of the operation const resultObservable = forward(operation) // When the operation is done, try to get the subscription ID from the server resultObservable.subscribe({ next: (data: any) => { // If the operation has the subscription header, it's a subscription const response = operation.getContext().response // Check to see if the response has the header subscriptionChannel = response.headers.get("X-Subscription-ID") if (subscriptionChannel) { // Set up the pusher subscription for updates from the server const pusherChannel = this.pusher.subscribe(subscriptionChannel) // Pass along the initial payload: if (data.data && Object.keys(data.data).length > 0) { observer.next(data) } // Subscribe for more update pusherChannel.bind("update", (payload: any) => { this._onUpdate(subscriptionChannel, observer, payload) }) } else { // This isn't a subscription, // So pass the data along and close the observer. observer.next(data) observer.complete() } }, error: observer.error, // complete: observer.complete Don't pass this because Apollo unsubscribes if you do }) // Return an object that will unsubscribe _if_ the query was a subscription. return { closed: false, unsubscribe: () => { subscriptionChannel && this.pusher.unsubscribe(subscriptionChannel) } } } return subscribeObservable } _onUpdate(subscriptionChannel: string, observer: { next: Function, complete: Function }, payload: {more: boolean, compressed_result?: string, result?: object}): void { let result: any if (payload.compressed_result) { result = this.decompress(payload.compressed_result) } else { result = payload.result } if (result) { // Send the new response to listeners observer.next(result) } if (!payload.more) { // This is the end, the server says to unsubscribe this.pusher.unsubscribe(subscriptionChannel) observer.complete() } } } // Turn `subscribe` arguments into an observer-like thing, see getObserver // https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/client.ts#L347-L361 function getObserver( observerOrNext: Function | Observer, onError?: (e: Error) => void, onComplete?: () => void, ) { if (typeof observerOrNext === 'function') { // Duck-type an observer return { next: (v: T) => observerOrNext(v), error: (e: Error) => onError && onError(e), complete: () => onComplete && onComplete(), } } else { // Make an object that calls to the given object, with safety checks return { next: (v: T) => observerOrNext.next && observerOrNext.next(v), error: (e: Error) => observerOrNext.error && observerOrNext.error(e), complete: () => observerOrNext.complete && observerOrNext.complete(), } } } export default PusherLink graphql-ruby-2.5.19/javascript_client/src/subscriptions/PusherSubscriber.ts000066400000000000000000000057001514115062600272740ustar00rootroot00000000000000import registry from "./registry" import Pusher from "pusher-js" interface ApolloNetworkInterface { use: Function useAfter: Function query: (req: object) => Promise } /** * Make a new subscriber for `addGraphQLSubscriptions` * * @param {Pusher} pusher */ class PusherSubscriber { _pusher: Pusher _networkInterface: ApolloNetworkInterface _decompress: (compressed: string) => any constructor(pusher: Pusher, networkInterface: ApolloNetworkInterface, decompress?: (compressed: string) => any) { this._pusher = pusher this._networkInterface = networkInterface this._decompress = decompress || function(_compressed) { throw new Error("Received compressed_result but this addGraphQLSubscriptions wasn't configured with `decompress: (result: string) => any`. Add this configuration.")} // This is a bit tricky: // only the _request_ is passed to the `subscribe` function, s // so we have to attach the subscription id to the `request`. // However, the request is _not_ available in the afterware function. // So: // - Add the request to `options` so it's available in afterware // - In the afterware, update the request to hold the header value // - Finally, in `subscribe`, read the subscription ID off of `request` networkInterface.use([{ applyMiddleware: function({request, options}: any, next: Function) { options.request = request next() } }]) networkInterface.useAfter([{ applyAfterware: function({response, options}: any, next: Function) { options.request.__subscriptionId = response.headers.get("X-Subscription-ID") next() } }]) } // Implement the Apollo subscribe API subscribe(request: {__subscriptionId: string}, handler: any) { var pusher = this._pusher var networkInterface = this._networkInterface var decompress = this._decompress var subscription = { _channelName: "", // set after the successful POST unsubscribe: function() { if (this._channelName) { pusher.unsubscribe(this._channelName) } } } var id = registry.add(subscription) // Send the subscription as a query // Get the channel ID from the response headers networkInterface.query(request).then(function(_executionResult: any){ var subscriptionChannel = request.__subscriptionId subscription._channelName = subscriptionChannel var pusherChannel = pusher.subscribe(subscriptionChannel) // When you get an update form Pusher, send it to Apollo pusherChannel.bind("update", function(payload: any) { var result = payload.compressed_result ? decompress(payload.compressed_result) : payload.result if (result) { handler(result.errors, result.data) } if (!payload.more) { registry.unsubscribe(id) } }) }) return id } unsubscribe(id: number) { registry.unsubscribe(id) } } export default PusherSubscriber graphql-ruby-2.5.19/javascript_client/src/subscriptions/SubscriptionExchange.ts000066400000000000000000000130121514115062600301240ustar00rootroot00000000000000import Pusher from "pusher-js" import Urql from "urql" import { Consumer, Subscription } from "@rails/actioncable" type ForwardCallback = (...args: any[]) => void const SubscriptionExchange = { create(options: { pusher?: Pusher, consumer?: Consumer, channelName?: string }) { if (options.pusher) { return createPusherSubscription(options.pusher) } else if (options.consumer) { return createUrqlActionCableSubscription(options.consumer, options?.channelName) } else { throw new Error("Either `pusher: ...` or `consumer: ...` is required.") } } } function createPusherSubscription(pusher: Pusher) { return function(operation: Urql.Operation) { // urql will call `.subscribed` on the returned object: // https://github.com/FormidableLabs/urql/blob/f89cfd06d9f14ae9cb3be10b21bd5cbd12ca275c/packages/core/src/exchanges/subscription.ts#L68-L73 // https://github.com/FormidableLabs/urql/blob/f89cfd06d9f14ae9cb3be10b21bd5cbd12ca275c/packages/core/src/exchanges/subscription.ts#L82-L97 return { subscribe: ({next, error, complete}: { next: ForwardCallback, error: ForwardCallback, complete: ForwardCallback}) => { // Somehow forward the operation to be POSTed to the server, // I don't see an option for passing this on to the `fetchExchange` const fetchBody = JSON.stringify({ query: operation.query, variables: operation.variables, }) var pusherChannelName: string const subscriptionId = "" + operation.key var fetchOptions = operation.context.fetchOptions if (typeof fetchOptions === "function") { fetchOptions = fetchOptions() } else if (fetchOptions == null) { fetchOptions = {} } const headers = { ...(fetchOptions.headers), ...{ 'Content-Type': 'application/json', 'X-Subscription-ID': subscriptionId } } const defaultFetchOptions = { method: "POST" } const mergedFetchOptions = { ...defaultFetchOptions, ...fetchOptions, body: fetchBody, headers: headers, } const fetchFn = operation.context.fetch || fetch fetchFn(operation.context.url, mergedFetchOptions) .then((fetchResult) => { // Get the server-provided subscription ID pusherChannelName = fetchResult.headers.get("X-Subscription-ID") as string // Set up a subscription to Pusher, forwarding updates to // the `next` function provided by urql const pusherChannel = pusher.subscribe(pusherChannelName) pusherChannel.bind("update", (payload: {result: object, more: boolean}) => { // Here's an update to this subscription, // pass it on: if (payload.result) { next(payload.result) } // If the server signals that this is the end, // then unsubscribe the client: if (!payload.more) { complete() } }) // Continue processing the initial result for the subscription return fetchResult.json() }) .then((jsonResult) => { // forward the initial result to urql next(jsonResult) }) .catch(error) // urql will call `.unsubscribe()` if it's returned here: // https://github.com/FormidableLabs/urql/blob/f89cfd06d9f14ae9cb3be10b21bd5cbd12ca275c/packages/core/src/exchanges/subscription.ts#L102 return { unsubscribe: () => { // When requested by urql, disconnect from this channel pusherChannelName && pusher.unsubscribe(pusherChannelName) } } } } } } function createUrqlActionCableSubscription(consumer: Consumer, channelName: string = "GraphqlChannel") { return function (operation: Urql.Operation) { const subscribe = ({ next, error, complete }: { next: ForwardCallback, error: ForwardCallback, complete: ForwardCallback }) => { let subscribed = false; const subscription: Subscription = consumer.subscriptions.create(channelName, { connected() { subscription.perform("execute", { query: operation.query, variables: operation.variables }); subscribed = true; }, received(data: any) { if (data?.result?.errors) { error(data.errors); } if (data?.result?.data) { next(data.result); } if (!data.more && subscribed) { complete(); } } }); // urql will call `.unsubscribe()` if it's returned here: // https://github.com/FormidableLabs/urql/blob/f89cfd06d9f14ae9cb3be10b21bd5cbd12ca275c/packages/core/src/exchanges/subscription.ts#L102 const unsubscribe = () => { if (subscribed) { subscribed = false; subscription?.unsubscribe(); } }; return { unsubscribe }; }; // urql will call `.subscribed` on the returned object: // https://github.com/FormidableLabs/urql/blob/f89cfd06d9f14ae9cb3be10b21bd5cbd12ca275c/packages/core/src/exchanges/subscription.ts#L68-L73 // https://github.com/FormidableLabs/urql/blob/f89cfd06d9f14ae9cb3be10b21bd5cbd12ca275c/packages/core/src/exchanges/subscription.ts#L82-L97 return { subscribe }; }; } export default SubscriptionExchange graphql-ruby-2.5.19/javascript_client/src/subscriptions/__tests__/000077500000000000000000000000001514115062600253665ustar00rootroot00000000000000graphql-ruby-2.5.19/javascript_client/src/subscriptions/__tests__/AblyLinkTest.ts000066400000000000000000000120531514115062600303040ustar00rootroot00000000000000import AblyLink from "../AblyLink" import { Realtime } from "ably" import { Operation } from "@apollo/client/core" import { parse } from "graphql" function createAbly() { const _channels: {[key: string]: any } = {} const log: any[] = [] const ably = { _channels: _channels, log: log, auth: { clientId: null, }, channels: { get(channelName: string) { return _channels[channelName] ||= { _listeners: [] as [string, Function][], name: channelName, presence: { enterClient(_clientName: string, _status: string) {}, leaveClient(_clientName: string) {}, }, detach(callback: Function) { callback() }, subscribe(eventName: string, callback: Function) { log.push(["subscribe", channelName, eventName]) this._listeners.push([eventName, callback]) }, unsubscribe(){ log.push(["unsubscribe", channelName]) this._listeners.splice(0, this._listeners.length) } } }, release(channelName: string) { delete _channels[channelName] } }, __testTrigger(channelName: string, eventName: string, data: any) { const channel = this.channels.get(channelName) const handler = channel._listeners.find((l: any) => l[0] == eventName) if (handler) { handler[1](data) } } } return (ably as unknown) as Realtime } function createOperation(options: { subscriptionId: string | null }) { return ({ query: parse("subscription { foo { bar } }"), variables: { a: 1 }, operationId: "operationId", operationName: "operationName", getContext: () => { return { response: { headers: { get: (key: string) => { if (key == "X-Subscription-ID") { return options.subscriptionId } else { return null } } } } } } } as unknown) as Operation } function createNextLink(log: any[]) { return (operation: any) => { log.push(["forward", operation.operationName]) return { subscribe(info: any) { info.next() return { unsubscribe() { log.push(["request unsubscribed"]) } } } } as any } } describe("AblyLink", () => { test("delegates to Ably", () => { var mockAbly = createAbly() var log = (mockAbly as any).log var operation = createOperation({subscriptionId: "sub-1234"}) var nextLink = createNextLink(log) var observable = new AblyLink({ ably: mockAbly}).request(operation, nextLink) observable.subscribe(function(result: any) { log.push(["received", result]) }); (mockAbly as any).__testTrigger("sub-1234", "update", { data: { result: { data: null }, more: true} }); (mockAbly as any).__testTrigger("sub-1234", "update", { data: { result: { data: "data 1" }, more: true} }); (mockAbly as any).__testTrigger("sub-1234", "update", { data: { result: { data: "data 2" }, more: false} }); expect(log).toEqual([ ["forward", "operationName"], ["subscribe", "sub-1234", "update"], ["received", { data: null }], ["received", { data: "data 1" }], ["received", { data: "data 2" }], ["unsubscribe", "sub-1234"] ]) }) test("it doesn't call ably when the subscription header isn't present", () => { var mockAbly = createAbly() var log = (mockAbly as any).log var operation = createOperation({subscriptionId: null}) var nextLink = createNextLink(log) var observable = new AblyLink({ ably: mockAbly}).request(operation, nextLink) observable.subscribe(function(result: any) { log.push(["received", result]) }); (mockAbly as any).__testTrigger("sub-1234", "update", { data: { result: { data: null }, more: true} }); (mockAbly as any).__testTrigger("sub-1234", "update", { data: { result: { data: "data 1" }, more: true} }); (mockAbly as any).__testTrigger("sub-1234", "update", { data: { result: { data: "data 2" }, more: false} }); expect(log).toEqual([["forward", "operationName"]]) }) test("it can unsubscribe", () => { var mockAbly = createAbly() var log = (mockAbly as any).log var operation = createOperation({subscriptionId: "sub-1234"}) var nextLink = createNextLink(log) var observable = new AblyLink({ ably: mockAbly}).request(operation, nextLink) var subscription = observable.subscribe(function(result: any) { log.push(["received", result]) }); (mockAbly as any).__testTrigger("sub-1234", "update", { data: { result: { data: "data1" }, more: true} }); subscription.unsubscribe(); // This is not received: (mockAbly as any).__testTrigger("sub-1234", "update", { data: { result: { data: "data2" }, more: true} }); expect(log).toEqual([ ["forward", "operationName"], ["subscribe", "sub-1234", "update"], ["received", { data: "data1" }], ["unsubscribe", "sub-1234"], ["request unsubscribed"] ]) }) }) graphql-ruby-2.5.19/javascript_client/src/subscriptions/__tests__/ActionCableLinkTest.ts000066400000000000000000000127331514115062600315660ustar00rootroot00000000000000import ActionCableLink from "../ActionCableLink" import { parse } from "graphql" import type { Consumer } from "@rails/actioncable" import { Operation } from "@apollo/client/core" describe("ActionCableLink", () => { var log: any[] var cable: any var options: any var query: any var operation: Operation beforeEach(() => { log = [] cable = { subscriptions: { create: function(channelName: string | object, options: {connected: Function, received: Function}) { var channel = channelName var params = typeof channel === "object" ? channel : { channel } var alreadyConnected = false var subscription = Object.assign( Object.create({ perform: function(actionName: string, options: object) { log.push(["perform", { actionName: actionName, options: options }]) }, unsubscribe: function() { log.push(["unsubscribe"]) } }), { params }, options ) subscription.connected = subscription.connected.bind(subscription) var received = subscription.received subscription.received = function(data: any) { if (!alreadyConnected) { alreadyConnected = true subscription.connected() } received(data) } subscription.__proto__.unsubscribe = subscription.__proto__.unsubscribe.bind(subscription) return subscription } } } options = { cable: (cable as unknown) as Consumer } query = parse("subscription { foo { bar } }") operation = ({ query: query, variables: { a: 1 }, operationId: "operationId", operationName: "operationName" } as unknown) as Operation }) it("delegates to the cable", () => { var observable = new ActionCableLink(options).request(operation, null as any) // unpack the underlying subscription var subscription: any = (observable.subscribe(function(result: any) { log.push(["received", result]) }) as any)._cleanup subscription.received({ result: { data: null }, more: true }) subscription.received({ result: { data: "data 1" }, more: true }) subscription.received({ result: { data: "data 2" }, more: false }) expect(log).toEqual([ [ "perform", { actionName: "execute", options: { query: "subscription {\n foo {\n bar\n }\n}", variables: { a: 1 }, operationId: "operationId", operationName: "operationName" } } ], ["received", { data: "data 1" }], ["received", { data: "data 2" }], ["unsubscribe"] ]) }) it("delegates a manual unsubscribe to the cable", () => { var observable = new ActionCableLink(options).request(operation, null as any) // unpack the underlying subscription var subscription: any = (observable.subscribe(function(result: any) { log.push(["received", result]) }) as any)._cleanup subscription.received({ result: { data: null }, more: true }) subscription.received({ result: { data: "data 1" }, more: true }) subscription.unsubscribe() expect(log).toEqual([ [ "perform", { actionName: "execute", options: { query: "subscription {\n foo {\n bar\n }\n}", variables: { a: 1 }, operationId: "operationId", operationName: "operationName" } } ], ["received", { data: "data 1" }], ["unsubscribe"] ]) }) it("forward object connectionParams to subscription creation", () => { var observable = new ActionCableLink(Object.assign(options, { connectionParams: { test: 1 } })). request(operation, null as any) // unpack the underlying subscription var subscription: any = (observable.subscribe(() => null) as any)._cleanup subscription.unsubscribe() expect(subscription.params["test"]).toEqual(1) }) it("calls connectionParams during subscription creation to fetch additional params", () => { var observable = new ActionCableLink( Object.assign(options, { connectionParams: () => ({ test: 1 })} ) ).request(operation, null as any) // unpack the underlying subscription var subscription: any = (observable.subscribe(() => null) as any)._cleanup subscription.unsubscribe() expect(subscription.params["test"]).toEqual(1) }) it('allows passing custom callbacks', () => { var connected = jest.fn() var received = jest.fn() var disconnected = jest.fn() var observable = new ActionCableLink( Object.assign(options, { callbacks: { connected, received, disconnected } }) ).request(operation, null as any) // unpack the underlying subscription var subscription: any = (observable.subscribe(() => null) as any)._cleanup subscription.received({ result: { data: "data 1" }, more: true }) subscription.received({ result: { data: "data 2" }, more: false }) subscription.disconnected() expect(connected).toHaveBeenCalledTimes(1) expect(received).toHaveBeenCalledWith({ result: { data: "data 1" }, more: true }) expect(received).toHaveBeenCalledWith({ result: { data: "data 2" }, more: false }) expect(disconnected).toHaveBeenCalledTimes(1) }) }) graphql-ruby-2.5.19/javascript_client/src/subscriptions/__tests__/PusherLinkTest.ts000066400000000000000000000170671514115062600306750ustar00rootroot00000000000000import PusherLink from "../PusherLink" import { parse } from "graphql" import Pusher from "pusher-js" import { Operation } from "@apollo/client/core" import pako from 'pako' type MockChannel = { bind: (action: string, handler: Function) => void, } describe("PusherLink", () => { var channelName = "abcd-efgh" var log: any[] var pusher: any var options: any var link: any var query: any var operation: Operation beforeEach(() => { log = [] pusher = { _channels: {}, trigger: function(channel: string, event: string, data: any) { var handlers = this._channels[channel] if (handlers) { handlers.forEach(function(handler: [string, Function]) { if (handler[0] == event) { handler[1](data) } }) } }, subscribe: function(channel: string): MockChannel { log.push(["subscribe", channel]) var handlers = this._channels[channel] if (!handlers) { handlers = this._channels[channel] = [] } return { bind: (action: string, handler: Function): void => { handlers.push([action, handler]) } } }, unsubscribe: (channel: string): void => { log.push(["unsubscribe", channel]) }, } options = { pusher: (pusher as unknown) as Pusher } link = new PusherLink(options) query = parse("subscription { foo { bar } }") operation = ({ query: query, variables: { a: 1 }, operationId: "operationId", operationName: "operationName", getContext: () => { return { response: { headers: { get: (headerName: string) => { if (headerName == "X-Subscription-ID") { return channelName } else { throw "Unsupported header name: " + headerName } } } } } } } as unknown) as Operation }) it("forwards errors to error handlers", () => { let passedErrorHandler: Function = () => {} var observable = link.request(operation, function(_operation: Operation): any { return { subscribe: (options: { next: Function, error: Function, complete: Function }): void => { passedErrorHandler = options.error {} } } }) let errorHandlerWasCalled = false function createdErrorHandler(_err: Error) { errorHandlerWasCalled = true } observable.subscribe(function(result: any) { log.push(["received", result]) }, createdErrorHandler) if (passedErrorHandler) { passedErrorHandler(new Error) } expect(errorHandlerWasCalled).toBe(true) }) it("doesn't call the link request's `complete` handler because otherwise Apollo would clean up subscriptions", () => { let passedComplete: Function = () => {} var observable = link.request(operation, function(_operation: Operation): any { return { subscribe: (options: { next: Function, error: Function, complete: Function }): void => { passedComplete = options.complete {} } } }) observable.subscribe(function(result: any) { log.push(["received", result]) }, null, function() { log.push(["completed"])}) expect(log).toEqual([]) expect(passedComplete).toBeUndefined() }) it("delegates to pusher", () => { var requestFinished: Function = () => {} var observable = link.request(operation, function(_operation: Operation): any { return { subscribe: (options: { next: Function }): void => { requestFinished = options.next } } }) // unpack the underlying subscription observable.subscribe(function(result: any) { log.push(["received", result]) }) // Pretend the HTTP link finished requestFinished({ data: "initial payload" }) pusher.trigger(channelName, "update", { result: { data: "data 1" }, more: true }) pusher.trigger(channelName, "update", { result: { data: "data 2" }, more: false }) expect(log).toEqual([ ["subscribe", "abcd-efgh"], ["received", { data: "initial payload"}], ["received", { data: "data 1" }], ["received", { data: "data 2" }], ["unsubscribe", "abcd-efgh"] ]) }) it("delegates a manual unsubscribe to pusher", () => { var requestFinished: Function = () => {} var observable = link.request(operation, function(_operation: Operation): any { return { subscribe: (options: { next: Function }): void => { requestFinished = options.next } } }) // unpack the underlying subscription var subscription = observable.subscribe(function(result: any) { log.push(["received", result]) }) // Pretend the HTTP link finished requestFinished({ data: "initial payload" }) pusher.trigger(channelName, "update", { result: { data: "data 1" }, more: true }) subscription.unsubscribe() expect(log).toEqual([ ["subscribe", "abcd-efgh"], ["received", { data: "initial payload"}], ["received", { data: "data 1" }], ["unsubscribe", "abcd-efgh"] ]) }) it("doesn't send empty initial responses", () => { var requestFinished: Function = () => {} var observable = link.request(operation, function(_operation: Operation): any { return { subscribe: (options: { next: Function }): void => { requestFinished = options.next } } }) // unpack the underlying subscription var subscription = observable.subscribe(function(result: any) { log.push(["received", result]) }) // Pretend the HTTP link finished requestFinished({ data: null }) pusher.trigger(channelName, "update", { result: { data: "data 1" }, more: true }) subscription.unsubscribe() expect(log).toEqual([ ["subscribe", "abcd-efgh"], ["received", { data: "data 1" }], ["unsubscribe", "abcd-efgh"] ]) }) it("throws an error when no `decompress:` is configured", () => { const link = new PusherLink({ pusher: new Pusher("123"), }) const observer = { next: (_result: object) => {}, complete: () => {}, } const payload = { more: true, compressed_result: "abcdef", } expect(() => { link._onUpdate("abc", observer, payload) }).toThrow("Received compressed_result but PusherLink wasn't configured with `decompress: (result: string) => any`. Add this configuration.") }) it("decompresses compressed_result", () => { const link = new PusherLink({ pusher: new Pusher("123"), decompress: (compressed) => { const buff = Buffer.from(compressed, 'base64'); return JSON.parse(pako.inflate(buff, { to: 'string' })); }, }) const results: Array = [] const observer = { next: (result: object) => { results.push(result) }, complete: () => { results.push("complete") }, } const compressedData = pako.deflate(JSON.stringify({ a: 1, b: 2})) // Browsers have `TextEncoder` for this const compressedStr = Buffer.from(compressedData).toString("base64") const payload = { more: true, compressed_result: compressedStr, } // Send a dummy payload and then terminate the subscription link._onUpdate("abc", observer, payload) link._onUpdate("abc", observer, { more: false }) expect(results).toEqual([{a: 1, b: 2}, "complete"]) }) }) graphql-ruby-2.5.19/javascript_client/src/subscriptions/__tests__/SubscriptionExchangeTest.ts000066400000000000000000000115541514115062600327330ustar00rootroot00000000000000import SubscriptionExchange from "../SubscriptionExchange" import Pusher from "pusher-js" import Urql from "urql" import {parse} from "graphql" import { nextTick } from "process" import { Consumer } from "@rails/actioncable" type MockChannel = { bind: (action: string, handler: Function) => void, } describe("SubscriptionExchange with Pusher", () => { var channelName = "1234" var log: any[] var pusher: any var options: any var pusherExchange: any var operation: any beforeEach(() => { log = [] pusher = { _channels: {}, trigger: function(channel: string, event: string, data: any) { var handlers = this._channels[channel] if (handlers) { handlers.forEach(function(handler: [string, Function]) { if (handler[0] == event) { handler[1](data) } }) } }, subscribe: function(channel: string): MockChannel { log.push(["subscribe", channel]) var handlers = this._channels[channel] if (!handlers) { handlers = this._channels[channel] = [] } return { bind: (action: string, handler: Function): void => { handlers.push([action, handler]) } } }, unsubscribe: (channel: string): void => { delete pusher._channels[channel] log.push(["unsubscribe", channel]) }, } options = { pusher: (pusher as unknown) as Pusher } pusherExchange = SubscriptionExchange.create(options) operation = { query: parse("{ foo { bar } }"), variables: {}, key: Number(channelName), context: { url: "/graphql", requestPolicy: "network-only", fetch: () => { var headers = new Headers headers.append("X-Subscription-ID", channelName) const jsonData = { data: { foo: "bar" }} return Promise.resolve(({ headers: headers, json: () => { return jsonData } } as unknown) as Response) } }, kind: "subscription", } as Urql.Operation }) it("calls through to handlers and can be unsubscribed", () => { const subscriber = pusherExchange(operation) const next = (data: any) => { log.push(["next", data]) } const error = (err: any) => { log.push(["error", err]) } const complete = (data: any) => { log.push(["complete", data]) } const subscription = subscriber.subscribe({ next, error, complete }) return new Promise((resolve, _reject) => { nextTick(() => { pusher.trigger(channelName, { result: {}, more: true }) expect(Object.keys(pusher._channels)).toEqual([channelName]) subscription.unsubscribe() expect(Object.keys(pusher._channels)).toEqual([]) const expectedLog = [ ["subscribe", "1234"], ["next", { data: { foo: "bar" } }], ["unsubscribe", "1234"] ] expect(log).toEqual(expectedLog) resolve(true) }) }) }) }) describe("SubscriptionExchange with ActionCable", () => { it("calls through to handlers", () => { var handlers: any var log: [string, any][]= [] var dummyActionCableConsumer = { subscriptions: { create: (channelName: string, newHandlers: any) => { log.push(["create", channelName]) handlers = newHandlers return { perform: (evt: string, data: any) => { log.push([evt, data]) }, unsubscribe: () => { log.push(["unsubscribed", null]) } } } } } var options = { consumer: (dummyActionCableConsumer as unknown) as Consumer, channelName: "CustomChannel" } var exchange = SubscriptionExchange.create(options); var parsedQuery = parse("{ foo { bar } }") var operation = { query: parsedQuery, variables: {}, context: { url: "/graphql", requestPolicy: "network-only", }, kind: "subscription", } as Urql.Operation var subscriber = exchange(operation) const next = (data: any) => { log.push(["next", data]) } const error = (err: any) => { log.push(["error", err]) } const complete = (data: any) => { log.push(["complete", data]) } const subscription = subscriber.subscribe({ next, error, complete }) return new Promise((resolve, _reject) => { nextTick(() => { handlers.connected() // trigger the GraphQL send handlers.received({ result: { data: { a: "1" } }, more: false }) subscription.unsubscribe() const expectedLog = [ ["create", "CustomChannel"], ["execute", { query: parsedQuery, variables: {} }], ["next", { data: { a: "1" } }], ["complete", undefined], ["unsubscribed", null], ] expect(log).toEqual(expectedLog) resolve(true) }) }) }) }) graphql-ruby-2.5.19/javascript_client/src/subscriptions/__tests__/addGraphQLSubscriptionsTest.ts000066400000000000000000000014741514115062600333430ustar00rootroot00000000000000import addGraphQLSubscriptions from "../addGraphQLSubscriptions" describe("addGraphQLSubscriptions", () => { it("delegates to the subscriber", () => { var state: {[key: string]: string} = {} var subscriber = { subscribe: function(req: string, handler: string) { state[req] = handler return req + "/" + handler }, unsubscribe(id: string) { var key = id.split("/")[0] delete state[key] } } var dummyNetworkInterface = addGraphQLSubscriptions({}, {subscriber: subscriber}) var id = dummyNetworkInterface.subscribe("abc", "def") expect(id).toEqual("abc/def") expect(Object.keys(state).length).toEqual(1) expect(state["abc"]).toEqual("def") dummyNetworkInterface.unsubscribe(id) expect(Object.keys(state).length).toEqual(0) }) }) graphql-ruby-2.5.19/javascript_client/src/subscriptions/__tests__/createAblyFetcherTest.ts000066400000000000000000000057641514115062600321660ustar00rootroot00000000000000import createAblyFetcher from "../createAblyFetcher" import { Realtime } from "ably" function createAbly() { const _channels: {[key: string]: any } = {} const ably = { _channels: _channels, channels: { get(channelName: string) { return _channels[channelName] ||= { _listeners: [] as [string, Function][], name: channelName, presence: { enterClient(_clientName: string, _status: string) {}, leaveClient(_clientName: string) {}, }, detach(callback: Function) { callback() }, subscribe(eventName: string, callback: Function) { this._listeners.push([eventName, callback]) }, unsubscribe(){} } }, release(channelName: string) { delete _channels[channelName] } }, __testTrigger(channelName: string, eventName: string, data: any) { const channel = this.channels.get(channelName) const handler = channel._listeners.find((l: any) => l[0] == eventName) if (handler) { handler[1](data) } } } return ably } describe("createAblyFetcher", () => { it("yields updates for subscriptions", () => { const ably = createAbly() const fetchLog: any[] = [] const dummyFetch = function(url: string, fetchArgs: any) { fetchLog.push([url, fetchArgs.customOpt]) const dummyResponse = { json: () => { return { data: { hi: "First response" } } }, headers: { get() { return fetchArgs.body.includes("subscription") ? "abcd" : null } } } return Promise.resolve(dummyResponse) } const fetcher = createAblyFetcher({ ably: (ably as unknown) as Realtime, url: "/graphql", fetch: ((dummyFetch as unknown) as typeof fetch), fetchOptions: {customOpt: true} }) const result = fetcher({ variables: {}, operationName: "hello", body: "subscription hello { hi }" }, {}) return result.next().then((res) => { expect(res.value.data.hi).toEqual("First response") expect(fetchLog).toEqual([["/graphql", true]]) }).then(() => { const promise = result.next().then((res2) => { expect(res2).toEqual({ value: { data: { hi: "Bonjour" } }, done: false }) }) ably.__testTrigger("abcd", "update", { data: { result: { data: { hi: "Bonjour" } } } }) return promise.then(() => { // Test non-subscriptions too: expect(Object.keys(ably._channels)).toEqual(["abcd"]) const queryResult = fetcher({ variables: {}, operationName: null, body: "{ __typename }"}, {}) return queryResult.next().then((res) => { expect(res.value.data).toEqual({ hi: "First response"}) return queryResult.next().then((res2) => { expect(res2.done).toEqual(true) expect(ably._channels).toEqual({}) }) }) }) }) }) }) graphql-ruby-2.5.19/javascript_client/src/subscriptions/__tests__/createAblyHandlerTest.ts000066400000000000000000000316431514115062600321560ustar00rootroot00000000000000import { OnErrorData, createAblyHandler } from "../createAblyHandler" import { Realtime, Types } from "ably" const dummyOperation = { text: "", name: "" } const channelTemplate = { presence: { enter() {}, enterClient() {}, leave(callback?: (err?: Types.ErrorInfo) => void) { if (callback) callback() } }, subscribe: () => {}, unsubscribe: () => {}, on: () => {}, detach: (callback?: (err?: Types.ErrorInfo) => void) => { if (callback) callback() } } const createDummyConsumer = ( channel: any = channelTemplate, release = (_channelName: string) => {} ): Realtime => (({ auth: { clientId: "foo" }, channels: { get: () => channel, release } } as unknown) as Realtime) const nextTick = () => new Promise(resolve => setTimeout(resolve, 0)) describe("createAblyHandler", () => { it("returns a function producing a disposable subscription", async () => { const subscriptionId = "dummy-subscription" var wasUnsubscribed = false var wasDetached = false var releasedChannelName const producer = createAblyHandler({ fetchOperation: () => new Promise(resolve => resolve({ headers: new Map([["X-Subscription-ID", subscriptionId]]), body: { data: { foo: "bar" } } }) ), ably: createDummyConsumer( { ...channelTemplate, unsubscribe: () => { wasUnsubscribed = true }, detach: (callback?: (err?: Types.ErrorInfo) => void) => { if (callback) callback() wasDetached = true }, name: subscriptionId }, (channelName: string) => { releasedChannelName = channelName } ) }) const subscription = producer( dummyOperation, {}, {}, { onError: () => {}, onNext: () => {}, onCompleted: () => {} } ) await nextTick() await subscription.dispose() expect(wasUnsubscribed).toEqual(true) expect(wasDetached).toEqual(true) expect(releasedChannelName).toEqual(subscriptionId) }) it("dispatches the immediate response in case of success", async () => { let errorInvokedWith = undefined let nextInvokedWith = undefined const producer = createAblyHandler({ fetchOperation: () => new Promise(resolve => resolve({ headers: new Map([["X-Subscription-ID", "foo"]]), body: { data: { foo: "bar" } } }) ), ably: createDummyConsumer() }) producer( dummyOperation, {}, {}, { onError: (errors: any) => { errorInvokedWith = errors }, onNext: (response: any) => { nextInvokedWith = response }, onCompleted: () => {} } ) await nextTick() expect(errorInvokedWith).toBeUndefined() expect(nextInvokedWith).toEqual({ data: { foo: "bar" } }) }) it("dispatches the immediate response in case of error", async () => { let errorInvokedWith = undefined let nextInvokedWith = undefined const dummyErrors = [{ message: "baz" }] const producer = createAblyHandler({ fetchOperation: () => new Promise(resolve => resolve({ headers: new Map([["X-Subscription-ID", "foo"]]), body: { errors: dummyErrors } }) ), ably: createDummyConsumer() }) producer( dummyOperation, {}, {}, { onError: (errors: any) => { errorInvokedWith = errors }, onNext: () => {}, onCompleted: () => {} } ) await nextTick() expect(errorInvokedWith).toEqual(dummyErrors) expect(nextInvokedWith).toBeUndefined() }) it("doesn't dispatch anything for an empty response", async () => { let errorInvokedWith = undefined let nextInvokedWith = undefined const producer = createAblyHandler({ fetchOperation: () => new Promise(resolve => resolve({ headers: new Map([["X-Subscription-ID", "foo"]]), body: {} }) ), ably: createDummyConsumer() }) producer( dummyOperation, {}, {}, { onError: (errors: any) => { errorInvokedWith = errors }, onNext: (response: any) => { nextInvokedWith = response }, onCompleted: () => {} } ) await nextTick() expect(errorInvokedWith).toBeUndefined() expect(nextInvokedWith).toBeUndefined() }) it("doesn't dispatch anything for an empty data object", async () => { let errorInvokedWith = undefined let nextInvokedWith = undefined const producer = createAblyHandler({ fetchOperation: () => new Promise(resolve => resolve({ headers: new Map([["X-Subscription-ID", "foo"]]), body: { data: {} } }) ), ably: createDummyConsumer() }) producer( dummyOperation, {}, {}, { onError: (errors: any) => { errorInvokedWith = errors }, onNext: (response: any) => { nextInvokedWith = response }, onCompleted: () => {} } ) await nextTick() expect(errorInvokedWith).toBeUndefined() expect(nextInvokedWith).toBeUndefined() }) it("dispatches caught errors", async () => { let errorInvokedWith = undefined let nextInvokedWith = undefined const error = new Error("blam") const producer = createAblyHandler({ fetchOperation: () => new Promise((_resolve, reject) => reject(error)), ably: createDummyConsumer() }) producer( dummyOperation, {}, {}, { onError: (errors: any) => { errorInvokedWith = errors }, onNext: (response: any) => { nextInvokedWith = response }, onCompleted: () => {} } ) await nextTick() expect(errorInvokedWith).toBe(error) expect(nextInvokedWith).toBeUndefined() }) it("detaches the channel when the subscription is disposed during initial response", async () => { let detached = false const ably = createDummyConsumer({ ...channelTemplate, detach() { detached = true } }) const producer = createAblyHandler({ fetchOperation: () => new Promise(resolve => resolve({ headers: new Map([["X-Subscription-ID", "foo"]]), body: { errors: {} } }) ), ably }) const { dispose } = producer( dummyOperation, {}, {}, { onError: async () => { dispose() }, onNext: async () => {}, onCompleted: () => {} } ) await nextTick() expect(detached).toBe(true) }) describe("integration with Ably", () => { const key = process.env.ABLY_KEY const testWithAblyKey = key ? test : test.skip test("onError is called when using invalid key", async () => { const ably = new Realtime({ key: "integration-test:invalid", log: { level: 0 } }) await new Promise(resolve => { const fetchOperation = async () => ({ headers: new Map([["X-Subscription-ID", "foo"]]) }) const ablyHandler = createAblyHandler({ ably, fetchOperation }) const operation = {} const variables = {} const cacheConfig = {} const onError = (error: any) => { expect(error.message).toEqual("unable to handle request; no application id found in request") resolve() } const onNext = () => console.log("onNext") const onCompleted = () => console.log("onCompleted") const observer = { onError, onNext, onCompleted } ablyHandler(operation, variables, cacheConfig, observer) }) ably.close() }) // For executing this test you need to provide a valid Ably API key in // environment variable ABLY_KEY testWithAblyKey( "onError is called for too many subscriptions", async () => { const ably = new Realtime({ key, log: { level: 0 } }) await new Promise(resolve => { let subscriptionCounter = 0 const fetchOperation = async () => { subscriptionCounter += 1 return { headers: new Map([ ["X-Subscription-ID", `foo-${subscriptionCounter}`] ]) } } const ablyHandler = createAblyHandler({ ably, fetchOperation }) const operation = {} const variables = {} const cacheConfig = {} const onError = (error: any) => { expect(error.message).toMatch(/Maximum number of channels/) resolve() } const onNext = () => console.log("onNext") const onCompleted = () => console.log("onCompleted") const observer = { onError, onNext, onCompleted } for (let i = 0; i < 201; ++i) { ablyHandler(operation, variables, cacheConfig, observer) } }) ably.close() } ) // For executing this test you need to provide a valid Ably API key in // environment variable ABLY_KEY // // This test might take longer than the default jest timeout of 5s. // Consider setting a higher timeout when running in CI. testWithAblyKey("can make more than 200 subscriptions", async () => { let caughtError = null const ably = new Realtime({ key, log: { level: 0 } }) let subscriptionCounter = 0 const fetchOperation = async () => { subscriptionCounter += 1 return { headers: new Map([ ["X-Subscription-ID", `foo-${subscriptionCounter}`] ]) } } const ablyHandler = createAblyHandler({ ably, fetchOperation }) const operation = {} const variables = {} const cacheConfig = {} const onError = (error: OnErrorData) => { caughtError = error } const onNext = () => {} const onCompleted = () => {} const observer = { onError, onNext, onCompleted } const disposals = [] for (let i = 0; i < 200; ++i) { const { dispose } = ablyHandler( operation, variables, cacheConfig, observer ) await new Promise(resolve => setTimeout(resolve, 0)) disposals.push(dispose()) } await Promise.all(disposals) // 201st subscription - should work now that previous 200 subscriptions have been disposed const { dispose } = ablyHandler( operation, variables, cacheConfig, observer ) await new Promise(resolve => setTimeout(resolve, 0)) await dispose() ably.close() if (caughtError) throw caughtError }) // For executing this test you need to provide a valid Ably API key in // environment variable ABLY_KEY testWithAblyKey( "receives message sent before subscribe takes effect", async () => { let caughtError = null const ably = new Realtime({ key, log: { level: 0 } }) ably.connect() const subscriptionId = Math.random().toString(36) const fetchOperation = async () => ({ headers: new Map([["X-Subscription-ID", subscriptionId]]), body: { data: "immediateResult" } }) const ablyHandler = createAblyHandler({ ably, fetchOperation }) const operation = {} const variables = {} const cacheConfig = {} const onError = (error: OnErrorData) => { caughtError = error } const messages: any[] = [] const onNext = (message: any) => { messages.push(message.data) } const onCompleted = () => {} const observer = { onError, onNext, onCompleted } // Publish before subscribe await new Promise((resolve, reject) => { const ablyPublisher = new Realtime({ key, log: { level: 0 } }) const publishChannel = ablyPublisher.channels.get(subscriptionId) publishChannel.publish( "update", { result: { data: "asyncResult" } }, err => { ablyPublisher.close() if (err) { reject(err) } else { resolve() } } ) }) const { dispose } = ablyHandler( operation, variables, cacheConfig, observer ) for (let i = 0; i < 20 && messages.length < 2; ++i) { await new Promise(resolve => setTimeout(resolve, 100)) } await dispose() ably.close() if (caughtError) throw caughtError expect(messages).toEqual(["immediateResult", "asyncResult"]) } ) }) }) graphql-ruby-2.5.19/javascript_client/src/subscriptions/__tests__/createActionCableFetcherTest.ts000066400000000000000000000037141514115062600334340ustar00rootroot00000000000000import createActionCableFetcher from "../createActionCableFetcher" import type { Consumer } from "@rails/actioncable" import { parse } from "graphql" describe("createActionCableFetcherTest", () => { it("yields updates for subscriptions", () => { var handlers: any var log: [string, any][]= [] var dummyActionCableConsumer = { subscriptions: { create: (_conn: any, newHandlers: any) => { handlers = newHandlers return { perform: (evt: string, data: any) => { log.push([evt, data]) } } } } } const fetchLog: any[] = [] const dummyFetch = function(url: string, fetchArgs: any) { fetchLog.push([url, fetchArgs.custom]) return Promise.resolve({ json: () => { {} } }) } var options = { consumer: (dummyActionCableConsumer as unknown) as Consumer, url: "/some_graphql_endpoint", fetch: dummyFetch as typeof fetch, fetchOptions: { custom: true, } } var fetcher = createActionCableFetcher(options) const queryStr = "subscription listen { update { message } }" const doc = parse(queryStr) const res = fetcher({ operationName: "listen", query: queryStr, variables: {}}, { documentAST: doc }) const promise = res.next().then((result) => { handlers.connected() // trigger the GraphQL send expect(result).toEqual({ value: { data: "hello" } , done: false }) expect(fetchLog).toEqual([]) expect(log).toEqual([ ["execute", { operationName: "listen", query: queryStr, variables: {} }], ]) }) handlers.received({ result: { data: "hello" } }) // simulate an update return promise.then(() => { let res2 = fetcher({ operationName: null, query: "{ __typename } ", variables: {}}, {}) const promise2 = res2.next().then(() => { expect(fetchLog).toEqual([["/some_graphql_endpoint", true]]) }) return promise2 }) }) }) graphql-ruby-2.5.19/javascript_client/src/subscriptions/__tests__/createActionCableHandlerTest.ts000066400000000000000000000037141514115062600334310ustar00rootroot00000000000000import { createActionCableHandler } from "../createActionCableHandler" import type { Consumer } from "@rails/actioncable" describe("createActionCableHandler", () => { it("returns a function producing a disposable subscription", () => { var wasDisposedCount = 0 var subscription = { unsubscribe: () => (wasDisposedCount += 1) } var dummyActionCableConsumer = { subscriptions: { create: () => subscription }, } var options = { cable: (dummyActionCableConsumer as unknown) as Consumer } var producer = createActionCableHandler(options) var relaySubscription = producer({text: "", name: ""}, {}, {}, { onError: () => {}, onNext: () => {}, onCompleted: () => {} }) relaySubscription.dispose() relaySubscription.dispose() expect(wasDisposedCount).toEqual(1) }) it("uses a provided clientName and operation.id", () => { var handlers: any var log: [string, any][]= [] var dummyActionCableConsumer = { subscriptions: { create: (_conn: any, newHandlers: any) => { handlers = newHandlers return { perform: (evt: string, data: any) => { log.push([evt, data]) } } } } } var options = { cable: (dummyActionCableConsumer as unknown) as Consumer, clientName: "client-1", } var producer = createActionCableHandler(options); producer( {text: "", name: "", id: "abcdef"}, {}, {}, { onError: () => {}, onNext: (result: any) => { log.push(["onNext", result])}, onCompleted: () => { log.push(["onCompleted", null])} } ) handlers.connected() // trigger the GraphQL send handlers.received({ result: { data: { a: "1" } }, more: false }) expect(log).toEqual([ ["execute", { operationId: "client-1/abcdef", operationName: "", query: "", variables: {} }], ["onNext", { data: { a: "1" } }], ["onCompleted", null], ]) }) }) graphql-ruby-2.5.19/javascript_client/src/subscriptions/__tests__/createPusherFetcherTest.ts000066400000000000000000000056461514115062600325440ustar00rootroot00000000000000import createPusherFetcher from "../createPusherFetcher" import type Pusher from "pusher-js" type MockChannel = { bind: (action: string, handler: Function) => void, unsubscribe: () => void, } describe("createPusherFetcher", () => { it("yields updates for subscriptions", () => { const pusher = { _channels: {} as {[key: string]: [string, Function][]}, trigger: function(channel: string, event: string, data: any) { var handlers = this._channels[channel] if (handlers) { handlers.forEach(function(handler: [string, Function]) { if (handler[0] == event) { handler[1](data) } }) } }, subscribe: function(channel: string): MockChannel { var handlers = this._channels[channel] if (!handlers) { handlers = this._channels[channel] = [] } return { bind: (action: string, handler: Function): void => { handlers.push([action, handler]) }, unsubscribe: () => { delete this._channels[channel] } } }, unsubscribe: (_channel: string): void => { }, } const fetchLog: any[] = [] const dummyFetch = function(url: string, fetchArgs: any) { fetchLog.push([url, fetchArgs.customOpt]) const dummyResponse = { json: () => { return { data: { hi: "First response" } } }, headers: { get() { return fetchArgs.body.includes("subscription") ? "abcd" : null } } } return Promise.resolve(dummyResponse) } const fetcher = createPusherFetcher({ pusher: (pusher as unknown) as Pusher, url: "/graphql", fetch: ((dummyFetch as unknown) as typeof fetch), fetchOptions: {customOpt: true} }) const result = fetcher({ variables: {}, operationName: "hello", body: "subscription hello { hi }" }, {}) return result.next().then((res) => { expect(res.value.data.hi).toEqual("First response") expect(fetchLog).toEqual([["/graphql", true]]) }).then(() => { const promise = result.next().then((res2) => { expect(res2).toEqual({ value: { data: { hi: "Bonjour" } }, done: false }) }) pusher.trigger("abcd", "update", { result: { data: { hi: "Bonjour" } } }) return promise.then(() => { // Test non-subscriptions too: expect(Object.keys(pusher._channels)).toEqual(["abcd"]) const queryResult = fetcher({ variables: {}, operationName: null, body: "{ __typename }"}, {}) return queryResult.next().then((res) => { expect(res.value.data).toEqual({ hi: "First response"}) return queryResult.next().then((res2) => { expect(res2.done).toEqual(true) expect(pusher._channels).toEqual({}) }) }) }) }) }) }) createRelaySubscriptionHandlerTest.ts000066400000000000000000000044461514115062600346720ustar00rootroot00000000000000graphql-ruby-2.5.19/javascript_client/src/subscriptions/__tests__import createRelaySubscriptionHandler from "../createRelaySubscriptionHandler" import { createLegacyRelaySubscriptionHandler } from "../createRelaySubscriptionHandler" import type { Consumer } from "@rails/actioncable" import { Network } from 'relay-runtime' describe("createRelaySubscriptionHandler", () => { it("returns a function producing a observable subscription", () => { var dummyActionCableConsumer = { subscriptions: { create: () => ({ unsubscribe: () => ( true) }) }, } var options = { cable: (dummyActionCableConsumer as unknown) as Consumer } var handler = createRelaySubscriptionHandler(options) var fetchQuery: any // basically, make sure this doesn't blow up during type-checking or runtime expect(Network.create(fetchQuery, handler)).toBeTruthy() }) it("doesn't send an empty string when no string is given", () => { var channel: any; var performLog: any[] = []; var dummyActionCableConsumer = { subscriptions: { create: (opts1: any, opts2: any) => { channel = Object.assign( opts1, opts2, { unsubscribe: () => true, perform: (event: string, payload: object) => performLog.push([event, payload]), } ) return channel } }, } var options = { cable: (dummyActionCableConsumer as unknown) as Consumer } var handler = createRelaySubscriptionHandler(options) var observable = handler({id: "abc", text: null, name: "def", operationKind: "subscription", metadata: {}}, { abc: true}); observable.subscribe({}) channel.connected() var expectedLog = [ [ 'execute', { variables: { abc: true }, operationName: 'def', query: null, operationId: null } ] ] expect(performLog).toEqual(expectedLog) }) }) describe("createLegacyRelaySubscriptionHandler", () => { it("still works", () => { var dummyActionCableConsumer = { subscriptions: { create: () => ({ unsubscribe: () => ( true) }) }, } var options = { cable: (dummyActionCableConsumer as unknown) as Consumer } expect(createLegacyRelaySubscriptionHandler(options)).toBeInstanceOf(Function) }) }) graphql-ruby-2.5.19/javascript_client/src/subscriptions/__tests__/registryTest.ts000066400000000000000000000021471514115062600304520ustar00rootroot00000000000000import registry from "../registry" describe("subscription registry", () => { it("adds and unsubscribes", () => { // A subscription is something that responds to `.unsubscribe` var wasUnsubscribed1 = false var subscription1 = { unsubscribe: function() { wasUnsubscribed1 = true } } var wasUnsubscribed2 = false var subscription2 = { unsubscribe: function() { wasUnsubscribed2 = true } } // Adding a subscription returns an ID for unsubscribing var id1 = registry.add(subscription1) var id2 = registry.add(subscription2) expect(typeof id1).toEqual("number") expect(typeof id2).toEqual("number") // Unsubscribing calls the `.unsubscribe `function registry.unsubscribe(id1) expect(wasUnsubscribed1).toEqual(true) expect(wasUnsubscribed2).toEqual(false) registry.unsubscribe(id2) expect(wasUnsubscribed1).toEqual(true) expect(wasUnsubscribed2).toEqual(true) }) it("raises on unknown ids", () => { expect(() => { registry.unsubscribe(999) }).toThrow("No subscription found for id: 999") }) }) graphql-ruby-2.5.19/javascript_client/src/subscriptions/addGraphQLSubscriptions.ts000066400000000000000000000056731514115062600305520ustar00rootroot00000000000000import ActionCableSubscriber from "./ActionCableSubscriber" import PusherSubscriber from "./PusherSubscriber" import Pusher from "pusher-js" import type { Consumer } from "@rails/actioncable" interface Subscriber { subscribe: Function unsubscribe: Function } /** * Modify an Apollo network interface to * subscribe an unsubscribe using `cable:`. * Based on `addGraphQLSubscriptions` from `subscriptions-transport-ws`. * * This function assigns `.subscribe` and `.unsubscribe` functions * to the provided networkInterface. * @example Adding ActionCable subscriptions to a HTTP network interface * // Load ActionCable and create a consumer * var ActionCable = require('@rails/actioncable') * var cable = ActionCable.createConsumer() * window.cable = cable * * // Load ApolloClient and create a network interface * var apollo = require('apollo-client') * var RailsNetworkInterface = apollo.createNetworkInterface({ * uri: '/graphql', * opts: { * credentials: 'include', * }, * headers: { * 'X-CSRF-Token': $("meta[name=csrf-token]").attr("content"), * } * }); * * // Add subscriptions to the network interface * var addGraphQLSubscriptions = require("graphql-ruby-client/subscriptions/addGraphQLSubscriptions") * addGraphQLSubscriptions(RailsNetworkInterface, {cable: cable}) * * // Optionally, add persisted query support: * var OperationStoreClient = require("./OperationStoreClient") * RailsNetworkInterface.use([OperationStoreClient.apolloMiddleware]) * * @example Subscriptions with Pusher & graphql-pro * var pusher = new Pusher(appId, options) * addGraphQLSubscriptions(RailsNetworkInterface, {pusher: pusher}) * * @param {Object} networkInterface - an HTTP NetworkInterface * @param {ActionCable.Consumer} options.cable - A cable for subscribing with * @param {Pusher} options.pusher - A pusher client for subscribing with */ function addGraphQLSubscriptions(networkInterface: any, options: { pusher?: Pusher, cable?: Consumer, subscriber?: Subscriber, decompress?: (compressed: string) => any, channelName?: string }) { if (!options) { options = {} } var subscriber: Subscriber if (options.subscriber) { // Right now this is just for testing subscriber = options.subscriber } else if (options.cable) { subscriber = new ActionCableSubscriber(options.cable, networkInterface, options.channelName) } else if (options.pusher) { subscriber = new PusherSubscriber(options.pusher, networkInterface, options.decompress) } else { throw new Error("Must provide cable: or pusher: option") } var networkInterfaceWithSubscriptions = Object.assign(networkInterface, { subscribe: function(request: any, handler: any) { var id = subscriber.subscribe(request, handler) return id }, unsubscribe(id: number) { subscriber.unsubscribe(id) }, }) return networkInterfaceWithSubscriptions } export default addGraphQLSubscriptions graphql-ruby-2.5.19/javascript_client/src/subscriptions/createAblyFetcher.ts000066400000000000000000000051511514115062600273560ustar00rootroot00000000000000import type Types from "ably" type AblyFetcherOptions = { ably: Types.Realtime, url: String, fetch?: typeof fetch, fetchOptions?: any, } type SubscriptionIteratorPayload = { value: any, done: Boolean } const clientName = "graphiql-subscriber" export default function createAblyFetcher(options: AblyFetcherOptions) { var currentChannel: Types.Types.RealtimeChannelCallbacks | null = null return async function*(graphqlParams: any, _fetcherParams: any) { var nextPromiseResolve: Function | null = null var shouldBreak = false var iterator = { [Symbol.asyncIterator]() { return { next(): Promise { return new Promise((resolve, _reject) => { nextPromiseResolve = resolve }) }, return(): Promise { if (currentChannel) { currentChannel.presence.leaveClient(clientName) currentChannel.unsubscribe() const channelName = currentChannel.name currentChannel.detach(() => { options.ably.channels.release(channelName) }) currentChannel = null nextPromiseResolve = null } return Promise.resolve({ value: null, done: true }) } } } } const fetchFn = options.fetch || window.fetch fetchFn("/graphql", { method: "POST", body: JSON.stringify(graphqlParams), headers: { 'content-type': 'application/json', }, ... options.fetchOptions }).then((r) => { const subId = r.headers.get("X-Subscription-ID") if (subId) { currentChannel && currentChannel.unsubscribe() currentChannel = options.ably.channels.get(subId, { modes: ["SUBSCRIBE", "PRESENCE"] }) currentChannel.presence.enterClient(clientName, "subscribed", (err) => { if (err) { console.error(err) } }) currentChannel.subscribe("update", (message: Types.Types.Message) => { console.log("update", message) if (nextPromiseResolve) { nextPromiseResolve({ value: message.data.result, done: false }) } }) if (nextPromiseResolve) { nextPromiseResolve({ value: r.json(), done: false }) } } else { shouldBreak = true if (nextPromiseResolve) { nextPromiseResolve({ value: r.json(), done: false}) } } }) for await (const payload of iterator) { yield payload if (shouldBreak) { break } } } } graphql-ruby-2.5.19/javascript_client/src/subscriptions/createAblyHandler.ts000066400000000000000000000140251514115062600273530ustar00rootroot00000000000000import { Realtime, Types } from "ably" // TODO: // - end-to-end test // - extract update code, inject it as a function? interface AblyHandlerOptions { ably: Realtime fetchOperation: Function } interface GraphQLError { message: string path: (string | number)[] locations: number[][] extensions?: object } type OnErrorData = AblyError | Error| GraphQLError[] interface ApolloObserver { onError: (err: OnErrorData) => void onNext: Function onCompleted: Function } const anonymousClientId = "graphql-subscriber" // Current max. number of rewound messages in the initial response to // subscribe. See // https://github.com/ably/docs/blob/baa0a4666079abba3a3e19e82eb99ca8b8a735d0/content/realtime/channels/channel-parameters/rewind.textile#additional-information // Note that using a higher value emits a warning. const maxNumRewindMessages = 100 class AblyError extends Error { constructor(public reason: Types.ErrorInfo) { super(reason.message) } get code() { return this.reason.code } get statusCode() { return this.reason.statusCode } } function createAblyHandler(options: AblyHandlerOptions) { const { ably, fetchOperation } = options const isAnonymousClient = () => !ably.auth.clientId || ably.auth.clientId === "*" return ( operation: object, variables: object, cacheConfig: object, observer: ApolloObserver ) => { let channel: Types.RealtimeChannelCallbacks | null = null const dispatchResult = (result: { errors?: GraphQLError[]; data: any }) => { if (result) { if (result.errors) { // What kind of error stuff belongs here? observer.onError(result.errors) } else if (result.data && Object.keys(result.data).length > 0) { observer.onNext({ data: result.data }) } } } const updateHandler = (message: Types.Message) => { // TODO Extract this code // When we get a response, send the update to `observer` const payload = message.data dispatchResult(payload.result) if (!payload.more) { // Subscription is finished observer.onCompleted() } } ;(async () => { try { // POST the subscription like a normal query const response = await fetchOperation(operation, variables, cacheConfig) const channelName = response.headers.get("X-Subscription-ID") if (!channelName) { throw new Error("Missing X-Subscription-ID header") } const channelKey = response.headers.get("X-Subscription-Key") channel = ably.channels.get(channelName, { params: { rewind: String(maxNumRewindMessages) }, cipher: channelKey ? { key: channelKey } : undefined, modes: ["SUBSCRIBE", "PRESENCE"] }) channel.on("failed", function(stateChange: Types.ChannelStateChange) { observer.onError( stateChange.reason ? new AblyError(stateChange.reason) : new Error("Ably channel changed to failed state") ) }) channel.on("suspended", function( stateChange: Types.ChannelStateChange ) { // Note: suspension can be a temporary condition and isn't necessarily // an error, however we handle the case where the channel gets // suspended before it is attached because that's the only way to // propagate error 90010 (see https://help.ably.io/error/90010) if ( stateChange.previous === "attaching" && stateChange.current === "suspended" ) { observer.onError( stateChange.reason ? new AblyError(stateChange.reason) : new Error("Ably channel suspended before being attached") ) } }) // Register presence, so that we can detect empty channels and clean them up server-side const enterCallback = (errorInfo: Types.ErrorInfo | null | undefined) => { if (errorInfo && channel) { observer.onError(new AblyError(errorInfo)) } } if (isAnonymousClient()) { channel.presence.enterClient( anonymousClientId, "subscribed", enterCallback ) } else { channel.presence.enter("subscribed", enterCallback) } // When you get an update from ably, give it to Relay channel.subscribe("update", updateHandler) // Dispatch the result _after_ setting up the channel, // because Relay might immediately dispose of the subscription. // (In that case, we want to make sure the channel is cleaned up properly.) dispatchResult(response.body) } catch (error) { observer.onError(error as Error) } })() return { dispose: async () => { try { if (channel) { const disposedChannel = channel channel = null disposedChannel.unsubscribe() // Ensure channel is no longer attaching, as otherwise detach does // nothing if (disposedChannel.state === "attaching") { await new Promise((resolve, _reject) => { const onStateChange = ( stateChange: Types.ChannelStateChange ) => { if (stateChange.current !== "attaching") { disposedChannel.off(onStateChange) resolve() } } disposedChannel.on(onStateChange) }) } await new Promise((resolve, reject) => { disposedChannel.detach(err => { if (err) { reject(new AblyError(err)) } else { resolve() } }) }) ably.channels.release(disposedChannel.name) } } catch (error) { observer.onError(error as Error) } } } } } export { createAblyHandler, AblyHandlerOptions, OnErrorData } graphql-ruby-2.5.19/javascript_client/src/subscriptions/createActionCableFetcher.ts000066400000000000000000000054131514115062600306340ustar00rootroot00000000000000 import { visit } from "graphql"; import type { Consumer, Subscription } from "@rails/actioncable" type ActionCableFetcherOptions = { consumer: Consumer, url: string, channelName?: string, fetch?: typeof fetch, fetchOptions?: any, } type SubscriptionIteratorPayload = { value: any, done: Boolean } export default function createActionCableFetcher(options: ActionCableFetcherOptions) { let currentChannel: Subscription | null = null const consumer = options.consumer const url = options.url || "/graphql" const channelName = options.channelName || "GraphqlChannel" const subscriptionFetcher = async function*(graphqlParams: any, fetcherOpts: any) { let isSubscription = false; let nextPromiseResolve: Function | null = null; fetcherOpts.documentAST && visit(fetcherOpts.documentAST, { OperationDefinition(node) { if (graphqlParams.operationName === node.name?.value && node.operation === 'subscription') { isSubscription = true; } }, }); if (isSubscription) { currentChannel?.unsubscribe() currentChannel = consumer.subscriptions.create(channelName, { connected: function() { currentChannel?.perform("execute", { query: graphqlParams.query, operationName: graphqlParams.operationName, variables: graphqlParams.variables, }) }, received: function(data: any) { if (nextPromiseResolve) { nextPromiseResolve({ value: data.result, done: false }) } } } as any ) var iterator = { [Symbol.asyncIterator]() { return { next(): Promise { return new Promise((resolve, _reject) => { nextPromiseResolve = resolve }) }, return(): Promise { if (currentChannel) { currentChannel.unsubscribe() currentChannel = null } return Promise.resolve({ value: null, done: true }) } } } } for await (const payload of iterator) { yield payload } } else { const fetchFn = options.fetch || window.fetch // Not a subscription fetcher, post to the given URL yield fetchFn(url, { method: "POST", body: JSON.stringify({ query: graphqlParams.query, operationName: graphqlParams.operationName, variables: graphqlParams.variables, }), headers: { 'content-type': 'application/json', }, ... options.fetchOptions }).then((r) => r.json()) return } } return subscriptionFetcher } graphql-ruby-2.5.19/javascript_client/src/subscriptions/createActionCableHandler.ts000066400000000000000000000053431514115062600306330ustar00rootroot00000000000000import type { Consumer } from "@rails/actioncable" /** * Create a Relay Modern-compatible subscription handler. * * @param {ActionCable.Consumer} cable - An ActionCable consumer from `.createConsumer` * @param {OperationStoreClient} operations - A generated OperationStoreClient for graphql-pro's OperationStore * @return {Function} */ interface ActionCableHandlerOptions { cable: Consumer operations?: { getOperationId: Function} channelName?: string clientName?: string } function createActionCableHandler(options: ActionCableHandlerOptions) { return function (operation: { text: string, name: string, id?: string }, variables: object, _cacheConfig: object, observer: {onError: Function, onNext: Function, onCompleted: Function}) { // unique-ish var channelId = Math.round(Date.now() + Math.random() * 100000).toString(16) var cable = options.cable var operations = options.operations var subscribed = true // Register the subscription by subscribing to the channel const channel = cable.subscriptions.create({ channel: options.channelName || "GraphqlChannel", channelId: channelId, }, { connected: function() { var channelParams: object // Once connected, send the GraphQL data over the channel // Use the stored operation alias if possible if (operations) { channelParams = { variables: variables, operationName: operation.name, operationId: operations.getOperationId(operation.name) } } else { channelParams = { variables: variables, operationName: operation.name, query: operation.text, operationId: (operation.id && options.clientName ? (options.clientName + "/" + operation.id) : null), } } channel.perform("execute", channelParams) }, // This result is sent back from ActionCable. received: function(payload: { result: { errors: any[], data: object }, more: boolean}) { // When we get a response, send the update to `observer` const result = payload.result if (result && result.errors) { // What kind of error stuff belongs here? observer.onError(result.errors) } else if (result) { observer.onNext({data: result.data}) } if (!payload.more && subscribed) { // Subscription is finished observer.onCompleted() } } }) // Return an object for Relay to unsubscribe with return { dispose: function() { if (subscribed) { subscribed = false channel.unsubscribe() } } } } } export { createActionCableHandler, ActionCableHandlerOptions } graphql-ruby-2.5.19/javascript_client/src/subscriptions/createPusherFetcher.ts000066400000000000000000000040651514115062600277400ustar00rootroot00000000000000import type Pusher from "pusher-js" import type { Channel } from "pusher-js" type PusherFetcherOptions = { pusher: Pusher, url: String, fetch?: typeof fetch, fetchOptions: any, } type SubscriptionIteratorPayload = { value: any, done: Boolean } export default function createPusherFetcher(options: PusherFetcherOptions) { var currentChannel: Channel | null = null return async function*(graphqlParams: any, _fetcherParams: any) { var nextPromiseResolve: Function | null = null var shouldBreak = false var iterator = { [Symbol.asyncIterator]() { return { next(): Promise { return new Promise((resolve, _reject) => { nextPromiseResolve = resolve }) }, return(): Promise { if (currentChannel) { currentChannel.unsubscribe() currentChannel = null } return Promise.resolve({ value: null, done: true }) } } } } const fetchFn = options.fetch || window.fetch fetchFn("/graphql", { method: "POST", body: JSON.stringify(graphqlParams), headers: { 'content-type': 'application/json', }, ...options.fetchOptions }).then((r) => { const subId = r.headers.get("X-Subscription-ID") if (subId) { currentChannel && currentChannel.unsubscribe() currentChannel = options.pusher.subscribe(subId) currentChannel.bind("update", (payload: any) => { if (nextPromiseResolve) { nextPromiseResolve({ value: payload.result, done: false }) } }) if (nextPromiseResolve) { nextPromiseResolve({ value: r.json(), done: false }) } } else { shouldBreak = true if (nextPromiseResolve) { nextPromiseResolve({ value: r.json(), done: false}) } } }) for await (const payload of iterator) { yield payload if (shouldBreak) { break } } } } graphql-ruby-2.5.19/javascript_client/src/subscriptions/createPusherHandler.ts000066400000000000000000000037331514115062600277360ustar00rootroot00000000000000import Pusher from "pusher-js" // TODO: // - end-to-end test // - extract update code, inject it as a function? interface PusherHandlerOptions { pusher: Pusher, fetchOperation: Function, decompress?: Function, } function createPusherHandler(options: PusherHandlerOptions) { var pusher = options.pusher var fetchOperation = options.fetchOperation var decompress = options.decompress || function(_compressed: string) { throw new Error("Received compressed_result but createPusherHandler wasn't configured with `decompress: (result: string) => any`. Add this configuration.") } return function (operation: object, variables: object, cacheConfig: object, observer: { onNext: Function, onError: Function, onCompleted: Function}) { var channelName: string // POST the subscription like a normal query fetchOperation(operation, variables, cacheConfig).then(function(response: { headers: { get: Function } }) { channelName = response.headers.get("X-Subscription-ID") var channel = pusher.subscribe(channelName) // When you get an update from pusher, give it to Relay channel.bind("update", function(payload: {more: boolean, result?: any, compressed_result?: string}) { // TODO Extract this code // When we get a response, send the update to `observer` let result: any = null if (payload.compressed_result) { result = decompress(payload.compressed_result) } else { result = payload.result } if (result && result.errors) { // What kind of error stuff belongs here? observer.onError(result.errors) } else if (result) { observer.onNext({data: result.data}) } if (!payload.more) { // Subscription is finished observer.onCompleted() } }) }) return { dispose: function() { pusher.unsubscribe(channelName) } } } } export { createPusherHandler, PusherHandlerOptions } graphql-ruby-2.5.19/javascript_client/src/subscriptions/createRelaySubscriptionHandler.ts000066400000000000000000000054511514115062600321500ustar00rootroot00000000000000import { createActionCableHandler, ActionCableHandlerOptions } from "./createActionCableHandler" import { createPusherHandler, PusherHandlerOptions } from "./createPusherHandler" import { createAblyHandler, AblyHandlerOptions } from "./createAblyHandler" import { RequestParameters, Variables, Observable } from "relay-runtime" function createLegacyRelaySubscriptionHandler(options: ActionCableHandlerOptions | PusherHandlerOptions | AblyHandlerOptions) { var handler: any if ((options as ActionCableHandlerOptions).cable) { handler = createActionCableHandler(options as ActionCableHandlerOptions) } else if ((options as PusherHandlerOptions).pusher) { handler = createPusherHandler(options as PusherHandlerOptions) } else if ((options as AblyHandlerOptions).ably) { handler = createAblyHandler(options as AblyHandlerOptions) } else { throw new Error("Missing options for subscription handler") } return handler } /** * Transport-agnostic wrapper for Relay Modern subscription handlers. * @example Add ActionCable subscriptions * var subscriptionHandler = createHandler({ * cable: cable, * operations: OperationStoreClient, * }) * var network = Network.create(fetchQuery, subscriptionHandler) * @param {ActionCable.Consumer} options.cable - A consumer from `.createConsumer` * @param {Pusher} options.pusher - A Pusher client * @param {Ably.Realtime} options.ably - An Ably client * @param {OperationStoreClient} options.operations - A generated `OperationStoreClient` for graphql-pro's OperationStore * @return {Function} A handler for a Relay Modern network */ function createRelaySubscriptionHandler(options: ActionCableHandlerOptions | PusherHandlerOptions | AblyHandlerOptions) { const handler = createLegacyRelaySubscriptionHandler(options) // Turn the handler into a relay-ready subscribe function return (request: RequestParameters, variables: Variables): any => { return Observable.from({ subscribe: (observer: { next: any | ((v: any) => void); complete: () => void; error: (error: Error) => void; }) => { const client = handler( { text: request.text, name: request.name, id: request.id, }, variables, {}, { onError: (_error: Error) => { observer.error; }, onNext: (res: any) => { if (!res || !res.data) { return; } observer.next(res); }, onCompleted: observer.complete, } ); return { unsubscribe: () => { client.dispose(); }, }; }, }); }; } export { createLegacyRelaySubscriptionHandler } export default createRelaySubscriptionHandler graphql-ruby-2.5.19/javascript_client/src/subscriptions/registry.ts000066400000000000000000000017041514115062600256520ustar00rootroot00000000000000interface ApolloSubscription { unsubscribe: Function } // State management for subscriptions. // Used to add subscriptions to an Apollo network interface. class ApolloSubscriptionRegistry { // Apollo expects unique ids to reference each subscription, // here's a simple incrementing ID generator which starts at 1 // (so it's always truthy) _id: number // for unsubscribing when Apollo asks us to _subscriptions: {[key: number]: ApolloSubscription} constructor() { this._id = 1 this._subscriptions = {} } add(subscription: ApolloSubscription): number { var id = this._id++ this._subscriptions[id] = subscription return id } unsubscribe(id: number): void { var subscription = this._subscriptions[id] if (!subscription) { throw new Error("No subscription found for id: " + id) } subscription.unsubscribe() delete this._subscriptions[id] } } export default new ApolloSubscriptionRegistry graphql-ruby-2.5.19/javascript_client/src/sync/000077500000000000000000000000001514115062600214755ustar00rootroot00000000000000graphql-ruby-2.5.19/javascript_client/src/sync/__tests__/000077500000000000000000000000001514115062600234335ustar00rootroot00000000000000graphql-ruby-2.5.19/javascript_client/src/sync/__tests__/__snapshots__/000077500000000000000000000000001514115062600262515ustar00rootroot00000000000000graphql-ruby-2.5.19/javascript_client/src/sync/__tests__/__snapshots__/dumpPayloadTest.ts.snap000066400000000000000000000002641514115062600327020ustar00rootroot00000000000000// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`printing out the HTTP Post payload prints the result to stdout 1`] = ` [ [ "{ "ok": { "1": true } } ", ], ] `; graphql-ruby-2.5.19/javascript_client/src/sync/__tests__/__snapshots__/generateClientTest.ts.snap000066400000000000000000000057421514115062600333620ustar00rootroot00000000000000// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`returns generated code 1`] = ` " /** * Generated by graphql-ruby-client * */ /** * Map local operation names to persisted keys on the server * @return {Object} * @private */ var _aliases = { "GetStuff": "b8086942c2fbb6ac69b97cbade848033" } /** * The client who synced these operations with the server * @return {String} * @private */ var _client = "test-client" var OperationStoreClient = { /** * Build a string for \`params[:operationId]\` * @param {String} operationName * @return {String} stored operation ID */ getOperationId: function(operationName) { return _client + "/" + OperationStoreClient.getPersistedQueryAlias(operationName) }, /** * Fetch a persisted alias from a local operation name * @param {String} operationName * @return {String} persisted alias */ getPersistedQueryAlias: function(operationName) { var persistedAlias = _aliases[operationName] if (!persistedAlias) { throw new Error("Failed to find persisted alias for operation name: " + operationName) } else { return persistedAlias } }, /** * Satisfy the Apollo Link API. * This link checks for an operation name, and if it's present, * sets the HTTP context to _not_ include the query, * and instead, include \`extensions.operationId\`. * (This is inspired by apollo-link-persisted-queries.) */ apolloLink: function(operation, forward) { if (operation.operationName) { const operationId = OperationStoreClient.getOperationId(operation.operationName) operation.setContext({ http: { includeQuery: false, includeExtensions: true, } }) operation.extensions.operationId = operationId } return forward(operation) }, /** * Satisfy the Apollo middleware API. * Replace the query with an operationId */ apolloMiddleware: { applyBatchMiddleware: function(options, next) { options.requests.forEach(function(req) { // Fetch the persisted alias for this operation req.operationId = OperationStoreClient.getOperationId(req.operationName) // Remove the now-unused query string delete req.query return req }) // Continue the request next() }, applyMiddleware: function(options, next) { var req = options.request // Fetch the persisted alias for this operation req.operationId = OperationStoreClient.getOperationId(req.operationName) // Remove the now-unused query string delete req.query // Continue the request next() } } } module.exports = OperationStoreClient " `; generateJsonClientTest.ts.snap000066400000000000000000000004341514115062600341260ustar00rootroot00000000000000graphql-ruby-2.5.19/javascript_client/src/sync/__tests__/__snapshots__// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`generates a valid json object string that maps names to operations 1`] = ` "{ "a": "b", "c-d": "e-f" }" `; exports[`generates a valid json object string that maps names to operations 2`] = ` { "a": "b", "c-d": "e-f", } `; prepareIsolatedFilesTest.ts.snap000066400000000000000000000016571514115062600344610ustar00rootroot00000000000000graphql-ruby-2.5.19/javascript_client/src/sync/__tests__/__snapshots__// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`builds out single operations 1`] = ` [ { "alias": "", "body": "query GetStuffIsolated { ...FragIsolated things { existHere } } fragment FragIsolated on Query { evenMoreStuff { stuffInside } }", "name": "GetStuffIsolated", }, { "alias": "", "body": "query GetStuffIsolated2 { things { existHere } }", "name": "GetStuffIsolated2", }, ] `; exports[`with --add-typename builds out single operations with __typename fields 1`] = ` [ { "alias": "", "body": "query GetStuffIsolated { ...FragIsolated things { existHere __typename } } fragment FragIsolated on Query { evenMoreStuff { stuffInside __typename } }", "name": "GetStuffIsolated", }, { "alias": "", "body": "query GetStuffIsolated2 { things { existHere __typename } }", "name": "GetStuffIsolated2", }, ] `; preparePersistedQueryListTest.ts.snap000066400000000000000000000007131514115062600355460ustar00rootroot00000000000000graphql-ruby-2.5.19/javascript_client/src/sync/__tests__/__snapshots__// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`reads generate-persisted-query-manifest output 1`] = ` [ { "alias": "4a29162b05ee4d82ad02e8f50af4bf112f47181ec558a7100a", "body": "query TestQuery1 { testing { id label description __typename } }", "name": "TestQuery1", }, { "alias": "xyz-123", "body": "query TestQuery2 { testing2 { id2 label description2 __typename } }", "name": "TestQuery2", }, ] `; graphql-ruby-2.5.19/javascript_client/src/sync/__tests__/__snapshots__/prepareProjectTest.ts.snap000066400000000000000000000016361514115062600334140ustar00rootroot00000000000000// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`merging a project builds out separate operations 1`] = ` [ { "alias": "", "body": "query GetStuff2 { stuff ...Frag1 ...Frag2 } fragment Frag1 on Query { moreStuff } fragment Frag2 on Query { ...Frag3 } fragment Frag3 on Query { evenMoreStuff }", "name": "GetStuff2", }, { "alias": "", "body": "query GetStuff { ...Frag1 } fragment Frag1 on Query { moreStuff }", "name": "GetStuff", }, ] `; exports[`merging a project with --add-typename builds out operation with __typename fields 1`] = ` [ { "alias": "", "body": "query GetStuff3 { stuff { withStuffInside __typename } ...Frag2 ...Frag4 } fragment Frag2 on Query { ...Frag3 } fragment Frag3 on Query { evenMoreStuff } fragment Frag4 on Query { evenMoreStuff { stuffInside __typename } }", "name": "GetStuff3", }, ] `; graphql-ruby-2.5.19/javascript_client/src/sync/__tests__/addTypenameToSelectionSetTest.ts000066400000000000000000000006671514115062600317340ustar00rootroot00000000000000import {addTypenameToSelectionSet} from "../addTypenameToSelectionSet" import { parse, print } from "graphql" describe("adding typename", () => { it("adds to fields and inline fragments", () => { var doc = parse("{ a { b ... { c } } }") var newDoc = addTypenameToSelectionSet(doc) var newString = print(newDoc).replace(/\s+/g, " ").trim() expect(newString).toEqual("{ a { b ... { c __typename } __typename } }") }) }) graphql-ruby-2.5.19/javascript_client/src/sync/__tests__/dumpPayloadTest.ts000066400000000000000000000014301514115062600271200ustar00rootroot00000000000000import dumpPayload from "../dumpPayload" import fs from 'fs' interface MockedObject { mock: { calls: object } } describe("printing out the HTTP Post payload", () => { beforeEach(() => { process.stdout.write = jest.fn() }) afterEach(() => { jest.clearAllMocks(); }) it("prints the result to stdout", () => { var spy = (process.stdout.write as unknown) as MockedObject dumpPayload({"ok": { "1": true}}, { dumpPayload: true }) expect(spy.mock.calls).toMatchSnapshot() }) it("writes the result to a file", () => { dumpPayload({"ok": { "1": true}}, {dumpPayload: "./DumpPayloadExample.json"}) let writtenContents = fs.readFileSync("./DumpPayloadExample.json", 'utf8') expect(writtenContents).toEqual(`{ "ok": { "1": true } } `) }) }) graphql-ruby-2.5.19/javascript_client/src/sync/__tests__/generate-persisted-query-manifest.json000066400000000000000000000007711514115062600330740ustar00rootroot00000000000000{ "format": "apollo-persisted-query-manifest", "version": 1, "operations": [ { "id": "4a29162b05ee4d82ad02e8f50af4bf112f47181ec558a7100a", "name": "TestQuery1", "type": "query", "body": "query TestQuery1 {\n testing {\n id\n label\n description\n __typename\n} }" }, { "id": "xyz-123", "name": "TestQuery2", "type": "mutation", "body": "query TestQuery2 {\n testing2 {\n id2\n label\n description2\n __typename\n} }" } ] } graphql-ruby-2.5.19/javascript_client/src/sync/__tests__/generateClientTest.ts000066400000000000000000000003611514115062600275740ustar00rootroot00000000000000import { generateClient } from "../generateClient" it("returns generated code", function() { var code = generateClient({ path: "./src/__tests__/documents/*.graphql", client: "test-client", }) expect(code).toMatchSnapshot() }) graphql-ruby-2.5.19/javascript_client/src/sync/__tests__/generateJsClientTest.ts000066400000000000000000000044021514115062600300710ustar00rootroot00000000000000import { generateClientCode, JS_TYPE, OperationStoreClient } from "../generateClient" import fs from "fs" function withExampleClient(mapName: string, callback: (client: OperationStoreClient) => void) { // Generate some code and write it to a file var exampleOperations = [ {name: "a", alias: "b", body: ""}, {name: "c-d", alias: "e-f", body: ""} ] var jsCode = generateClientCode("example-client", exampleOperations, JS_TYPE) var filename = "./src/sync/__tests__/" + mapName + ".js" fs.writeFileSync(filename, jsCode) // Load the module and use it var exampleModule = require("./" + mapName) callback(exampleModule) // Clean up the generated file fs.unlinkSync(filename) } it("generates a valid JavaScript module that maps names to operations", () => { withExampleClient("map1", (exampleClient) => { // It does the mapping expect(exampleClient.getPersistedQueryAlias("a")).toEqual("b") expect(exampleClient.getPersistedQueryAlias("c-d")).toEqual("e-f") // It returns a param expect(exampleClient.getOperationId("a")).toEqual("example-client/b") }) }) it("generates an Apollo middleware", () => { withExampleClient("map2", (exampleClient) => { var nextWasCalled = false var next = () => { nextWasCalled = true } var req = { operationName: "a", query: "x", operationId: "", } exampleClient.apolloMiddleware.applyMiddleware({request: req}, next) expect(nextWasCalled).toEqual(true) expect(req.query).toBeUndefined() expect(req.operationId).toEqual("example-client/b") }) }) it("generates an Apollo Link", () => { var fakeOperation = { operationName: "a", context: { http: {} }, setContext: function(c: { http: object }) { this.context = c }, extensions: { operationId: "", }, } var forwardedOperation: object var fakeForward = function(operation: object) { forwardedOperation = operation } withExampleClient("map3", (exampleClient) => { exampleClient.apolloLink(fakeOperation, fakeForward) expect(fakeOperation.extensions.operationId).toEqual("example-client/b") expect(fakeOperation.context.http).toEqual({includeQuery: false, includeExtensions: true}) expect(forwardedOperation).toEqual(fakeOperation) }) }) graphql-ruby-2.5.19/javascript_client/src/sync/__tests__/generateJsonClientTest.ts000066400000000000000000000012641514115062600304310ustar00rootroot00000000000000import { generateClientCode, JSON_TYPE } from "../generateClient" function withExampleClient(callback: (client: string) => void) { // Generate some code and write it to a file var exampleOperations = [ {name: "a", alias: "b", body: ""}, {name: "c-d", alias: "e-f", body: ""} ] var json = generateClientCode("example-client", exampleOperations, JSON_TYPE) // Run callback with generated client callback(json) } it("generates a valid json object string that maps names to operations", () => { withExampleClient((json) => { expect(json).toMatchSnapshot() // String version expect(JSON.parse(json)).toMatchSnapshot() // Object version (i.e., valid JSON) }) }) graphql-ruby-2.5.19/javascript_client/src/sync/__tests__/prepareIsolatedFilesTest.ts000066400000000000000000000012241514115062600307500ustar00rootroot00000000000000import prepareIsolatedFiles from "../prepareIsolatedFiles" it("builds out single operations", () => { var filenames = [ "./src/__tests__/project/op_isolated_1.graphql", "./src/__tests__/project/op_isolated_2.graphql", ] var ops = prepareIsolatedFiles(filenames, false) expect(ops).toMatchSnapshot() }) describe("with --add-typename", () => { it("builds out single operations with __typename fields", () => { var filenames = [ "./src/__tests__/project/op_isolated_1.graphql", "./src/__tests__/project/op_isolated_2.graphql", ] var ops = prepareIsolatedFiles(filenames, true) expect(ops).toMatchSnapshot() }) }) graphql-ruby-2.5.19/javascript_client/src/sync/__tests__/preparePersistedQueryListTest.ts000066400000000000000000000004571514115062600320540ustar00rootroot00000000000000import preparePersistedQueryList from "../preparePersistedQueryList" it("reads generate-persisted-query-manifest output", () => { const manifestPath = "./src/sync/__tests__/generate-persisted-query-manifest.json" var ops = preparePersistedQueryList(manifestPath) expect(ops).toMatchSnapshot() }) graphql-ruby-2.5.19/javascript_client/src/sync/__tests__/prepareProjectTest.ts000066400000000000000000000025101514115062600276260ustar00rootroot00000000000000import prepareProject from "../prepareProject" describe("merging a project", () => { it("builds out separate operations", () => { var filenames = [ "./src/__tests__/project/op_2.graphql", "./src/__tests__/project/op_1.graphql", "./src/__tests__/project/frag_1.graphql", "./src/__tests__/project/frag_2.graphql", "./src/__tests__/project/frag_3.graphql", ] var ops = prepareProject(filenames, false) expect(ops).toMatchSnapshot() }) describe("with --add-typename", () => { it("builds out operation with __typename fields", () => { var filenames = [ "./src/__tests__/project/op_3.graphql", "./src/__tests__/project/frag_2.graphql", "./src/__tests__/project/frag_3.graphql", "./src/__tests__/project/frag_4.graphql", ] var ops = prepareProject(filenames, true) expect(ops).toMatchSnapshot() }) }) it("blows up on duplicate names", () => { var filenames = [ "./src/__tests__/documents/doc1.graphql", "./src/__tests__/project/op_2.graphql", "./src/__tests__/project/op_1.graphql", "./src/__tests__/project/frag_1.graphql", ] expect(() => { prepareProject(filenames, false) }).toThrow("Found duplicate definition name: GetStuff, fragment & operation names must be unique to sync") }) }) graphql-ruby-2.5.19/javascript_client/src/sync/__tests__/removeClientFieldsTest.ts000066400000000000000000000046141514115062600304330ustar00rootroot00000000000000import { removeClientFieldsFromString } from "../removeClientFields" describe("removing @client fields", () => { function normalizeString(str: string) { return str.replace(/\s+/g, " ").trim() } it("returns a string without any fields with @client", () => { var newString = removeClientFieldsFromString("{ f1 f2 @client { a b } f3 { a b @client } }") var expectedString = "{ f1 f3 { a } }" expect(normalizeString(newString)).toEqual(expectedString) }) it("leaves other strings unchanged", () => { var originalString = "{ f1 f2 @other { a b } f3 { a b @notClient } }" var newString = removeClientFieldsFromString(originalString) expect(normalizeString(newString)).toEqual(originalString) }) it("removes references to fragments that contain all client fields", () => { var originalString = ` { f1 ...Fragment1 ... on Query { f3 ...Fragment2 } ...Fragment3 } fragment Fragment1 on Query { f2 @client f3 ...Fragment2 } fragment Fragment2 on Query { f4 @client f5 @client f6 @client { f7 f8 } } fragment Fragment3 on Query { ...Fragment2 } ` var expectedString = ` { f1 ...Fragment1 ... on Query { f3 } } fragment Fragment1 on Query { f3 } ` var newString = removeClientFieldsFromString(originalString) expect(normalizeString(newString)).toEqual(normalizeString(expectedString)) }) it("removes now-unused variables", () => { var newString = removeClientFieldsFromString("query($thing: ID!){ f1 f2(thing: $thing) @client }") var expectedString = "{ f1 }" expect(normalizeString(newString)).toEqual(expectedString) }) it("removes fragments that are spread inside client fields", () => { // from https://github.com/apollographql/apollo-client/pull/6892/ var originalString = ` query Simple { networkField field @client { ...ClientFragment } } fragment ClientFragment on Thing { ...NestedFragment } fragment NestedFragment on Thing { otherField bar }` var expectedString = ` query Simple { networkField } ` var newString = removeClientFieldsFromString(originalString) expect(normalizeString(newString)).toEqual(normalizeString(expectedString)) }) }) graphql-ruby-2.5.19/javascript_client/src/sync/__tests__/sendPayloadTest.ts000066400000000000000000000065101514115062600271100ustar00rootroot00000000000000jest.dontMock('nock'); import nock from "nock" import Logger from "../logger"; import sendPayload from "../sendPayload" var fakeLogger = { log: function() {}, bright: function(str: string) { return str }, colorize: function(str: string) { return str }, red: function(str: string) { return str }, green: function(str: string) { return str }, error: function() {}, isQuiet: true, } as Logger describe("Posting GraphQL to OperationStore Endpoint", () => { it("Posts to the specified URL", () => { var mock = nock("http://example.com") .post("/stored_operations/sync") .reply(200, { "ok" : "ok" }) return sendPayload("payload", { url: "http://example.com/stored_operations/sync", logger: fakeLogger }).then(function() { expect(mock.isDone()).toEqual(true) }) }) it("Uses HTTPS when provided", () => { var mock = nock("https://example2.com") .post("/stored_operations/sync") .reply(200, { "ok" : "ok" }) return sendPayload("payload", { url: "https://example2.com/stored_operations/sync", logger: fakeLogger }).then(function() { expect(mock.isDone()).toEqual(true) }) }) it("Uses auth, port, and query", () => { var mock = nock("https://example2.com:229") .post("/stored_operations/sync?q=1") .basicAuth({ user: "username", pass: "pass" }) .reply(200, { "ok" : "ok" }) return sendPayload("payload", { url: "https://username:pass@example2.com:229/stored_operations/sync?q=1", logger: fakeLogger }).then(function() { expect(mock.isDone()).toEqual(true) }) }) it("Returns the response JSON to the promise", () => { nock("http://example.com") .post("/stored_operations/sync") .reply(200, { result: "ok" }) return sendPayload("payload", { url: "http://example.com/stored_operations/sync", logger: fakeLogger }).then(function(response) { expect(response).toEqual('{"result":"ok"}') }) }) it("Sends headers and changeset version", () => { var mock = nock("http://example.com", { reqheaders: { thing: "Stuff", "Changeset-Version": "2023-01-01", } }) .post("/stored_operations/sync") .reply(200, { result: "ok" }) return sendPayload("payload", { url: "http://example.com/stored_operations/sync", logger: fakeLogger, headers: { thing: "Stuff" }, changesetVersion: "2023-01-01" }).then(function(_response) { expect(mock.isDone()).toEqual(true) }) }) it("Adds an hmac-sha256 header if key is present", () => { var payload = { "payload": [1,2,3] } var key = "2f26b770ded2a04279bc4bf824ca54ac" // ruby -ropenssl -e 'puts OpenSSL::HMAC.hexdigest("SHA256", "2f26b770ded2a04279bc4bf824ca54ac", "{\"payload\":[1,2,3]}")' // f6eab31abc2fa446dbfd2e9c10a778aaffd4d0c1d62dd9513d6f7ea60557987c var signature = "f6eab31abc2fa446dbfd2e9c10a778aaffd4d0c1d62dd9513d6f7ea60557987c" var mock = nock("http://example.com", { reqheaders: { 'authorization': 'GraphQL::Pro Abc ' + signature }}) .post("/stored_operations/sync") .reply(200, { result: "ok" }) var opts = {secret: key, client: "Abc", url: "http://example.com/stored_operations/sync", logger: fakeLogger } return sendPayload(payload, opts).then(function(response) { expect(response).toEqual('{"result":"ok"}') expect(mock.isDone()).toEqual(true) }) }) }) graphql-ruby-2.5.19/javascript_client/src/sync/addTypenameToSelectionSet.ts000066400000000000000000000022651514115062600271320ustar00rootroot00000000000000import { visit, ASTNode, FieldNode, InlineFragmentNode, Kind } from "graphql" const TYPENAME_FIELD: FieldNode = { kind: Kind.FIELD, name: { kind: Kind.NAME, value: "__typename", }, selectionSet: { kind: Kind.SELECTION_SET, selections: [] } } function addTypenameIfAbsent(node: FieldNode | InlineFragmentNode): undefined | FieldNode | InlineFragmentNode { if (node.selectionSet) { const alreadyHasThisField = node.selectionSet.selections.some(function(selection) { return ( selection.kind === "Field" && selection.name.value === "__typename" ) }) if (!alreadyHasThisField) { return { ...node, selectionSet: { ...node.selectionSet, selections: [...node.selectionSet.selections, TYPENAME_FIELD] } } } else { return undefined } } else { return undefined } } function addTypenameToSelectionSet(node: ASTNode) { var visitor = { Field: { leave: addTypenameIfAbsent, }, InlineFragment: { leave: addTypenameIfAbsent, } } var newNode = visit(node, visitor) return newNode } export { addTypenameToSelectionSet, addTypenameIfAbsent } graphql-ruby-2.5.19/javascript_client/src/sync/dumpPayload.ts000066400000000000000000000005761514115062600243340ustar00rootroot00000000000000import fs from 'fs'; interface DumpPayloadOptions { dumpPayload: string | true, } export default function dumpPayload(payload: Object, options: DumpPayloadOptions) { let payloadStr = JSON.stringify(payload, null, 2) + "\n" if (options.dumpPayload == true) { process.stdout.write(payloadStr) } else { fs.writeFileSync(options.dumpPayload, payloadStr, 'utf8') } } graphql-ruby-2.5.19/javascript_client/src/sync/generateClient.ts000066400000000000000000000117201514115062600247770ustar00rootroot00000000000000import { globSync } from "glob" import prepareRelay from "./prepareRelay" import prepareIsolatedFiles from './prepareIsolatedFiles' import prepareProject from "./prepareProject" import md5 from "./md5" import generateJs from "./outfileGenerators/js" import generateJson from "./outfileGenerators/json" var JS_TYPE = "js"; var JSON_TYPE = "json"; var generators = { [JS_TYPE]: generateJs, [JSON_TYPE]: generateJson, }; interface GenerateClientCodeOptions { path?: string // A glob to recursively search for `.graphql` files (Default is `./`) mode?: string // If `"file"`, treat each file separately. If `"project"`, concatenate all files and extract each operation. If `"relay"`, treat it as relay-compiler output addTypename?: boolean // Indicates if the "__typename" field are automatically added to your queries clientType?: string // The type of the generated code (i.e., json, js) client: string // the Client ID that these operations belong to hash?: Function // A custom hash function for query strings with the signature `options.hash(string) => digest` (Default is `md5(string) => digest`) verbose?: boolean // If true, print debug output } interface OperationStoreClient { getOperationId: (operationName: string) => string getPersistedQueryAlias: (operationName: string) => string apolloMiddleware: { applyMiddleware: (req: any, next: any) => any } apolloLink: (operation: any, forward: any) => any } /** * Generate a JavaScript client module based on local `.graphql` files. * * See {gatherOperations} and {generateClientCode} for options. * @return {String} The generated JavaScript code */ function generateClient(options: GenerateClientCodeOptions): string { var payload = gatherOperations(options) var generatedCode = generateClientCode(options.client, payload.operations, options.clientType) return generatedCode } interface ClientOperation { alias: string, name?: string, body: string, } /** * Parse files in the specified path and generate an alias for each operation. */ function gatherOperations(options: GenerateClientCodeOptions) { var graphqlGlob = options.path || "./" // Check for file ext already, add it if missing var containsFileExt = graphqlGlob.indexOf(".graphql") > -1 || graphqlGlob.indexOf(".gql") > -1 if (!containsFileExt) { graphqlGlob = graphqlGlob + "**/*.graphql*" } var hashFunc = options.hash || md5 var filesMode = options.mode || (graphqlGlob.indexOf("__generated__") > -1 ? "relay" : "project") var addTypename = !!options.addTypename var verbose = !!options.verbose var operations: ClientOperation[] = [] var filenames: string[] = globSync(graphqlGlob, {}).sort() if (verbose) { console.log("[Sync] glob: ", graphqlGlob) console.log("[Sync] " + filenames.length + " files:") console.log(filenames.map(function(f) { return "[Sync] - " + f }).join("\n")) } if (filesMode == "relay") { operations = prepareRelay(filenames) } else { if (filesMode === "file") { operations = prepareIsolatedFiles(filenames, addTypename) } else if (filesMode === "project") { operations = prepareProject(filenames, addTypename) } else { throw new Error("Unexpected mode: " + filesMode) } // Update the operations with the hash of the body operations.forEach(function(op) { op.alias = hashFunc(op.body) // console.log("operation", op.alias, op.body) }) } return { operations: operations } } /** * Given a map of { name => alias } pairs, generate outfile based on type. * @param {String} clientName - the client ID that this map belongs to * @param {Object} nameToAlias - `name => alias` pairs * @param {String} type - the outfile's type * @return {String} generated outfile code */ function generateClientCode(clientName: string, operations: ClientOperation[], type?: string): string { if (!clientName) { throw new Error("Client name is required to generate a persisted alias lookup map"); } var nameToAlias: {[key: string] : string | null} = {} operations.forEach(function(op) { // This can be blank from relay-perisisted-output, // but typescript doesn't know that we don't use this function in that case // (Er, I should make _two_ interfaces, but I haven't yet.) if (op.name) { nameToAlias[op.name] = op.alias } }) // Build up the map var keyValuePairs = "{" keyValuePairs += Object.keys(nameToAlias).map(function(operationName) { var persistedAlias = nameToAlias[operationName] return "\n \"" + operationName + "\": \"" + persistedAlias + "\"" }).join(",") keyValuePairs += "\n}" var outfileType = type || JS_TYPE var generateOutfile = generators[outfileType]; if (!generateOutfile) { throw new Error("Unknown generator type " + outfileType + " encountered for generating the outFile"); } return generateOutfile(outfileType, clientName, keyValuePairs); } export { generateClient, generateClientCode, gatherOperations, JS_TYPE, JSON_TYPE, ClientOperation, OperationStoreClient, } graphql-ruby-2.5.19/javascript_client/src/sync/index.ts000066400000000000000000000257231514115062600231650ustar00rootroot00000000000000import sendPayload from "./sendPayload" import dumpPayload from "./dumpPayload" import { generateClientCode, gatherOperations, ClientOperation } from "./generateClient" import Logger from "./logger" import fs from "fs" import { removeClientFieldsFromString } from "./removeClientFields" import preparePersistedQueryList from "./preparePersistedQueryList" export interface SyncOptions { path?: string, relayPersistedOutput?: string, apolloAndroidOperationOutput?: string, apolloCodegenJsonOutput?: string, apolloPersistedQueryManifest?: string, secret?: string url?: string, mode?: string, dumpPayload?: string | true, outfile?: string, outfileType?: string, client: string, send?: Function, hash?: Function, verbose?: boolean, quiet?: boolean, addTypename?: boolean, changesetVersion?: string, headers?: {[key: string]: string}, } /** * Find `.graphql` files in `path`, * then prepare them & send them to the configured endpoint. * * @param {Object} options * @param {String} options.path - A glob to recursively search for `.graphql` files (Default is `./`) * @param {String} options.relayPersistedOutput - A path to a `.json` file from `relay-compiler`'s `--persist-output` option * @param {String} options.apolloCodegenJsonOutput - A path to a `.json` file from `apollo client:codegen ... --type json` * @param {String} options.apolloPersistedQueryManifest - A path to a `.json` file from `generate-persisted-query-manifest` * @param {String} options.secret - HMAC-SHA256 key which must match the server secret (default is no encryption) * @param {String} options.url - Target URL for sending prepared queries. If omitted, then an outfile is generated without sending operations to the server. * @param {String} options.mode - If `"file"`, treat each file separately. If `"project"`, concatenate all files and extract each operation. If `"relay"`, treat it as relay-compiler output * @param {Boolean} options.addTypename - Indicates if the "__typename" field are automatically added to your queries * @param {String} options.outfile - Where the generated code should be written * @param {String} options.outfileType - The type of the generated code (i.e., json, js) * @param {String} options.client - the Client ID that these operations belong to * @param {Function} options.send - A function for sending the payload to the server, with the signature `options.send(payload)`. (Default is an HTTP `POST` request) * @param {Function} options.hash - A custom hash function for query strings with the signature `options.hash(string) => digest` (Default is `md5(string) => digest`) * @param {Boolean} options.verbose - If true, log debug output * @param {Object} options.headers - If present, extra headers to add to the HTTP request * @param {String|true} options.dumpPayload - If a filename is given, write the HTTP Post data to that file. If present without a filename, print it to stdout. * @param {String} options.changesetVersion - If present, sent to populate `context[:changeset_version]` on the server * @return {Promise} Rejects with an Error or String if something goes wrong. Resolves with the operation payload if successful. */ function sync(options: SyncOptions) { var logger = new Logger(!!options.quiet) var verbose = !!options.verbose var url = options.url var dumpingPayload = "dumpPayload" in options var dumpingToStdout = options.dumpPayload == true if (!url && !dumpingPayload) { logger.log("No URL; Generating artifacts without syncing them") } var clientName = options.client if (!clientName) { throw new Error("Client name must be provided for sync") } var encryptionKey = options.secret if (encryptionKey && options.dumpPayload != null) { logger.log("Authenticating with HMAC") } var graphqlGlob = options.path var hashFunc = options.hash var sendFunc = options.send || (dumpingPayload ? dumpPayload : sendPayload) var gatherMode = options.mode var clientType = options.outfileType if (options.relayPersistedOutput) { // relay-compiler has already generated an artifact for us var payload: { operations: ClientOperation[] } = { operations: [] } var relayOutputText = fs.readFileSync(options.relayPersistedOutput, "utf8") var relayOutput = JSON.parse(relayOutputText) var operationBody for (var hash in relayOutput) { operationBody = relayOutput[hash] payload.operations.push({ body: operationBody, alias: hash, }) } } else if (options.apolloAndroidOperationOutput) { // Apollo Android has already generated an artifact (https://www.apollographql.com/docs/android/advanced/persisted-queries/#operationoutputjson) var payload: { operations: ClientOperation[] } = { operations: [] } var apolloAndroidOutputText = fs.readFileSync(options.apolloAndroidOperationOutput, "utf8") var apolloAndroidOutput = JSON.parse(apolloAndroidOutputText) var operationData // Structure is { operationId => { "name" => "...", "source" => "query { ... } " } } for (var operationId in apolloAndroidOutput) { operationData = apolloAndroidOutput[operationId] let bodyWithoutClientFields = removeClientFieldsFromString(operationData.source) payload.operations.push({ body: bodyWithoutClientFields, alias: operationId, }) } } else if (options.apolloCodegenJsonOutput) { var payload: { operations: ClientOperation[] } = { operations: [] } const jsonText = fs.readFileSync(options.apolloCodegenJsonOutput).toString() const jsonData = JSON.parse(jsonText) jsonData.operations.map(function(operation: {operationId: string, operationName: string, sourceWithFragments: string}) { const bodyWithoutClientFields = removeClientFieldsFromString(operation.sourceWithFragments) payload.operations.push({ alias: operation.operationId, name: operation.operationName, body: bodyWithoutClientFields, }) }) } else if (options.apolloPersistedQueryManifest) { var payload: { operations: ClientOperation[] } = { operations: preparePersistedQueryList(options.apolloPersistedQueryManifest) } } else { var payload = gatherOperations({ path: graphqlGlob, hash: hashFunc, mode: gatherMode, addTypename: !!options.addTypename, clientType: clientType, client: clientName, verbose: verbose, }) } var outfile: string | null if (options.outfile) { outfile = options.outfile } else if (options.relayPersistedOutput || options.apolloAndroidOperationOutput || options.apolloCodegenJsonOutput || options.apolloPersistedQueryManifest) { // These artifacts have embedded IDs in its generated files, // no need to generate an outfile. outfile = null } else if (fs.existsSync("src")) { outfile = "src/OperationStoreClient.js" } else { outfile = "OperationStoreClient.js" } var syncPromise = new Promise(function(resolve, reject) { if (payload.operations.length === 0) { logger.log("No operations found in " + options.path + ", not syncing anything") resolve(null) return } else if (url) { logger.log("Syncing " + payload.operations.length + " operations to " + logger.bright(url) + "...") var sendOpts = { url: url, client: clientName, secret: encryptionKey, headers: options.headers, changesetVersion: options.changesetVersion, logger: logger, } var sendPromise = Promise.resolve(sendFunc(payload, sendOpts)) return sendPromise.then(function(response) { var responseData if (response) { try { responseData = JSON.parse(response) var aliasToNameMap: {[key: string] : string | undefined} = {} payload.operations.forEach(function(op) { aliasToNameMap[op.alias] = op.name }) var failed = responseData.failed.length // These might get overridden for status output var notModified = responseData.not_modified.length var added = responseData.added.length if (failed) { // Override these to reflect reality notModified = 0 added = 0 } var addedColor = added ? "green" : "dim" logger.log(" " + logger.colorize(addedColor, added + " added")) var notModifiedColor = notModified ? "reset" : "dim" logger.log(" " + logger.colorize(notModifiedColor, notModified + " not modified")) var failedColor = failed ? "red" : "dim" logger.log(" " + logger.colorize(failedColor, failed + " failed")) if (failed) { logger.error("Sync failed, errors:") var failedOperationAlias: string var failedOperationName: string var errors var allErrors: string[] = [] for (failedOperationAlias in responseData.errors) { failedOperationName = aliasToNameMap[failedOperationAlias] || failedOperationAlias logger.error(" " + failedOperationName + ":") errors = responseData.errors[failedOperationAlias] errors.forEach(function(errMessage: string) { allErrors.push(failedOperationName + ": " + errMessage) logger.error(" " + logger.red("✘") + " " + errMessage) }) } reject("Sync failed: " + allErrors.join(", ")) return } } catch (err) { logger.log("Failed to print sync result:", err as string) reject(err) return } } resolve(payload) return }).catch(function(err) { logger.error(logger.red("Sync failed:")) logger.error(err) reject(err) return }) } else if (dumpingPayload) { sendFunc(payload, { dumpPayload: options.dumpPayload }) resolve(payload) return } else { // This is a local-only run to generate an artifact resolve(payload) return } }) return syncPromise.then(function(_payload) { // The payload is yielded when sync was successful, but typescript had // trouble using it from ^^ here. So instead, just use its presence as a signal to continue. // Don't generate a new file when we're using relay-compiler's --persist-output if (_payload && outfile) { var generatedCode = generateClientCode(clientName, payload.operations, clientType) var finishedPayload = { operations: payload.operations, generatedCode, } if (!dumpingToStdout) { logger.log("Generating client module in " + logger.colorize("bright", outfile) + "...") } fs.writeFileSync(outfile, generatedCode, "utf8") if (!dumpingToStdout) { logger.log(logger.green("✓ Done!")) } return finishedPayload } else { if (!dumpingToStdout) { logger.log(logger.green("✓ Done!")) } return payload } }) } export default sync graphql-ruby-2.5.19/javascript_client/src/sync/logger.ts000066400000000000000000000017441514115062600233320ustar00rootroot00000000000000class Logger { isQuiet: boolean constructor(isQuiet: boolean) { this.isQuiet = isQuiet } log(...args: string[]) { this.isQuiet ? null : console.log(...args) } error(...args: string[]) { this.isQuiet ? null : console.error(...args) } colorize(color: string, text: string) { var prefix = colors[color] if (!prefix) { throw new Error("No color named: " + color) } return prefix + text + colors.reset } // Shortcuts to `.colorize`, add more as-needed. red(text: string) { return this.colorize("red", text) } green(text: string) { return this.colorize("green", text) } bright(text: string) { return this.colorize("bright", text) } } const colors: {[key: string]: string} = { yellow: "\x1b[33m", red: "\x1b[31m", green: "\x1b[32m", blue: "\x1b[34m", magenta: "\x1b[35m", cyan: "\x1b[36m", reset: "\x1b[0m", bright: "\x1b[1m", dim: "\x1b[2m", } export default Logger graphql-ruby-2.5.19/javascript_client/src/sync/md5.ts000066400000000000000000000003411514115062600225300ustar00rootroot00000000000000import crypto from 'crypto' // Return the hex-encoded md5 hash of `inputString` function md5(inputString: string): string { return crypto.createHash("md5") .update(inputString) .digest("hex") } export default md5 graphql-ruby-2.5.19/javascript_client/src/sync/outfileGenerators/000077500000000000000000000000001514115062600251765ustar00rootroot00000000000000graphql-ruby-2.5.19/javascript_client/src/sync/outfileGenerators/js.ts000066400000000000000000000057521514115062600261730ustar00rootroot00000000000000function generateOutfile(_type: string, clientName: string, keyValuePairs: string) { return ` /** * Generated by graphql-ruby-client * */ /** * Map local operation names to persisted keys on the server * @return {Object} * @private */ var _aliases = ${keyValuePairs} /** * The client who synced these operations with the server * @return {String} * @private */ var _client = "${clientName}" var OperationStoreClient = { /** * Build a string for \`params[:operationId]\` * @param {String} operationName * @return {String} stored operation ID */ getOperationId: function(operationName) { return _client + "/" + OperationStoreClient.getPersistedQueryAlias(operationName) }, /** * Fetch a persisted alias from a local operation name * @param {String} operationName * @return {String} persisted alias */ getPersistedQueryAlias: function(operationName) { var persistedAlias = _aliases[operationName] if (!persistedAlias) { throw new Error("Failed to find persisted alias for operation name: " + operationName) } else { return persistedAlias } }, /** * Satisfy the Apollo Link API. * This link checks for an operation name, and if it's present, * sets the HTTP context to _not_ include the query, * and instead, include \`extensions.operationId\`. * (This is inspired by apollo-link-persisted-queries.) */ apolloLink: function(operation, forward) { if (operation.operationName) { const operationId = OperationStoreClient.getOperationId(operation.operationName) operation.setContext({ http: { includeQuery: false, includeExtensions: true, } }) operation.extensions.operationId = operationId } return forward(operation) }, /** * Satisfy the Apollo middleware API. * Replace the query with an operationId */ apolloMiddleware: { applyBatchMiddleware: function(options, next) { options.requests.forEach(function(req) { // Fetch the persisted alias for this operation req.operationId = OperationStoreClient.getOperationId(req.operationName) // Remove the now-unused query string delete req.query return req }) // Continue the request next() }, applyMiddleware: function(options, next) { var req = options.request // Fetch the persisted alias for this operation req.operationId = OperationStoreClient.getOperationId(req.operationName) // Remove the now-unused query string delete req.query // Continue the request next() } } } module.exports = OperationStoreClient ` } export default generateOutfile; graphql-ruby-2.5.19/javascript_client/src/sync/outfileGenerators/json.ts000066400000000000000000000002251514115062600265160ustar00rootroot00000000000000function generateOutfile(_type: string, _clientName: string, keyValuePairs: string) { return `${keyValuePairs}` } export default generateOutfile; graphql-ruby-2.5.19/javascript_client/src/sync/prepareIsolatedFiles.ts000066400000000000000000000027541514115062600261630ustar00rootroot00000000000000import fs from "fs" import {parse, visit, print, OperationDefinitionNode} from "graphql" import {addTypenameIfAbsent} from "./addTypenameToSelectionSet" import { removeClientFields } from "./removeClientFields" /** * Read a bunch of GraphQL files and treat them as islands. * Don't join any fragments from other files. * Don't make assertions about name uniqueness. * */ function prepareIsolatedFiles(filenames: string[], addTypename: boolean) { return filenames.map(function(filename) { var fileOperationBody = fs.readFileSync(filename, "utf8") var fileOperationName = "" var ast = parse(fileOperationBody) var visitor = { OperationDefinition: { enter: function(node: OperationDefinitionNode) { if (fileOperationName.length > 0) { throw new Error("Found multiple operations in " + filename + ": " + fileOperationName + ", " + node.name + ". Files must contain only one operation") } else if (node.name && node.name.value) { fileOperationName = node.name.value } }, }, InlineFragment: { leave: addTypename ? addTypenameIfAbsent : () => {} }, Field: { leave: addTypename ? addTypenameIfAbsent : () => {} } } ast = visit(ast, visitor) ast = removeClientFields(ast) return { // populate alias later, when hashFunc is available alias: "", name: fileOperationName, body: print(ast), } }) } export default prepareIsolatedFiles graphql-ruby-2.5.19/javascript_client/src/sync/preparePersistedQueryList.ts000066400000000000000000000010621514115062600272470ustar00rootroot00000000000000import fs from "fs" // Transform the output from generate-persisted-query-manifest // to something that OperationStore `sync` can use. export default function preparePersistedQueryList(pqlPath: string) { const pqlString = fs.readFileSync(pqlPath, "utf8") const pqlJson = JSON.parse(pqlString) return pqlJson.operations.map(function(persistedQueryConfig: { body: string, id: string, name: string, type: string }) { return { body: persistedQueryConfig.body, alias: persistedQueryConfig.id, name: persistedQueryConfig.name } }) } graphql-ruby-2.5.19/javascript_client/src/sync/prepareProject.ts000066400000000000000000000073401514115062600250360ustar00rootroot00000000000000import { addTypenameIfAbsent } from "./addTypenameToSelectionSet"; import fs from "fs" import {parse, visit, print, OperationDefinitionNode, FragmentDefinitionNode, FragmentSpreadNode, DocumentNode} from "graphql" import { removeClientFields } from "./removeClientFields"; /** * Take a whole bunch of GraphQL in one big string * and validate it, especially: * * - operation names are unique * - fragment names are unique * * Then, split each operation into a free-standing document, * so it has all the fragments it needs. */ function prepareProject(filenames: string[], addTypename: boolean) { if(!filenames.length) { return []; } var allGraphQL = "" filenames.forEach(function(filename) { allGraphQL += fs.readFileSync(filename) }) var ast = parse(allGraphQL) // This will contain { name: [name, name] } pairs var definitionDependencyNames: {[key: string] : string[] } = {} var allOperationNames: string[] = [] var currentDependencyNames = null // When entering a fragment or operation, // start recording its dependencies var enterDefinition = function(node: FragmentDefinitionNode | OperationDefinitionNode) { // Technically, it could be an anonymous definition if (node.name) { var definitionName = node.name.value if (definitionDependencyNames[definitionName]) { throw new Error("Found duplicate definition name: " + definitionName + ", fragment & operation names must be unique to sync") } else { currentDependencyNames = definitionDependencyNames[definitionName] = [] } } } var visitor = { OperationDefinition: { enter: function(node: OperationDefinitionNode) { enterDefinition(node) node.name && allOperationNames.push(node.name.value) }, }, FragmentDefinition: { enter: enterDefinition, }, // When entering a fragment spread, register it as a // dependency of its context FragmentSpread: { enter: function(node: FragmentSpreadNode) { currentDependencyNames.push(node.name.value) } }, Field: { leave: addTypename ? addTypenameIfAbsent : () => {} }, InlineFragment: { leave: addTypename ? addTypenameIfAbsent : () => {} } } // Find the dependencies, build the accumulator ast = visit(ast, visitor) ast = removeClientFields(ast) // For each operation, build a separate document of that operation and its deps // then print the new document to a string var operations = allOperationNames.map(function(operationName) { var visitedDepNames: string[] = [] var depNamesToVisit = [operationName] var depName while (depNamesToVisit.length > 0) { depName = depNamesToVisit.shift() if (depName) { visitedDepNames.push(depName) definitionDependencyNames[depName].forEach(function(nextDepName) { if (visitedDepNames.indexOf(nextDepName) === -1) { depNamesToVisit.push(nextDepName) } }) } } var newAST = extractDefinitions(ast, visitedDepNames) return { name: operationName, body: print(newAST), alias: "", // will be filled in later, when hashFunc is available } }) return operations } // Return a new AST which contains only `definitionNames` function extractDefinitions(ast: DocumentNode, definitionNames: string[]) { var removeDefinitionNode = function(node: FragmentDefinitionNode | OperationDefinitionNode) { if (node.name && definitionNames.indexOf(node.name.value) === -1) { return null } else { return undefined } } var visitor = { OperationDefinition: removeDefinitionNode, FragmentDefinition: removeDefinitionNode, } var newAST = visit(ast, visitor) return newAST } export default prepareProject graphql-ruby-2.5.19/javascript_client/src/sync/prepareRelay.ts000066400000000000000000000035261514115062600245060ustar00rootroot00000000000000import path from "path" import fs from "fs" interface RelayCompilerOperation { params?: RelayCompilerOperation text: string name: string } /** * Read relay-compiler output * and extract info for persisting them & writing a map: * * - alias: get the relayHash from the header * - name: get the name from the JavaScript object * - body: get the text from the JavaScript object * * @param {Array} filenames - Filenames to read * @return {Array} List of operations to persist & write to a map */ function prepareRelay(filenames: string[]) { var currentDirectory = process.cwd() var operations = filenames.map(function(filename) { // Search the file for the relayHash var textContent = fs.readFileSync(filename, "utf8") var operationAlias = textContent.match(/@relayHash ([a-z0-9]+)/) // Only operations get `relayHash`, so // skip over generated fragments if (operationAlias) { // Require the file to get values from the JavaScript code var absoluteFilename = path.resolve(currentDirectory, filename) var operation: RelayCompilerOperation = require(absoluteFilename) var operationBody, operationName // Support Relay version ^2.0.0 if (operation.params) { operationBody = operation.params.text operationName = operation.params.name } else { // Support Relay versions < 2.0.0 operationBody = operation.text operationName = operation.name } return { alias: operationAlias[1], name: operationName, body: operationBody, } } else { return { alias: "", name: "not-found", body: "not-found", } } }) // Remove the nulls var operationsWithoutNulls = operations.filter(function(o) { return o.alias.length }) return operationsWithoutNulls } export default prepareRelay graphql-ruby-2.5.19/javascript_client/src/sync/removeClientFields.ts000066400000000000000000000076101514115062600256340ustar00rootroot00000000000000import { parse, DocumentNode, VariableDefinitionNode, print, visit } from "graphql" function removeClientFields(node: DocumentNode) { // Deleting fields can create invalid documents: // - If variables were used by those fields (or their subfields), then their definitions are invalid // - If a fragment contained only deleted fields, it is now empty and therefore invalid and should be deleted // - If a fragment spread names a deleted fragment, it is now invalid // - If a client field contained a fragment spread and it's deleted, then a fragment may be left unspread let anythingWasRemoved = false const usedVariables: string[] = [] let definedFragments: string[] = [] let spreadFragments: string[] = [] // First pass: remove as much as possible, even if the document is left invalid. // - remove fields that have @client // - remove fragment definitions that become empty let newDoc = visit(node, { Field: { enter: (node) => { if (node.directives && node.directives.some((d) => { return d.name.value === "client" })) { anythingWasRemoved = true // Delete this node return null } else { return undefined } } }, // FragmentSpread: ... Don't do this now, because we might find some in Fragment Definitions that are deleted later. FragmentDefinition: { leave: (node) => { if (node.selectionSet.selections.length == 0) { // All the fields in this fragment were removed return null } else { definedFragments.push(node.name.value) return undefined } } }, FragmentSpread: { enter: (node) => { spreadFragments.push(node.name.value) } }, Variable: { enter: (node, _key, parent) => { if ((parent as VariableDefinitionNode).kind !== 'VariableDefinition') { // This will only find variables that are used _after_ `@client` fields are deleted. // (If `@client` fields are deleted, then their arguments aren't visited) usedVariables.push(node.name.value) } }, }, }) if (anythingWasRemoved) { // At this point, we can remove variables that aren't used. newDoc = visit(newDoc, { VariableDefinition: { enter: (node) => { if (!usedVariables.includes(node.variable.name.value)) { return null } else { return undefined } } }, }) // Then, remove spreads of empty fragment definitions as long as we keep finding them // Also remove definitions of fragments that aren't spread anymore while (anythingWasRemoved) { let previouslyDefinedFragments = definedFragments let previouslySpreadFragments = spreadFragments definedFragments = [] spreadFragments = [] anythingWasRemoved = false newDoc = visit(newDoc, { FragmentSpread: { enter: (node) => { if (!previouslyDefinedFragments.includes(node.name.value)) { anythingWasRemoved = true return null } else { spreadFragments.push(node.name.value) return undefined } } }, FragmentDefinition: { enter: (node) => { if (node.selectionSet.selections.length == 0 || !previouslySpreadFragments.includes(node.name.value)) { anythingWasRemoved = true return null } else { definedFragments.push(node.name.value) return undefined } } } }) } } return newDoc } function removeClientFieldsFromString(body: string): string { if (body.includes("@client")) { const ast = parse(body) const newAst = removeClientFields(ast) return print(newAst) } else { return body } } export { removeClientFields, removeClientFieldsFromString } graphql-ruby-2.5.19/javascript_client/src/sync/sendPayload.ts000066400000000000000000000067351514115062600243230ustar00rootroot00000000000000import http from "http" import https from "https" import url from "url" import crypto from 'crypto' import Logger from './logger' interface SendPayloadOptions { url: string, logger: Logger, secret?: string, client?: string, headers?: { [key: string]: string }, changesetVersion?: string, } /** * Use HTTP POST to send this payload to the endpoint. * * Override this function with `options.send` to use custom auth. * * @private * @param {Object} payload - JS object to be posted as form data * @param {String} options.url - Target URL * @param {String} options.secret - (optional) used for HMAC header if provided * @param {String} options.client - (optional) used for HMAC header if provided * @param {Logger} options.logger - A logger for when `verbose` is true * @param {Object} options.headers - (optional) extra headers for the request * @return {Promise} */ function sendPayload(payload: any, options: SendPayloadOptions) { var syncUrl = options.url var key = options.secret var clientName = options.client var logger = options.logger // Prepare JS object as form data var postData = JSON.stringify(payload) // Get parts of URL for request options var parsedURL = url.parse(syncUrl) // Prep options for HTTP request var defaultHeaders: {[key: string]: string} = { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData).toString() } if (options.changesetVersion) { logger.log("Changeset Version: ", logger.bright(options.changesetVersion)) defaultHeaders["Changeset-Version"] = options.changesetVersion } var allHeaders = Object.assign({}, options.headers, defaultHeaders) var httpOptions = { protocol: parsedURL.protocol, hostname: parsedURL.hostname, port: parsedURL.port, path: parsedURL.path, auth: parsedURL.auth, method: 'POST', headers: allHeaders, }; // If an auth key was provided, add a HMAC header var authDigest = null if (key) { authDigest = crypto.createHmac('sha256', key) .update(postData) .digest('hex') var header = "GraphQL::Pro " + clientName + " " + authDigest httpOptions.headers["Authorization"] = header } var headerNames = Object.keys(httpOptions.headers) logger.log("[Sync] " + headerNames.length + " Headers:") headerNames.forEach((headerName) => { logger.log("[Sync] " + headerName + ": " + httpOptions.headers[headerName]) }) logger.log("[Sync] Data:", postData) var httpClient = parsedURL.protocol === "https:" ? https : http var promise = new Promise(function(resolve, reject) { // Make the request, // hook up response handler const req = httpClient.request(httpOptions, (res) => { res.setEncoding('utf8'); // Gather the response from the server var body = "" res.on('data', (chunk) => { body += chunk }); res.on("end", () => { logger.log("[Sync] Response Headers: ", JSON.stringify(res.headers)) logger.log("[Sync] Response Body: ", body) var status = res.statusCode // 422 gets special treatment because // the body has error messages if (status && status > 299 && status != 422) { reject(" Server responded with " + res.statusCode) } else { resolve(body) } }) }); req.on('error', (e) => { reject(e) }); // Send the data, fire the request req.write(postData); req.end(); }) return promise } export default sendPayload graphql-ruby-2.5.19/javascript_client/tsconfig.json000066400000000000000000000134401514115062600224430ustar00rootroot00000000000000{ "include": ["./src"], "exclude": ["**/*.js"], "compilerOptions": { /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ "outDir": "./", /* Redirect output structure to the directory. */ "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ // "removeComments": true, /* Do not emit comments to output. */ // "noEmit": true, /* Do not emit outputs. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ /* Strict Type-Checking Options */ "strict": true, /* Enable all strict type-checking options. */ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* Enable strict null checks. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ /* Additional Checks */ "noUnusedLocals": true, /* Report errors on unused locals. */ "noUnusedParameters": true, /* Report errors on unused parameters. */ "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ /* Module Resolution Options */ // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ /* Source Map Options */ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ /* Experimental Options */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ /* Advanced Options */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ } } graphql-ruby-2.5.19/lib/000077500000000000000000000000001514115062600147745ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/generators/000077500000000000000000000000001514115062600171455ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/generators/graphql/000077500000000000000000000000001514115062600206035ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/generators/graphql/core.rb000066400000000000000000000031621514115062600220620ustar00rootroot00000000000000# frozen_string_literal: true require 'rails/generators/base' module Graphql module Generators module Core def self.included(base) base.send( :class_option, :directory, type: :string, default: "app/graphql", desc: "Directory where generated files should be saved" ) end def insert_root_type(type, name) log :add_root_type, type sentinel = /< GraphQL::Schema\s*\n/m in_root do if File.exist?(schema_file_path) inject_into_file schema_file_path, " #{type}(Types::#{name})\n", after: sentinel, verbose: false, force: false end end end def schema_file_path "#{options[:directory]}/#{schema_name.underscore}.rb" end def create_dir(dir) empty_directory(dir) if !options[:skip_keeps] create_file("#{dir}/.keep") end end def module_namespacing_when_supported if defined?(module_namespacing) module_namespacing { yield } else yield end end private def schema_name @schema_name ||= begin if options[:schema] options[:schema] else "#{parent_name}Schema" end end end def parent_name require File.expand_path("config/application", destination_root) if Rails.application.class.respond_to?(:module_parent_name) Rails.application.class.module_parent_name else Rails.application.class.parent_name end end end end end graphql-ruby-2.5.19/lib/generators/graphql/detailed_trace_generator.rb000066400000000000000000000054261514115062600261360ustar00rootroot00000000000000# frozen_string_literal: true require 'rails/generators/active_record' module Graphql module Generators class DetailedTraceGenerator < ::Rails::Generators::Base include ::Rails::Generators::Migration desc "Install GraphQL::Tracing::DetailedTrace for your schema" source_root File.expand_path('../templates', __FILE__) class_option :redis, type: :boolean, default: false, desc: "Use Redis for persistence instead of ActiveRecord" def self.next_migration_number(dirname) ::ActiveRecord::Generators::Base.next_migration_number(dirname) end def install_detailed_traces schema_glob = File.expand_path("app/graphql/*_schema.rb", destination_root) schema_file = Dir.glob(schema_glob).first if !schema_file raise ArgumentError, "Failed to find schema definition file (checked: #{schema_glob.inspect})" end schema_file_match = /( *)class ([A-Za-z:]+) < GraphQL::Schema/.match(File.read(schema_file)) schema_name = schema_file_match[2] indent = schema_file_match[1] + " " if !options.redis? migration_template 'create_graphql_detailed_traces.erb', 'db/migrate/create_graphql_detailed_traces.rb' end log :add_detailed_traces_plugin sentinel = /< GraphQL::Schema\s*\n/m code = <<-RUBY #{indent}use GraphQL::Tracing::DetailedTrace#{options.redis? ? ", redis: raise(\"TODO: pass a connection to a persistent redis database\")" : ""}, limit: 50 #{indent}# When this returns true, DetailedTrace will trace the query #{indent}# Could use `query.context`, `query.selected_operation_name`, `query.query_string` here #{indent}# Could call out to Flipper, etc #{indent}def self.detailed_trace?(query) #{indent} rand <= 0.000_1 # one in ten thousand #{indent}end RUBY in_root do inject_into_file schema_file, code, after: sentinel, force: false end routes_source = File.read(File.expand_path("config/routes.rb", destination_root)) already_has_dashboard = routes_source.include?("GraphQL::Dashboard") || routes_source.include?("Schema.dashboard") || routes_source.include?("GraphQL::Pro::Routes::Lazy") if (!already_has_dashboard || behavior == :revoke) log :route, "GraphQL::Dashboard" shell.mute do route <<~RUBY # TODO: add authorization to this route and expose it in production # See https://graphql-ruby.org/pro/dashboard.html#authorizing-the-dashboard if Rails.env.development? mount GraphQL::Dashboard, at: "/graphql/dashboard", schema: #{schema_name.inspect} end RUBY end gem("google-protobuf") end end end end end graphql-ruby-2.5.19/lib/generators/graphql/enum_generator.rb000066400000000000000000000012511514115062600241410ustar00rootroot00000000000000# frozen_string_literal: true require 'generators/graphql/type_generator' module Graphql module Generators # Generate an enum type by name, with the given values. # To add a `value:` option, add another value after a `:`. # # ``` # rails g graphql:enum ProgrammingLanguage RUBY PYTHON PERL PERL6:"PERL" # ``` class EnumGenerator < TypeGeneratorBase desc "Create a GraphQL::EnumType with the given name and values" source_root File.expand_path('../templates', __FILE__) private def graphql_type "enum" end def prepared_values custom_fields.map { |v| v.split(":", 2) } end end end end graphql-ruby-2.5.19/lib/generators/graphql/field_extractor.rb000066400000000000000000000013411514115062600243050ustar00rootroot00000000000000# frozen_string_literal: true require 'rails/generators/base' module Graphql module Generators module FieldExtractor def fields columns = [] columns += (klass&.columns&.map { |c| generate_column_string(c) } || []) columns + custom_fields end def generate_column_string(column) name = column.name required = column.null ? "" : "!" type = column_type_string(column) "#{name}:#{required}#{type}" end def column_type_string(column) column.name == "id" ? "ID" : column.type.to_s.camelize end def klass @klass ||= Module.const_get(name.camelize) rescue NameError @klass = nil end end end end graphql-ruby-2.5.19/lib/generators/graphql/input_generator.rb000066400000000000000000000023631514115062600243410ustar00rootroot00000000000000# frozen_string_literal: true require 'generators/graphql/type_generator' require 'generators/graphql/field_extractor' module Graphql module Generators # Generate an input type by name, # with the specified fields. # # ``` # rails g graphql:object PostType name:string! # ``` class InputGenerator < TypeGeneratorBase desc "Create a GraphQL::InputObjectType with the given name and fields" source_root File.expand_path('../templates', __FILE__) include FieldExtractor def self.normalize_type_expression(type_expression, mode:, null: true) case type_expression.camelize when "Text", "Citext" ["String", null] when "Decimal" ["Float", null] when "DateTime", "Datetime" ["GraphQL::Types::ISO8601DateTime", null] when "Date" ["GraphQL::Types::ISO8601Date", null] when "Json", "Jsonb", "Hstore" ["GraphQL::Types::JSON", null] else super end end private def graphql_type "input" end def type_ruby_name super.gsub(/Type\z/, "InputType") end def type_file_name super.gsub(/_type\z/, "_input_type") end end end end graphql-ruby-2.5.19/lib/generators/graphql/install/000077500000000000000000000000001514115062600222515ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/generators/graphql/install/mutation_root_generator.rb000066400000000000000000000020601514115062600275450ustar00rootroot00000000000000# frozen_string_literal: true require "rails/generators/base" require_relative "../core" module Graphql module Generators module Install class MutationRootGenerator < Rails::Generators::Base include Core desc "Create mutation base type, mutation root type, and adds the latter to the schema" source_root File.expand_path('../templates', __FILE__) class_option :schema, type: :string, default: nil, desc: "Name for the schema constant (default: {app_name}Schema)" class_option :skip_keeps, type: :boolean, default: false, desc: "Skip .keep files for source control" def generate create_dir("#{options[:directory]}/mutations") template("base_mutation.erb", "#{options[:directory]}/mutations/base_mutation.rb", { skip: true }) template("mutation_type.erb", "#{options[:directory]}/types/mutation_type.rb", { skip: true }) insert_root_type('mutation', 'MutationType') end end end end end graphql-ruby-2.5.19/lib/generators/graphql/install/templates/000077500000000000000000000000001514115062600242475ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/generators/graphql/install/templates/base_mutation.erb000066400000000000000000000005071514115062600275750ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Mutations class BaseMutation < GraphQL::Schema::RelayClassicMutation argument_class Types::BaseArgument field_class Types::BaseField input_object_class Types::BaseInputObject object_class Types::BaseObject end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/install/templates/mutation_type.erb000066400000000000000000000005041514115062600276410ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Types class MutationType < Types::BaseObject # TODO: remove me field :test_field, String, null: false, description: "An example field added by the generator" def test_field "Hello World" end end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/install_generator.rb000066400000000000000000000201001514115062600246350ustar00rootroot00000000000000# frozen_string_literal: true require 'rails/generators' require 'rails/generators/base' require_relative 'core' require_relative 'relay' module Graphql module Generators # Add GraphQL to a Rails app with `rails g graphql:install`. # # Setup a folder structure for GraphQL: # # ``` # - app/ # - graphql/ # - resolvers/ # - types/ # - base_argument.rb # - base_field.rb # - base_enum.rb # - base_input_object.rb # - base_interface.rb # - base_object.rb # - base_scalar.rb # - base_union.rb # - query_type.rb # - loaders/ # - mutations/ # - base_mutation.rb # - {app_name}_schema.rb # ``` # # (Add `.gitkeep`s by default, support `--skip-keeps`) # # Add a controller for serving GraphQL queries: # # ``` # app/controllers/graphql_controller.rb # ``` # # Add a route for that controller: # # ```ruby # # config/routes.rb # post "/graphql", to: "graphql#execute" # ``` # # Add ActiveRecord::QueryLogs metadata: # ```ruby # current_graphql_operation: -> { GraphQL::Current.operation_name }, # current_graphql_field: -> { GraphQL::Current.field&.path }, # current_dataloader_source: -> { GraphQL::Current.dataloader_source_class }, # ``` # # Accept a `--batch` option which adds `GraphQL::Batch` setup. # # Use `--skip-graphiql` to skip `graphiql-rails` installation. # # TODO: also add base classes class InstallGenerator < Rails::Generators::Base include Core include Relay desc "Install GraphQL folder structure and boilerplate code" source_root File.expand_path('../templates', __FILE__) class_option :schema, type: :string, default: nil, desc: "Name for the schema constant (default: {app_name}Schema)" class_option :skip_keeps, type: :boolean, default: false, desc: "Skip .keep files for source control" class_option :skip_graphiql, type: :boolean, default: false, desc: "Skip graphiql-rails installation" class_option :skip_mutation_root_type, type: :boolean, default: false, desc: "Skip creation of the mutation root type" class_option :relay, type: :boolean, default: true, desc: "Include installation of Relay conventions (nodes, connections, edges)" class_option :batch, type: :boolean, default: false, desc: "Include GraphQL::Batch installation" class_option :playground, type: :boolean, default: false, desc: "Use GraphQL Playground over Graphiql as IDE" class_option :skip_query_logs, type: :boolean, default: false, desc: "Skip ActiveRecord::QueryLogs hooks in config/application.rb" # These two options are taken from Rails' own generators' class_option :api, type: :boolean, desc: "Preconfigure smaller stack for API only apps" def create_folder_structure create_dir("#{options[:directory]}/types") template("schema.erb", schema_file_path) ["base_object", "base_argument", "base_field", "base_enum", "base_input_object", "base_interface", "base_scalar", "base_union"].each do |base_type| template("#{base_type}.erb", "#{options[:directory]}/types/#{base_type}.rb") end # All resolvers are defined as living in their own module, including this class. template("base_resolver.erb", "#{options[:directory]}/resolvers/base_resolver.rb") # Note: You can't have a schema without the query type, otherwise introspection breaks template("query_type.erb", "#{options[:directory]}/types/query_type.rb") insert_root_type('query', 'QueryType') invoke "graphql:install:mutation_root" unless options.skip_mutation_root_type? template("graphql_controller.erb", "app/controllers/graphql_controller.rb") route('post "/graphql", to: "graphql#execute"') if options[:batch] gem("graphql-batch") create_dir("#{options[:directory]}/loaders") end if options.api? say("Skipped graphiql, as this rails project is API only") say(" You may wish to use GraphiQL.app for development: https://github.com/skevy/graphiql-app") elsif !options[:skip_graphiql] # `gem(...)` uses `gsub_file(...)` under the hood, which is a no-op for `rails destroy...` (when `behavior == :revoke`). # So handle that case by calling `gsub_file` with `force: true`. if behavior == :invoke && !File.read(Rails.root.join("Gemfile")).include?("graphiql-rails") gem("graphiql-rails", group: :development) elsif behavior == :revoke gemfile_pattern = /\n\s*gem ('|")graphiql-rails('|"), :?group(:| =>) :development/ gsub_file Rails.root.join("Gemfile"), gemfile_pattern, "", { force: true } end # This is a little cheat just to get cleaner shell output: log :route, 'graphiql-rails' shell.mute do # Rails 5.2 has better support for `route`? if Rails::VERSION::STRING > "5.2" route <<-RUBY if Rails.env.development? mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql" end RUBY else route <<-RUBY if Rails.env.development? mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql" end RUBY end end end if options[:playground] gem("graphql_playground-rails", group: :development) log :route, 'graphql_playground-rails' shell.mute do if Rails::VERSION::STRING > "5.2" route <<-RUBY if Rails.env.development? mount GraphqlPlayground::Rails::Engine, at: "/playground", graphql_path: "/graphql" end RUBY else route <<-RUBY if Rails.env.development? mount GraphqlPlayground::Rails::Engine, at: "/playground", graphql_path: "/graphql" end RUBY end end end if options[:relay] install_relay end if !options[:skip_query_logs] config_file = "config/application.rb" current_app_rb = File.read(Rails.root.join(config_file)) existing_log_tags_pattern = /config.active_record.query_log_tags = \[\n?(\s*:[a-z_]+,?\s*\n?|\s*#[^\]]*\n)*/m existing_log_tags = existing_log_tags_pattern.match(current_app_rb) if existing_log_tags && behavior == :invoke code = <<-RUBY # GraphQL-Ruby query log tags: current_graphql_operation: -> { GraphQL::Current.operation_name }, current_graphql_field: -> { GraphQL::Current.field&.path }, current_dataloader_source: -> { GraphQL::Current.dataloader_source_class }, RUBY if !existing_log_tags.to_s.end_with?(",") code = ",\n#{code} " end # Try to insert this code _after_ any plain symbol entries in the array of query log tags: after_code = existing_log_tags_pattern else code = <<-RUBY config.active_record.query_log_tags_enabled = true config.active_record.query_log_tags = [ # Rails query log tags: :application, :controller, :action, :job, # GraphQL-Ruby query log tags: current_graphql_operation: -> { GraphQL::Current.operation_name }, current_graphql_field: -> { GraphQL::Current.field&.path }, current_dataloader_source: -> { GraphQL::Current.dataloader_source_class }, ] RUBY after_code = "class Application < Rails::Application\n" end insert_into_file(config_file, code, after: after_code) end if gemfile_modified? say "Gemfile has been modified, make sure you `bundle install`" end end private def gemfile_modified? @gemfile_modified end def gem(*args) @gemfile_modified = true super(*args) end end end end graphql-ruby-2.5.19/lib/generators/graphql/interface_generator.rb000066400000000000000000000011231514115062600251330ustar00rootroot00000000000000# frozen_string_literal: true require 'generators/graphql/type_generator' module Graphql module Generators # Generate an interface type by name, # with the specified fields. # # ``` # rails g graphql:interface NamedEntityType name:String! # ``` class InterfaceGenerator < TypeGeneratorBase desc "Create a GraphQL::InterfaceType with the given name and fields" source_root File.expand_path('../templates', __FILE__) private def graphql_type "interface" end def fields custom_fields end end end end graphql-ruby-2.5.19/lib/generators/graphql/loader_generator.rb000066400000000000000000000011151514115062600244420ustar00rootroot00000000000000# frozen_string_literal: true require 'rails/generators' require "rails/generators/named_base" require_relative "core" module Graphql module Generators # @example Generate a `GraphQL::Batch` loader by name. # rails g graphql:loader RecordLoader class LoaderGenerator < Rails::Generators::NamedBase include Core desc "Create a GraphQL::Batch::Loader by name" source_root File.expand_path('../templates', __FILE__) def create_loader_file template "loader.erb", "#{options[:directory]}/loaders/#{file_path}.rb" end end end end graphql-ruby-2.5.19/lib/generators/graphql/mutation_create_generator.rb000066400000000000000000000010771514115062600263660ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'orm_mutations_base' module Graphql module Generators # TODO: What other options should be supported? # # @example Generate a `GraphQL::Schema::RelayClassicMutation` by name # rails g graphql:mutation CreatePostMutation class MutationCreateGenerator < OrmMutationsBase desc "Scaffold a Relay Classic ORM create mutation for the given model class" source_root File.expand_path('../templates', __FILE__) private def operation_type "create" end end end end graphql-ruby-2.5.19/lib/generators/graphql/mutation_delete_generator.rb000066400000000000000000000010771514115062600263650ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'orm_mutations_base' module Graphql module Generators # TODO: What other options should be supported? # # @example Generate a `GraphQL::Schema::RelayClassicMutation` by name # rails g graphql:mutation DeletePostMutation class MutationDeleteGenerator < OrmMutationsBase desc "Scaffold a Relay Classic ORM delete mutation for the given model class" source_root File.expand_path('../templates', __FILE__) private def operation_type "delete" end end end end graphql-ruby-2.5.19/lib/generators/graphql/mutation_generator.rb000066400000000000000000000021411514115062600250340ustar00rootroot00000000000000# frozen_string_literal: true require 'rails/generators' require 'rails/generators/named_base' require_relative 'core' module Graphql module Generators # TODO: What other options should be supported? # # @example Generate a `GraphQL::Schema::RelayClassicMutation` by name # rails g graphql:mutation CreatePostMutation class MutationGenerator < Rails::Generators::NamedBase include Core desc "Create a Relay Classic mutation by name" source_root File.expand_path('../templates', __FILE__) def create_mutation_file template "mutation.erb", File.join(options[:directory], "/mutations/", class_path, "#{file_name}.rb") sentinel = /class .*MutationType\s*<\s*[^\s]+?\n/m in_root do path = "#{options[:directory]}/types/mutation_type.rb" invoke "graphql:install:mutation_root" unless File.exist?(path) inject_into_file "#{options[:directory]}/types/mutation_type.rb", " field :#{file_name}, mutation: Mutations::#{class_name}\n", after: sentinel, verbose: false, force: false end end end end end graphql-ruby-2.5.19/lib/generators/graphql/mutation_update_generator.rb000066400000000000000000000010771514115062600264050ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'orm_mutations_base' module Graphql module Generators # TODO: What other options should be supported? # # @example Generate a `GraphQL::Schema::RelayClassicMutation` by name # rails g graphql:mutation UpdatePostMutation class MutationUpdateGenerator < OrmMutationsBase desc "Scaffold a Relay Classic ORM update mutation for the given model class" source_root File.expand_path('../templates', __FILE__) private def operation_type "update" end end end end graphql-ruby-2.5.19/lib/generators/graphql/object_generator.rb000066400000000000000000000027011514115062600244440ustar00rootroot00000000000000# frozen_string_literal: true require 'generators/graphql/type_generator' require 'generators/graphql/field_extractor' module Graphql module Generators # Generate an object type by name, # with the specified fields. # # ``` # rails g graphql:object PostType name:String! # ``` # # Add the Node interface with `--node`. class ObjectGenerator < TypeGeneratorBase desc "Create a GraphQL::ObjectType with the given name and fields." \ "If the given type name matches an existing ActiveRecord model, the generated type will automatically include fields for the models database columns." source_root File.expand_path('../templates', __FILE__) include FieldExtractor class_option :node, type: :boolean, default: false, desc: "Include the Relay Node interface" def self.normalize_type_expression(type_expression, mode:, null: true) case type_expression.camelize when "Text", "Citext" ["String", null] when "Decimal" ["Float", null] when "DateTime", "Datetime" ["GraphQL::Types::ISO8601DateTime", null] when "Date" ["GraphQL::Types::ISO8601Date", null] when "Json", "Jsonb", "Hstore" ["GraphQL::Types::JSON", null] else super end end private def graphql_type "object" end end end end graphql-ruby-2.5.19/lib/generators/graphql/orm_mutations_base.rb000066400000000000000000000027711514115062600250310ustar00rootroot00000000000000# frozen_string_literal: true require 'rails/generators' require 'rails/generators/named_base' require_relative 'core' module Graphql module Generators # TODO: What other options should be supported? # # @example Generate a `GraphQL::Schema::RelayClassicMutation` by name # rails g graphql:mutation CreatePostMutation class OrmMutationsBase < Rails::Generators::NamedBase include Core include Rails::Generators::ResourceHelpers desc "Create a Relay Classic mutation by name" class_option :orm, banner: "NAME", type: :string, required: true, desc: "ORM to generate the controller for" class_option :namespaced_types, type: :boolean, required: false, default: false, banner: "Namespaced", desc: "If the generated types will be namespaced" def create_mutation_file template "mutation_#{operation_type}.erb", File.join(options[:directory], "/mutations/", class_path, "#{file_name}_#{operation_type}.rb") sentinel = /class .*MutationType\s*<\s*[^\s]+?\n/m in_root do path = "#{options[:directory]}/types/mutation_type.rb" invoke "graphql:install:mutation_root" unless File.exist?(path) inject_into_file "#{options[:directory]}/types/mutation_type.rb", " field :#{file_name}_#{operation_type}, mutation: Mutations::#{class_name}#{operation_type.classify}\n", after: sentinel, verbose: false, force: false end end end end end graphql-ruby-2.5.19/lib/generators/graphql/relay.rb000066400000000000000000000050341514115062600222460ustar00rootroot00000000000000# frozen_string_literal: true module Graphql module Generators module Relay def install_relay # Add Node, `node(id:)`, and `nodes(ids:)` template("node_type.erb", "#{options[:directory]}/types/node_type.rb") in_root do fields = <<-RUBY field :node, Types::NodeType, null: true, description: "Fetches an object given its ID." do argument :id, ID, required: true, description: "ID of the object." end def node(id:) context.schema.object_from_id(id, context) end field :nodes, [Types::NodeType, null: true], null: true, description: "Fetches a list of objects given a list of IDs." do argument :ids, [ID], required: true, description: "IDs of the objects." end def nodes(ids:) ids.map { |id| context.schema.object_from_id(id, context) } end RUBY inject_into_file "#{options[:directory]}/types/query_type.rb", fields, after: /class .*QueryType\s*<\s*[^\s]+?\n/m, force: false end # Add connections and edges template("base_connection.erb", "#{options[:directory]}/types/base_connection.rb") template("base_edge.erb", "#{options[:directory]}/types/base_edge.rb") connectionable_type_files = { "#{options[:directory]}/types/base_object.rb" => /class .*BaseObject\s*<\s*[^\s]+?\n/m, "#{options[:directory]}/types/base_union.rb" => /class .*BaseUnion\s*<\s*[^\s]+?\n/m, "#{options[:directory]}/types/base_interface.rb" => /include GraphQL::Schema::Interface\n/m, } in_root do connectionable_type_files.each do |type_class_file, sentinel| inject_into_file type_class_file, " connection_type_class(Types::BaseConnection)\n", after: sentinel, force: false inject_into_file type_class_file, " edge_type_class(Types::BaseEdge)\n", after: sentinel, force: false end end # Add object ID hooks & connection plugin schema_code = <<-RUBY # Relay-style Object Identification: # Return a string UUID for `object` def self.id_from_object(object, type_definition, query_ctx) # For example, use Rails' GlobalID library (https://github.com/rails/globalid): object.to_gid_param end # Given a string UUID, find the object def self.object_from_id(global_id, query_ctx) # For example, use Rails' GlobalID library (https://github.com/rails/globalid): GlobalID.find(global_id) end RUBY inject_into_file schema_file_path, schema_code, before: /^end\n/m, force: false end end end end graphql-ruby-2.5.19/lib/generators/graphql/relay_generator.rb000066400000000000000000000007261514115062600243170ustar00rootroot00000000000000# frozen_string_literal: true require 'rails/generators' require 'rails/generators/base' require_relative 'core' require_relative 'relay' module Graphql module Generators class RelayGenerator < Rails::Generators::Base include Core include Relay desc "Add base types and fields for Relay-style nodes and connections" source_root File.expand_path('../templates', __FILE__) def install_relay super end end end end graphql-ruby-2.5.19/lib/generators/graphql/scalar_generator.rb000066400000000000000000000007231514115062600244450ustar00rootroot00000000000000# frozen_string_literal: true require 'generators/graphql/type_generator' module Graphql module Generators # Generate a scalar type by given name. # # ``` # rails g graphql:scalar Date # ``` class ScalarGenerator < TypeGeneratorBase desc "Create a GraphQL::ScalarType with the given name" source_root File.expand_path('../templates', __FILE__) private def graphql_type "scalar" end end end end graphql-ruby-2.5.19/lib/generators/graphql/templates/000077500000000000000000000000001514115062600226015ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/generators/graphql/templates/base_argument.erb000066400000000000000000000002361514115062600261100ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Types class BaseArgument < GraphQL::Schema::Argument end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/base_connection.erb000066400000000000000000000004671514115062600264330ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Types class BaseConnection < Types::BaseObject # add `nodes` and `pageInfo` fields, as well as `edge_type(...)` and `node_nullable(...)` overrides include GraphQL::Types::Relay::ConnectionBehaviors end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/base_edge.erb000066400000000000000000000004161514115062600251720ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Types class BaseEdge < Types::BaseObject # add `node` and `cursor` fields, as well as `node_type(...)` override include GraphQL::Types::Relay::EdgeBehaviors end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/base_enum.erb000066400000000000000000000002261514115062600252310ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Types class BaseEnum < GraphQL::Schema::Enum end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/base_field.erb000066400000000000000000000002771514115062600253560ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Types class BaseField < GraphQL::Schema::Field argument_class Types::BaseArgument end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/base_input_object.erb000066400000000000000000000003131514115062600267470ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Types class BaseInputObject < GraphQL::Schema::InputObject argument_class Types::BaseArgument end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/base_interface.erb000066400000000000000000000003151514115062600262240ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Types module BaseInterface include GraphQL::Schema::Interface field_class Types::BaseField end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/base_object.erb000066400000000000000000000002731514115062600255350ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Types class BaseObject < GraphQL::Schema::Object field_class Types::BaseField end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/base_resolver.erb000066400000000000000000000002421514115062600261240ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Resolvers class BaseResolver < GraphQL::Schema::Resolver end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/base_scalar.erb000066400000000000000000000002321514115062600255270ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Types class BaseScalar < GraphQL::Schema::Scalar end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/base_union.erb000066400000000000000000000002301514115062600254100ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Types class BaseUnion < GraphQL::Schema::Union end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/create_graphql_detailed_traces.erb000066400000000000000000000005531514115062600314530ustar00rootroot00000000000000class CreateGraphqlDetailedTraces < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>] def change create_table :graphql_detailed_traces, force: true do |t| t.bigint :begin_ms, null: false t.float :duration_ms, null: false t.binary :trace_data, null: false t.string :operation_name, null: false end end end graphql-ruby-2.5.19/lib/generators/graphql/templates/enum.erb000066400000000000000000000004671514115062600242460ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Types class <%= ruby_class_name %> < Types::BaseEnum description "<%= human_name %> enum" <% prepared_values.each do |v| %> value "<%= v[0] %>"<%= v.length > 1 ? ", value: #{v[1]}" : "" %> <% end %> end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/graphql_controller.erb000066400000000000000000000031511514115062600271740ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> class GraphqlController < ApplicationController # If accessing from outside this domain, nullify the session # This allows for outside API access while preventing CSRF attacks, # but you'll have to authenticate your user separately # protect_from_forgery with: :null_session def execute variables = prepare_variables(params[:variables]) query = params[:query] operation_name = params[:operationName] context = { # Query context goes here, for example: # current_user: current_user, } result = <%= schema_name %>.execute(query, variables: variables, context: context, operation_name: operation_name) render json: result rescue StandardError => e raise e unless Rails.env.development? handle_error_in_development(e) end private # Handle variables in form data, JSON body, or a blank value def prepare_variables(variables_param) case variables_param when String if variables_param.present? JSON.parse(variables_param) || {} else {} end when Hash variables_param when ActionController::Parameters variables_param.to_unsafe_hash # GraphQL-Ruby will validate name and type of incoming variables. when nil {} else raise ArgumentError, "Unexpected parameter: #{variables_param}" end end def handle_error_in_development(e) logger.error e.message logger.error e.backtrace.join("\n") render json: { errors: [{ message: e.message, backtrace: e.backtrace }], data: {} }, status: 500 end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/input.erb000066400000000000000000000003601514115062600244310ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Types class <%= ruby_class_name %> < Types::BaseInputObject <% normalized_fields.each do |f| %> <%= f.to_input_argument %> <% end %> end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/interface.erb000066400000000000000000000003671514115062600252410ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Types module <%= ruby_class_name %> include Types::BaseInterface <% normalized_fields.each do |f| %> <%= f.to_object_field %> <% end %> end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/loader.erb000066400000000000000000000010141514115062600245350ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Loaders class <%= class_name %> < GraphQL::Batch::Loader # Define `initialize` to store grouping arguments, eg # # Loaders::<%= class_name %>.for(group).load(value) # # def initialize() # end # `keys` contains each key from `.load(key)`. # Find the corresponding values, then # call `fulfill(key, value)` or `fulfill(key, nil)` # for each key. def perform(keys) end end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/mutation.erb000066400000000000000000000006221514115062600251330ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Mutations class <%= class_name %> < BaseMutation # TODO: define return fields # field :post, Types::PostType, null: false # TODO: define arguments # argument :name, String, required: true # TODO: define resolve method # def resolve(name:) # { post: ... } # end end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/mutation_create.erb000066400000000000000000000015041514115062600264560ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Mutations class <%= class_name %>Create < BaseMutation description "Creates a new <%= file_name %>" field :<%= file_name %>, Types::<%= options[:namespaced_types] ? 'Objects::' : '' %><%= class_name %>Type, null: false argument :<%= file_name %>_input, Types::<%= options[:namespaced_types] ? 'Inputs::' : '' %><%= class_name %>InputType, required: true def resolve(<%= file_name %>_input:) <%= singular_table_name %> = ::<%= orm_class.build(class_name, "**#{file_name}_input") %> raise GraphQL::ExecutionError.new "Error creating <%= file_name %>", extensions: <%= singular_table_name %>.errors.to_hash unless <%= orm_instance.save %> { <%= file_name %>: <%= singular_table_name %> } end end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/mutation_delete.erb000066400000000000000000000012741514115062600264610ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Mutations class <%= class_name %>Delete < BaseMutation description "Deletes a <%= file_name %> by ID" field :<%= file_name %>, Types::<%= options[:namespaced_types] ? 'Objects::' : '' %><%= class_name %>Type, null: false argument :id, ID, required: true def resolve(id:) <%= singular_table_name %> = ::<%= orm_class.find(class_name, "id") %> raise GraphQL::ExecutionError.new "Error deleting <%= file_name %>", extensions: <%= singular_table_name %>.errors.to_hash unless <%= orm_instance.destroy %> { <%= file_name %>: <%= singular_table_name %> } end end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/mutation_update.erb000066400000000000000000000015671514115062600265060ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Mutations class <%= class_name %>Update < BaseMutation description "Updates a <%= file_name %> by id" field :<%= file_name %>, Types::<%= options[:namespaced_types] ? 'Objects::' : '' %><%= class_name %>Type, null: false argument :id, ID, required: true argument :<%= file_name %>_input, Types::<%= options[:namespaced_types] ? 'Inputs::' : '' %><%= class_name %>InputType, required: true def resolve(id:, <%= file_name %>_input:) <%= singular_table_name %> = ::<%= orm_class.find(class_name, "id") %> raise GraphQL::ExecutionError.new "Error updating <%= file_name %>", extensions: <%= singular_table_name %>.errors.to_hash unless <%= orm_instance.update("**#{file_name}_input") %> { <%= file_name %>: <%= singular_table_name %> } end end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/node_type.erb000066400000000000000000000003521514115062600252610ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Types module NodeType include Types::BaseInterface # Add the `id` field include GraphQL::Types::Relay::NodeBehaviors end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/object.erb000066400000000000000000000004621514115062600245430ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Types class <%= ruby_class_name %> < Types::BaseObject <% if options.node %> implements GraphQL::Types::Relay::Node <% end %><% normalized_fields.each do |f| %> <%= f.to_object_field %> <% end %> end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/query_type.erb000066400000000000000000000006411514115062600255020ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Types class QueryType < Types::BaseObject # Add root-level fields here. # They will be entry points for queries on your schema. # TODO: remove me field :test_field, String, null: false, description: "An example field added by the generator" def test_field "Hello World!" end end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/scalar.erb000066400000000000000000000007261514115062600245450ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Types class <%= ruby_class_name %> < Types::BaseScalar def self.coerce_input(input_value, context) # Override this to prepare a client-provided GraphQL value for your Ruby code input_value end def self.coerce_result(ruby_value, context) # Override this to serialize a Ruby value for the GraphQL response ruby_value.to_s end end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/schema.erb000066400000000000000000000017461514115062600245430ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> class <%= schema_name %> < GraphQL::Schema query(Types::QueryType) <% if options[:batch] %> # GraphQL::Batch setup: use GraphQL::Batch <% else %> # For batch-loading (see https://graphql-ruby.org/dataloader/overview.html) use GraphQL::Dataloader <% end %> # GraphQL-Ruby calls this when something goes wrong while running a query: def self.type_error(err, context) # if err.is_a?(GraphQL::InvalidNullError) # # report to your bug tracker here # return nil # end super end # Union and Interface Resolution def self.resolve_type(abstract_type, obj, ctx) # TODO: Implement this method # to return the correct GraphQL object type for `obj` raise(GraphQL::RequiredImplementationMissingError) end # Limit the size of incoming queries: max_query_string_tokens(5000) # Stop validating when it encounters this many errors: validate_max_errors(100) end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/templates/union.erb000066400000000000000000000004021514115062600244170ustar00rootroot00000000000000# frozen_string_literal: true <% module_namespacing_when_supported do -%> module Types class <%= ruby_class_name %> < Types::BaseUnion <% if custom_fields.any? %> possible_types <%= normalized_possible_types.join(", ") %> <% end %> end end <% end -%> graphql-ruby-2.5.19/lib/generators/graphql/type_generator.rb000066400000000000000000000110061514115062600241550ustar00rootroot00000000000000# frozen_string_literal: true require 'rails/generators' require 'rails/generators/base' require 'graphql' require 'active_support' require 'active_support/core_ext/string/inflections' require_relative 'core' module Graphql module Generators class TypeGeneratorBase < Rails::Generators::NamedBase include Core class_option :namespaced_types, type: :boolean, required: false, default: false, banner: "Namespaced", desc: "If the generated types will be namespaced" argument :custom_fields, type: :array, default: [], banner: "name:type name:type ...", desc: "Fields for this object (type may be expressed as Ruby or GraphQL)" attr_accessor :graphql_type def create_type_file template "#{graphql_type}.erb", "#{options[:directory]}/types#{subdirectory}/#{type_file_name}.rb" end # Take a type expression in any combination of GraphQL or Ruby styles # and return it in a specified output style # TODO: nullability / list with `mode: :graphql` doesn't work # @param type_expresson [String] # @param mode [Symbol] # @param null [Boolean] # @return [(String, Boolean)] The type expression, followed by `null:` value def self.normalize_type_expression(type_expression, mode:, null: true) if type_expression.start_with?("!") normalize_type_expression(type_expression[1..-1], mode: mode, null: false) elsif type_expression.end_with?("!") normalize_type_expression(type_expression[0..-2], mode: mode, null: false) elsif type_expression.start_with?("[") && type_expression.end_with?("]") name, is_null = normalize_type_expression(type_expression[1..-2], mode: mode, null: null) ["[#{name}]", is_null] elsif type_expression.end_with?("Type") normalize_type_expression(type_expression[0..-5], mode: mode, null: null) elsif type_expression.start_with?("Types::") normalize_type_expression(type_expression[7..-1], mode: mode, null: null) elsif type_expression.start_with?("types.") normalize_type_expression(type_expression[6..-1], mode: mode, null: null) else case mode when :ruby case type_expression when "Int" ["Integer", null] when "Integer", "Float", "Boolean", "String", "ID" [type_expression, null] else ["Types::#{type_expression.camelize}Type", null] end when :graphql [type_expression.camelize, null] else raise "Unexpected normalize mode: #{mode}" end end end private # @return [String] The user-provided type name, normalized to Ruby code def type_ruby_name @type_ruby_name ||= self.class.normalize_type_expression(name, mode: :ruby)[0] end # @return [String] The user-provided type name, as a GraphQL name def type_graphql_name @type_graphql_name ||= self.class.normalize_type_expression(name, mode: :graphql)[0] end # @return [String] The user-provided type name, as a file name (without extension) def type_file_name @type_file_name ||= "#{type_graphql_name}Type".underscore end # @return [Array] User-provided fields, in `(name, Ruby type name)` pairs def normalized_fields @normalized_fields ||= fields.map { |f| name, raw_type = f.split(":", 2) type_expr, null = self.class.normalize_type_expression(raw_type, mode: :ruby) NormalizedField.new(name, type_expr, null) } end def ruby_class_name class_prefix = if options[:namespaced_types] "#{graphql_type.pluralize.camelize}::" else "" end @ruby_class_name || class_prefix + type_ruby_name.sub(/^Types::/, "") end def subdirectory if options[:namespaced_types] "/#{graphql_type.pluralize}" else "" end end class NormalizedField def initialize(name, type_expr, null) @name = name @type_expr = type_expr @null = null end def to_object_field "field :#{@name}, #{@type_expr}#{@null ? '' : ', null: false'}" end def to_input_argument "argument :#{@name}, #{@type_expr}, required: false" end end end end end graphql-ruby-2.5.19/lib/generators/graphql/union_generator.rb000066400000000000000000000015441514115062600243320ustar00rootroot00000000000000# frozen_string_literal: true require 'generators/graphql/type_generator' module Graphql module Generators # Generate a union type by name # with the specified member types. # # ``` # rails g graphql:union SearchResultType ImageType AudioType # ``` class UnionGenerator < TypeGeneratorBase desc "Create a GraphQL::UnionType with the given name and possible types" source_root File.expand_path('../templates', __FILE__) argument :possible_types, type: :array, default: [], banner: "type type ...", desc: "Possible types for this union (expressed as Ruby or GraphQL)" private def graphql_type "union" end def normalized_possible_types custom_fields.map { |t| self.class.normalize_type_expression(t, mode: :ruby)[0] } end end end end graphql-ruby-2.5.19/lib/graphql.rb000066400000000000000000000116351514115062600167650ustar00rootroot00000000000000# frozen_string_literal: true require "delegate" require "json" require "set" require "singleton" require "forwardable" require "fiber/storage" if RUBY_VERSION < "3.2.0" require "graphql/autoload" module GraphQL extend Autoload # Load all `autoload`-configured classes, and also eager-load dependents who have autoloads of their own. def self.eager_load! super Query.eager_load! Types.eager_load! Schema.eager_load! end class Error < StandardError end # This error is raised when GraphQL-Ruby encounters a situation # that it *thought* would never happen. Please report this bug! class InvariantError < Error def initialize(message) message += " This is probably a bug in GraphQL-Ruby, please report this error on GitHub: https://github.com/rmosolgo/graphql-ruby/issues/new?template=bug_report.md" super(message) end end class RequiredImplementationMissingError < Error end class << self def default_parser @default_parser ||= GraphQL::Language::Parser end attr_writer :default_parser end # Turn a query string or schema definition into an AST # @param graphql_string [String] a GraphQL query string or schema definition # @return [GraphQL::Language::Nodes::Document] def self.parse(graphql_string, trace: GraphQL::Tracing::NullTrace, filename: nil, max_tokens: nil) default_parser.parse(graphql_string, trace: trace, filename: filename, max_tokens: max_tokens) end # Read the contents of `filename` and parse them as GraphQL # @param filename [String] Path to a `.graphql` file containing IDL or query # @return [GraphQL::Language::Nodes::Document] def self.parse_file(filename) content = File.read(filename) default_parser.parse(content, filename: filename) end # @return [Array] def self.scan(graphql_string) default_parser.scan(graphql_string) end def self.parse_with_racc(string, filename: nil, trace: GraphQL::Tracing::NullTrace) warn "`GraphQL.parse_with_racc` is deprecated; GraphQL-Ruby no longer uses racc for parsing. Call `GraphQL.parse` or `GraphQL::Language::Parser.parse` instead." GraphQL::Language::Parser.parse(string, filename: filename, trace: trace) end def self.scan_with_ruby(graphql_string) GraphQL::Language::Lexer.tokenize(graphql_string) end NOT_CONFIGURED = Object.new.freeze private_constant :NOT_CONFIGURED module EmptyObjects EMPTY_HASH = {}.freeze EMPTY_ARRAY = [].freeze end class << self # If true, the parser should raise when an integer or float is followed immediately by an identifier (instead of a space or punctuation) attr_accessor :reject_numbers_followed_by_names end self.reject_numbers_followed_by_names = false autoload :ExecutionError, "graphql/execution_error" autoload :RuntimeTypeError, "graphql/runtime_type_error" autoload :UnresolvedTypeError, "graphql/unresolved_type_error" autoload :InvalidNullError, "graphql/invalid_null_error" autoload :AnalysisError, "graphql/analysis_error" autoload :CoercionError, "graphql/coercion_error" autoload :InvalidNameError, "graphql/invalid_name_error" autoload :IntegerDecodingError, "graphql/integer_decoding_error" autoload :IntegerEncodingError, "graphql/integer_encoding_error" autoload :StringEncodingError, "graphql/string_encoding_error" autoload :DateEncodingError, "graphql/date_encoding_error" autoload :DurationEncodingError, "graphql/duration_encoding_error" autoload :TypeKinds, "graphql/type_kinds" autoload :NameValidator, "graphql/name_validator" autoload :Language, "graphql/language" autoload :Analysis, "graphql/analysis" autoload :Tracing, "graphql/tracing" autoload :Dig, "graphql/dig" autoload :Execution, "graphql/execution" autoload :Pagination, "graphql/pagination" autoload :Schema, "graphql/schema" autoload :Query, "graphql/query" autoload :Dataloader, "graphql/dataloader" autoload :Types, "graphql/types" autoload :StaticValidation, "graphql/static_validation" autoload :Execution, "graphql/execution" autoload :Introspection, "graphql/introspection" autoload :Relay, "graphql/relay" autoload :Subscriptions, "graphql/subscriptions" autoload :ParseError, "graphql/parse_error" autoload :Backtrace, "graphql/backtrace" autoload :UnauthorizedError, "graphql/unauthorized_error" autoload :UnauthorizedEnumValueError, "graphql/unauthorized_enum_value_error" autoload :UnauthorizedFieldError, "graphql/unauthorized_field_error" autoload :LoadApplicationObjectFailedError, "graphql/load_application_object_failed_error" autoload :Testing, "graphql/testing" autoload :Current, "graphql/current" if defined?(::Rails::Engine) # This needs to be defined before Rails runs `add_routing_paths`, # otherwise GraphQL::Dashboard's routes won't have been gathered for loading # when that initializer runs. require 'graphql/dashboard' end end require "graphql/version" require "graphql/railtie" if defined? Rails::Railtie graphql-ruby-2.5.19/lib/graphql/000077500000000000000000000000001514115062600164325ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/analysis.rb000066400000000000000000000060161514115062600206050ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/analysis/visitor" require "graphql/analysis/analyzer" require "graphql/analysis/field_usage" require "graphql/analysis/query_complexity" require "graphql/analysis/max_query_complexity" require "graphql/analysis/query_depth" require "graphql/analysis/max_query_depth" module GraphQL module Analysis AST = self class TimeoutError < AnalysisError def initialize(...) super("Timeout on validation of query") end end module_function # Analyze a multiplex, and all queries within. # Multiplex analyzers are ran for all queries, keeping state. # Query analyzers are ran per query, without carrying state between queries. # # @param multiplex [GraphQL::Execution::Multiplex] # @param analyzers [Array] # @return [Array] Results from multiplex analyzers def analyze_multiplex(multiplex, analyzers) multiplex_analyzers = analyzers.map { |analyzer| analyzer.new(multiplex) } multiplex.current_trace.analyze_multiplex(multiplex: multiplex) do query_results = multiplex.queries.map do |query| if query.valid? analyze_query( query, query.analyzers, multiplex_analyzers: multiplex_analyzers ) else [] end end multiplex_results = multiplex_analyzers.map(&:result) multiplex_errors = analysis_errors(multiplex_results) multiplex.queries.each_with_index do |query, idx| query.analysis_errors = multiplex_errors + analysis_errors(query_results[idx]) end multiplex_results end end # @param query [GraphQL::Query] # @param analyzers [Array] # @return [Array] Results from those analyzers def analyze_query(query, analyzers, multiplex_analyzers: []) query.current_trace.analyze_query(query: query) do query_analyzers = analyzers .map { |analyzer| analyzer.new(query) } .tap { _1.select!(&:analyze?) } analyzers_to_run = query_analyzers + multiplex_analyzers if !analyzers_to_run.empty? analyzers_to_run.select!(&:visit?) if !analyzers_to_run.empty? visitor = GraphQL::Analysis::Visitor.new( query: query, analyzers: analyzers_to_run, timeout: query.validate_timeout_remaining, ) visitor.visit if !visitor.rescued_errors.empty? return visitor.rescued_errors end end query_analyzers.map(&:result) else [] end end rescue TimeoutError => err [err] rescue GraphQL::UnauthorizedError, GraphQL::ExecutionError # This error was raised during analysis and will be returned the client before execution [] end def analysis_errors(results) results.flatten.tap { _1.select! { |r| r.is_a?(GraphQL::AnalysisError) } } end end end graphql-ruby-2.5.19/lib/graphql/analysis/000077500000000000000000000000001514115062600202555ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/analysis/analyzer.rb000066400000000000000000000056611514115062600224370ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Analysis # Query analyzer for query ASTs. Query analyzers respond to visitor style methods # but are prefixed by `enter` and `leave`. # # When an analyzer is initialized with a Multiplex, you can always get the current query from # `visitor.query` in the visit methods. # # @param [GraphQL::Query, GraphQL::Execution::Multiplex] The query or multiplex to analyze class Analyzer def initialize(subject) @subject = subject if subject.is_a?(GraphQL::Query) @query = subject @multiplex = nil else @multiplex = subject @query = nil end end # Analyzer hook to decide at analysis time whether a query should # be analyzed or not. # @return [Boolean] If the query should be analyzed or not def analyze? true end # Analyzer hook to decide at analysis time whether analysis # requires a visitor pass; can be disabled for precomputed results. # @return [Boolean] If analysis requires visitation or not def visit? true end # The result for this analyzer. Returning {GraphQL::AnalysisError} results # in a query error. # @return [Any] The analyzer result def result raise GraphQL::RequiredImplementationMissingError end # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time class << self private def build_visitor_hooks(member_name) class_eval(<<-EOS, __FILE__, __LINE__ + 1) def on_enter_#{member_name}(node, parent, visitor) end def on_leave_#{member_name}(node, parent, visitor) end EOS end end build_visitor_hooks :argument build_visitor_hooks :directive build_visitor_hooks :document build_visitor_hooks :enum build_visitor_hooks :field build_visitor_hooks :fragment_spread build_visitor_hooks :inline_fragment build_visitor_hooks :input_object build_visitor_hooks :list_type build_visitor_hooks :non_null_type build_visitor_hooks :null_value build_visitor_hooks :operation_definition build_visitor_hooks :type_name build_visitor_hooks :variable_definition build_visitor_hooks :variable_identifier build_visitor_hooks :abstract_node # rubocop:enable Development/NoEvalCop protected # @return [GraphQL::Query, GraphQL::Execution::Multiplex] Whatever this analyzer is analyzing attr_reader :subject # @return [GraphQL::Query, nil] `nil` if this analyzer is visiting a multiplex # (When this is `nil`, use `visitor.query` inside visit methods to get the current query) attr_reader :query # @return [GraphQL::Execution::Multiplex, nil] `nil` if this analyzer is visiting a query attr_reader :multiplex end end end graphql-ruby-2.5.19/lib/graphql/analysis/field_usage.rb000066400000000000000000000056421514115062600230600ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Analysis class FieldUsage < Analyzer def initialize(query) super @used_fields = Set.new @used_deprecated_fields = Set.new @used_deprecated_arguments = Set.new @used_deprecated_enum_values = Set.new end def on_leave_field(node, parent, visitor) field_defn = visitor.field_definition field = "#{visitor.parent_type_definition.graphql_name}.#{field_defn.graphql_name}" @used_fields << field @used_deprecated_fields << field if field_defn.deprecation_reason arguments = visitor.query.arguments_for(node, field_defn) # If there was an error when preparing this argument object, # then this might be an error or something: if arguments.respond_to?(:argument_values) extract_deprecated_arguments(arguments.argument_values) end end def result { used_fields: @used_fields.to_a, used_deprecated_fields: @used_deprecated_fields.to_a, used_deprecated_arguments: @used_deprecated_arguments.to_a, used_deprecated_enum_values: @used_deprecated_enum_values.to_a, } end private def extract_deprecated_arguments(argument_values) argument_values.each_pair do |_argument_name, argument| if argument.definition.deprecation_reason @used_deprecated_arguments << argument.definition.path end arg_val = argument.value next if arg_val.nil? argument_type = argument.definition.type if argument_type.non_null? argument_type = argument_type.of_type end if argument_type.kind.input_object? extract_deprecated_arguments(argument.original_value.arguments.argument_values) # rubocop:disable Development/ContextIsPassedCop -- runtime args instance elsif argument_type.kind.enum? extract_deprecated_enum_value(argument_type, arg_val) elsif argument_type.list? inner_type = argument_type.unwrap case inner_type.kind when TypeKinds::INPUT_OBJECT argument.original_value.each do |value| extract_deprecated_arguments(value.arguments.argument_values) # rubocop:disable Development/ContextIsPassedCop -- runtime args instance end when TypeKinds::ENUM arg_val.each do |value| extract_deprecated_enum_value(inner_type, value) end else # Not a kind of input that we track end end end end def extract_deprecated_enum_value(enum_type, value) enum_value = @query.types.enum_values(enum_type).find { |ev| ev.value == value } if enum_value&.deprecation_reason @used_deprecated_enum_values << enum_value.path end end end end end graphql-ruby-2.5.19/lib/graphql/analysis/max_query_complexity.rb000066400000000000000000000011441514115062600250710ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Analysis # Used under the hood to implement complexity validation, # see {Schema#max_complexity} and {Query#max_complexity} class MaxQueryComplexity < QueryComplexity def result return if subject.max_complexity.nil? total_complexity = max_possible_complexity if total_complexity > subject.max_complexity GraphQL::AnalysisError.new("Query has complexity of #{total_complexity}, which exceeds max complexity of #{subject.max_complexity}") else nil end end end end end graphql-ruby-2.5.19/lib/graphql/analysis/max_query_depth.rb000066400000000000000000000007731514115062600240070ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Analysis class MaxQueryDepth < QueryDepth def result configured_max_depth = if query query.max_depth else multiplex.schema.max_depth end if configured_max_depth && @max_depth > configured_max_depth GraphQL::AnalysisError.new("Query has depth of #{@max_depth}, which exceeds max depth of #{configured_max_depth}") else nil end end end end end graphql-ruby-2.5.19/lib/graphql/analysis/query_complexity.rb000066400000000000000000000270721514115062600242340ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Analysis # Calculate the complexity of a query, using {Field#complexity} values. class QueryComplexity < Analyzer # State for the query complexity calculation: # - `complexities_on_type` holds complexity scores for each type def initialize(query) super @skip_introspection_fields = !query.schema.max_complexity_count_introspection_fields @complexities_on_type_by_query = {} end # Override this method to use the complexity result def result case subject.schema.complexity_cost_calculation_mode_for(subject.context) when :future max_possible_complexity when :legacy max_possible_complexity(mode: :legacy) when :compare future_complexity = max_possible_complexity legacy_complexity = max_possible_complexity(mode: :legacy) if future_complexity != legacy_complexity subject.schema.legacy_complexity_cost_calculation_mismatch(subject, future_complexity, legacy_complexity) else future_complexity end when nil subject.logger.warn <<~GRAPHQL GraphQL-Ruby's complexity cost system is getting some "breaking fixes" in a future version. See the migration notes at https://graphql-ruby.org/api-doc/#{GraphQL::VERSION}/GraphQL/Schema.html#complexity_cost_calculation_mode_for-class_method To opt into the future behavior, configure your schema (#{subject.schema.name ? subject.schema.name : subject.schema.ancestors}) with: complexity_cost_calculation_mode(:future) # or `:legacy`, `:compare` GRAPHQL max_possible_complexity(mode: :legacy) else raise ArgumentError, "Expected `:future`, `:legacy`, `:compare`, or `nil` from `#{query.schema}.complexity_cost_calculation_mode_for` but got: #{query.schema.complexity_cost_calculation_mode.inspect}" end end # ScopedTypeComplexity models a tree of GraphQL types mapped to inner selections, ie: # Hash> class ScopedTypeComplexity < Hash # A proc for defaulting empty namespace requests as a new scope hash. DEFAULT_PROC = ->(h, k) { h[k] = {} } attr_reader :field_definition, :response_path, :query # @param parent_type [Class] The owner of `field_definition` # @param field_definition [GraphQL::Field, GraphQL::Schema::Field] Used for getting the `.complexity` configuration # @param query [GraphQL::Query] Used for `query.possible_types` # @param response_path [Array] The path to the response key for the field # @return [Hash>] def initialize(parent_type, field_definition, query, response_path) super(&DEFAULT_PROC) @parent_type = parent_type @field_definition = field_definition @query = query @response_path = response_path @nodes = [] end # @return [Array] attr_reader :nodes def own_complexity(child_complexity) @field_definition.calculate_complexity(query: @query, nodes: @nodes, child_complexity: child_complexity) end def composite? !empty? end end def on_enter_field(node, parent, visitor) # We don't want to visit fragment definitions, # we'll visit them when we hit the spreads instead return if visitor.visiting_fragment_definition? return if visitor.skipping? return if @skip_introspection_fields && visitor.field_definition.introspection? parent_type = visitor.parent_type_definition field_key = node.alias || node.name # Find or create a complexity scope stack for this query. scopes_stack = @complexities_on_type_by_query[visitor.query] ||= [ScopedTypeComplexity.new(nil, nil, query, visitor.response_path)] # Find or create the complexity costing node for this field. scope = scopes_stack.last[parent_type][field_key] ||= ScopedTypeComplexity.new(parent_type, visitor.field_definition, visitor.query, visitor.response_path) scope.nodes.push(node) scopes_stack.push(scope) end def on_leave_field(node, parent, visitor) # We don't want to visit fragment definitions, # we'll visit them when we hit the spreads instead return if visitor.visiting_fragment_definition? return if visitor.skipping? return if @skip_introspection_fields && visitor.field_definition.introspection? scopes_stack = @complexities_on_type_by_query[visitor.query] scopes_stack.pop end private # @return [Integer] def max_possible_complexity(mode: :future) @complexities_on_type_by_query.reduce(0) do |total, (query, scopes_stack)| total + merged_max_complexity_for_scopes(query, [scopes_stack.first], mode) end end # @param query [GraphQL::Query] Used for `query.possible_types` # @param scopes [Array] Array of scoped type complexities # @param mode [:future, :legacy] # @return [Integer] def merged_max_complexity_for_scopes(query, scopes, mode) # Aggregate a set of all possible scope types encountered (scope keys). # Use a hash, but ignore the values; it's just a fast way to work with the keys. possible_scope_types = scopes.each_with_object({}) do |scope, memo| memo.merge!(scope) end # Expand abstract scope types into their concrete implementations; # overlapping abstracts coalesce through their intersecting types. possible_scope_types.keys.each do |possible_scope_type| next unless possible_scope_type.kind.abstract? query.types.possible_types(possible_scope_type).each do |impl_type| possible_scope_types[impl_type] ||= true end possible_scope_types.delete(possible_scope_type) end # Aggregate the lexical selections that may apply to each possible type, # and then return the maximum cost among possible typed selections. possible_scope_types.each_key.reduce(0) do |max, possible_scope_type| # Collect inner selections from all scopes that intersect with this possible type. all_inner_selections = scopes.each_with_object([]) do |scope, memo| scope.each do |scope_type, inner_selections| memo << inner_selections if types_intersect?(query, scope_type, possible_scope_type) end end # Find the maximum complexity for the scope type among possible lexical branches. complexity = case mode when :legacy legacy_merged_max_complexity(query, all_inner_selections) when :future merged_max_complexity(query, all_inner_selections) else raise ArgumentError, "Expected :legacy or :future, not: #{mode.inspect}" end complexity > max ? complexity : max end end def types_intersect?(query, a, b) return true if a == b a_types = query.types.possible_types(a) query.types.possible_types(b).any? { |t| a_types.include?(t) } end # A hook which is called whenever a field's max complexity is calculated. # Override this method to capture individual field complexity details. # # @param scoped_type_complexity [ScopedTypeComplexity] # @param max_complexity [Numeric] Field's maximum complexity including child complexity # @param child_complexity [Numeric, nil] Field's child complexity def field_complexity(scoped_type_complexity, max_complexity:, child_complexity: nil) end # @param inner_selections [Array>] Field selections for a scope # @return [Integer] Total complexity value for all these selections in the parent scope def merged_max_complexity(query, inner_selections) # Aggregate a set of all unique field selection keys across all scopes. # Use a hash, but ignore the values; it's just a fast way to work with the keys. unique_field_keys = inner_selections.each_with_object({}) do |inner_selection, memo| memo.merge!(inner_selection) end # Add up the total cost for each unique field name's coalesced selections unique_field_keys.each_key.reduce(0) do |total, field_key| # Collect all child scopes for this field key; # all keys come with at least one scope. child_scopes = inner_selections.filter_map { _1[field_key] } # Compute maximum possible cost of child selections; # composites merge their maximums, while leaf scopes are always zero. # FieldsWillMerge validation assures all scopes are uniformly composite or leaf. maximum_children_cost = if child_scopes.any?(&:composite?) merged_max_complexity_for_scopes(query, child_scopes, :future) else 0 end # Identify the maximum cost and scope among possibilities maximum_cost = 0 maximum_scope = child_scopes.reduce(child_scopes.last) do |max_scope, possible_scope| scope_cost = possible_scope.own_complexity(maximum_children_cost) if scope_cost > maximum_cost maximum_cost = scope_cost possible_scope else max_scope end end field_complexity( maximum_scope, max_complexity: maximum_cost, child_complexity: maximum_children_cost, ) total + maximum_cost end end def legacy_merged_max_complexity(query, inner_selections) # Aggregate a set of all unique field selection keys across all scopes. # Use a hash, but ignore the values; it's just a fast way to work with the keys. unique_field_keys = inner_selections.each_with_object({}) do |inner_selection, memo| memo.merge!(inner_selection) end # Add up the total cost for each unique field name's coalesced selections unique_field_keys.each_key.reduce(0) do |total, field_key| composite_scopes = nil field_cost = 0 # Collect composite selection scopes for further aggregation, # leaf selections report their costs directly. inner_selections.each do |inner_selection| child_scope = inner_selection[field_key] next unless child_scope # Empty child scopes are leaf nodes with zero child complexity. if child_scope.empty? field_cost = child_scope.own_complexity(0) field_complexity(child_scope, max_complexity: field_cost, child_complexity: nil) else composite_scopes ||= [] composite_scopes << child_scope end end if composite_scopes child_complexity = merged_max_complexity_for_scopes(query, composite_scopes, :legacy) # This is the last composite scope visited; assume it's representative (for backwards compatibility). # Note: it would be more correct to score each composite scope and use the maximum possibility. field_cost = composite_scopes.last.own_complexity(child_complexity) field_complexity(composite_scopes.last, max_complexity: field_cost, child_complexity: child_complexity) end total + field_cost end end end end end graphql-ruby-2.5.19/lib/graphql/analysis/query_depth.rb000066400000000000000000000031151514115062600231330ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Analysis # A query reducer for measuring the depth of a given query. # # See https://graphql-ruby.org/queries/ast_analysis.html for more examples. # # @example Logging the depth of a query # class LogQueryDepth < GraphQL::Analysis::QueryDepth # def result # log("GraphQL query depth: #{@max_depth}") # end # end # # # In your Schema file: # # class MySchema < GraphQL::Schema # query_analyzer LogQueryDepth # end # # # When you run the query, the depth will get logged: # # Schema.execute(query_str) # # GraphQL query depth: 8 # class QueryDepth < Analyzer def initialize(query) @max_depth = 0 @current_depth = 0 @count_introspection_fields = query.schema.count_introspection_fields super end def on_enter_field(node, parent, visitor) return if visitor.skipping? || visitor.visiting_fragment_definition? || (@count_introspection_fields == false && visitor.field_definition.introspection?) @current_depth += 1 end def on_leave_field(node, parent, visitor) return if visitor.skipping? || visitor.visiting_fragment_definition? || (@count_introspection_fields == false && visitor.field_definition.introspection?) if @max_depth < @current_depth @max_depth = @current_depth end @current_depth -= 1 end def result @max_depth end end end end graphql-ruby-2.5.19/lib/graphql/analysis/visitor.rb000066400000000000000000000212531514115062600223040ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Analysis # Depth first traversal through a query AST, calling AST analyzers # along the way. # # The visitor is a special case of GraphQL::Language::StaticVisitor, visiting # only the selected operation, providing helpers for common use cases such # as skipped fields and visiting fragment spreads. # # @see {GraphQL::Analysis::Analyzer} AST Analyzers for queries class Visitor < GraphQL::Language::StaticVisitor def initialize(query:, analyzers:, timeout:) @analyzers = analyzers @path = [] @object_types = [] @directives = [] @field_definitions = [] @argument_definitions = [] @directive_definitions = [] @rescued_errors = [] @query = query @schema = query.schema @types = query.types @response_path = [] @skip_stack = [false] @timeout_time = if timeout Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second) + timeout else Float::INFINITY end super(query.selected_operation) end # @return [GraphQL::Query] the query being visited attr_reader :query # @return [Array] Types whose scope we've entered attr_reader :object_types # @return [Array] The path to the response key for the current field def response_path @response_path.dup end # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time # Visitor Hooks [ :operation_definition, :fragment_definition, :inline_fragment, :field, :directive, :argument, :fragment_spread ].each do |node_type| module_eval <<-RUBY, __FILE__, __LINE__ def call_on_enter_#{node_type}(node, parent) @analyzers.each do |a| a.on_enter_#{node_type}(node, parent, self) rescue AnalysisError => err @rescued_errors << err end end def call_on_leave_#{node_type}(node, parent) @analyzers.each do |a| a.on_leave_#{node_type}(node, parent, self) rescue AnalysisError => err @rescued_errors << err end end RUBY end # rubocop:enable Development/NoEvalCop def on_operation_definition(node, parent) check_timeout object_type = @schema.root_type_for_operation(node.operation_type) @object_types.push(object_type) @path.push("#{node.operation_type}#{node.name ? " #{node.name}" : ""}") call_on_enter_operation_definition(node, parent) super call_on_leave_operation_definition(node, parent) @object_types.pop @path.pop end def on_inline_fragment(node, parent) check_timeout object_type = if node.type @types.type(node.type.name) else @object_types.last end @object_types.push(object_type) @path.push("...#{node.type ? " on #{node.type.name}" : ""}") @skipping = @skip_stack.last || skip?(node) @skip_stack << @skipping call_on_enter_inline_fragment(node, parent) super @skipping = @skip_stack.pop call_on_leave_inline_fragment(node, parent) @object_types.pop @path.pop end def on_field(node, parent) check_timeout @response_path.push(node.alias || node.name) parent_type = @object_types.last # This could be nil if the previous field wasn't found: field_definition = parent_type && @types.field(parent_type, node.name) @field_definitions.push(field_definition) if !field_definition.nil? next_object_type = field_definition.type.unwrap @object_types.push(next_object_type) else @object_types.push(nil) end @path.push(node.alias || node.name) @skipping = @skip_stack.last || skip?(node) @skip_stack << @skipping call_on_enter_field(node, parent) super @skipping = @skip_stack.pop call_on_leave_field(node, parent) @response_path.pop @field_definitions.pop @object_types.pop @path.pop end def on_directive(node, parent) check_timeout directive_defn = @schema.directives[node.name] @directive_definitions.push(directive_defn) call_on_enter_directive(node, parent) super call_on_leave_directive(node, parent) @directive_definitions.pop end def on_argument(node, parent) check_timeout argument_defn = if (arg = @argument_definitions.last) arg_type = arg.type.unwrap if arg_type.kind.input_object? @types.argument(arg_type, node.name) else nil end elsif (directive_defn = @directive_definitions.last) @types.argument(directive_defn, node.name) elsif (field_defn = @field_definitions.last) @types.argument(field_defn, node.name) else nil end @argument_definitions.push(argument_defn) @path.push(node.name) call_on_enter_argument(node, parent) super call_on_leave_argument(node, parent) @argument_definitions.pop @path.pop end def on_fragment_spread(node, parent) check_timeout @path.push("... #{node.name}") @skipping = @skip_stack.last || skip?(node) @skip_stack << @skipping call_on_enter_fragment_spread(node, parent) enter_fragment_spread_inline(node) super @skipping = @skip_stack.pop leave_fragment_spread_inline(node) call_on_leave_fragment_spread(node, parent) @path.pop end # @return [GraphQL::BaseType] The current object type def type_definition @object_types.last end # @return [GraphQL::BaseType] The type which the current type came from def parent_type_definition @object_types[-2] end # @return [GraphQL::Field, nil] The most-recently-entered GraphQL::Field, if currently inside one def field_definition @field_definitions.last end # @return [GraphQL::Field, nil] The GraphQL field which returned the object that the current field belongs to def previous_field_definition @field_definitions[-2] end # @return [GraphQL::Directive, nil] The most-recently-entered GraphQL::Directive, if currently inside one def directive_definition @directive_definitions.last end # @return [GraphQL::Argument, nil] The most-recently-entered GraphQL::Argument, if currently inside one def argument_definition @argument_definitions.last end # @return [GraphQL::Argument, nil] The previous GraphQL argument def previous_argument_definition @argument_definitions[-2] end private # Visit a fragment spread inline instead of visiting the definition # by itself. def enter_fragment_spread_inline(fragment_spread) fragment_def = query.fragments[fragment_spread.name] object_type = if fragment_def.type @types.type(fragment_def.type.name) else object_types.last end object_types << object_type on_fragment_definition_children(fragment_def) end # Visit a fragment spread inline instead of visiting the definition # by itself. def leave_fragment_spread_inline(_fragment_spread) object_types.pop end def skip?(ast_node) dir = ast_node.directives !dir.empty? && !GraphQL::Execution::DirectiveChecks.include?(dir, query) end def check_timeout if Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second) > @timeout_time raise GraphQL::Analysis::TimeoutError end end end end end graphql-ruby-2.5.19/lib/graphql/analysis_error.rb000066400000000000000000000001471514115062600220150ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class AnalysisError < GraphQL::ExecutionError end end graphql-ruby-2.5.19/lib/graphql/autoload.rb000066400000000000000000000021121514115062600205630ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL # @see GraphQL::Railtie for automatic Rails integration module Autoload # Register a constant named `const_name` to be loaded from `path`. # This is like `Kernel#autoload` but it tracks the constants so they can be eager-loaded with {#eager_load!} # @param const_name [Symbol] # @param path [String] # @return [void] def autoload(const_name, path) @_eagerloaded_constants ||= [] @_eagerloaded_constants << const_name super const_name, path end # Call this to load this constant's `autoload` dependents and continue calling recursively # @return [void] def eager_load! @_eager_loading = true if @_eagerloaded_constants @_eagerloaded_constants.each { |const_name| const_get(const_name) } @_eagerloaded_constants = nil end nil ensure @_eager_loading = false end private # @return [Boolean] `true` if GraphQL-Ruby is currently eager-loading its constants def eager_loading? @_eager_loading ||= false end end end graphql-ruby-2.5.19/lib/graphql/backtrace.rb000066400000000000000000000016311514115062600206770ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/backtrace/table" require "graphql/backtrace/traced_error" module GraphQL # Wrap unhandled errors with {TracedError}. # # {TracedError} provides a GraphQL backtrace with arguments and return values. # The underlying error is available as {TracedError#cause}. # # @example toggling backtrace annotation # class MySchema < GraphQL::Schema # if Rails.env.development? || Rails.env.test? # use GraphQL::Backtrace # end # end # class Backtrace include Enumerable extend Forwardable def_delegators :to_a, :each, :[] def self.use(schema_defn) schema_defn.using_backtrace = true end def initialize(context, value: nil) @table = Table.new(context, value: value) end def inspect @table.to_table end alias :to_s :inspect def to_a @table.to_backtrace end end end graphql-ruby-2.5.19/lib/graphql/backtrace/000077500000000000000000000000001514115062600203515ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/backtrace/table.rb000066400000000000000000000126101514115062600217650ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Backtrace # A class for turning a context into a human-readable table or array class Table MIN_COL_WIDTH = 4 MAX_COL_WIDTH = 100 HEADERS = [ "Loc", "Field", "Object", "Arguments", "Result", ] def initialize(context, value:) @context = context @override_value = value end # @return [String] A table layout of backtrace with metadata def to_table @to_table ||= render_table(rows) end # @return [Array] An array of position + field name entries def to_backtrace @to_backtrace ||= begin backtrace = rows.map { |r| "#{r[0]}: #{r[1]}" } # skip the header entry backtrace.shift backtrace end end private def rows @rows ||= begin query = @context.query query_ctx = @context runtime_inst = query_ctx.namespace(:interpreter_runtime)[:runtime] result = runtime_inst.instance_variable_get(:@response) rows = [] result_path = [] last_part = nil path = @context.current_path path.each do |path_part| value = value_at(runtime_inst, result_path) if result_path.empty? name = query.selected_operation.operation_type || "query" if (n = query.selected_operation_name) name += " #{n}" end args = query.variables else name = result.graphql_field.path args = result.graphql_arguments end object = result.graphql_parent ? result.graphql_parent.graphql_application_value : result.graphql_application_value object = object.object.inspect rows << [ result.ast_node.position.join(":"), name, "#{object}", args.to_h.inspect, inspect_result(value), ] result_path << path_part if path_part == path.last last_part = path_part else result = result[path_part] end end object = result.graphql_application_value.object.inspect ast_node = nil result.graphql_selections.each do |s| found_ast_node = find_ast_node(s, last_part) if found_ast_node ast_node = found_ast_node break end end if ast_node field_defn = query.get_field(result.graphql_result_type, ast_node.name) args = query.arguments_for(ast_node, field_defn).to_h field_path = field_defn.path if ast_node.alias field_path += " as #{ast_node.alias}" end rows << [ ast_node.position.join(":"), field_path, "#{object}", args.inspect, inspect_result(@override_value) ] end rows << HEADERS rows.reverse! rows end end def find_ast_node(node, last_part) return nil unless node return node if node.respond_to?(:alias) && node.respond_to?(:name) && (node.alias == last_part || node.name == last_part) return nil unless node.respond_to?(:selections) return nil if node.selections.nil? || node.selections.empty? node.selections.each do |child| child_ast_node = find_ast_node(child, last_part) return child_ast_node if child_ast_node end nil end # @return [String] def render_table(rows) max = Array.new(HEADERS.length, MIN_COL_WIDTH) rows.each do |row| row.each_with_index do |col, idx| col_len = col.length max_len = max[idx] if col_len > max_len if col_len > MAX_COL_WIDTH max[idx] = MAX_COL_WIDTH else max[idx] = col_len end end end end table = "".dup last_col_idx = max.length - 1 rows.each do |row| table << row.map.each_with_index do |col, idx| max_len = max[idx] if idx < last_col_idx col = col.ljust(max_len) end if col.length > max_len col = col[0, max_len - 3] + "..." end col end.join(" | ") table << "\n" end table end def value_at(runtime, path) response = runtime.final_result path.each do |key| response && (response = response[key]) end response end def inspect_result(obj) case obj when Hash "{" + obj.map do |key, val| "#{key}: #{inspect_truncated(val)}" end.join(", ") + "}" when Array "[" + obj.map { |v| inspect_truncated(v) }.join(", ") + "]" else inspect_truncated(obj) end end def inspect_truncated(obj) case obj when Hash "{...}" when Array "[...]" when GraphQL::Execution::Lazy "(unresolved)" else "#{obj.inspect}" end end end end end graphql-ruby-2.5.19/lib/graphql/backtrace/traced_error.rb000066400000000000000000000033251514115062600233540ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Backtrace # When {Backtrace} is enabled, raised errors are wrapped with {TracedError}. class TracedError < GraphQL::Error # @return [Array] Printable backtrace of GraphQL error context attr_reader :graphql_backtrace # @return [GraphQL::Query::Context] The context at the field where the error was raised attr_reader :context MESSAGE_TEMPLATE = <<-MESSAGE Unhandled error during GraphQL execution: %{cause_message} %{cause_backtrace} %{cause_backtrace_more} Use #cause to access the original exception (including #cause.backtrace). GraphQL Backtrace: %{graphql_table} MESSAGE # This many lines of the original Ruby backtrace # are included in the message CAUSE_BACKTRACE_PREVIEW_LENGTH = 10 def initialize(err, current_ctx) @context = current_ctx backtrace = Backtrace.new(current_ctx, value: err) @graphql_backtrace = backtrace.to_a cause_backtrace_preview = err.backtrace.first(CAUSE_BACKTRACE_PREVIEW_LENGTH).join("\n ") cause_backtrace_remainder_length = err.backtrace.length - CAUSE_BACKTRACE_PREVIEW_LENGTH cause_backtrace_more = if cause_backtrace_remainder_length < 0 "" elsif cause_backtrace_remainder_length == 1 "... and 1 more line\n" else "... and #{cause_backtrace_remainder_length} more lines\n" end message = MESSAGE_TEMPLATE % { cause_message: err.message, cause_backtrace: cause_backtrace_preview, cause_backtrace_more: cause_backtrace_more, graphql_table: backtrace.inspect, } super(message) end end end end graphql-ruby-2.5.19/lib/graphql/coercion_error.rb000066400000000000000000000001471514115062600217730ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class CoercionError < GraphQL::ExecutionError end end graphql-ruby-2.5.19/lib/graphql/current.rb000066400000000000000000000034711514115062600204460ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL # This module exposes Fiber-level runtime information. # # It won't work across unrelated fibers, although it will work in child Fibers. # # @example Setting Up ActiveRecord::QueryLogs # # config.active_record.query_log_tags = [ # :namespaced_controller, # :action, # :job, # # ... # { # # GraphQL runtime info: # current_graphql_operation: -> { GraphQL::Current.operation_name }, # current_graphql_field: -> { GraphQL::Current.field&.path }, # current_dataloader_source: -> { GraphQL::Current.dataloader_source_class }, # # ... # }, # ] # module Current # @return [String, nil] Comma-joined operation names for the currently-running {Execution::Multiplex}. `nil` if all operations are anonymous. def self.operation_name if (m = Fiber[:__graphql_current_multiplex]) m.context[:__graphql_current_operation_name] ||= begin names = m.queries.map { |q| q.selected_operation_name } if names.all?(&:nil?) nil else names.join(",") end end else nil end end # @see GraphQL::Field#path for a string identifying this field # @return [GraphQL::Field, nil] The currently-running field, if there is one. def self.field Fiber[:__graphql_runtime_info]&.values&.first&.current_field end # @return [Class, nil] The currently-running {Dataloader::Source} class, if there is one. def self.dataloader_source_class Fiber[:__graphql_current_dataloader_source]&.class end # @return [GraphQL::Dataloader::Source, nil] The currently-running source, if there is one def self.dataloader_source Fiber[:__graphql_current_dataloader_source] end end end graphql-ruby-2.5.19/lib/graphql/dashboard.rb000066400000000000000000000142521514115062600207120ustar00rootroot00000000000000# frozen_string_literal: true require 'rails/engine' require 'action_controller' module Graphql # `GraphQL::Dashboard` is a `Rails::Engine`-based dashboard for viewing metadata about your GraphQL schema. # # Pass the class name of your schema when mounting it. # @see GraphQL::Tracing::DetailedTrace DetailedTrace for viewing production traces in the Dashboard # # @example Mounting the Dashboard in your app # mount GraphQL::Dashboard, at: "graphql_dashboard", schema: "MySchema" # # @example Authenticating the Dashboard with HTTP Basic Auth # # config/initializers/graphql_dashboard.rb # GraphQL::Dashboard.middleware.use(Rack::Auth::Basic) do |username, password| # # Compare the provided username/password to an application setting: # ActiveSupport::SecurityUtils.secure_compare(Rails.application.credentials.graphql_dashboard_username, username) && # ActiveSupport::SecurityUtils.secure_compare(Rails.application.credentials.graphql_dashboard_username, password) # end # # @example Custom Rails authentication # # config/initializers/graphql_dashboard.rb # ActiveSupport.on_load(:graphql_dashboard_application_controller) do # # context here is GraphQL::Dashboard::ApplicationController # # before_action do # raise ActionController::RoutingError.new('Not Found') unless current_user&.admin? # end # # def current_user # # load current user # end # end # class Dashboard < Rails::Engine engine_name "graphql_dashboard" isolate_namespace(Graphql::Dashboard) routes do root "landings#show" resources :statics, only: :show, constraints: { id: /[0-9A-Za-z\-.]+/ } namespace :detailed_traces do resources :traces, only: [:index, :show, :destroy] do collection do delete :delete_all, to: "traces#delete_all", as: :delete_all end end end namespace :limiters do resources :limiters, only: [:show, :update], param: :name end namespace :operation_store do resources :clients, param: :name do resources :operations, param: :digest, only: [:index] do collection do get :archived, to: "operations#index", archived_status: :archived, as: :archived post :archive, to: "operations#update", modification: :archive, as: :archive post :unarchive, to: "operations#update", modification: :unarchive, as: :unarchive end end end resources :operations, param: :digest, only: [:index, :show] do collection do get :archived, to: "operations#index", archived_status: :archived, as: :archived post :archive, to: "operations#update", modification: :archive, as: :archive post :unarchive, to: "operations#update", modification: :unarchive, as: :unarchive end end resources :index_entries, only: [:index, :show], param: :name, constraints: { name: /[A-Za-z0-9_.]+/} end namespace :subscriptions do resources :topics, only: [:index, :show], param: :name, constraints: { name: /.*/ } resources :subscriptions, only: [:show], constraints: { id: /[a-zA-Z0-9\-]+/ } post "/subscriptions/clear_all", to: "subscriptions#clear_all", as: :clear_all end ApplicationController.include(Dashboard.routes.url_helpers) end class ApplicationController < ActionController::Base protect_from_forgery with: :exception prepend_view_path(File.join(__FILE__, "../dashboard/views")) content_security_policy do |policy| policy.default_src(:self) if policy.default_src(*policy.default_src).blank? policy.connect_src(:self) if policy.connect_src(*policy.connect_src).blank? policy.base_uri(:none) if policy.base_uri(*policy.base_uri).blank? policy.font_src(:self) if policy.font_src(*policy.font_src).blank? policy.img_src(:self, :data) if policy.img_src(*policy.img_src).blank? policy.object_src(:none) if policy.object_src(*policy.object_src).blank? policy.script_src(:self) if policy.script_src(*policy.script_src).blank? policy.style_src(:self) if policy.style_src(*policy.style_src).blank? policy.form_action(:self) if policy.form_action(*policy.form_action).blank? policy.frame_ancestors(:none) if policy.frame_ancestors(*policy.frame_ancestors).blank? end def schema_class @schema_class ||= begin schema_param = request.query_parameters["schema"] || params[:schema] case schema_param when Class schema_param when String schema_param.constantize else raise "Missing `params[:schema]`, please provide a class or string to `mount GraphQL::Dashboard, schema: ...`" end end end helper_method :schema_class end class LandingsController < ApplicationController def show end end class StaticsController < ApplicationController skip_forgery_protection # Use an explicit list of files to avoid any chance of reading other files from disk STATICS = {} [ "icon.png", "header-icon.png", "charts.min.css", "dashboard.css", "dashboard.js", "bootstrap-5.3.3.min.css", "bootstrap-5.3.3.min.js", ].each do |static_file| STATICS[static_file] = File.expand_path("../dashboard/statics/#{static_file}", __FILE__) end def show expires_in 1.year, public: true if (filepath = STATICS[params[:id]]) render file: filepath else head :not_found end end end end end require 'graphql/dashboard/detailed_traces' require 'graphql/dashboard/limiters' require 'graphql/dashboard/operation_store' require 'graphql/dashboard/subscriptions' # Rails expects the engine to be called `Graphql::Dashboard`, # but `GraphQL::Dashboard` is consistent with this gem's naming. # So define both constants to refer to the same class. GraphQL::Dashboard = Graphql::Dashboard ActiveSupport.run_load_hooks(:graphql_dashboard_application_controller, GraphQL::Dashboard::ApplicationController) graphql-ruby-2.5.19/lib/graphql/dashboard/000077500000000000000000000000001514115062600203615ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/detailed_traces.rb000066400000000000000000000026571514115062600240340ustar00rootroot00000000000000# frozen_string_literal: true require_relative "./installable" module Graphql class Dashboard < Rails::Engine module DetailedTraces class TracesController < Graphql::Dashboard::ApplicationController include Installable def index @last = params[:last]&.to_i || 50 @before = params[:before]&.to_i @traces = schema_class.detailed_trace.traces(last: @last, before: @before) end def show trace = schema_class.detailed_trace.find_trace(params[:id].to_i) send_data(trace.trace_data) end def destroy schema_class.detailed_trace.delete_trace(params[:id]) flash[:success] = "Trace deleted." head :no_content end def delete_all schema_class.detailed_trace.delete_all_traces flash[:success] = "Deleted all traces." head :no_content end private def feature_installed? !!schema_class.detailed_trace end INSTALLABLE_COMPONENT_HEADER_HTML = "Detailed traces aren't installed yet." INSTALLABLE_COMPONENT_MESSAGE_HTML = <<~HTML.html_safe GraphQL-Ruby can instrument production traffic and save tracing artifacts here for later review.
Read more in the detailed tracing docs. HTML end end end end graphql-ruby-2.5.19/lib/graphql/dashboard/installable.rb000066400000000000000000000012431514115062600232000ustar00rootroot00000000000000# frozen_string_literal: true module Graphql class Dashboard < Rails::Engine module Installable def self.included(child_module) child_module.before_action(:check_installed) end def feature_installed? raise "Implement #{self.class}#feature_installed? to check whether this should render `not_installed` or not." end def check_installed if !feature_installed? @component_header_html = self.class::INSTALLABLE_COMPONENT_HEADER_HTML @component_message_html = self.class::INSTALLABLE_COMPONENT_MESSAGE_HTML render "graphql/dashboard/not_installed" end end end end end graphql-ruby-2.5.19/lib/graphql/dashboard/limiters.rb000066400000000000000000000065231514115062600225440ustar00rootroot00000000000000# frozen_string_literal: true require_relative "./installable" module Graphql class Dashboard < Rails::Engine module Limiters class LimitersController < Dashboard::ApplicationController include Installable FALLBACK_CSP_NONCE_GENERATOR = ->(_req) { SecureRandom.hex(32) } def show name = params[:name] @title = case name when "runtime" "Runtime Limiter" when "active_operations" "Active Operation Limiter" when "mutations" "Mutation Limiter" else raise ArgumentError, "Unknown limiter name: #{name}" end limiter = limiter_for(name) if limiter.nil? @install_path = "http://graphql-ruby.org/limiters/#{name}" else @chart_mode = params[:chart] || "day" @current_soft = limiter.soft_limit_enabled? @histogram = limiter.dashboard_histogram(@chart_mode) # These configs may have already been defined by the application; provide overrides here if not. request.content_security_policy_nonce_generator ||= FALLBACK_CSP_NONCE_GENERATOR nonce_dirs = request.content_security_policy_nonce_directives || [] if !nonce_dirs.include?("style-src") nonce_dirs += ["style-src"] request.content_security_policy_nonce_directives = nonce_dirs end @csp_nonce = request.content_security_policy_nonce end end def update name = params[:name] limiter = limiter_for(name) if limiter limiter.toggle_soft_limit flash[:success] = if limiter.soft_limit_enabled? "Enabled soft limiting -- over-limit traffic will be logged but not rejected." else "Disabled soft limiting -- over-limit traffic will be rejected." end else flash[:warning] = "No limiter configured for #{name.inspect}" end redirect_to graphql_dashboard.limiters_limiter_path(name, chart: params[:chart]) end private def limiter_for(name) case name when "runtime" schema_class.enterprise_runtime_limiter when "active_operations" schema_class.enterprise_active_operation_limiter when "mutations" schema_class.enterprise_mutation_limiter else raise ArgumentError, "Unknown limiter: #{name}" end end def feature_installed? defined?(GraphQL::Enterprise::Limiter) && ( schema_class.enterprise_active_operation_limiter || schema_class.enterprise_runtime_limiter || (schema_class.respond_to?(:enterprise_mutation_limiter) && schema_class.enterprise_mutation_limiter) ) end INSTALLABLE_COMPONENT_HEADER_HTML = "Rate limiters aren't installed on this schema yet." INSTALLABLE_COMPONENT_MESSAGE_HTML = <<-HTML.html_safe Check out the docs to get started with GraphQL-Enterprise's runtime limiter or active operation limiter. HTML end end end end graphql-ruby-2.5.19/lib/graphql/dashboard/operation_store.rb000066400000000000000000000161471514115062600241330ustar00rootroot00000000000000# frozen_string_literal: true require_relative "./installable" module Graphql class Dashboard < Rails::Engine module OperationStore class BaseController < Dashboard::ApplicationController include Installable private def feature_installed? schema_class.respond_to?(:operation_store) && schema_class.operation_store.is_a?(GraphQL::Pro::OperationStore) end INSTALLABLE_COMPONENT_HEADER_HTML = "OperationStore isn't installed for this schema yet.".html_safe INSTALLABLE_COMPONENT_MESSAGE_HTML = <<-HTML.html_safe Learn more about improving performance and security with stored operations in the OperationStore docs. HTML end class ClientsController < BaseController def index @order_by = params[:order_by] || "name" @order_dir = params[:order_dir].presence || "asc" clients_page = schema_class.operation_store.all_clients( page: params[:page]&.to_i || 1, per_page: params[:per_page]&.to_i || 25, order_by: @order_by, order_dir: @order_dir, ) @clients_page = clients_page end def new @client = init_client(secret: SecureRandom.hex(32)) end def create client_params = params.require(:client).permit(:name, :secret) schema_class.operation_store.upsert_client(client_params[:name], client_params[:secret]) flash[:success] = "Created #{client_params[:name].inspect}" redirect_to graphql_dashboard.operation_store_clients_path end def edit @client = schema_class.operation_store.get_client(params[:name]) end def update client_name = params[:name] client_secret = params.require(:client).permit(:secret)[:secret] schema_class.operation_store.upsert_client(client_name, client_secret) flash[:success] = "Updated #{client_name.inspect}" redirect_to graphql_dashboard.operation_store_clients_path end def destroy client_name = params[:name] schema_class.operation_store.delete_client(client_name) flash[:success] = "Deleted #{client_name.inspect}" redirect_to graphql_dashboard.operation_store_clients_path end private def init_client(name: nil, secret: nil) GraphQL::Pro::OperationStore::ClientRecord.new( name: name, secret: secret, created_at: nil, operations_count: 0, archived_operations_count: 0, last_synced_at: nil, last_used_at: nil, ) end end class OperationsController < BaseController def index @client_operations = client_name = params[:client_name] per_page = params[:per_page]&.to_i || 25 page = params[:page]&.to_i || 1 @is_archived = params[:archived_status] == :archived order_by = params[:order_by] || "name" order_dir = params[:order_dir]&.to_sym || :asc if @client_operations @operations_page = schema_class.operation_store.get_client_operations_by_client( client_name, page: page, per_page: per_page, is_archived: @is_archived, order_by: order_by, order_dir: order_dir, ) opposite_archive_mode_count = schema_class.operation_store.get_client_operations_by_client( client_name, page: 1, per_page: 1, is_archived: !@is_archived, order_by: order_by, order_dir: order_dir, ).total_count else @operations_page = schema_class.operation_store.all_operations( page: page, per_page: per_page, is_archived: @is_archived, order_by: order_by, order_dir: order_dir, ) opposite_archive_mode_count = schema_class.operation_store.all_operations( page: 1, per_page: 1, is_archived: !@is_archived, order_by: order_by, order_dir: order_dir, ).total_count end if @is_archived @archived_operations_count = @operations_page.total_count @unarchived_operations_count = opposite_archive_mode_count else @archived_operations_count = opposite_archive_mode_count @unarchived_operations_count = @operations_page.total_count end end def show digest = params[:digest] @operation = schema_class.operation_store.get_operation_by_digest(digest) if @operation # Parse & re-format the query document = GraphQL.parse(@operation.body) @graphql_source = document.to_query_string @client_operations = schema_class.operation_store.get_client_operations_by_digest(digest) @entries = schema_class.operation_store.get_index_entries_by_digest(digest) end end def update is_archived = case params[:modification] when :archive true when :unarchive false else raise ArgumentError, "Unexpected modification: #{params[:modification].inspect}" end if (client_name = params[:client_name]) operation_aliases = params[:operation_aliases] schema_class.operation_store.archive_client_operations( client_name: client_name, operation_aliases: operation_aliases, is_archived: is_archived ) flash[:success] = "#{is_archived ? "Archived" : "Activated"} #{operation_aliases.size} #{"operation".pluralize(operation_aliases.size)}" else digests = params[:digests] schema_class.operation_store.archive_operations( digests: digests, is_archived: is_archived ) flash[:success] = "#{is_archived ? "Archived" : "Activated"} #{digests.size} #{"operation".pluralize(digests.size)}" end head :no_content end end class IndexEntriesController < BaseController def index @search_term = if request.params["q"] && request.params["q"].length > 0 request.params["q"] else nil end @index_entries_page = schema_class.operation_store.all_index_entries( search_term: @search_term, page: params[:page]&.to_i || 1, per_page: params[:per_page]&.to_i || 25, ) end def show name = params[:name] @entry = schema_class.operation_store.index.get_entry(name) @chain = schema_class.operation_store.index.index_entry_chain(name) @operations = schema_class.operation_store.get_operations_by_index_entry(name) end end end end end graphql-ruby-2.5.19/lib/graphql/dashboard/statics/000077500000000000000000000000001514115062600220335ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.css000066400000000000000000007065431514115062600260270ustar00rootroot00000000000000@charset "UTF-8";/*! * Bootstrap v5.3.3 (https://getbootstrap.com/) * Copyright 2011-2024 The Bootstrap Authors * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#6ea8fe;--bs-link-hover-color:#8bb9fe;--bs-link-color-rgb:110,168,254;--bs-link-hover-color-rgb:139,185,254;--bs-code-color:#e685b5;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#664d03;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#75b798;--bs-form-valid-border-color:#75b798;--bs-form-invalid-color:#ea868f;--bs-form-invalid-border-color:#ea868f}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color-type:initial;--bs-table-bg-type:initial;--bs-table-color-state:initial;--bs-table-bg-state:initial;--bs-table-color:var(--bs-emphasis-color);--bs-table-bg:var(--bs-body-bg);--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-emphasis-color);--bs-table-striped-bg:rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color:var(--bs-emphasis-color);--bs-table-active-bg:rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color:var(--bs-emphasis-color);--bs-table-hover-bg:rgba(var(--bs-emphasis-color-rgb), 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state,var(--bs-table-color-type,var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state,var(--bs-table-bg-type,var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width) * 2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-active{--bs-table-color-state:var(--bs-table-active-color);--bs-table-bg-state:var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state:var(--bs-table-hover-color);--bs-table-bg-state:var(--bs-table-hover-bg)}.table-primary{--bs-table-color:#000;--bs-table-bg:#cfe2ff;--bs-table-border-color:#a6b5cc;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:#e2e3e5;--bs-table-border-color:#b5b6b7;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#000;--bs-table-bg:#d1e7dd;--bs-table-border-color:#a7b9b1;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#000;--bs-table-bg:#cff4fc;--bs-table-border-color:#a6c3ca;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#000;--bs-table-bg:#fff3cd;--bs-table-border-color:#ccc2a4;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:#f8d7da;--bs-table-border-color:#c6acae;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#f8f9fa;--bs-table-border-color:#c6c7c8;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:#4d5154;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + var(--bs-border-width));padding-bottom:calc(.375rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + var(--bs-border-width));padding-bottom:calc(.5rem + var(--bs-border-width));font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + var(--bs-border-width));padding-bottom:calc(.25rem + var(--bs-border-width));font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-clip:padding-box;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-body-color);background-color:var(--bs-body-bg);border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:var(--bs-secondary-color);opacity:1}.form-control::placeholder{color:var(--bs-secondary-color);opacity:1}.form-control:disabled{background-color:var(--bs-secondary-bg);opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:var(--bs-secondary-bg)}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:transparent;border:solid transparent;border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:var(--bs-secondary-bg)}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 var(--bs-body-color)}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg:var(--bs-body-bg);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--bs-border-width) solid var(--bs-border-color);-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;-webkit-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder-shown),.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:not(:-moz-placeholder-shown)~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>.form-control-plaintext~label::after,.form-floating>.form-control:focus~label::after,.form-floating>.form-control:not(:placeholder-shown)~label::after,.form-floating>.form-select~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>.form-control:-webkit-autofill~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>.form-control:disabled~label,.form-floating>:disabled~label{color:#6c757d}.form-floating>.form-control:disabled~label::after,.form-floating>:disabled~label::after{background-color:var(--bs-secondary-bg)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);text-align:center;white-space:nowrap;background-color:var(--bs-tertiary-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius)}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(var(--bs-border-width) * -1);border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-success);border-radius:var(--bs-border-radius)}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:var(--bs-form-valid-border-color)}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--bs-form-valid-border-color)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--bs-form-valid-color)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-danger);border-radius:var(--bs-border-radius)}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:var(--bs-form-invalid-border-color)}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--bs-form-invalid-border-color)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--bs-form-invalid-color)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:var(--bs-body-color);--bs-btn-bg:transparent;--bs-btn-border-width:var(--bs-border-width);--bs-btn-border-color:transparent;--bs-btn-border-radius:var(--bs-border-radius);--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5c636a;--bs-btn-hover-border-color:#565e64;--bs-btn-focus-shadow-rgb:130,138,145;--bs-btn-active-color:#fff;--bs-btn-active-bg:#565e64;--bs-btn-active-border-color:#51585e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6c757d;--bs-btn-disabled-border-color:#6c757d}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#157347;--bs-btn-hover-border-color:#146c43;--bs-btn-focus-shadow-rgb:60,153,110;--bs-btn-active-color:#fff;--bs-btn-active-bg:#146c43;--bs-btn-active-border-color:#13653f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#198754;--bs-btn-disabled-border-color:#198754}.btn-info{--bs-btn-color:#000;--bs-btn-bg:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#31d2f2;--bs-btn-hover-border-color:#25cff2;--bs-btn-focus-shadow-rgb:11,172,204;--bs-btn-active-color:#000;--bs-btn-active-bg:#3dd5f3;--bs-btn-active-border-color:#25cff2;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#0dcaf0;--bs-btn-disabled-border-color:#0dcaf0}.btn-warning{--bs-btn-color:#000;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffca2c;--bs-btn-hover-border-color:#ffc720;--bs-btn-focus-shadow-rgb:217,164,6;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffcd39;--bs-btn-active-border-color:#ffc720;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#bb2d3b;--bs-btn-hover-border-color:#b02a37;--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b02a37;--bs-btn-active-border-color:#a52834;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#d3d4d5;--bs-btn-hover-border-color:#c6c7c8;--bs-btn-focus-shadow-rgb:211,212,213;--bs-btn-active-color:#000;--bs-btn-active-bg:#c6c7c8;--bs-btn-active-border-color:#babbbc;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#f8f9fa;--bs-btn-disabled-border-color:#f8f9fa}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#424649;--bs-btn-hover-border-color:#373b3e;--bs-btn-focus-shadow-rgb:66,70,73;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4d5154;--bs-btn-active-border-color:#373b3e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0d6efd;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6c757d;--bs-btn-hover-border-color:#6c757d;--bs-btn-focus-shadow-rgb:108,117,125;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6c757d;--bs-btn-active-border-color:#6c757d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6c757d;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#198754;--bs-btn-hover-border-color:#198754;--bs-btn-focus-shadow-rgb:25,135,84;--bs-btn-active-color:#fff;--bs-btn-active-bg:#198754;--bs-btn-active-border-color:#198754;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#198754;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#198754;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#0dcaf0;--bs-btn-hover-border-color:#0dcaf0;--bs-btn-focus-shadow-rgb:13,202,240;--bs-btn-active-color:#000;--bs-btn-active-bg:#0dcaf0;--bs-btn-active-border-color:#0dcaf0;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0dcaf0;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0dcaf0;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#dc3545;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f8f9fa;--bs-btn-hover-border-color:#f8f9fa;--bs-btn-focus-shadow-rgb:248,249,250;--bs-btn-active-color:#000;--bs-btn-active-bg:#f8f9fa;--bs-btn-active-border-color:#f8f9fa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f8f9fa;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#f8f9fa;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33,37,41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:0 0 0 #000;--bs-btn-focus-shadow-rgb:49,132,253;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:var(--bs-border-radius-lg)}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:var(--bs-border-radius-sm)}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:var(--bs-body-color);--bs-dropdown-bg:var(--bs-body-bg);--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:var(--bs-border-radius);--bs-dropdown-border-width:var(--bs-border-width);--bs-dropdown-inner-border-radius:calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:var(--bs-box-shadow);--bs-dropdown-link-color:var(--bs-body-color);--bs-dropdown-link-hover-color:var(--bs-body-color);--bs-dropdown-link-hover-bg:var(--bs-tertiary-bg);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:var(--bs-tertiary-color);--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#6c757d;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0;border-radius:var(--bs-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-header-color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:var(--bs-border-radius)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(var(--bs-border-width) * -1)}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:calc(var(--bs-border-width) * -1)}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:0 0;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:var(--bs-border-width);--bs-nav-tabs-border-color:var(--bs-border-color);--bs-nav-tabs-border-radius:var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color:var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color:var(--bs-emphasis-color);--bs-nav-tabs-link-active-bg:var(--bs-body-bg);--bs-nav-tabs-link-active-border-color:var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:var(--bs-border-radius);--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0d6efd}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap:1rem;--bs-nav-underline-border-width:0.125rem;--bs-nav-underline-link-active-color:var(--bs-emphasis-color);gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid transparent}.nav-underline .nav-link:focus,.nav-underline .nav-link:hover{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width:var(--bs-border-width);--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(var(--bs-body-color-rgb), 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:var(--bs-body-bg);--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:var(--bs-body-bg);--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:var(--bs-border-width);--bs-accordion-border-radius:var(--bs-border-radius);--bs-accordion-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:var(--bs-primary-text-emphasis);--bs-accordion-active-bg:var(--bs-primary-bg-subtle)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type>.accordion-header .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type>.accordion-header .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type>.accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush>.accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush>.accordion-item:first-child{border-top:0}.accordion-flush>.accordion-item:last-child{border-bottom:0}.accordion-flush>.accordion-item>.accordion-header .accordion-button,.accordion-flush>.accordion-item>.accordion-header .accordion-button.collapsed{border-radius:0}.accordion-flush>.accordion-item>.accordion-collapse{border-radius:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:var(--bs-secondary-color);--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:var(--bs-body-bg);--bs-pagination-border-width:var(--bs-border-width);--bs-pagination-border-color:var(--bs-border-color);--bs-pagination-border-radius:var(--bs-border-radius);--bs-pagination-hover-color:var(--bs-link-hover-color);--bs-pagination-hover-bg:var(--bs-tertiary-bg);--bs-pagination-hover-border-color:var(--bs-border-color);--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:var(--bs-secondary-bg);--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#0d6efd;--bs-pagination-active-border-color:#0d6efd;--bs-pagination-disabled-color:var(--bs-secondary-color);--bs-pagination-disabled-bg:var(--bs-secondary-bg);--bs-pagination-disabled-border-color:var(--bs-border-color);display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(var(--bs-border-width) * -1)}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius:var(--bs-border-radius);--bs-alert-link-color:inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:var(--bs-primary-text-emphasis);--bs-alert-bg:var(--bs-primary-bg-subtle);--bs-alert-border-color:var(--bs-primary-border-subtle);--bs-alert-link-color:var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color:var(--bs-secondary-text-emphasis);--bs-alert-bg:var(--bs-secondary-bg-subtle);--bs-alert-border-color:var(--bs-secondary-border-subtle);--bs-alert-link-color:var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color:var(--bs-success-text-emphasis);--bs-alert-bg:var(--bs-success-bg-subtle);--bs-alert-border-color:var(--bs-success-border-subtle);--bs-alert-link-color:var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color:var(--bs-info-text-emphasis);--bs-alert-bg:var(--bs-info-bg-subtle);--bs-alert-border-color:var(--bs-info-border-subtle);--bs-alert-link-color:var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color:var(--bs-warning-text-emphasis);--bs-alert-bg:var(--bs-warning-bg-subtle);--bs-alert-border-color:var(--bs-warning-border-subtle);--bs-alert-link-color:var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color:var(--bs-danger-text-emphasis);--bs-alert-bg:var(--bs-danger-bg-subtle);--bs-alert-border-color:var(--bs-danger-border-subtle);--bs-alert-link-color:var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color:var(--bs-light-text-emphasis);--bs-alert-bg:var(--bs-light-bg-subtle);--bs-alert-border-color:var(--bs-light-border-subtle);--bs-alert-link-color:var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color:var(--bs-dark-text-emphasis);--bs-alert-bg:var(--bs-dark-bg-subtle);--bs-alert-border-color:var(--bs-dark-border-subtle);--bs-alert-link-color:var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress,.progress-stacked{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:var(--bs-secondary-bg);--bs-progress-border-radius:var(--bs-border-radius);--bs-progress-box-shadow:var(--bs-box-shadow-inset);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#0d6efd;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:var(--bs-body-color);--bs-list-group-bg:var(--bs-body-bg);--bs-list-group-border-color:var(--bs-border-color);--bs-list-group-border-width:var(--bs-border-width);--bs-list-group-border-radius:var(--bs-border-radius);--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:var(--bs-secondary-color);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-tertiary-bg);--bs-list-group-action-active-color:var(--bs-body-color);--bs-list-group-action-active-bg:var(--bs-secondary-bg);--bs-list-group-disabled-color:var(--bs-secondary-color);--bs-list-group-disabled-bg:var(--bs-body-bg);--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color:var(--bs-primary-text-emphasis);--bs-list-group-bg:var(--bs-primary-bg-subtle);--bs-list-group-border-color:var(--bs-primary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-primary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-primary-border-subtle);--bs-list-group-active-color:var(--bs-primary-bg-subtle);--bs-list-group-active-bg:var(--bs-primary-text-emphasis);--bs-list-group-active-border-color:var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color:var(--bs-secondary-text-emphasis);--bs-list-group-bg:var(--bs-secondary-bg-subtle);--bs-list-group-border-color:var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-secondary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-secondary-border-subtle);--bs-list-group-active-color:var(--bs-secondary-bg-subtle);--bs-list-group-active-bg:var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color:var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color:var(--bs-success-text-emphasis);--bs-list-group-bg:var(--bs-success-bg-subtle);--bs-list-group-border-color:var(--bs-success-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-success-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-success-border-subtle);--bs-list-group-active-color:var(--bs-success-bg-subtle);--bs-list-group-active-bg:var(--bs-success-text-emphasis);--bs-list-group-active-border-color:var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color:var(--bs-info-text-emphasis);--bs-list-group-bg:var(--bs-info-bg-subtle);--bs-list-group-border-color:var(--bs-info-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-info-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-info-border-subtle);--bs-list-group-active-color:var(--bs-info-bg-subtle);--bs-list-group-active-bg:var(--bs-info-text-emphasis);--bs-list-group-active-border-color:var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color:var(--bs-warning-text-emphasis);--bs-list-group-bg:var(--bs-warning-bg-subtle);--bs-list-group-border-color:var(--bs-warning-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-warning-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-warning-border-subtle);--bs-list-group-active-color:var(--bs-warning-bg-subtle);--bs-list-group-active-bg:var(--bs-warning-text-emphasis);--bs-list-group-active-border-color:var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color:var(--bs-danger-text-emphasis);--bs-list-group-bg:var(--bs-danger-bg-subtle);--bs-list-group-border-color:var(--bs-danger-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-danger-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-danger-border-subtle);--bs-list-group-active-color:var(--bs-danger-bg-subtle);--bs-list-group-active-bg:var(--bs-danger-text-emphasis);--bs-list-group-active-border-color:var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color:var(--bs-light-text-emphasis);--bs-list-group-bg:var(--bs-light-bg-subtle);--bs-list-group-border-color:var(--bs-light-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-light-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-light-border-subtle);--bs-list-group-active-color:var(--bs-light-bg-subtle);--bs-list-group-active-bg:var(--bs-light-text-emphasis);--bs-list-group-active-border-color:var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color:var(--bs-dark-text-emphasis);--bs-list-group-bg:var(--bs-dark-bg-subtle);--bs-list-group-border-color:var(--bs-dark-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-dark-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-dark-border-subtle);--bs-list-group-active-color:var(--bs-dark-bg-subtle);--bs-list-group-active-bg:var(--bs-dark-text-emphasis);--bs-list-group-active-border-color:var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color:#000;--bs-btn-close-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");--bs-btn-close-opacity:0.5;--bs-btn-close-hover-opacity:0.75;--bs-btn-close-focus-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-btn-close-focus-opacity:1;--bs-btn-close-disabled-opacity:0.25;--bs-btn-close-white-filter:invert(1) grayscale(100%) brightness(200%);box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/1em auto no-repeat;border:0;border-radius:.375rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{filter:var(--bs-btn-close-white-filter)}[data-bs-theme=dark] .btn-close{filter:var(--bs-btn-close-white-filter)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-border-width:var(--bs-border-width);--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:var(--bs-border-radius);--bs-toast-box-shadow:var(--bs-box-shadow);--bs-toast-header-color:var(--bs-secondary-color);--bs-toast-header-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-header-border-color:var(--bs-border-color-translucent);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color: ;--bs-modal-bg:var(--bs-body-bg);--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:var(--bs-border-width);--bs-modal-border-radius:var(--bs-border-radius-lg);--bs-modal-box-shadow:var(--bs-box-shadow-sm);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:var(--bs-border-width);--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin:calc(-.5 * var(--bs-modal-header-padding-y)) calc(-.5 * var(--bs-modal-header-padding-x)) calc(-.5 * var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:var(--bs-body-bg);--bs-tooltip-bg:var(--bs-emphasis-color);--bs-tooltip-border-radius:var(--bs-border-radius);--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:var(--bs-body-bg);--bs-popover-border-width:var(--bs-border-width);--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:var(--bs-border-radius-lg);--bs-popover-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow:var(--bs-box-shadow);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:inherit;--bs-popover-header-bg:var(--bs-secondary-bg);--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:var(--bs-body-color);--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}[data-bs-theme=dark] .carousel .carousel-control-next-icon,[data-bs-theme=dark] .carousel .carousel-control-prev-icon,[data-bs-theme=dark].carousel .carousel-control-next-icon,[data-bs-theme=dark].carousel .carousel-control-prev-icon{filter:invert(1) grayscale(100)}[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target],[data-bs-theme=dark].carousel .carousel-indicators [data-bs-target]{background-color:#000}[data-bs-theme=dark] .carousel .carousel-caption,[data-bs-theme=dark].carousel .carousel-caption{color:#000}.spinner-border,.spinner-grow{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color:var(--bs-body-color);--bs-offcanvas-bg:var(--bs-body-bg);--bs-offcanvas-border-width:var(--bs-border-width);--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:var(--bs-box-shadow-sm);--bs-offcanvas-transition:transform 0.3s ease-in-out;--bs-offcanvas-title-line-height:1.5}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin:calc(-.5 * var(--bs-offcanvas-padding-y)) calc(-.5 * var(--bs-offcanvas-padding-x)) calc(-.5 * var(--bs-offcanvas-padding-y)) auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity,1))!important}.text-bg-info{color:#000!important;background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity,1))!important}.link-primary{color:RGBA(var(--bs-primary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important}.link-primary:focus,.link-primary:hover{color:RGBA(10,88,202,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important}.link-secondary{color:RGBA(var(--bs-secondary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important}.link-secondary:focus,.link-secondary:hover{color:RGBA(86,94,100,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important}.link-success{color:RGBA(var(--bs-success-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important}.link-success:focus,.link-success:hover{color:RGBA(20,108,67,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important}.link-info{color:RGBA(var(--bs-info-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important}.link-info:focus,.link-info:hover{color:RGBA(61,213,243,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important}.link-warning{color:RGBA(var(--bs-warning-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important}.link-warning:focus,.link-warning:hover{color:RGBA(255,205,57,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important}.link-danger{color:RGBA(var(--bs-danger-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important}.link-danger:focus,.link-danger:hover{color:RGBA(176,42,55,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important}.link-light{color:RGBA(var(--bs-light-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important}.link-light:focus,.link-light:hover{color:RGBA(249,250,251,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important}.link-dark{color:RGBA(var(--bs-dark-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important}.link-dark:focus,.link-dark:hover{color:RGBA(26,30,33,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-body-emphasis:focus,.link-body-emphasis:hover{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,.75))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x,0) var(--bs-focus-ring-y,0) var(--bs-focus-ring-blur,0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-underline-offset:0.25em;-webkit-backface-visibility:hidden;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media (prefers-reduced-motion:reduce){.icon-link>.bi{transition:none}}.icon-link-hover:focus-visible>.bi,.icon-link-hover:hover>.bi{transform:var(--bs-icon-link-transform,translate3d(.25em,0,0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption),.visually-hidden:not(caption){position:absolute!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.object-fit-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-none{-o-object-fit:none!important;object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:var(--bs-box-shadow)!important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm)!important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg)!important}.shadow-none{box-shadow:none!important}.focus-ring-primary{--bs-focus-ring-color:rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color:rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color:rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color:rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color:rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color:rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color:rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color:rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-black{--bs-border-opacity:1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.25rem!important}.row-gap-2{row-gap:.5rem!important}.row-gap-3{row-gap:1rem!important}.row-gap-4{row-gap:1.5rem!important}.row-gap-5{row-gap:3rem!important}.column-gap-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity:1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity:1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--bs-success-text-emphasis)!important}.text-info-emphasis{color:var(--bs-info-text-emphasis)!important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis)!important}.text-light-emphasis{color:var(--bs-light-text-emphasis)!important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis)!important}.link-opacity-10{--bs-link-opacity:0.1}.link-opacity-10-hover:hover{--bs-link-opacity:0.1}.link-opacity-25{--bs-link-opacity:0.25}.link-opacity-25-hover:hover{--bs-link-opacity:0.25}.link-opacity-50{--bs-link-opacity:0.5}.link-opacity-50-hover:hover{--bs-link-opacity:0.5}.link-opacity-75{--bs-link-opacity:0.75}.link-opacity-75-hover:hover{--bs-link-opacity:0.75}.link-opacity-100{--bs-link-opacity:1}.link-opacity-100-hover:hover{--bs-link-opacity:1}.link-offset-1{text-underline-offset:0.125em!important}.link-offset-1-hover:hover{text-underline-offset:0.125em!important}.link-offset-2{text-underline-offset:0.25em!important}.link-offset-2-hover:hover{text-underline-offset:0.25em!important}.link-offset-3{text-underline-offset:0.375em!important}.link-offset-3-hover:hover{text-underline-offset:0.375em!important}.link-underline-primary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-secondary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-success{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important}.link-underline-info{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important}.link-underline-warning{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important}.link-underline-danger{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important}.link-underline-light{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important}.link-underline-dark{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important}.link-underline{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-underline-opacity-0{--bs-link-underline-opacity:0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity:0}.link-underline-opacity-10{--bs-link-underline-opacity:0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity:0.1}.link-underline-opacity-25{--bs-link-underline-opacity:0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity:0.25}.link-underline-opacity-50{--bs-link-underline-opacity:0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity:0.5}.link-underline-opacity-75{--bs-link-underline-opacity:0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity:0.75}.link-underline-opacity-100{--bs-link-underline-opacity:1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-xxl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl)!important;border-top-right-radius:var(--bs-border-radius-xxl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-right-radius:var(--bs-border-radius-xxl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-left-radius:var(--bs-border-radius-xxl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl)!important;border-top-left-radius:var(--bs-border-radius-xxl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-n1{z-index:-1!important}.z-0{z-index:0!important}.z-1{z-index:1!important}.z-2{z-index:2!important}.z-3{z-index:3!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-sm-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-sm-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-sm-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-sm-none{-o-object-fit:none!important;object-fit:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.25rem!important}.row-gap-sm-2{row-gap:.5rem!important}.row-gap-sm-3{row-gap:1rem!important}.row-gap-sm-4{row-gap:1.5rem!important}.row-gap-sm-5{row-gap:3rem!important}.column-gap-sm-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-sm-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-sm-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-sm-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-sm-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-sm-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.object-fit-md-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-md-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-md-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-md-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-md-none{-o-object-fit:none!important;object-fit:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.25rem!important}.row-gap-md-2{row-gap:.5rem!important}.row-gap-md-3{row-gap:1rem!important}.row-gap-md-4{row-gap:1.5rem!important}.row-gap-md-5{row-gap:3rem!important}.column-gap-md-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-md-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-md-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-md-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-md-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-md-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.object-fit-lg-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-lg-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-lg-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-lg-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-lg-none{-o-object-fit:none!important;object-fit:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.25rem!important}.row-gap-lg-2{row-gap:.5rem!important}.row-gap-lg-3{row-gap:1rem!important}.row-gap-lg-4{row-gap:1.5rem!important}.row-gap-lg-5{row-gap:3rem!important}.column-gap-lg-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-lg-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-lg-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-lg-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-lg-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-lg-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.object-fit-xl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xl-none{-o-object-fit:none!important;object-fit:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.25rem!important}.row-gap-xl-2{row-gap:.5rem!important}.row-gap-xl-3{row-gap:1rem!important}.row-gap-xl-4{row-gap:1.5rem!important}.row-gap-xl-5{row-gap:3rem!important}.column-gap-xl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.object-fit-xxl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xxl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xxl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xxl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xxl-none{-o-object-fit:none!important;object-fit:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.25rem!important}.row-gap-xxl-2{row-gap:.5rem!important}.row-gap-xxl-3{row-gap:1rem!important}.row-gap-xxl-4{row-gap:1.5rem!important}.row-gap-xxl-5{row-gap:3rem!important}.column-gap-xxl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xxl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xxl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xxl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xxl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xxl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-inline-grid{display:inline-grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} /*# sourceMappingURL=bootstrap.min.css.map */ graphql-ruby-2.5.19/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.js000066400000000000000000002355221514115062600256450ustar00rootroot00000000000000/*! * Bootstrap v5.3.3 (https://getbootstrap.com/) * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=new Map,e={set(e,i,n){t.has(e)||t.set(e,new Map);const s=t.get(e);s.has(i)||0===s.size?s.set(i,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(e,i)=>t.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=I(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),n.oneOff&&N.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return P(n,{delegateTarget:t}),i.oneOff&&N.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function $(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function I(t){return t=t.replace(y,""),T[t]||t}const N={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))$(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(w,"");a&&!e.includes(s)||D(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;D(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==I(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=P(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function P(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function j(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function M(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const F={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${M(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${M(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=j(t.dataset[n])}return e},getDataAttribute:(t,e)=>j(t.getAttribute(`data-bs-${M(e)}`))};class H{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=o(e)?F.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...o(e)?F.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],r=o(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${r}" but expected type "${s}".`)}var i}}class W extends H{constructor(t,i){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(i),e.set(this._element,this.constructor.DATA_KEY,this))}dispose(){e.remove(this._element,this.constructor.DATA_KEY),N.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return e.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.3"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const B=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map((t=>n(t))).join(","):null},z={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!l(t)&&a(t)))},getSelectorFromElement(t){const e=B(t);return e&&z.findOne(e)?e:null},getElementFromSelector(t){const e=B(t);return e?z.findOne(e):null},getMultipleElementsFromSelector(t){const e=B(t);return e?z.find(e):[]}},R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;N.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),l(this))return;const s=z.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},q=".bs.alert",V=`close${q}`,K=`closed${q}`;class Q extends W{static get NAME(){return"alert"}close(){if(N.trigger(this._element,V).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),N.trigger(this._element,K),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Q.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(Q,"close"),m(Q);const X='[data-bs-toggle="button"]';class Y extends W{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=Y.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}N.on(document,"click.bs.button.data-api",X,(t=>{t.preventDefault();const e=t.target.closest(X);Y.getOrCreateInstance(e).toggle()})),m(Y);const U=".bs.swipe",G=`touchstart${U}`,J=`touchmove${U}`,Z=`touchend${U}`,tt=`pointerdown${U}`,et=`pointerup${U}`,it={endCallback:null,leftCallback:null,rightCallback:null},nt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class st extends H{constructor(t,e){super(),this._element=t,t&&st.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return it}static get DefaultType(){return nt}static get NAME(){return"swipe"}dispose(){N.off(this._element,U)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),g(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&g(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(N.on(this._element,tt,(t=>this._start(t))),N.on(this._element,et,(t=>this._end(t))),this._element.classList.add("pointer-event")):(N.on(this._element,G,(t=>this._start(t))),N.on(this._element,J,(t=>this._move(t))),N.on(this._element,Z,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const ot=".bs.carousel",rt=".data-api",at="next",lt="prev",ct="left",ht="right",dt=`slide${ot}`,ut=`slid${ot}`,ft=`keydown${ot}`,pt=`mouseenter${ot}`,mt=`mouseleave${ot}`,gt=`dragstart${ot}`,_t=`load${ot}${rt}`,bt=`click${ot}${rt}`,vt="carousel",yt="active",wt=".active",At=".carousel-item",Et=wt+At,Tt={ArrowLeft:ht,ArrowRight:ct},Ct={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Ot={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class xt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=z.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===vt&&this.cycle()}static get Default(){return Ct}static get DefaultType(){return Ot}static get NAME(){return"carousel"}next(){this._slide(at)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(lt)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?N.one(this._element,ut,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void N.one(this._element,ut,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?at:lt;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&N.on(this._element,ft,(t=>this._keydown(t))),"hover"===this._config.pause&&(N.on(this._element,pt,(()=>this.pause())),N.on(this._element,mt,(()=>this._maybeEnableCycle()))),this._config.touch&&st.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of z.find(".carousel-item img",this._element))N.on(t,gt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(ct)),rightCallback:()=>this._slide(this._directionToOrder(ht)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new st(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Tt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=z.findOne(wt,this._indicatorsElement);e.classList.remove(yt),e.removeAttribute("aria-current");const i=z.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(yt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===at,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>N.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(dt).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(yt),i.classList.remove(yt,c,l),this._isSliding=!1,r(ut)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return z.findOne(Et,this._element)}_getItems(){return z.find(At,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===ct?lt:at:t===ct?at:lt}_orderToDirection(t){return p()?t===lt?ct:ht:t===lt?ht:ct}static jQueryInterface(t){return this.each((function(){const e=xt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}N.on(document,bt,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=z.getElementFromSelector(this);if(!e||!e.classList.contains(vt))return;t.preventDefault();const i=xt.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===F.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),N.on(window,_t,(()=>{const t=z.find('[data-bs-ride="carousel"]');for(const e of t)xt.getOrCreateInstance(e)})),m(xt);const kt=".bs.collapse",Lt=`show${kt}`,St=`shown${kt}`,Dt=`hide${kt}`,$t=`hidden${kt}`,It=`click${kt}.data-api`,Nt="show",Pt="collapse",jt="collapsing",Mt=`:scope .${Pt} .${Pt}`,Ft='[data-bs-toggle="collapse"]',Ht={parent:null,toggle:!0},Wt={parent:"(null|element)",toggle:"boolean"};class Bt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=z.find(Ft);for(const t of i){const e=z.getSelectorFromElement(t),i=z.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Ht}static get DefaultType(){return Wt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Bt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(N.trigger(this._element,Lt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Pt),this._element.classList.add(jt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(jt),this._element.classList.add(Pt,Nt),this._element.style[e]="",N.trigger(this._element,St)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(N.trigger(this._element,Dt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(jt),this._element.classList.remove(Pt,Nt);for(const t of this._triggerArray){const e=z.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(jt),this._element.classList.add(Pt),N.trigger(this._element,$t)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(Nt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Ft);for(const e of t){const t=z.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=z.find(Mt,this._config.parent);return z.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Bt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}N.on(document,It,Ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of z.getMultipleElementsFromSelector(this))Bt.getOrCreateInstance(t,{toggle:!1}).toggle()})),m(Bt);var zt="top",Rt="bottom",qt="right",Vt="left",Kt="auto",Qt=[zt,Rt,qt,Vt],Xt="start",Yt="end",Ut="clippingParents",Gt="viewport",Jt="popper",Zt="reference",te=Qt.reduce((function(t,e){return t.concat([e+"-"+Xt,e+"-"+Yt])}),[]),ee=[].concat(Qt,[Kt]).reduce((function(t,e){return t.concat([e,e+"-"+Xt,e+"-"+Yt])}),[]),ie="beforeRead",ne="read",se="afterRead",oe="beforeMain",re="main",ae="afterMain",le="beforeWrite",ce="write",he="afterWrite",de=[ie,ne,se,oe,re,ae,le,ce,he];function ue(t){return t?(t.nodeName||"").toLowerCase():null}function fe(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function pe(t){return t instanceof fe(t).Element||t instanceof Element}function me(t){return t instanceof fe(t).HTMLElement||t instanceof HTMLElement}function ge(t){return"undefined"!=typeof ShadowRoot&&(t instanceof fe(t).ShadowRoot||t instanceof ShadowRoot)}const _e={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];me(s)&&ue(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});me(n)&&ue(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function be(t){return t.split("-")[0]}var ve=Math.max,ye=Math.min,we=Math.round;function Ae(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function Ee(){return!/^((?!chrome|android).)*safari/i.test(Ae())}function Te(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&me(t)&&(s=t.offsetWidth>0&&we(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&we(n.height)/t.offsetHeight||1);var r=(pe(t)?fe(t):window).visualViewport,a=!Ee()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function Ce(t){var e=Te(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Oe(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&ge(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function xe(t){return fe(t).getComputedStyle(t)}function ke(t){return["table","td","th"].indexOf(ue(t))>=0}function Le(t){return((pe(t)?t.ownerDocument:t.document)||window.document).documentElement}function Se(t){return"html"===ue(t)?t:t.assignedSlot||t.parentNode||(ge(t)?t.host:null)||Le(t)}function De(t){return me(t)&&"fixed"!==xe(t).position?t.offsetParent:null}function $e(t){for(var e=fe(t),i=De(t);i&&ke(i)&&"static"===xe(i).position;)i=De(i);return i&&("html"===ue(i)||"body"===ue(i)&&"static"===xe(i).position)?e:i||function(t){var e=/firefox/i.test(Ae());if(/Trident/i.test(Ae())&&me(t)&&"fixed"===xe(t).position)return null;var i=Se(t);for(ge(i)&&(i=i.host);me(i)&&["html","body"].indexOf(ue(i))<0;){var n=xe(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Ie(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function Ne(t,e,i){return ve(t,ye(e,i))}function Pe(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function je(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const Me={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=be(i.placement),l=Ie(a),c=[Vt,qt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return Pe("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:je(t,Qt))}(s.padding,i),d=Ce(o),u="y"===l?zt:Vt,f="y"===l?Rt:qt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=$e(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=Ne(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Oe(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Fe(t){return t.split("-")[1]}var He={top:"auto",right:"auto",bottom:"auto",left:"auto"};function We(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,m=void 0===p?0:p,g="function"==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=Vt,y=zt,w=window;if(c){var A=$e(i),E="clientHeight",T="clientWidth";A===fe(i)&&"static"!==xe(A=Le(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===zt||(s===Vt||s===qt)&&o===Yt)&&(y=Rt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==Vt&&(s!==zt&&s!==Rt||o!==Yt)||(v=qt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&He),x=!0===h?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:we(i*s)/s||0,y:we(n*s)/s||0}}({x:f,y:m},fe(i)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?m+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const Be={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:be(e.placement),variation:Fe(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,We(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,We(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var ze={passive:!0};const Re={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=fe(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,ze)})),a&&l.addEventListener("resize",i.update,ze),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,ze)})),a&&l.removeEventListener("resize",i.update,ze)}},data:{}};var qe={left:"right",right:"left",bottom:"top",top:"bottom"};function Ve(t){return t.replace(/left|right|bottom|top/g,(function(t){return qe[t]}))}var Ke={start:"end",end:"start"};function Qe(t){return t.replace(/start|end/g,(function(t){return Ke[t]}))}function Xe(t){var e=fe(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ye(t){return Te(Le(t)).left+Xe(t).scrollLeft}function Ue(t){var e=xe(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ge(t){return["html","body","#document"].indexOf(ue(t))>=0?t.ownerDocument.body:me(t)&&Ue(t)?t:Ge(Se(t))}function Je(t,e){var i;void 0===e&&(e=[]);var n=Ge(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=fe(n),r=s?[o].concat(o.visualViewport||[],Ue(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Je(Se(r)))}function Ze(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function ti(t,e,i){return e===Gt?Ze(function(t,e){var i=fe(t),n=Le(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Ee();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Ye(t),y:l}}(t,i)):pe(e)?function(t,e){var i=Te(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):Ze(function(t){var e,i=Le(t),n=Xe(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ve(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ve(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ye(t),l=-n.scrollTop;return"rtl"===xe(s||i).direction&&(a+=ve(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Le(t)))}function ei(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?be(s):null,r=s?Fe(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case zt:e={x:a,y:i.y-n.height};break;case Rt:e={x:a,y:i.y+i.height};break;case qt:e={x:i.x+i.width,y:l};break;case Vt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?Ie(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case Xt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Yt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ii(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Ut:a,c=i.rootBoundary,h=void 0===c?Gt:c,d=i.elementContext,u=void 0===d?Jt:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=Pe("number"!=typeof g?g:je(g,Qt)),b=u===Jt?Zt:Jt,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=Je(Se(t)),i=["absolute","fixed"].indexOf(xe(t).position)>=0&&me(t)?$e(t):t;return pe(i)?e.filter((function(t){return pe(t)&&Oe(t,i)&&"body"!==ue(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=ti(t,i,n);return e.top=ve(s.top,e.top),e.right=ye(s.right,e.right),e.bottom=ye(s.bottom,e.bottom),e.left=ve(s.left,e.left),e}),ti(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(pe(y)?y:y.contextElement||Le(t.elements.popper),l,h,r),A=Te(t.elements.reference),E=ei({reference:A,element:v,strategy:"absolute",placement:s}),T=Ze(Object.assign({},v,E)),C=u===Jt?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===Jt&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[qt,Rt].indexOf(t)>=0?1:-1,i=[zt,Rt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function ni(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?ee:l,h=Fe(n),d=h?a?te:te.filter((function(t){return Fe(t)===h})):Qt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ii(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[be(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const si={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=be(g),b=l||(_!==g&&p?function(t){if(be(t)===Kt)return[];var e=Ve(t);return[Qe(t),e,Qe(e)]}(g):[Ve(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(be(i)===Kt?ni(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?"width":"height",D=ii(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),$=L?k?qt:Vt:k?Rt:zt;y[S]>w[S]&&($=Ve($));var I=Ve($),N=[];if(o&&N.push(D[x]<=0),a&&N.push(D[$]<=0,D[I]<=0),N.every((function(t){return t}))){T=O,E=!1;break}A.set(O,N)}if(E)for(var P=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},j=p?3:1;j>0&&"break"!==P(j);j--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function oi(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function ri(t){return[zt,qt,Rt,Vt].some((function(e){return t[e]>=0}))}const ai={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ii(e,{elementContext:"reference"}),a=ii(e,{altBoundary:!0}),l=oi(r,n),c=oi(a,s,o),h=ri(l),d=ri(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},li={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=ee.reduce((function(t,i){return t[i]=function(t,e,i){var n=be(t),s=[Vt,zt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[Vt,qt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},ci={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=ei({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},hi={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ii(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=be(e.placement),b=Fe(e.placement),v=!b,y=Ie(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S="y"===y?zt:Vt,D="y"===y?Rt:qt,$="y"===y?"height":"width",I=A[y],N=I+g[S],P=I-g[D],j=f?-T[$]/2:0,M=b===Xt?E[$]:T[$],F=b===Xt?-T[$]:-E[$],H=e.elements.arrow,W=f&&H?Ce(H):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=Ne(0,E[$],W[$]),V=v?E[$]/2-j-q-z-O.mainAxis:M-q-z-O.mainAxis,K=v?-E[$]/2+j+q+R+O.mainAxis:F+q+R+O.mainAxis,Q=e.elements.arrow&&$e(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=I+K-Y,G=Ne(f?ye(N,I+V-Y-X):N,I,f?ve(P,U):P);A[y]=G,k[y]=G-I}if(a){var J,Z="x"===y?zt:Vt,tt="x"===y?Rt:qt,et=A[w],it="y"===w?"height":"width",nt=et+g[Z],st=et-g[tt],ot=-1!==[zt,Vt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=Ne(t,e,i);return n>i?i:n}(at,et,lt):Ne(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function di(t,e,i){void 0===i&&(i=!1);var n,s,o=me(e),r=me(e)&&function(t){var e=t.getBoundingClientRect(),i=we(e.width)/t.offsetWidth||1,n=we(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=Le(e),l=Te(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==ue(e)||Ue(a))&&(c=(n=e)!==fe(n)&&me(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:Xe(n)),me(e)?((h=Te(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Ye(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function ui(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var fi={placement:"bottom",modifiers:[],strategy:"absolute"};function pi(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(F.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...g(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=z.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Ti,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=qi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=z.find(Ni);for(const i of e){const e=qi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Ei,Ti].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Ii)?this:z.prev(this,Ii)[0]||z.next(this,Ii)[0]||z.findOne(Ii,t.delegateTarget.parentNode),o=qi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}N.on(document,Si,Ii,qi.dataApiKeydownHandler),N.on(document,Si,Pi,qi.dataApiKeydownHandler),N.on(document,Li,qi.clearMenus),N.on(document,Di,qi.clearMenus),N.on(document,Li,Ii,(function(t){t.preventDefault(),qi.getOrCreateInstance(this).toggle()})),m(qi);const Vi="backdrop",Ki="show",Qi=`mousedown.bs.${Vi}`,Xi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Yi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ui extends H{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Xi}static get DefaultType(){return Yi}static get NAME(){return Vi}show(t){if(!this._config.isVisible)return void g(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(Ki),this._emulateAnimation((()=>{g(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Ki),this._emulateAnimation((()=>{this.dispose(),g(t)}))):g(t)}dispose(){this._isAppended&&(N.off(this._element,Qi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),N.on(t,Qi,(()=>{g(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const Gi=".bs.focustrap",Ji=`focusin${Gi}`,Zi=`keydown.tab${Gi}`,tn="backward",en={autofocus:!0,trapElement:null},nn={autofocus:"boolean",trapElement:"element"};class sn extends H{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return en}static get DefaultType(){return nn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),N.off(document,Gi),N.on(document,Ji,(t=>this._handleFocusin(t))),N.on(document,Zi,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,N.off(document,Gi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=z.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===tn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?tn:"forward")}}const on=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",rn=".sticky-top",an="padding-right",ln="margin-right";class cn{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,an,(e=>e+t)),this._setElementAttributes(on,an,(e=>e+t)),this._setElementAttributes(rn,ln,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,an),this._resetElementAttributes(on,an),this._resetElementAttributes(rn,ln)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&F.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=F.getDataAttribute(t,e);null!==i?(F.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of z.find(t,this._element))e(i)}}const hn=".bs.modal",dn=`hide${hn}`,un=`hidePrevented${hn}`,fn=`hidden${hn}`,pn=`show${hn}`,mn=`shown${hn}`,gn=`resize${hn}`,_n=`click.dismiss${hn}`,bn=`mousedown.dismiss${hn}`,vn=`keydown.dismiss${hn}`,yn=`click${hn}.data-api`,wn="modal-open",An="show",En="modal-static",Tn={backdrop:!0,focus:!0,keyboard:!0},Cn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class On extends W{constructor(t,e){super(t,e),this._dialog=z.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new cn,this._addEventListeners()}static get Default(){return Tn}static get DefaultType(){return Cn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||N.trigger(this._element,pn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(wn),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(N.trigger(this._element,dn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(An),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){N.off(window,hn),N.off(this._dialog,hn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ui({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=z.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(An),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,N.trigger(this._element,mn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){N.on(this._element,vn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),N.on(window,gn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),N.on(this._element,bn,(t=>{N.one(this._element,_n,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(wn),this._resetAdjustments(),this._scrollBar.reset(),N.trigger(this._element,fn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(N.trigger(this._element,un).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(En)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(En),this._queueCallback((()=>{this._element.classList.remove(En),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=On.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}N.on(document,yn,'[data-bs-toggle="modal"]',(function(t){const e=z.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),N.one(e,pn,(t=>{t.defaultPrevented||N.one(e,fn,(()=>{a(this)&&this.focus()}))}));const i=z.findOne(".modal.show");i&&On.getInstance(i).hide(),On.getOrCreateInstance(e).toggle(this)})),R(On),m(On);const xn=".bs.offcanvas",kn=".data-api",Ln=`load${xn}${kn}`,Sn="show",Dn="showing",$n="hiding",In=".offcanvas.show",Nn=`show${xn}`,Pn=`shown${xn}`,jn=`hide${xn}`,Mn=`hidePrevented${xn}`,Fn=`hidden${xn}`,Hn=`resize${xn}`,Wn=`click${xn}${kn}`,Bn=`keydown.dismiss${xn}`,zn={backdrop:!0,keyboard:!0,scroll:!1},Rn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class qn extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return zn}static get DefaultType(){return Rn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||N.trigger(this._element,Nn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new cn).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Dn),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Sn),this._element.classList.remove(Dn),N.trigger(this._element,Pn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(N.trigger(this._element,jn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add($n),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Sn,$n),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new cn).reset(),N.trigger(this._element,Fn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ui({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():N.trigger(this._element,Mn)}:null})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_addEventListeners(){N.on(this._element,Bn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():N.trigger(this._element,Mn))}))}static jQueryInterface(t){return this.each((function(){const e=qn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}N.on(document,Wn,'[data-bs-toggle="offcanvas"]',(function(t){const e=z.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this))return;N.one(e,Fn,(()=>{a(this)&&this.focus()}));const i=z.findOne(In);i&&i!==e&&qn.getInstance(i).hide(),qn.getOrCreateInstance(e).toggle(this)})),N.on(window,Ln,(()=>{for(const t of z.find(In))qn.getOrCreateInstance(t).show()})),N.on(window,Hn,(()=>{for(const t of z.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&qn.getOrCreateInstance(t).hide()})),R(qn),m(qn);const Vn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Kn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Qn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Xn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Kn.has(i)||Boolean(Qn.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Yn={allowList:Vn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Un={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Gn={entry:"(string|element|function|null)",selector:"(string|element)"};class Jn extends H{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Yn}static get DefaultType(){return Un}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Gn)}_setContent(t,e,i){const n=z.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Xn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return g(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Zn=new Set(["sanitize","allowList","sanitizeFn"]),ts="fade",es="show",is=".modal",ns="hide.bs.modal",ss="hover",os="focus",rs={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},as={allowList:Vn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},ls={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class cs extends W{constructor(t,e){if(void 0===vi)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return as}static get DefaultType(){return ls}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),N.off(this._element.closest(is),ns,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=N.trigger(this._element,this.constructor.eventName("show")),e=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),N.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.on(t,"mouseover",h);this._queueCallback((()=>{N.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!N.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.off(t,"mouseover",h);this._activeTrigger.click=!1,this._activeTrigger[os]=!1,this._activeTrigger[ss]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),N.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ts,es),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ts),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Jn({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ts)}_isShown(){return this.tip&&this.tip.classList.contains(es)}_createPopper(t){const e=g(this._config.placement,[this,t,this._element]),i=rs[e.toUpperCase()];return bi(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return g(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...g(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)N.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===ss?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===ss?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");N.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?os:ss]=!0,e._enter()})),N.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?os:ss]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},N.on(this._element.closest(is),ns,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=F.getDataAttributes(this._element);for(const t of Object.keys(e))Zn.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=cs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(cs);const hs={...cs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},ds={...cs.DefaultType,content:"(null|string|element|function)"};class us extends cs{static get Default(){return hs}static get DefaultType(){return ds}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=us.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(us);const fs=".bs.scrollspy",ps=`activate${fs}`,ms=`click${fs}`,gs=`load${fs}.data-api`,_s="active",bs="[href]",vs=".nav-link",ys=`${vs}, .nav-item > ${vs}, .list-group-item`,ws={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},As={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Es extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return ws}static get DefaultType(){return As}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=r(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(N.off(this._config.target,ms),N.on(this._config.target,ms,bs,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=z.find(bs,this._config.target);for(const e of t){if(!e.hash||l(e))continue;const t=z.findOne(decodeURI(e.hash),this._element);a(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(_s),this._activateParents(t),N.trigger(this._element,ps,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))z.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(_s);else for(const e of z.parents(t,".nav, .list-group"))for(const t of z.prev(e,ys))t.classList.add(_s)}_clearActiveClass(t){t.classList.remove(_s);const e=z.find(`${bs}.${_s}`,t);for(const t of e)t.classList.remove(_s)}static jQueryInterface(t){return this.each((function(){const e=Es.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(window,gs,(()=>{for(const t of z.find('[data-bs-spy="scroll"]'))Es.getOrCreateInstance(t)})),m(Es);const Ts=".bs.tab",Cs=`hide${Ts}`,Os=`hidden${Ts}`,xs=`show${Ts}`,ks=`shown${Ts}`,Ls=`click${Ts}`,Ss=`keydown${Ts}`,Ds=`load${Ts}`,$s="ArrowLeft",Is="ArrowRight",Ns="ArrowUp",Ps="ArrowDown",js="Home",Ms="End",Fs="active",Hs="fade",Ws="show",Bs=".dropdown-toggle",zs=`:not(${Bs})`,Rs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',qs=`.nav-link${zs}, .list-group-item${zs}, [role="tab"]${zs}, ${Rs}`,Vs=`.${Fs}[data-bs-toggle="tab"], .${Fs}[data-bs-toggle="pill"], .${Fs}[data-bs-toggle="list"]`;class Ks extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),N.on(this._element,Ss,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?N.trigger(e,Cs,{relatedTarget:t}):null;N.trigger(t,xs,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Fs),this._activate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),N.trigger(t,ks,{relatedTarget:e})):t.classList.add(Ws)}),t,t.classList.contains(Hs)))}_deactivate(t,e){t&&(t.classList.remove(Fs),t.blur(),this._deactivate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),N.trigger(t,Os,{relatedTarget:e})):t.classList.remove(Ws)}),t,t.classList.contains(Hs)))}_keydown(t){if(![$s,Is,Ns,Ps,js,Ms].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!l(t)));let i;if([js,Ms].includes(t.key))i=e[t.key===js?0:e.length-1];else{const n=[Is,Ps].includes(t.key);i=b(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Ks.getOrCreateInstance(i).show())}_getChildren(){return z.find(qs,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=z.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=z.findOne(t,i);s&&s.classList.toggle(n,e)};n(Bs,Fs),n(".dropdown-menu",Ws),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Fs)}_getInnerElement(t){return t.matches(qs)?t:z.findOne(qs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Ks.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(document,Ls,Rs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this)||Ks.getOrCreateInstance(this).show()})),N.on(window,Ds,(()=>{for(const t of z.find(Vs))Ks.getOrCreateInstance(t)})),m(Ks);const Qs=".bs.toast",Xs=`mouseover${Qs}`,Ys=`mouseout${Qs}`,Us=`focusin${Qs}`,Gs=`focusout${Qs}`,Js=`hide${Qs}`,Zs=`hidden${Qs}`,to=`show${Qs}`,eo=`shown${Qs}`,io="hide",no="show",so="showing",oo={animation:"boolean",autohide:"boolean",delay:"number"},ro={animation:!0,autohide:!0,delay:5e3};class ao extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return ro}static get DefaultType(){return oo}static get NAME(){return"toast"}show(){N.trigger(this._element,to).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(io),d(this._element),this._element.classList.add(no,so),this._queueCallback((()=>{this._element.classList.remove(so),N.trigger(this._element,eo),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(N.trigger(this._element,Js).defaultPrevented||(this._element.classList.add(so),this._queueCallback((()=>{this._element.classList.add(io),this._element.classList.remove(so,no),N.trigger(this._element,Zs)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(no),super.dispose()}isShown(){return this._element.classList.contains(no)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){N.on(this._element,Xs,(t=>this._onInteraction(t,!0))),N.on(this._element,Ys,(t=>this._onInteraction(t,!1))),N.on(this._element,Us,(t=>this._onInteraction(t,!0))),N.on(this._element,Gs,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=ao.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(ao),m(ao),{Alert:Q,Button:Y,Carousel:xt,Collapse:Bt,Dropdown:qi,Modal:On,Offcanvas:qn,Popover:us,ScrollSpy:Es,Tab:Ks,Toast:ao,Tooltip:cs}})); //# sourceMappingURL=bootstrap.bundle.min.js.map graphql-ruby-2.5.19/lib/graphql/dashboard/statics/charts.min.css000066400000000000000000002247251514115062600246270ustar00rootroot00000000000000@property --color-1{syntax:"";initial-value:transparent;inherits:true}@property --color-2{syntax:"";initial-value:transparent;inherits:true}@property --color-3{syntax:"";initial-value:transparent;inherits:true}@property --color-4{syntax:"";initial-value:transparent;inherits:true}@property --color-5{syntax:"";initial-value:transparent;inherits:true}@property --color-6{syntax:"";initial-value:transparent;inherits:true}@property --color-7{syntax:"";initial-value:transparent;inherits:true}@property --color-8{syntax:"";initial-value:transparent;inherits:true}@property --color-9{syntax:"";initial-value:transparent;inherits:true}@property --color-10{syntax:"";initial-value:transparent;inherits:true}@property --color{syntax:"";inherits:true}@property --chart-bg-color{syntax:"";inherits:true}@property --aspect-ratio{syntax:"";initial-value:auto;inherits:true}@property --labels-size{syntax:"";initial-value:0;inherits:true}@property --labels-align-block{syntax:"";inherits:true}@property --labels-align-inline{syntax:"";inherits:true}@property --primary-axis-width{syntax:"";initial-value:1px;inherits:true}@property --secondary-axes-width{syntax:"";initial-value:1px;inherits:true}@property --data-axes-width{syntax:"";initial-value:1px;inherits:true}@property --legend-border-width{syntax:"";initial-value:1px;inherits:true}@property --primary-axis-style{syntax:"";initial-value:solid;inherits:true}@property --secondary-axes-style{syntax:"";initial-value:solid;inherits:true}@property --data-axes-style{syntax:"";initial-value:solid;inherits:true}@property --legend-border-style{syntax:"";initial-value:solid;inherits:true}@property --primary-axis-color{syntax:"";initial-value:transparent;inherits:true}@property --secondary-axes-color{syntax:"";initial-value:transparent;inherits:true}@property --data-axes-color{syntax:"";initial-value:transparent;inherits:true}@property --legend-border-color{syntax:"";initial-value:transparent;inherits:true}@property --start{syntax:"";inherits:true}@property --end{syntax:"";inherits:true}@property --size{syntax:"";inherits:true}@property --line-size{syntax:"";inherits:true}.charts-css{--color-1:rgba(240,50,50,.75);--color-2:rgba(255,180,50,.75);--color-3:rgba(255,220,90,.75);--color-4:rgba(100,210,80,.75);--color-5:rgba(90,165,255,.75);--color-6:rgba(170,90,240,.75);--color-7:hsla(0,0%,71%,.75);--color-8:hsla(0,0%,43%,.75);--color-9:hsla(40,26%,55%,.75);--color-10:rgba(130,50,20,.75);--chart-bg-color:#f5f5f5;--primary-axis-color:#000;--primary-axis-style:solid;--primary-axis-width:1px;--secondary-axes-color:rgba(0,0,0,.15);--secondary-axes-style:solid;--secondary-axes-width:1px;--data-axes-color:rgba(0,0,0,.15);--data-axes-style:solid;--data-axes-width:1px;--legend-border-color:#c8c8c8;--legend-border-style:solid;--legend-border-width:1px;border:0;display:block;height:100%;margin:0 auto;padding:0;position:relative;-webkit-print-color-adjust:exact;print-color-adjust:exact;width:100%}.charts-css,.charts-css *,.charts-css ::after,.charts-css ::before,.charts-css::after,.charts-css::before{-webkit-box-sizing:border-box;box-sizing:border-box}table.charts-css{background-color:transparent;border-collapse:collapse;border-spacing:0;empty-cells:show;overflow:initial}table.charts-css caption,table.charts-css colgroup,table.charts-css tbody,table.charts-css td,table.charts-css th,table.charts-css thead,table.charts-css tr{background-color:transparent;border:0;display:block;margin:0;padding:0}.charts-css.area.show-labels th.hide-label,.charts-css.area.show-labels tr.hide-label th,.charts-css.area:not(.show-labels) tbody tr th,.charts-css.bar.show-labels th.hide-label,.charts-css.bar.show-labels tr.hide-label th,.charts-css.bar:not(.show-labels) tbody tr th,.charts-css.column.show-labels th.hide-label,.charts-css.column.show-labels tr.hide-label th,.charts-css.column:not(.show-labels) tbody tr th,.charts-css.hide-data .data,.charts-css.hide-data .data:not(:focus):not(:focus-within),.charts-css.line.show-labels th.hide-label,.charts-css.line.show-labels tr.hide-label th,.charts-css.line:not(.show-labels) tbody tr th,.charts-css.pie tbody tr th,.charts-css.polar tbody tr,.charts-css.radar tbody tr,.charts-css.radial tbody tr,.charts-css:not(.show-heading) caption,table.charts-css colgroup,table.charts-css tfoot,table.charts-css thead{clip:rect(0,0,0,0);border:0;-webkit-clip-path:inset(50%);clip-path:inset(50%);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}table.charts-css tbody{position:relative}ol.charts-css,ul.charts-css{list-style-type:none}ol.charts-css li,ul.charts-css li{border:0;margin:0;padding:0}.charts-css.show-heading caption{display:block;width:100%}.charts-css.area tbody tr td:nth-of-type(10n+1)::before,.charts-css.bar tbody tr:nth-of-type(10n+1) td,.charts-css.bar.multiple tbody tr td:nth-of-type(10n+1),.charts-css.column tbody tr:nth-of-type(10n+1) td,.charts-css.column.multiple tbody tr td:nth-of-type(10n+1),.charts-css.line tbody tr td:nth-of-type(10n+1)::before{background:var(--color,var(--color-1))}.charts-css.pie tbody tr:nth-of-type(10n+1) td,.charts-css.pie.multiple tbody tr td:nth-of-type(10n+1){--c:var(--color,var(--color-1,transparent))}.charts-css.area tbody tr td:nth-of-type(10n+2)::before,.charts-css.bar tbody tr:nth-of-type(10n+2) td,.charts-css.bar.multiple tbody tr td:nth-of-type(10n+2),.charts-css.column tbody tr:nth-of-type(10n+2) td,.charts-css.column.multiple tbody tr td:nth-of-type(10n+2),.charts-css.line tbody tr td:nth-of-type(10n+2)::before{background:var(--color,var(--color-2))}.charts-css.pie tbody tr:nth-of-type(10n+2) td,.charts-css.pie.multiple tbody tr td:nth-of-type(10n+2){--c:var(--color,var(--color-2,transparent))}.charts-css.area tbody tr td:nth-of-type(10n+3)::before,.charts-css.bar tbody tr:nth-of-type(10n+3) td,.charts-css.bar.multiple tbody tr td:nth-of-type(10n+3),.charts-css.column tbody tr:nth-of-type(10n+3) td,.charts-css.column.multiple tbody tr td:nth-of-type(10n+3),.charts-css.line tbody tr td:nth-of-type(10n+3)::before{background:var(--color,var(--color-3))}.charts-css.pie tbody tr:nth-of-type(10n+3) td,.charts-css.pie.multiple tbody tr td:nth-of-type(10n+3){--c:var(--color,var(--color-3,transparent))}.charts-css.area tbody tr td:nth-of-type(10n+4)::before,.charts-css.bar tbody tr:nth-of-type(10n+4) td,.charts-css.bar.multiple tbody tr td:nth-of-type(10n+4),.charts-css.column tbody tr:nth-of-type(10n+4) td,.charts-css.column.multiple tbody tr td:nth-of-type(10n+4),.charts-css.line tbody tr td:nth-of-type(10n+4)::before{background:var(--color,var(--color-4))}.charts-css.pie tbody tr:nth-of-type(10n+4) td,.charts-css.pie.multiple tbody tr td:nth-of-type(10n+4){--c:var(--color,var(--color-4,transparent))}.charts-css.area tbody tr td:nth-of-type(10n+5)::before,.charts-css.bar tbody tr:nth-of-type(10n+5) td,.charts-css.bar.multiple tbody tr td:nth-of-type(10n+5),.charts-css.column tbody tr:nth-of-type(10n+5) td,.charts-css.column.multiple tbody tr td:nth-of-type(10n+5),.charts-css.line tbody tr td:nth-of-type(10n+5)::before{background:var(--color,var(--color-5))}.charts-css.pie tbody tr:nth-of-type(10n+5) td,.charts-css.pie.multiple tbody tr td:nth-of-type(10n+5){--c:var(--color,var(--color-5,transparent))}.charts-css.area tbody tr td:nth-of-type(10n+6)::before,.charts-css.bar tbody tr:nth-of-type(10n+6) td,.charts-css.bar.multiple tbody tr td:nth-of-type(10n+6),.charts-css.column tbody tr:nth-of-type(10n+6) td,.charts-css.column.multiple tbody tr td:nth-of-type(10n+6),.charts-css.line tbody tr td:nth-of-type(10n+6)::before{background:var(--color,var(--color-6))}.charts-css.pie tbody tr:nth-of-type(10n+6) td,.charts-css.pie.multiple tbody tr td:nth-of-type(10n+6){--c:var(--color,var(--color-6,transparent))}.charts-css.area tbody tr td:nth-of-type(10n+7)::before,.charts-css.bar tbody tr:nth-of-type(10n+7) td,.charts-css.bar.multiple tbody tr td:nth-of-type(10n+7),.charts-css.column tbody tr:nth-of-type(10n+7) td,.charts-css.column.multiple tbody tr td:nth-of-type(10n+7),.charts-css.line tbody tr td:nth-of-type(10n+7)::before{background:var(--color,var(--color-7))}.charts-css.pie tbody tr:nth-of-type(10n+7) td,.charts-css.pie.multiple tbody tr td:nth-of-type(10n+7){--c:var(--color,var(--color-7,transparent))}.charts-css.area tbody tr td:nth-of-type(10n+8)::before,.charts-css.bar tbody tr:nth-of-type(10n+8) td,.charts-css.bar.multiple tbody tr td:nth-of-type(10n+8),.charts-css.column tbody tr:nth-of-type(10n+8) td,.charts-css.column.multiple tbody tr td:nth-of-type(10n+8),.charts-css.line tbody tr td:nth-of-type(10n+8)::before{background:var(--color,var(--color-8))}.charts-css.pie tbody tr:nth-of-type(10n+8) td,.charts-css.pie.multiple tbody tr td:nth-of-type(10n+8){--c:var(--color,var(--color-8,transparent))}.charts-css.area tbody tr td:nth-of-type(10n+9)::before,.charts-css.bar tbody tr:nth-of-type(10n+9) td,.charts-css.bar.multiple tbody tr td:nth-of-type(10n+9),.charts-css.column tbody tr:nth-of-type(10n+9) td,.charts-css.column.multiple tbody tr td:nth-of-type(10n+9),.charts-css.line tbody tr td:nth-of-type(10n+9)::before{background:var(--color,var(--color-9))}.charts-css.pie tbody tr:nth-of-type(10n+9) td,.charts-css.pie.multiple tbody tr td:nth-of-type(10n+9){--c:var(--color,var(--color-9,transparent))}.charts-css.area tbody tr td:nth-of-type(10n+10)::before,.charts-css.bar tbody tr:nth-of-type(10n+10) td,.charts-css.bar.multiple tbody tr td:nth-of-type(10n+10),.charts-css.column tbody tr:nth-of-type(10n+10) td,.charts-css.column.multiple tbody tr td:nth-of-type(10n+10),.charts-css.line tbody tr td:nth-of-type(10n+10)::before{background:var(--color,var(--color-10))}.charts-css.pie tbody tr:nth-of-type(10n+10) td,.charts-css.pie.multiple tbody tr td:nth-of-type(10n+10){--c:var(--color,var(--color-10,transparent))}.charts-css .data{display:-webkit-box;display:-ms-flexbox;display:flex}.charts-css.show-data-on-hover .data{opacity:0;-webkit-transition-duration:.3s;transition-duration:.3s}.charts-css.pie.show-data-on-hover tbody:hover .data,.charts-css.polar.show-data-on-hover tbody:hover .data,.charts-css.radar.show-data-on-hover tbody:hover .data,.charts-css.radial.show-data-on-hover tbody:hover .data,.charts-css.show-data-on-hover tr:hover .data{opacity:1;-webkit-transition-duration:.3s;transition-duration:.3s}.charts-css.bar.data-center tbody tr td,.charts-css.column.data-center tbody tr td{--data-position:center}.charts-css.bar.data-end.reverse tbody tr td,.charts-css.bar.data-outside.reverse tbody tr td,.charts-css.bar.data-start:not(.reverse) tbody tr td,.charts-css.column.data-end:not(.reverse) tbody tr td,.charts-css.column.data-outside:not(.reverse) tbody tr td,.charts-css.column.data-start.reverse tbody tr td{--data-position:flex-start}.charts-css.bar.data-end:not(.reverse) tbody tr td,.charts-css.bar.data-outside:not(.reverse) tbody tr td,.charts-css.bar.data-start.reverse tbody tr td,.charts-css.column.data-end.reverse tbody tr td,.charts-css.column.data-outside.reverse tbody tr td,.charts-css.column.data-start:not(.reverse) tbody tr td{--data-position:flex-end}.charts-css.bar.data-outside:not(.reverse) tbody tr td .data{-webkit-transform:translateX(100%);transform:translateX(100%)}.charts-css.bar.data-outside.reverse tbody tr td .data{-webkit-transform:translateX(-100%);transform:translateX(-100%)}.charts-css.column.data-outside:not(.reverse) tbody tr td .data,.charts-css.column:not(.reverse) tbody tr td .data.outside{-webkit-transform:translateY(-100%);transform:translateY(-100%)}.charts-css.column.data-outside.reverse tbody tr td .data,.charts-css.column.reverse tbody tr td .data.outside{-webkit-transform:translateY(100%);transform:translateY(100%)}.charts-css.area.reverse tbody tr td .data.inside,.charts-css.area.reverse tbody tr td.inside .data,.charts-css.area:not(.reverse) tbody tr td .data.inside,.charts-css.area:not(.reverse) tbody tr td.inside .data,.charts-css.bar.reverse tbody tr td .data.inside,.charts-css.bar.reverse tbody tr td.inside .data,.charts-css.bar:not(.reverse) tbody tr td .data.inside,.charts-css.bar:not(.reverse) tbody tr td.inside .data,.charts-css.column.reverse tbody tr td .data.inside,.charts-css.column.reverse tbody tr td.inside .data,.charts-css.column:not(.reverse) tbody tr td .data.inside,.charts-css.column:not(.reverse) tbody tr td.inside .data,.charts-css.line.reverse tbody tr td .data.inside,.charts-css.line.reverse tbody tr td.inside .data,.charts-css.line:not(.reverse) tbody tr td .data.inside,.charts-css.line:not(.reverse) tbody tr td.inside .data{-webkit-transform:unset;transform:unset}.charts-css.bar{--labels-size:80px}.charts-css.area:not(.show-labels),.charts-css.bar:not(.show-labels),.charts-css.column:not(.show-labels),.charts-css.line:not(.show-labels){--labels-size:0}.charts-css.bar.show-labels tbody tr th{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-box-align:var(--labels-align-block,center);-ms-flex-align:var(--labels-align-block,center);align-items:var(--labels-align-block,center);display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;gap:5px}.charts-css.bar.show-labels.reverse.reverse-labels tbody tr th,.charts-css.bar.show-labels:not(.reverse):not(.reverse-labels) tbody tr th{-webkit-box-pack:var(--labels-align-inline,flex-start);-ms-flex-pack:var(--labels-align-inline,flex-start);justify-content:var(--labels-align-inline,flex-start)}.charts-css.bar.show-labels.reverse:not(.reverse-labels) tbody tr th,.charts-css.bar.show-labels:not(.reverse).reverse-labels tbody tr th{-webkit-box-pack:var(--labels-align-inline,flex-end);-ms-flex-pack:var(--labels-align-inline,flex-end);justify-content:var(--labels-align-inline,flex-end)}.charts-css.area,.charts-css.column,.charts-css.line{--labels-size:1.5rem}.charts-css.area.show-labels tbody tr th,.charts-css.column.show-labels tbody tr th,.charts-css.line.show-labels tbody tr th{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-box-align:var(--labels-align-inline,center);-ms-flex-align:var(--labels-align-inline,center);align-items:var(--labels-align-inline,center);display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.charts-css.area.show-labels.reverse.reverse-labels tbody tr th,.charts-css.area.show-labels:not(.reverse):not(.reverse-labels) tbody tr th,.charts-css.column.show-labels.reverse.reverse-labels tbody tr th,.charts-css.column.show-labels:not(.reverse):not(.reverse-labels) tbody tr th,.charts-css.line.show-labels.reverse.reverse-labels tbody tr th,.charts-css.line.show-labels:not(.reverse):not(.reverse-labels) tbody tr th{-webkit-box-pack:var(--labels-align-block,flex-end);-ms-flex-pack:var(--labels-align-block,flex-end);justify-content:var(--labels-align-block,flex-end)}.charts-css.area.show-labels.reverse:not(.reverse-labels) tbody tr th,.charts-css.area.show-labels:not(.reverse).reverse-labels tbody tr th,.charts-css.column.show-labels.reverse:not(.reverse-labels) tbody tr th,.charts-css.column.show-labels:not(.reverse).reverse-labels tbody tr th,.charts-css.line.show-labels.reverse:not(.reverse-labels) tbody tr th,.charts-css.line.show-labels:not(.reverse).reverse-labels tbody tr th{-webkit-box-pack:var(--labels-align-block,flex-start);-ms-flex-pack:var(--labels-align-block,flex-start);justify-content:var(--labels-align-block,flex-start)}.charts-css.area.labels-align-inline-start tbody tr th,.charts-css.bar.labels-align-inline-start tbody tr th,.charts-css.column.labels-align-inline-start tbody tr th,.charts-css.line.labels-align-inline-start tbody tr th{--labels-align-inline:flex-start}.charts-css.area.labels-align-inline-end tbody tr th,.charts-css.bar.labels-align-inline-end tbody tr th,.charts-css.column.labels-align-inline-end tbody tr th,.charts-css.line.labels-align-inline-end tbody tr th{--labels-align-inline:flex-end}.charts-css.area.labels-align-inline-center tbody tr th,.charts-css.bar.labels-align-inline-center tbody tr th,.charts-css.column.labels-align-inline-center tbody tr th,.charts-css.line.labels-align-inline-center tbody tr th{--labels-align-inline:center}.charts-css.area.labels-align-block-start tbody tr th,.charts-css.bar.labels-align-block-start tbody tr th,.charts-css.column.labels-align-block-start tbody tr th,.charts-css.line.labels-align-block-start tbody tr th{--labels-align-block:flex-start}.charts-css.area.labels-align-block-end tbody tr th,.charts-css.bar.labels-align-block-end tbody tr th,.charts-css.column.labels-align-block-end tbody tr th,.charts-css.line.labels-align-block-end tbody tr th{--labels-align-block:flex-end}.charts-css.area.labels-align-block-center tbody tr th,.charts-css.bar.labels-align-block-center tbody tr th,.charts-css.column.labels-align-block-center tbody tr th,.charts-css.line.labels-align-block-center tbody tr th{--labels-align-block:center}.charts-css.area.show-primary-axis:not(.reverse) tbody tr,.charts-css.column.show-primary-axis:not(.reverse) tbody tr,.charts-css.line.show-primary-axis:not(.reverse) tbody tr{-webkit-border-after:var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color);border-block-end:var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color)}.charts-css.area.show-primary-axis.reverse tbody tr,.charts-css.column.show-primary-axis.reverse tbody tr,.charts-css.line.show-primary-axis.reverse tbody tr{-webkit-border-before:var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color);border-block-start:var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color)}.charts-css.area.show-1-secondary-axes:not(.reverse) tbody tr,.charts-css.column.show-1-secondary-axes:not(.reverse) tbody tr,.charts-css.line.show-1-secondary-axes:not(.reverse) tbody tr{background-image:-webkit-gradient(linear,left top,left bottom,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:100% calc(100% / 1)}.charts-css.area.show-1-secondary-axes.reverse tbody tr,.charts-css.column.show-1-secondary-axes.reverse tbody tr,.charts-css.line.show-1-secondary-axes.reverse tbody tr{background-image:-webkit-gradient(linear,left bottom,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(0deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:100% calc(100% / 1)}.charts-css.area.show-2-secondary-axes:not(.reverse) tbody tr,.charts-css.column.show-2-secondary-axes:not(.reverse) tbody tr,.charts-css.line.show-2-secondary-axes:not(.reverse) tbody tr{background-image:-webkit-gradient(linear,left top,left bottom,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:100% calc(100% / 2)}.charts-css.area.show-2-secondary-axes.reverse tbody tr,.charts-css.column.show-2-secondary-axes.reverse tbody tr,.charts-css.line.show-2-secondary-axes.reverse tbody tr{background-image:-webkit-gradient(linear,left bottom,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(0deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:100% calc(100% / 2)}.charts-css.area.show-3-secondary-axes:not(.reverse) tbody tr,.charts-css.column.show-3-secondary-axes:not(.reverse) tbody tr,.charts-css.line.show-3-secondary-axes:not(.reverse) tbody tr{background-image:-webkit-gradient(linear,left top,left bottom,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:100% calc(100% / 3)}.charts-css.area.show-3-secondary-axes.reverse tbody tr,.charts-css.column.show-3-secondary-axes.reverse tbody tr,.charts-css.line.show-3-secondary-axes.reverse tbody tr{background-image:-webkit-gradient(linear,left bottom,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(0deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:100% calc(100% / 3)}.charts-css.area.show-4-secondary-axes:not(.reverse) tbody tr,.charts-css.column.show-4-secondary-axes:not(.reverse) tbody tr,.charts-css.line.show-4-secondary-axes:not(.reverse) tbody tr{background-image:-webkit-gradient(linear,left top,left bottom,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:100% calc(100% / 4)}.charts-css.area.show-4-secondary-axes.reverse tbody tr,.charts-css.column.show-4-secondary-axes.reverse tbody tr,.charts-css.line.show-4-secondary-axes.reverse tbody tr{background-image:-webkit-gradient(linear,left bottom,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(0deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:100% calc(100% / 4)}.charts-css.area.show-5-secondary-axes:not(.reverse) tbody tr,.charts-css.column.show-5-secondary-axes:not(.reverse) tbody tr,.charts-css.line.show-5-secondary-axes:not(.reverse) tbody tr{background-image:-webkit-gradient(linear,left top,left bottom,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:100% calc(100% / 5)}.charts-css.area.show-5-secondary-axes.reverse tbody tr,.charts-css.column.show-5-secondary-axes.reverse tbody tr,.charts-css.line.show-5-secondary-axes.reverse tbody tr{background-image:-webkit-gradient(linear,left bottom,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(0deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:100% calc(100% / 5)}.charts-css.area.show-6-secondary-axes:not(.reverse) tbody tr,.charts-css.column.show-6-secondary-axes:not(.reverse) tbody tr,.charts-css.line.show-6-secondary-axes:not(.reverse) tbody tr{background-image:-webkit-gradient(linear,left top,left bottom,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:100% calc(100% / 6)}.charts-css.area.show-6-secondary-axes.reverse tbody tr,.charts-css.column.show-6-secondary-axes.reverse tbody tr,.charts-css.line.show-6-secondary-axes.reverse tbody tr{background-image:-webkit-gradient(linear,left bottom,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(0deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:100% calc(100% / 6)}.charts-css.area.show-7-secondary-axes:not(.reverse) tbody tr,.charts-css.column.show-7-secondary-axes:not(.reverse) tbody tr,.charts-css.line.show-7-secondary-axes:not(.reverse) tbody tr{background-image:-webkit-gradient(linear,left top,left bottom,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:100% calc(100% / 7)}.charts-css.area.show-7-secondary-axes.reverse tbody tr,.charts-css.column.show-7-secondary-axes.reverse tbody tr,.charts-css.line.show-7-secondary-axes.reverse tbody tr{background-image:-webkit-gradient(linear,left bottom,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(0deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:100% calc(100% / 7)}.charts-css.area.show-8-secondary-axes:not(.reverse) tbody tr,.charts-css.column.show-8-secondary-axes:not(.reverse) tbody tr,.charts-css.line.show-8-secondary-axes:not(.reverse) tbody tr{background-image:-webkit-gradient(linear,left top,left bottom,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:100% calc(100% / 8)}.charts-css.area.show-8-secondary-axes.reverse tbody tr,.charts-css.column.show-8-secondary-axes.reverse tbody tr,.charts-css.line.show-8-secondary-axes.reverse tbody tr{background-image:-webkit-gradient(linear,left bottom,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(0deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:100% calc(100% / 8)}.charts-css.area.show-9-secondary-axes:not(.reverse) tbody tr,.charts-css.column.show-9-secondary-axes:not(.reverse) tbody tr,.charts-css.line.show-9-secondary-axes:not(.reverse) tbody tr{background-image:-webkit-gradient(linear,left top,left bottom,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:100% calc(100% / 9)}.charts-css.area.show-9-secondary-axes.reverse tbody tr,.charts-css.column.show-9-secondary-axes.reverse tbody tr,.charts-css.line.show-9-secondary-axes.reverse tbody tr{background-image:-webkit-gradient(linear,left bottom,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(0deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:100% calc(100% / 9)}.charts-css.area.show-10-secondary-axes:not(.reverse) tbody tr,.charts-css.column.show-10-secondary-axes:not(.reverse) tbody tr,.charts-css.line.show-10-secondary-axes:not(.reverse) tbody tr{background-image:-webkit-gradient(linear,left top,left bottom,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:100% calc(100% / 10)}.charts-css.area.show-10-secondary-axes.reverse tbody tr,.charts-css.column.show-10-secondary-axes.reverse tbody tr,.charts-css.line.show-10-secondary-axes.reverse tbody tr{background-image:-webkit-gradient(linear,left bottom,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(0deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:100% calc(100% / 10)}.charts-css.area.show-data-axes tbody tr,.charts-css.area.show-dataset-axes tbody tr td,.charts-css.column.show-data-axes tbody tr,.charts-css.column.show-dataset-axes tbody tr td,.charts-css.line.show-data-axes tbody tr,.charts-css.line.show-dataset-axes tbody tr td{-webkit-border-end:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color);border-inline-end:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color)}.charts-css.area.show-data-axes.reverse-data tbody tr:last-of-type,.charts-css.area.show-data-axes:not(.reverse-data) tbody tr:first-of-type,.charts-css.area.show-dataset-axes.reverse-data tbody tr:last-of-type td,.charts-css.area.show-dataset-axes:not(.reverse-data) tbody tr:first-of-type td,.charts-css.column.show-data-axes.reverse-data tbody tr:last-of-type,.charts-css.column.show-data-axes:not(.reverse-data) tbody tr:first-of-type,.charts-css.column.show-dataset-axes.reverse-data tbody tr:last-of-type td,.charts-css.column.show-dataset-axes:not(.reverse-data) tbody tr:first-of-type td,.charts-css.line.show-data-axes.reverse-data tbody tr:last-of-type,.charts-css.line.show-data-axes:not(.reverse-data) tbody tr:first-of-type,.charts-css.line.show-dataset-axes.reverse-data tbody tr:last-of-type td,.charts-css.line.show-dataset-axes:not(.reverse-data) tbody tr:first-of-type td{-webkit-border-start:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color);border-inline-start:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color)}.charts-css.bar.show-primary-axis:not(.reverse) tbody tr{-webkit-border-start:var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color);border-inline-start:var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color)}.charts-css.bar.show-primary-axis.reverse tbody tr{-webkit-border-end:var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color);border-inline-end:var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color)}.charts-css.bar.show-1-secondary-axes:not(.reverse) tbody tr{background-image:-webkit-gradient(linear,right top,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(-90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:calc(100% / 1) 100%}.charts-css.bar.show-1-secondary-axes.reverse tbody tr{background-image:-webkit-gradient(linear,left top,right top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:calc(100% / 1) 100%}.charts-css.bar.show-2-secondary-axes:not(.reverse) tbody tr{background-image:-webkit-gradient(linear,right top,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(-90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:calc(100% / 2) 100%}.charts-css.bar.show-2-secondary-axes.reverse tbody tr{background-image:-webkit-gradient(linear,left top,right top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:calc(100% / 2) 100%}.charts-css.bar.show-3-secondary-axes:not(.reverse) tbody tr{background-image:-webkit-gradient(linear,right top,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(-90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:calc(100% / 3) 100%}.charts-css.bar.show-3-secondary-axes.reverse tbody tr{background-image:-webkit-gradient(linear,left top,right top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:calc(100% / 3) 100%}.charts-css.bar.show-4-secondary-axes:not(.reverse) tbody tr{background-image:-webkit-gradient(linear,right top,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(-90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:calc(100% / 4) 100%}.charts-css.bar.show-4-secondary-axes.reverse tbody tr{background-image:-webkit-gradient(linear,left top,right top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:calc(100% / 4) 100%}.charts-css.bar.show-5-secondary-axes:not(.reverse) tbody tr{background-image:-webkit-gradient(linear,right top,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(-90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:calc(100% / 5) 100%}.charts-css.bar.show-5-secondary-axes.reverse tbody tr{background-image:-webkit-gradient(linear,left top,right top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:calc(100% / 5) 100%}.charts-css.bar.show-6-secondary-axes:not(.reverse) tbody tr{background-image:-webkit-gradient(linear,right top,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(-90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:calc(100% / 6) 100%}.charts-css.bar.show-6-secondary-axes.reverse tbody tr{background-image:-webkit-gradient(linear,left top,right top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:calc(100% / 6) 100%}.charts-css.bar.show-7-secondary-axes:not(.reverse) tbody tr{background-image:-webkit-gradient(linear,right top,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(-90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:calc(100% / 7) 100%}.charts-css.bar.show-7-secondary-axes.reverse tbody tr{background-image:-webkit-gradient(linear,left top,right top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:calc(100% / 7) 100%}.charts-css.bar.show-8-secondary-axes:not(.reverse) tbody tr{background-image:-webkit-gradient(linear,right top,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(-90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:calc(100% / 8) 100%}.charts-css.bar.show-8-secondary-axes.reverse tbody tr{background-image:-webkit-gradient(linear,left top,right top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:calc(100% / 8) 100%}.charts-css.bar.show-9-secondary-axes:not(.reverse) tbody tr{background-image:-webkit-gradient(linear,right top,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(-90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:calc(100% / 9) 100%}.charts-css.bar.show-9-secondary-axes.reverse tbody tr{background-image:-webkit-gradient(linear,left top,right top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:calc(100% / 9) 100%}.charts-css.bar.show-10-secondary-axes:not(.reverse) tbody tr{background-image:-webkit-gradient(linear,right top,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(-90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:calc(100% / 10) 100%}.charts-css.bar.show-10-secondary-axes.reverse tbody tr{background-image:-webkit-gradient(linear,left top,right top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width));background-size:calc(100% / 10) 100%}.charts-css.bar.show-data-axes tbody tr,.charts-css.bar.show-dataset-axes tbody tr td{-webkit-border-after:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color);border-block-end:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color)}.charts-css.bar.show-data-axes.reverse-data tbody tr:last-of-type,.charts-css.bar.show-data-axes:not(.reverse-data) tbody tr:first-of-type,.charts-css.bar.show-dataset-axes.reverse-data tbody tr:last-of-type td,.charts-css.bar.show-dataset-axes:not(.reverse-data) tbody tr:first-of-type td{-webkit-border-before:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color);border-block-start:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color)}.charts-css.pie.show-primary-axis tbody,.charts-css.polar.show-primary-axis tbody,.charts-css.radar.show-primary-axis tbody,.charts-css.radial.show-primary-axis tbody{border:var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color)}.charts-css.pie.show-1-secondary-axes tbody::after,.charts-css.polar.show-1-secondary-axes tbody::after,.charts-css.radar.show-1-secondary-axes tbody::after,.charts-css.radial.show-1-secondary-axes tbody::after{background:repeating-radial-gradient(closest-side,transparent 0,transparent calc(100% / 2 - var(--secondary-axes-width)),var(--secondary-axes-color) calc(100% / 2 - var(--secondary-axes-width)),var(--secondary-axes-color) calc(100% / 2),transparent calc(100% / 2 + var(--secondary-axes-width)),transparent calc(100% / 2 + var(--secondary-axes-width)));border-radius:50%;bottom:0;content:"";height:100%;left:0;position:absolute;right:0;top:0;width:100%;z-index:2}.charts-css.pie.show-2-secondary-axes tbody::after,.charts-css.polar.show-2-secondary-axes tbody::after,.charts-css.radar.show-2-secondary-axes tbody::after,.charts-css.radial.show-2-secondary-axes tbody::after{background:repeating-radial-gradient(closest-side,transparent 0,transparent calc(100% / 3 - var(--secondary-axes-width)),var(--secondary-axes-color) calc(100% / 3 - var(--secondary-axes-width)),var(--secondary-axes-color) calc(100% / 3),transparent calc(100% / 3 + var(--secondary-axes-width)),transparent calc(100% / 3 + var(--secondary-axes-width)));border-radius:50%;bottom:0;content:"";height:100%;left:0;position:absolute;right:0;top:0;width:100%;z-index:2}.charts-css.pie.show-3-secondary-axes tbody::after,.charts-css.polar.show-3-secondary-axes tbody::after,.charts-css.radar.show-3-secondary-axes tbody::after,.charts-css.radial.show-3-secondary-axes tbody::after{background:repeating-radial-gradient(closest-side,transparent 0,transparent calc(100% / 4 - var(--secondary-axes-width)),var(--secondary-axes-color) calc(100% / 4 - var(--secondary-axes-width)),var(--secondary-axes-color) calc(100% / 4),transparent calc(100% / 4 + var(--secondary-axes-width)),transparent calc(100% / 4 + var(--secondary-axes-width)));border-radius:50%;bottom:0;content:"";height:100%;left:0;position:absolute;right:0;top:0;width:100%;z-index:2}.charts-css.pie.show-4-secondary-axes tbody::after,.charts-css.polar.show-4-secondary-axes tbody::after,.charts-css.radar.show-4-secondary-axes tbody::after,.charts-css.radial.show-4-secondary-axes tbody::after{background:repeating-radial-gradient(closest-side,transparent 0,transparent calc(100% / 5 - var(--secondary-axes-width)),var(--secondary-axes-color) calc(100% / 5 - var(--secondary-axes-width)),var(--secondary-axes-color) calc(100% / 5),transparent calc(100% / 5 + var(--secondary-axes-width)),transparent calc(100% / 5 + var(--secondary-axes-width)));border-radius:50%;bottom:0;content:"";height:100%;left:0;position:absolute;right:0;top:0;width:100%;z-index:2}.charts-css.pie.show-5-secondary-axes tbody::after,.charts-css.polar.show-5-secondary-axes tbody::after,.charts-css.radar.show-5-secondary-axes tbody::after,.charts-css.radial.show-5-secondary-axes tbody::after{background:repeating-radial-gradient(closest-side,transparent 0,transparent calc(100% / 6 - var(--secondary-axes-width)),var(--secondary-axes-color) calc(100% / 6 - var(--secondary-axes-width)),var(--secondary-axes-color) calc(100% / 6),transparent calc(100% / 6 + var(--secondary-axes-width)),transparent calc(100% / 6 + var(--secondary-axes-width)));border-radius:50%;bottom:0;content:"";height:100%;left:0;position:absolute;right:0;top:0;width:100%;z-index:2}.charts-css.pie.show-6-secondary-axes tbody::after,.charts-css.polar.show-6-secondary-axes tbody::after,.charts-css.radar.show-6-secondary-axes tbody::after,.charts-css.radial.show-6-secondary-axes tbody::after{background:repeating-radial-gradient(closest-side,transparent 0,transparent calc(100% / 7 - var(--secondary-axes-width)),var(--secondary-axes-color) calc(100% / 7 - var(--secondary-axes-width)),var(--secondary-axes-color) calc(100% / 7),transparent calc(100% / 7 + var(--secondary-axes-width)),transparent calc(100% / 7 + var(--secondary-axes-width)));border-radius:50%;bottom:0;content:"";height:100%;left:0;position:absolute;right:0;top:0;width:100%;z-index:2}.charts-css.pie.show-7-secondary-axes tbody::after,.charts-css.polar.show-7-secondary-axes tbody::after,.charts-css.radar.show-7-secondary-axes tbody::after,.charts-css.radial.show-7-secondary-axes tbody::after{background:repeating-radial-gradient(closest-side,transparent 0,transparent calc(100% / 8 - var(--secondary-axes-width)),var(--secondary-axes-color) calc(100% / 8 - var(--secondary-axes-width)),var(--secondary-axes-color) calc(100% / 8),transparent calc(100% / 8 + var(--secondary-axes-width)),transparent calc(100% / 8 + var(--secondary-axes-width)));border-radius:50%;bottom:0;content:"";height:100%;left:0;position:absolute;right:0;top:0;width:100%;z-index:2}.charts-css.pie.show-8-secondary-axes tbody::after,.charts-css.polar.show-8-secondary-axes tbody::after,.charts-css.radar.show-8-secondary-axes tbody::after,.charts-css.radial.show-8-secondary-axes tbody::after{background:repeating-radial-gradient(closest-side,transparent 0,transparent calc(100% / 9 - var(--secondary-axes-width)),var(--secondary-axes-color) calc(100% / 9 - var(--secondary-axes-width)),var(--secondary-axes-color) calc(100% / 9),transparent calc(100% / 9 + var(--secondary-axes-width)),transparent calc(100% / 9 + var(--secondary-axes-width)));border-radius:50%;bottom:0;content:"";height:100%;left:0;position:absolute;right:0;top:0;width:100%;z-index:2}.charts-css.pie.show-9-secondary-axes tbody::after,.charts-css.polar.show-9-secondary-axes tbody::after,.charts-css.radar.show-9-secondary-axes tbody::after,.charts-css.radial.show-9-secondary-axes tbody::after{background:repeating-radial-gradient(closest-side,transparent 0,transparent calc(100% / 10 - var(--secondary-axes-width)),var(--secondary-axes-color) calc(100% / 10 - var(--secondary-axes-width)),var(--secondary-axes-color) calc(100% / 10),transparent calc(100% / 10 + var(--secondary-axes-width)),transparent calc(100% / 10 + var(--secondary-axes-width)));border-radius:50%;bottom:0;content:"";height:100%;left:0;position:absolute;right:0;top:0;width:100%;z-index:2}.charts-css.pie.show-10-secondary-axes tbody::after,.charts-css.polar.show-10-secondary-axes tbody::after,.charts-css.radar.show-10-secondary-axes tbody::after,.charts-css.radial.show-10-secondary-axes tbody::after{background:repeating-radial-gradient(closest-side,transparent 0,transparent calc(100% / 11 - var(--secondary-axes-width)),var(--secondary-axes-color) calc(100% / 11 - var(--secondary-axes-width)),var(--secondary-axes-color) calc(100% / 11),transparent calc(100% / 11 + var(--secondary-axes-width)),transparent calc(100% / 11 + var(--secondary-axes-width)));border-radius:50%;bottom:0;content:"";height:100%;left:0;position:absolute;right:0;top:0;width:100%;z-index:2}.charts-css.legend{border:var(--legend-border-width) var(--legend-border-style) var(--legend-border-color);font-size:1rem;list-style:none;padding:1rem}.charts-css.legend li{-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex;line-height:2}.charts-css.legend li::before{-webkit-margin-end:.5rem;border-style:solid;border-width:2px;content:"";display:inline-block;margin-inline-end:.5rem;vertical-align:middle}.charts-css.legend li:nth-child(10n+1)::before{background-color:var(--color-1,transparent);border-color:var(--border-color-1,var(--border-color,#000))}.charts-css.legend li:nth-child(10n+2)::before{background-color:var(--color-2,transparent);border-color:var(--border-color-2,var(--border-color,#000))}.charts-css.legend li:nth-child(10n+3)::before{background-color:var(--color-3,transparent);border-color:var(--border-color-3,var(--border-color,#000))}.charts-css.legend li:nth-child(10n+4)::before{background-color:var(--color-4,transparent);border-color:var(--border-color-4,var(--border-color,#000))}.charts-css.legend li:nth-child(10n+5)::before{background-color:var(--color-5,transparent);border-color:var(--border-color-5,var(--border-color,#000))}.charts-css.legend li:nth-child(10n+6)::before{background-color:var(--color-6,transparent);border-color:var(--border-color-6,var(--border-color,#000))}.charts-css.legend li:nth-child(10n+7)::before{background-color:var(--color-7,transparent);border-color:var(--border-color-7,var(--border-color,#000))}.charts-css.legend li:nth-child(10n+8)::before{background-color:var(--color-8,transparent);border-color:var(--border-color-8,var(--border-color,#000))}.charts-css.legend li:nth-child(10n+9)::before{background-color:var(--color-9,transparent);border-color:var(--border-color-9,var(--border-color,#000))}.charts-css.legend li:nth-child(10n+10)::before{background-color:var(--color-10,transparent);border-color:var(--border-color-10,var(--border-color,#000))}.charts-css:not(.legend-inline){-webkit-box-orient:vertical;-ms-flex-direction:column;flex-direction:column;-ms-flex-wrap:nowrap;flex-wrap:nowrap}.charts-css.legend-inline,.charts-css:not(.legend-inline){-webkit-box-direction:normal;display:-webkit-box;display:-ms-flexbox;display:flex}.charts-css.legend-inline{-webkit-box-orient:horizontal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:wrap;flex-wrap:wrap}.charts-css.legend-inline li{-webkit-margin-end:1rem;margin-inline-end:1rem}.charts-css.legend-circle li::before{border-radius:50%;height:1rem;width:1rem}.charts-css.legend-ellipse li::before{border-radius:50%;height:1rem;width:2rem}.charts-css.legend-rhombus li::before,.charts-css.legend-square li::before{border-radius:3px;height:1rem;width:1rem}.charts-css.legend-rhombus li::before{-webkit-transform:rotate(45deg) scale(.85);transform:rotate(45deg) scale(.85)}.charts-css.legend-rectangle li::before{border-radius:3px;height:1rem;width:2rem}.charts-css.legend-line li::before{border-radius:2px;-webkit-box-sizing:content-box;box-sizing:content-box;height:3px;width:2rem}.charts-css .tooltip{background-color:#555;border-radius:6px;bottom:50%;color:#fff;font-size:.9rem;left:50%;opacity:0;padding:5px 10px;position:absolute;text-align:center;-webkit-transform:translateX(-50%);transform:translateX(-50%);-webkit-transition:opacity .3s;transition:opacity .3s;visibility:hidden;width:-webkit-max-content;width:-moz-max-content;width:max-content;z-index:1}.charts-css .tooltip::after{border:5px solid transparent;border-top-color:#555;content:"";left:50%;margin-left:-5px;position:absolute;top:100%}.charts-css td:hover .tooltip{opacity:1;visibility:visible}.charts-css.bar tbody{-webkit-box-pack:justify;-ms-flex-pack:justify;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;aspect-ratio:var(--aspect-ratio,auto);display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:space-between;width:100%}.charts-css.area tbody tr,.charts-css.bar tbody tr,.charts-css.column tbody tr,.charts-css.line tbody tr{-webkit-box-pack:start;-ms-flex-pack:start;-webkit-box-flex:1;-ms-flex-positive:1;-ms-flex-negative:1;-ms-flex-preferred-size:0;display:-webkit-box;display:-ms-flexbox;display:flex;flex-basis:0;flex-grow:1;flex-shrink:1;justify-content:flex-start;overflow-wrap:anywhere;position:relative}.charts-css.bar tbody tr th{bottom:0;left:0;position:absolute;right:0;top:0;width:var(--labels-size)}.charts-css.bar tbody tr td{-webkit-box-align:center;-ms-flex-align:center;-webkit-padding-before:10px;-webkit-padding-after:10px;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex;height:100%;min-height:1rem;padding-block-end:10px;padding-block-start:10px;position:relative;width:calc(100% * var(--end, var(--size, 1)))}.charts-css.bar:not(.reverse) tbody tr td{-webkit-box-pack:var(--data-position,flex-end);-ms-flex-pack:var(--data-position,flex-end);justify-content:var(--data-position,flex-end)}.charts-css.bar:not(.reverse) tbody tr td .data.outside{-webkit-transform:translateX(100%);transform:translateX(100%);white-space:nowrap}.charts-css.bar.reverse tbody tr td{-webkit-box-pack:var(--data-position,flex-start);-ms-flex-pack:var(--data-position,flex-start);justify-content:var(--data-position,flex-start)}.charts-css.bar.reverse tbody tr td .data.outside{-webkit-transform:translateX(-100%);transform:translateX(-100%);white-space:nowrap}.charts-css.area.reverse tbody tr,.charts-css.area:not(.reverse) tbody tr td .data,.charts-css.bar:not(.reverse) tbody tr,.charts-css.column.reverse tbody tr,.charts-css.line.reverse tbody tr,.charts-css.line:not(.reverse) tbody tr td .data{-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.charts-css.area.reverse tbody tr td .data,.charts-css.area:not(.reverse) tbody tr,.charts-css.bar.reverse tbody tr,.charts-css.column:not(.reverse) tbody tr,.charts-css.line.reverse tbody tr td .data,.charts-css.line:not(.reverse) tbody tr{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}.charts-css.bar.reverse-labels.reverse tbody tr,.charts-css.bar:not(.reverse-labels):not(.reverse) tbody tr{-webkit-margin-start:var(--labels-size);margin-inline-start:var(--labels-size)}.charts-css.bar:not(.reverse-labels):not(.reverse) tbody tr th{-webkit-margin-end:auto;-webkit-margin-start:calc(-1 * var(--labels-size) - var(--primary-axis-width));margin-inline-end:auto;margin-inline-start:calc(-1 * var(--labels-size) - var(--primary-axis-width))}.charts-css.bar.reverse-labels:not(.reverse) tbody tr,.charts-css.bar:not(.reverse-labels).reverse tbody tr{-webkit-margin-end:var(--labels-size);margin-inline-end:var(--labels-size)}.charts-css.bar:not(.reverse-labels).reverse tbody tr th{-webkit-margin-start:auto;-webkit-margin-end:calc(-1 * var(--labels-size) - var(--primary-axis-width));margin-inline-end:calc(-1 * var(--labels-size) - var(--primary-axis-width));margin-inline-start:auto}.charts-css.bar.reverse-labels:not(.reverse) tbody tr th{-webkit-margin-start:auto;-webkit-margin-end:calc(-1 * var(--labels-size));margin-inline-end:calc(-1 * var(--labels-size));margin-inline-start:auto}.charts-css.bar.reverse-labels.reverse tbody tr th{-webkit-margin-end:auto;-webkit-margin-start:calc(-1 * var(--labels-size));margin-inline-end:auto;margin-inline-start:calc(-1 * var(--labels-size))}.charts-css.bar:not(.stacked) tbody tr td,.charts-css.column:not(.stacked) tbody tr td{-webkit-box-flex:1;-ms-flex-positive:1;-ms-flex-negative:1;-ms-flex-preferred-size:0;flex-basis:0;flex-grow:1;flex-shrink:1}.charts-css.bar.stacked tbody tr td,.charts-css.column.stacked tbody tr td{-webkit-box-flex:unset;-ms-flex-positive:unset;-ms-flex-negative:unset;-ms-flex-preferred-size:unset;flex-basis:unset;flex-grow:unset;flex-shrink:unset}.charts-css.area:not(.reverse) tbody tr th,.charts-css.bar.stacked.reverse-datasets tbody tr,.charts-css.column.stacked.reverse-datasets tbody tr,.charts-css.column:not(.reverse) tbody tr th,.charts-css.line:not(.reverse) tbody tr th{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.charts-css.bar:not(.reverse-data) tbody,.charts-css.bar:not(.reverse-datasets):not(.stacked) tbody tr,.charts-css.column.reverse-datasets.stacked:not(.reverse) tbody tr,.charts-css.column:not(.reverse-datasets).stacked.reverse tbody tr{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.charts-css.bar.reverse-data tbody,.charts-css.bar.reverse-datasets:not(.stacked) tbody tr,.charts-css.column.reverse-datasets.stacked.reverse tbody tr,.charts-css.column:not(.reverse-datasets).stacked:not(.reverse) tbody tr{-webkit-box-orient:vertical;-webkit-box-direction:reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse}.charts-css.area:not(.reverse-data) tbody,.charts-css.area:not(.reverse-datasets) tbody tr,.charts-css.bar.reverse-datasets.stacked.reverse tbody tr,.charts-css.bar:not(.reverse-datasets).stacked:not(.reverse) tbody tr,.charts-css.column.reverse-labels.reverse-data tbody,.charts-css.column:not(.reverse-datasets):not(.stacked) tbody tr,.charts-css.column:not(.reverse-labels):not(.reverse-data) tbody,.charts-css.line:not(.reverse-data) tbody,.charts-css.line:not(.reverse-datasets) tbody tr{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.charts-css.area.reverse-data tbody,.charts-css.area.reverse-datasets tbody tr,.charts-css.bar.reverse-datasets.stacked:not(.reverse) tbody tr,.charts-css.bar:not(.reverse-datasets).stacked.reverse tbody tr,.charts-css.column.reverse-datasets:not(.stacked) tbody tr,.charts-css.column.reverse-labels:not(.reverse-data) tbody,.charts-css.column:not(.reverse-labels).reverse-data tbody,.charts-css.line.reverse-data tbody,.charts-css.line.reverse-datasets tbody tr{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.charts-css.bar.data-spacing-1 tbody tr{-webkit-padding-before:1px;-webkit-padding-after:1px;padding-block-end:1px;padding-block-start:1px}.charts-css.bar.data-spacing-2 tbody tr{-webkit-padding-before:2px;-webkit-padding-after:2px;padding-block-end:2px;padding-block-start:2px}.charts-css.bar.data-spacing-3 tbody tr{-webkit-padding-before:3px;-webkit-padding-after:3px;padding-block-end:3px;padding-block-start:3px}.charts-css.bar.data-spacing-4 tbody tr{-webkit-padding-before:4px;-webkit-padding-after:4px;padding-block-end:4px;padding-block-start:4px}.charts-css.bar.data-spacing-5 tbody tr{-webkit-padding-before:5px;-webkit-padding-after:5px;padding-block-end:5px;padding-block-start:5px}.charts-css.bar.data-spacing-6 tbody tr{-webkit-padding-before:6px;-webkit-padding-after:6px;padding-block-end:6px;padding-block-start:6px}.charts-css.bar.data-spacing-7 tbody tr{-webkit-padding-before:7px;-webkit-padding-after:7px;padding-block-end:7px;padding-block-start:7px}.charts-css.bar.data-spacing-8 tbody tr{-webkit-padding-before:8px;-webkit-padding-after:8px;padding-block-end:8px;padding-block-start:8px}.charts-css.bar.data-spacing-9 tbody tr{-webkit-padding-before:9px;-webkit-padding-after:9px;padding-block-end:9px;padding-block-start:9px}.charts-css.bar.data-spacing-10 tbody tr{-webkit-padding-before:10px;-webkit-padding-after:10px;padding-block-end:10px;padding-block-start:10px}.charts-css.bar.data-spacing-11 tbody tr{-webkit-padding-before:11px;-webkit-padding-after:11px;padding-block-end:11px;padding-block-start:11px}.charts-css.bar.data-spacing-12 tbody tr{-webkit-padding-before:12px;-webkit-padding-after:12px;padding-block-end:12px;padding-block-start:12px}.charts-css.bar.data-spacing-13 tbody tr{-webkit-padding-before:13px;-webkit-padding-after:13px;padding-block-end:13px;padding-block-start:13px}.charts-css.bar.data-spacing-14 tbody tr{-webkit-padding-before:14px;-webkit-padding-after:14px;padding-block-end:14px;padding-block-start:14px}.charts-css.bar.data-spacing-15 tbody tr{-webkit-padding-before:15px;-webkit-padding-after:15px;padding-block-end:15px;padding-block-start:15px}.charts-css.bar.data-spacing-16 tbody tr{-webkit-padding-before:16px;-webkit-padding-after:16px;padding-block-end:16px;padding-block-start:16px}.charts-css.bar.data-spacing-17 tbody tr{-webkit-padding-before:17px;-webkit-padding-after:17px;padding-block-end:17px;padding-block-start:17px}.charts-css.bar.data-spacing-18 tbody tr{-webkit-padding-before:18px;-webkit-padding-after:18px;padding-block-end:18px;padding-block-start:18px}.charts-css.bar.data-spacing-19 tbody tr{-webkit-padding-before:19px;-webkit-padding-after:19px;padding-block-end:19px;padding-block-start:19px}.charts-css.bar.data-spacing-20 tbody tr{-webkit-padding-before:20px;-webkit-padding-after:20px;padding-block-end:20px;padding-block-start:20px}.charts-css.bar.datasets-spacing-1 tbody tr td{-webkit-margin-before:1px;-webkit-margin-after:1px;margin-block-end:1px;margin-block-start:1px}.charts-css.bar.datasets-spacing-2 tbody tr td{-webkit-margin-before:2px;-webkit-margin-after:2px;margin-block-end:2px;margin-block-start:2px}.charts-css.bar.datasets-spacing-3 tbody tr td{-webkit-margin-before:3px;-webkit-margin-after:3px;margin-block-end:3px;margin-block-start:3px}.charts-css.bar.datasets-spacing-4 tbody tr td{-webkit-margin-before:4px;-webkit-margin-after:4px;margin-block-end:4px;margin-block-start:4px}.charts-css.bar.datasets-spacing-5 tbody tr td{-webkit-margin-before:5px;-webkit-margin-after:5px;margin-block-end:5px;margin-block-start:5px}.charts-css.bar.datasets-spacing-6 tbody tr td{-webkit-margin-before:6px;-webkit-margin-after:6px;margin-block-end:6px;margin-block-start:6px}.charts-css.bar.datasets-spacing-7 tbody tr td{-webkit-margin-before:7px;-webkit-margin-after:7px;margin-block-end:7px;margin-block-start:7px}.charts-css.bar.datasets-spacing-8 tbody tr td{-webkit-margin-before:8px;-webkit-margin-after:8px;margin-block-end:8px;margin-block-start:8px}.charts-css.bar.datasets-spacing-9 tbody tr td{-webkit-margin-before:9px;-webkit-margin-after:9px;margin-block-end:9px;margin-block-start:9px}.charts-css.bar.datasets-spacing-10 tbody tr td{-webkit-margin-before:10px;-webkit-margin-after:10px;margin-block-end:10px;margin-block-start:10px}.charts-css.bar.datasets-spacing-11 tbody tr td{-webkit-margin-before:11px;-webkit-margin-after:11px;margin-block-end:11px;margin-block-start:11px}.charts-css.bar.datasets-spacing-12 tbody tr td{-webkit-margin-before:12px;-webkit-margin-after:12px;margin-block-end:12px;margin-block-start:12px}.charts-css.bar.datasets-spacing-13 tbody tr td{-webkit-margin-before:13px;-webkit-margin-after:13px;margin-block-end:13px;margin-block-start:13px}.charts-css.bar.datasets-spacing-14 tbody tr td{-webkit-margin-before:14px;-webkit-margin-after:14px;margin-block-end:14px;margin-block-start:14px}.charts-css.bar.datasets-spacing-15 tbody tr td{-webkit-margin-before:15px;-webkit-margin-after:15px;margin-block-end:15px;margin-block-start:15px}.charts-css.bar.datasets-spacing-16 tbody tr td{-webkit-margin-before:16px;-webkit-margin-after:16px;margin-block-end:16px;margin-block-start:16px}.charts-css.bar.datasets-spacing-17 tbody tr td{-webkit-margin-before:17px;-webkit-margin-after:17px;margin-block-end:17px;margin-block-start:17px}.charts-css.bar.datasets-spacing-18 tbody tr td{-webkit-margin-before:18px;-webkit-margin-after:18px;margin-block-end:18px;margin-block-start:18px}.charts-css.bar.datasets-spacing-19 tbody tr td{-webkit-margin-before:19px;-webkit-margin-after:19px;margin-block-end:19px;margin-block-start:19px}.charts-css.bar.datasets-spacing-20 tbody tr td{-webkit-margin-before:20px;-webkit-margin-after:20px;margin-block-end:20px;margin-block-start:20px}.charts-css.area tbody,.charts-css.column tbody,.charts-css.line tbody{-webkit-box-pack:justify;-ms-flex-pack:justify;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;aspect-ratio:var(--aspect-ratio,21/9);display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:space-between;width:100%}.charts-css.area tbody tr th,.charts-css.column tbody tr th,.charts-css.line tbody tr th{bottom:0;height:var(--labels-size);left:0;position:absolute;right:0;top:0}.charts-css.column tbody tr td{-webkit-box-pack:center;-ms-flex-pack:center;display:-webkit-box;display:-ms-flexbox;display:flex;height:calc(100% * var(--end, var(--size, 1)));justify-content:center;position:relative;width:100%}.charts-css.column:not(.reverse) tbody tr td{-webkit-box-align:var(--data-position,flex-start);-ms-flex-align:var(--data-position,flex-start);align-items:var(--data-position,flex-start)}.charts-css.column.reverse tbody tr td{-webkit-box-align:var(--data-position,flex-end);-ms-flex-align:var(--data-position,flex-end);align-items:var(--data-position,flex-end)}.charts-css.area.reverse tbody tr td,.charts-css.area:not(.reverse) tbody tr td,.charts-css.column.reverse tbody tr td,.charts-css.column:not(.reverse) tbody tr td,.charts-css.line.reverse tbody tr td,.charts-css.line:not(.reverse) tbody tr td{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.charts-css.area.reverse tbody tr th,.charts-css.column.reverse tbody tr th,.charts-css.line.reverse tbody tr th{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.charts-css.area.reverse-labels.reverse tbody tr,.charts-css.area:not(.reverse-labels):not(.reverse) tbody tr,.charts-css.column.reverse-labels.reverse tbody tr,.charts-css.column:not(.reverse-labels):not(.reverse) tbody tr,.charts-css.line.reverse-labels.reverse tbody tr,.charts-css.line:not(.reverse-labels):not(.reverse) tbody tr{-webkit-margin-after:var(--labels-size);margin-block-end:var(--labels-size)}.charts-css.area:not(.reverse-labels):not(.reverse) tbody tr th,.charts-css.column:not(.reverse-labels):not(.reverse) tbody tr th,.charts-css.line:not(.reverse-labels):not(.reverse) tbody tr th{-webkit-margin-before:auto;-webkit-margin-after:calc(-1 * var(--labels-size) - var(--primary-axis-width));margin-block-end:calc(-1 * var(--labels-size) - var(--primary-axis-width));margin-block-start:auto}.charts-css.area.reverse-labels:not(.reverse) tbody tr,.charts-css.area:not(.reverse-labels).reverse tbody tr,.charts-css.column.reverse-labels:not(.reverse) tbody tr,.charts-css.column:not(.reverse-labels).reverse tbody tr,.charts-css.line.reverse-labels:not(.reverse) tbody tr,.charts-css.line:not(.reverse-labels).reverse tbody tr{-webkit-margin-before:var(--labels-size);margin-block-start:var(--labels-size)}.charts-css.area:not(.reverse-labels).reverse tbody tr th,.charts-css.column:not(.reverse-labels).reverse tbody tr th,.charts-css.line:not(.reverse-labels).reverse tbody tr th{-webkit-margin-after:auto;-webkit-margin-before:calc(-1 * var(--labels-size) - var(--primary-axis-width));margin-block-end:auto;margin-block-start:calc(-1 * var(--labels-size) - var(--primary-axis-width))}.charts-css.area.reverse-labels:not(.reverse) tbody tr th,.charts-css.column.reverse-labels:not(.reverse) tbody tr th,.charts-css.line.reverse-labels:not(.reverse) tbody tr th{-webkit-margin-after:auto;-webkit-margin-before:calc(-1 * var(--labels-size));margin-block-end:auto;margin-block-start:calc(-1 * var(--labels-size))}.charts-css.area.reverse-labels.reverse tbody tr th,.charts-css.column.reverse-labels.reverse tbody tr th,.charts-css.line.reverse-labels.reverse tbody tr th{-webkit-margin-before:auto;-webkit-margin-after:calc(-1 * var(--labels-size));margin-block-end:calc(-1 * var(--labels-size));margin-block-start:auto}.charts-css.column.data-spacing-1 tbody tr{-webkit-padding-start:1px;-webkit-padding-end:1px;padding-inline-end:1px;padding-inline-start:1px}.charts-css.column.data-spacing-2 tbody tr{-webkit-padding-start:2px;-webkit-padding-end:2px;padding-inline-end:2px;padding-inline-start:2px}.charts-css.column.data-spacing-3 tbody tr{-webkit-padding-start:3px;-webkit-padding-end:3px;padding-inline-end:3px;padding-inline-start:3px}.charts-css.column.data-spacing-4 tbody tr{-webkit-padding-start:4px;-webkit-padding-end:4px;padding-inline-end:4px;padding-inline-start:4px}.charts-css.column.data-spacing-5 tbody tr{-webkit-padding-start:5px;-webkit-padding-end:5px;padding-inline-end:5px;padding-inline-start:5px}.charts-css.column.data-spacing-6 tbody tr{-webkit-padding-start:6px;-webkit-padding-end:6px;padding-inline-end:6px;padding-inline-start:6px}.charts-css.column.data-spacing-7 tbody tr{-webkit-padding-start:7px;-webkit-padding-end:7px;padding-inline-end:7px;padding-inline-start:7px}.charts-css.column.data-spacing-8 tbody tr{-webkit-padding-start:8px;-webkit-padding-end:8px;padding-inline-end:8px;padding-inline-start:8px}.charts-css.column.data-spacing-9 tbody tr{-webkit-padding-start:9px;-webkit-padding-end:9px;padding-inline-end:9px;padding-inline-start:9px}.charts-css.column.data-spacing-10 tbody tr{-webkit-padding-start:10px;-webkit-padding-end:10px;padding-inline-end:10px;padding-inline-start:10px}.charts-css.column.data-spacing-11 tbody tr{-webkit-padding-start:11px;-webkit-padding-end:11px;padding-inline-end:11px;padding-inline-start:11px}.charts-css.column.data-spacing-12 tbody tr{-webkit-padding-start:12px;-webkit-padding-end:12px;padding-inline-end:12px;padding-inline-start:12px}.charts-css.column.data-spacing-13 tbody tr{-webkit-padding-start:13px;-webkit-padding-end:13px;padding-inline-end:13px;padding-inline-start:13px}.charts-css.column.data-spacing-14 tbody tr{-webkit-padding-start:14px;-webkit-padding-end:14px;padding-inline-end:14px;padding-inline-start:14px}.charts-css.column.data-spacing-15 tbody tr{-webkit-padding-start:15px;-webkit-padding-end:15px;padding-inline-end:15px;padding-inline-start:15px}.charts-css.column.data-spacing-16 tbody tr{-webkit-padding-start:16px;-webkit-padding-end:16px;padding-inline-end:16px;padding-inline-start:16px}.charts-css.column.data-spacing-17 tbody tr{-webkit-padding-start:17px;-webkit-padding-end:17px;padding-inline-end:17px;padding-inline-start:17px}.charts-css.column.data-spacing-18 tbody tr{-webkit-padding-start:18px;-webkit-padding-end:18px;padding-inline-end:18px;padding-inline-start:18px}.charts-css.column.data-spacing-19 tbody tr{-webkit-padding-start:19px;-webkit-padding-end:19px;padding-inline-end:19px;padding-inline-start:19px}.charts-css.column.data-spacing-20 tbody tr{-webkit-padding-start:20px;-webkit-padding-end:20px;padding-inline-end:20px;padding-inline-start:20px}.charts-css.column.datasets-spacing-1 tbody tr td{-webkit-margin-start:1px;-webkit-margin-end:1px;margin-inline-end:1px;margin-inline-start:1px}.charts-css.column.datasets-spacing-2 tbody tr td{-webkit-margin-start:2px;-webkit-margin-end:2px;margin-inline-end:2px;margin-inline-start:2px}.charts-css.column.datasets-spacing-3 tbody tr td{-webkit-margin-start:3px;-webkit-margin-end:3px;margin-inline-end:3px;margin-inline-start:3px}.charts-css.column.datasets-spacing-4 tbody tr td{-webkit-margin-start:4px;-webkit-margin-end:4px;margin-inline-end:4px;margin-inline-start:4px}.charts-css.column.datasets-spacing-5 tbody tr td{-webkit-margin-start:5px;-webkit-margin-end:5px;margin-inline-end:5px;margin-inline-start:5px}.charts-css.column.datasets-spacing-6 tbody tr td{-webkit-margin-start:6px;-webkit-margin-end:6px;margin-inline-end:6px;margin-inline-start:6px}.charts-css.column.datasets-spacing-7 tbody tr td{-webkit-margin-start:7px;-webkit-margin-end:7px;margin-inline-end:7px;margin-inline-start:7px}.charts-css.column.datasets-spacing-8 tbody tr td{-webkit-margin-start:8px;-webkit-margin-end:8px;margin-inline-end:8px;margin-inline-start:8px}.charts-css.column.datasets-spacing-9 tbody tr td{-webkit-margin-start:9px;-webkit-margin-end:9px;margin-inline-end:9px;margin-inline-start:9px}.charts-css.column.datasets-spacing-10 tbody tr td{-webkit-margin-start:10px;-webkit-margin-end:10px;margin-inline-end:10px;margin-inline-start:10px}.charts-css.column.datasets-spacing-11 tbody tr td{-webkit-margin-start:11px;-webkit-margin-end:11px;margin-inline-end:11px;margin-inline-start:11px}.charts-css.column.datasets-spacing-12 tbody tr td{-webkit-margin-start:12px;-webkit-margin-end:12px;margin-inline-end:12px;margin-inline-start:12px}.charts-css.column.datasets-spacing-13 tbody tr td{-webkit-margin-start:13px;-webkit-margin-end:13px;margin-inline-end:13px;margin-inline-start:13px}.charts-css.column.datasets-spacing-14 tbody tr td{-webkit-margin-start:14px;-webkit-margin-end:14px;margin-inline-end:14px;margin-inline-start:14px}.charts-css.column.datasets-spacing-15 tbody tr td{-webkit-margin-start:15px;-webkit-margin-end:15px;margin-inline-end:15px;margin-inline-start:15px}.charts-css.column.datasets-spacing-16 tbody tr td{-webkit-margin-start:16px;-webkit-margin-end:16px;margin-inline-end:16px;margin-inline-start:16px}.charts-css.column.datasets-spacing-17 tbody tr td{-webkit-margin-start:17px;-webkit-margin-end:17px;margin-inline-end:17px;margin-inline-start:17px}.charts-css.column.datasets-spacing-18 tbody tr td{-webkit-margin-start:18px;-webkit-margin-end:18px;margin-inline-end:18px;margin-inline-start:18px}.charts-css.column.datasets-spacing-19 tbody tr td{-webkit-margin-start:19px;-webkit-margin-end:19px;margin-inline-end:19px;margin-inline-start:19px}.charts-css.column.datasets-spacing-20 tbody tr td{-webkit-margin-start:20px;-webkit-margin-end:20px;margin-inline-end:20px;margin-inline-start:20px}.charts-css.area tbody tr td,.charts-css.line tbody tr td{-webkit-box-orient:vertical;-webkit-box-direction:normal;bottom:0;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-flow:column;flex-flow:column;height:100%;left:0;position:absolute;right:0;top:0;width:100%;z-index:0}.charts-css.area tbody tr td::before,.charts-css.line tbody tr td::before{bottom:0;content:"";left:0;position:absolute;right:0;top:0;z-index:-1}.charts-css.area tbody tr td::after,.charts-css.line tbody tr td::after,.charts-css.pie tbody tr td::after{content:"";width:100%}.charts-css.area.reverse:not(.reverse-data) tbody tr td,.charts-css.area:not(.reverse):not(.reverse-data) tbody tr td,.charts-css.line.reverse:not(.reverse-data) tbody tr td,.charts-css.line:not(.reverse):not(.reverse-data) tbody tr td{-webkit-box-pack:end;-ms-flex-pack:end;-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end;justify-content:flex-end}.charts-css.area:not(.reverse):not(.reverse-data) tbody tr td::before{-webkit-clip-path:polygon(0 calc(100% * (1 - var(--start, var(--end, var(--size))))),100% calc(100% * (1 - var(--end, var(--size)))),100% 100%,0 100%);clip-path:polygon(0 calc(100% * (1 - var(--start, var(--end, var(--size))))),100% calc(100% * (1 - var(--end, var(--size)))),100% 100%,0 100%)}.charts-css.area.reverse:not(.reverse-data) tbody tr td .data,.charts-css.area:not(.reverse):not(.reverse-data) tbody tr td .data,.charts-css.line.reverse:not(.reverse-data) tbody tr td .data,.charts-css.line:not(.reverse):not(.reverse-data) tbody tr td .data{-webkit-transform:translateX(50%);transform:translateX(50%)}.charts-css.area:not(.reverse).reverse-data tbody tr td::after,.charts-css.area:not(.reverse):not(.reverse-data) tbody tr td::after,.charts-css.line:not(.reverse).reverse-data tbody tr td::after,.charts-css.line:not(.reverse):not(.reverse-data) tbody tr td::after{height:calc(100% * var(--end, var(--size)))}.charts-css.area.reverse.reverse-data tbody tr td,.charts-css.area:not(.reverse).reverse-data tbody tr td,.charts-css.line.reverse.reverse-data tbody tr td,.charts-css.line:not(.reverse).reverse-data tbody tr td{-webkit-box-pack:end;-ms-flex-pack:end;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;justify-content:flex-end}.charts-css.area:not(.reverse).reverse-data tbody tr td::before{-webkit-clip-path:polygon(0 calc(100% * (1 - var(--end, var(--size)))),100% calc(100% * (1 - var(--start, var(--end, var(--size))))),100% 100%,0 100%);clip-path:polygon(0 calc(100% * (1 - var(--end, var(--size)))),100% calc(100% * (1 - var(--start, var(--end, var(--size))))),100% 100%,0 100%)}.charts-css.area.reverse.reverse-data tbody tr td .data,.charts-css.area:not(.reverse).reverse-data tbody tr td .data,.charts-css.line.reverse.reverse-data tbody tr td .data,.charts-css.line:not(.reverse).reverse-data tbody tr td .data{-webkit-transform:translateX(-50%);transform:translateX(-50%)}.charts-css.area.reverse:not(.reverse-data) tbody tr td::before{-webkit-clip-path:polygon(0 0,100% 0,100% calc(100% * var(--end, var(--size))),0 calc(100% * var(--start, var(--end, var(--size)))));clip-path:polygon(0 0,100% 0,100% calc(100% * var(--end, var(--size))),0 calc(100% * var(--start, var(--end, var(--size)))))}.charts-css.area.reverse.reverse-data tbody tr td::after,.charts-css.area.reverse:not(.reverse-data) tbody tr td::after,.charts-css.line.reverse.reverse-data tbody tr td::after,.charts-css.line.reverse:not(.reverse-data) tbody tr td::after{height:calc(100% * (1 - var(--end, var(--size))))}.charts-css.area.reverse.reverse-data tbody tr td::before{-webkit-clip-path:polygon(0 0,100% 0,100% calc(100% * var(--start, var(--end, var(--size)))),0 calc(100% * var(--end, var(--size))));clip-path:polygon(0 0,100% 0,100% calc(100% * var(--start, var(--end, var(--size)))),0 calc(100% * var(--end, var(--size))))}.charts-css.line{--line-size:3px}.charts-css.line:not(.reverse):not(.reverse-data) tbody tr td::before{-webkit-clip-path:polygon(0 calc(100% * (1 - var(--start, var(--end, var(--size))))),100% calc(100% * (1 - var(--end, var(--size)))),100% calc(100% * (1 - var(--end, var(--size))) - var(--line-size)),0 calc(100% * (1 - var(--start, var(--end, var(--size)))) - var(--line-size)));clip-path:polygon(0 calc(100% * (1 - var(--start, var(--end, var(--size))))),100% calc(100% * (1 - var(--end, var(--size)))),100% calc(100% * (1 - var(--end, var(--size))) - var(--line-size)),0 calc(100% * (1 - var(--start, var(--end, var(--size)))) - var(--line-size)))}.charts-css.line:not(.reverse).reverse-data tbody tr td::before{-webkit-clip-path:polygon(0 calc(100% * (1 - var(--end, var(--size)))),100% calc(100% * (1 - var(--start, var(--end, var(--size))))),100% calc(100% * (1 - var(--start, var(--end, var(--size)))) - var(--line-size)),0 calc(100% * (1 - var(--end, var(--size))) - var(--line-size)));clip-path:polygon(0 calc(100% * (1 - var(--end, var(--size)))),100% calc(100% * (1 - var(--start, var(--end, var(--size))))),100% calc(100% * (1 - var(--start, var(--end, var(--size)))) - var(--line-size)),0 calc(100% * (1 - var(--end, var(--size))) - var(--line-size)))}.charts-css.line.reverse:not(.reverse-data) tbody tr td::before{-webkit-clip-path:polygon(0 calc(100% * var(--start, var(--end, var(--size))) - var(--line-size)),100% calc(100% * var(--end, var(--size)) - var(--line-size)),100% calc(100% * var(--end, var(--size))),0 calc(100% * var(--start, var(--end, var(--size)))));clip-path:polygon(0 calc(100% * var(--start, var(--end, var(--size))) - var(--line-size)),100% calc(100% * var(--end, var(--size)) - var(--line-size)),100% calc(100% * var(--end, var(--size))),0 calc(100% * var(--start, var(--end, var(--size)))))}.charts-css.line.reverse.reverse-data tbody tr td::before{-webkit-clip-path:polygon(0 calc(100% * var(--end, var(--size)) - var(--line-size)),100% calc(100% * var(--start, var(--end, var(--size))) - var(--line-size)),100% calc(100% * var(--start, var(--end, var(--size)))),0 calc(100% * var(--end, var(--size))));clip-path:polygon(0 calc(100% * var(--end, var(--size)) - var(--line-size)),100% calc(100% * var(--start, var(--end, var(--size))) - var(--line-size)),100% calc(100% * var(--start, var(--end, var(--size)))),0 calc(100% * var(--end, var(--size))))}.charts-css.pie tbody,.charts-css.polar tbody,.charts-css.radar tbody,.charts-css.radial tbody{aspect-ratio:1;background-color:var(--chart-bg-color);border-radius:50%;display:block;width:100%}.charts-css.pie tbody tr td{-webkit-box-pack:center;-ms-flex-pack:center;background:conic-gradient(transparent 0 calc(1turn * var(--start)),var(--c,transparent) calc(1turn * var(--start, 0)) calc(1turn * var(--end)),transparent calc(1turn * var(--end)) 1turn);border-radius:50%;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:center}.charts-css.pie tbody tr td,.charts-css.pie tbody tr td::before{bottom:0;height:100%;left:0;position:absolute;right:0;top:0;width:100%}.charts-css.pie tbody tr td::before{content:""}.charts-css.pie tbody tr td .data{-webkit-box-pack:center;-ms-flex-pack:center;border-radius:50%;bottom:0;display:-webkit-box;display:-ms-flexbox;display:flex;height:100%;justify-content:center;left:0;position:absolute;right:0;top:0;-webkit-transform:rotate(calc(.5turn * var(--start, 0) + .5turn * var(--end, 0)));transform:rotate(calc(.5turn * var(--start, 0) + .5turn * var(--end, 0)));width:100%}graphql-ruby-2.5.19/lib/graphql/dashboard/statics/dashboard.css000066400000000000000000000010401514115062600244670ustar00rootroot00000000000000#header-icon { max-height: 2em; } .graphql-highlight { font-family:'Courier New', Courier, monospace; width: 100%; white-space: pre-wrap; } #limiter-histogram .column { max-height: 300px; } #limiter-histogram .column td { --color-1: var(--bs-gray); --color-2: var(--bs-red); opacity: 0.6; } #limiter-histogram .column td:hover { opacity: 1; } #limiter-histogram .column tbody tr th[scope=row] { width: 150px; transform: rotate(-75deg) translateY(55px) translateX(-50px); left: auto; --labels-align-inline: end; } graphql-ruby-2.5.19/lib/graphql/dashboard/statics/dashboard.js000066400000000000000000000104761514115062600243300ustar00rootroot00000000000000function detectTheme() { var storedTheme = localStorage.getItem("graphql_dashboard:theme") var preferredTheme = !!window.matchMedia('(prefers-color-scheme: dark)').matches ? "dark" : "light" setTheme(storedTheme || preferredTheme) } function toggleTheme() { var nextTheme = document.documentElement.getAttribute("data-bs-theme") == "dark" ? "light" : "dark" setTheme(nextTheme) } function setTheme(theme) { localStorage.setItem("graphql_dashboard:theme", theme) document.documentElement.setAttribute("data-bs-theme", theme) var icon = theme == "dark" ? "🌙" : "🌞" var toggle = document.getElementById("themeToggle") if (toggle) { toggle.innerText = icon } else { document.addEventListener("DOMContentLoaded", function(_ev) { document.getElementById("themeToggle").innerText = icon }) } } detectTheme() var perfettoUrl = "https://ui.perfetto.dev" async function openOnPerfetto(operationName, tracePath) { var resp = await fetch(tracePath); var blob = await resp.blob(); var nextPerfettoData = await blob.arrayBuffer(); nextPerfettoWindow = window.open(perfettoUrl) var messageHandler = function(event) { if (event.origin == perfettoUrl && event.data == "PONG") { clearInterval(perfettoWaiting) window.removeEventListener("message", messageHandler) nextPerfettoWindow.postMessage({ perfetto: { buffer: nextPerfettoData, title: operationName + " - GraphQL", filename: "perfetto-" + operationName + ".dump", } }, perfettoUrl) } } window.addEventListener("message", messageHandler, false) perfettoWaiting = setInterval(function() { nextPerfettoWindow.postMessage("PING", perfettoUrl) }, 100) } function getCsrfToken() { return document.querySelector("meta[name='csrf-token']").content } function deleteTrace(tracePath) { if (confirm("Are you sure you want to permanently delete this trace?")) { fetch(tracePath, { method: "DELETE", headers: { "X-CSRF-Token": getCsrfToken() } }).then(function(_response) { window.location.reload() }) } } function deleteAllTraces(path) { if (confirm("Are you sure you want to permanently delete ALL traces?")) { fetch(path, { method: "DELETE", headers: { "X-CSRF-Token": getCsrfToken() } }).then(function(_response) { window.location.reload() }) } } function deleteAllSubscriptions(path) { if (confirm("This will:\n\n- Remove all subscriptions from the database\n- Stop updates to all current subscribers\n\nAre you sure?")) { fetch(path, { method: "POST", headers: { "X-CSRF-Token": getCsrfToken() } }).then(function(_response) { window.location.reload() }) } } function sendArchive(clientName) { var values = [] document.querySelectorAll(".archive-check:checked").forEach(function(el) { values.push(el.value) }) if (values.length == 0) { return } var mode = window.location.pathname.includes("/archived") ? "/unarchive" : "/archive" if (mode == "/archive") { if (!confirm("Are you sure you want to archive these operations? They won't be usable by clients while archived.")) { return } } else { if (!confirm("Are you sure you want to reactivate these operations? They'll be available to clients again.")) { return } } var url = window.location.pathname.replace("/archived", "") url += mode var data if (clientName) { data = { operation_aliases: values } } else { data = { digests: values } } fetch(url, { method: "POST", body: JSON.stringify(data), headers: { "X-CSRF-Token": getCsrfToken(), "Content-Type": "application/json", }}).then(function(_response) { window.location.reload() }) } document.addEventListener("click", function(event) { var dataset = event.target.dataset if (dataset.perfettoOpen) { openOnPerfetto(dataset.perfettoOpen, dataset.perfettoPath) } else if (dataset.perfettoDelete) { deleteTrace(dataset.perfettoDelete, event) } else if (dataset.perfettoDeleteAll) { deleteAllTraces(dataset.perfettoDeleteAll) } else if (dataset.subscriptionsDeleteAll) { deleteAllSubscriptions(dataset.subscriptionsDeleteAll) } else if (event.target.id == "themeToggle") { toggleTheme() } else if (dataset.archiveClient || dataset.archiveAll) { sendArchive(dataset.archiveClient) } }) graphql-ruby-2.5.19/lib/graphql/dashboard/statics/header-icon.png000066400000000000000000000165421514115062600247270ustar00rootroot00000000000000PNG  IHDR`V0sRGBPeXIfMM*i&`V 1 ^DATx] tTչ{sf&I޼ >BT JZJ}WW{k_Voj(&>JTDArA Bw& $3}[H2 f9P(0D! QP^iFBNYBK$NW7,aX%,O`*Ō3Ź,~h 7"-ǕcZ-Mh;97פXճ`΀ Ezw} b JyAj)#z2OKy{cTb``R;tk?p% KRa9 @{R==/ٿ#Xk4 ֦53 uh#gnQsrp"*$h0GMG*RMP~s=IGa5#.K!08z|ͯޖweE 8LE=gUeAdUi38g>]X[ !.ʝ*,ՌU&LS\\Y $W !,b6i2]oӪ^5b+,B^lPI6c"\K@o@! xnc\l`wG 3!SFx.<8 5+ GR̂}LCS etLpt6`G9t>C>LIc[ 9 |a>Fl,r6aQgĞQ_1906Q(G20(q\ TjSHj4~7/TBL:Q6̞ajT[a ]蠟X+}`=Mx-TI-3Dg^^P| 0޹8rCss]h,cUǙ'111167i/7AdH6~DWUnJM:G}8#܌kt~? I_erYNZc.{e] Rv6v$eol#c:)> 0spG%%mrQ[K̞|fl\&pfA[U+))uI^OA}x|iӌ6{@E_ 6 ~9w[hobݘej ~9aTղ(gA9Yirr@ 9q^ s+Q1e_p's%V,j]]kȘv0F_n1c}[jj:P~0c4 .kꀰqGPCϢ_%s|䨘1poAK׮>:*`.HɆ)͕G+ڗĦ5BpEPYv}Vs@xAKusx܀c|ň<3&&ފܱY0!7JNt?uToy.p$ n3P}2Kiw  +Z4:w¨bw ^foVH#٭BLIaPRaJ< <Ų*5_*Z|ь;iE˹ %uua}{ءVŴgg VuSoLĥ/ybI̶pI2oCriT/T:Vh5˗ :>N2֥B"0 X0>/a&U>S3=SŀnM𥹍 Xeض:MJد@ F4 hCWnXքWnjظ*8-S.!URB511J24D6tO45DDR!t ,Ș DD2m|j kWֺE;ºa;P\z@]6>l!.c dXAXH 2 {¯p~[Z?X@Jk6!ߋujX@i 稂KZhׄ;/,bRQ?zezM-Sj3LvK(y_D@z2wec^ v@`3$i~mmܘMvV}^RWͨEBфiiխl9yOncBL 0d`?-8H; < (j.hXOt]1 &;BOD'wJchrآ{%g;s`]̆F`᜵HMU T@,ܚvp#IZj0\JxBotWШ+X_߉!: 5 >+Q䶾1܅Q ?x%~77Ѱ 1 dc˒Nr%v7y5MRamĎ{dk@0&<7u\35ɣH#']/9'|tw;a‡CІ!>:%1cjOkx; %6EA+Rk5şF g3hʷb]1ov FbG+]G٧U>nfZr@c _3/uߥp:^Ao+V"VUb׃`,l{S3=d46l_3ָ͚ "A]`SഃDMZ{0{ %0{,L3HBn ΘϽϵ${{kߩӿ5< oݍ$402OpBnIytdAͯWr)X02%ojP"ºZ6Ϝ0}6i>ヨZ kǿ( @A| Fj '7A JS@E_wX^S0`M=rRX8`鴖rH vg%te2pȀ=ȶ4OёH}TqJ!x`Z6M ?p7\\,Hw3k5gt)SrSxk^zY-xXޘ;X?*&$v+.J'%D|󃪙\1 RgrGŵx`F0Sf&lOƁ =Hq)N Tօ.3 "Ec{~=m$C< lj\3bV5r+B>q{&LO|VQ鈯&1[.PYxQn+s_N0HY#{9eM8[4_e?8$$!RlIګ0@كQ`Hտn4R+ Fڙy$n$m-mM}7ˎ6v^%VzIe< >_L-^rp]|70lA #&}8筷bT0,|y!}#z']2л[6]څ3L4eGa;)kYT$xESX3y;ڥ ;2 \{MIW Q߅1~|Aq1WɛĢE\v;pMy=28)+s pgj~>ҿg̜0 A_!< P}5~oΛT< cR ?Sޏؓ<` .t_lµ$ǔ=N_-lj; ms7!X ?=Wt6@ȖYxN1CI c ]=Ȏݷ`d4ri19[KpSaw >v\>.1xǁ2. Ja?a*1M(90-JYQ18[ՄƁ}^^Z4ꐘ,1C34RT? ~Vg@! 왊c a_ CNO{!1fi$Br`ق2@v8.`7*G1P'h*Tx? C(,Ϩ )qMƕ_hLێ0R_L5 `Z (ݚK ;%0`i7lNr\M>{]2k"5^UDF- QoCg՜7I,x)ꓼL8ɽgBxzk+!vL} #9hg5QE)`F]f5_AR,; jch&pfE3V6+qvӸm \;zk%fZ$t5} PQ J|"y8nr%5dS> yAl;vǼlٻק'>e!gK68= Q]_@4go Rdkz>+e#h>~G]Z| 1EYom̈wKh454bB;A`&Bȭ8?[e. Bqnѿ񹟛>̂}.k49seH`E[ | |i[q)7?;╬hVKg!go %(.No$FQxgYpȗ%K^3€*_KgԃX( !in5ޒjkx3.L(1"k3H^\~(0kskp5@^qyv% H>ÆC >Q?''+cNt>I>7`w,4޾} !vmNJ:10ȴ,Gu5/AЦ)R;xJl_/hߎ-g4tbjw<5@ -:i-,AP.Ŀcħ>Tk~оJ>p 3L/uNPGbazZ#dB ^VȚ_ay>GWcNfy]LA| Vj0K[7y72;-sOqgKSY&2> ;0 .^f# x0V hZe.>0/unxaCY1__pugV7c)?*Xn JРiZRGXŇ1^z%_ѓ.-- Mo7ژ.%Tάcg1?.h*_ RVG\{o!A63\ ~=b.&f]X<킦W+0ЬoLx{]G/D4Y5OCGB洸84JYQ=4hA01=S{y $}aajoabx>-X]Rߧ xZ^jjpM ` [z\Z{볂ܢ fO*Y*x$Ӛ0ΰ5㴬' A (1B_ gG9+@Li΄e xY' LmFU?l?/L,9kt[.HnKZ4Do=[ǹ6àBSgO|e6Fe7-_OLM;1uPs! Q`C(0D_s3IENDB`graphql-ruby-2.5.19/lib/graphql/dashboard/statics/icon.png000066400000000000000000000115601514115062600234740ustar00rootroot00000000000000PNG  IHDR``w87IDATx^] U( I'fvOc PQq, pvղ,+듣bD3]z #rȲD]BHB@[=ݙ鮞ib}{fnݺI$bĈ#F1bĈ#F1^XWΙS`o75[7XUOJKJ[, >'sd}It"rAߎNgo67%>8;o`Ӽ%>PdV;` lGiշ)vW9190dK{ }q،{ܝO_{#7*'mw=k)o=Of248' 75vLj ;:/^m?yj 8nIy}e(jGyO%z9l[~0]29y7 ѡb|(8ȿ9R`cПAb׏DAJƾa}aλ)3c8 GAb;嵷ON[f^->ǩ}ϐ`6WҖ7pEz8]適mBQM+ӍHm~BVd`9ލyc4`rB*0zּ~4r2)lk^oc?,8Εs6Ёq#VɒtyaB7=P@9-k|m܍&ͨgi,7v8Ej,q.t PN[y'}B5 ݰ4P }[ZlmW_-s8~**SA< X̸֗g\7C's$v;_ apd8)-~zh8p.(I+jQL$8tܜKqɁ/0pr-o?nP-XK|}B ތ!!2V-ĺΙC=SfL)@Yc 7^G0Zٺ-qDe0@qWiv8ǁyE:/5BQRO`{(;lFIRzC@݁{<Ef;\ QB1f(@'jIT[{6pf̶V0+y-g \hɼlC|W2&X<\JP:@;^+8bqϔSvQRԂӾ~:9k`ڍ=jYL Y8i!Z&EYHya t,QAJ_&ſ}_d7uc 4p;A~Jw4J b'ⶶCv&: ]P,l!I}ȌyLHnb\,aJ@:8AQf`߁kk%oyLp`#bnĽv|=SxϠlVvF4!tdDZ@r,#.blH_씲hSŁ[6P0K6vc!~N+la#李9-{/Ά@?CQ i= < ʂ`,%U<ǻL)]g/M~UVaR5rXVy:;q<cXvNm+La)bsqTȟy(c##dgnǠw\r ;99yz4v82s]*'b ]FnL5oX/h΂'f2wLkp|R|jOAJ,`8l[/ǘ(_4t 6(Bs}z8[7*]7Q+ut?۽ CZm|^. |ǽ B&U`IyqI;;TzMA_p.q_"xt p7>Cgfxf۱.$@0&c*5pO1\J>8yZ$9c4@;j!rwmqF7{Jg* ÕL-mƒdL_,KٶH?8$2ʞF^rN(]s5+4>JQW 0~Dl?Z@)zKY( D⠰imImUowj&0PVb:wբ!v8y=S`k0&#>USӫK#d:g1jxb1~9#6QyhIVF;]V1B^j L\Lb phżWa]r{r=Hc$byJ&*ڟ,}KS3X>7ۍsz0~>v%$fL9s`< G]<fLn.ENTZZV9(5 :|L2 \ KJ`>&\8T֭r@F4Qiigja P>S('6 ZHP`UClK+;1tU+D,R=>?BoBjR\Opy"|vA[OKa.,8L=S!bV|̗J߆WHlPuޓ)@e̖YĢ֫>&9`C?)3*L7r;(`[%{A w~T^MB3 aLL8TV\]-UO$L5h ?E14k*X-pNO 6 X@h'nx0> f&Bj䄔לwӀv2I6z@l <n8y7  ',c4_"|֖Wh{~jyB%Y*߀xdPq'{*s-C9>`UZJw޳QPOyBHNx5ØG=K봽,2E z<+}Dzw/ UbHB9 9lf3f4?+A\鸗baW]ǧia";zSbAjV.xs, zw㿍qX޿ ^0'U({wagQcPNCpi-g_7Pa[כ\hZJm 3gɢM(IR_n,Jη|@_F۟a|_'*hX3ϓEn\&OyMC'lqeJÇB<|yoet. &%.v:CȨ{滂LӣTK)̥A<+^8Z$;DQ62wV@%$w_K).MT^=Um vކ~B_y$26}`~=Op'af Rע^\ d9J=Wٹ9-[Jǡu1RgưR9^9|j>Μpr9% ܁%Vop7󥰸neh¢L͆Uh:k| E`++0,pBcwrԮTo]'a#K >bԁu; 4QԳk-*Zo'f1@-+u$Cc~}KG@[|2K'+_'ű{Gy(F Pſ|׿ FŲkᇤXםpsw *ަ/#F1bĈ#F1bĘXNIENDB`graphql-ruby-2.5.19/lib/graphql/dashboard/subscriptions.rb000066400000000000000000000075201514115062600236210ustar00rootroot00000000000000# frozen_string_literal: true module Graphql class Dashboard < Rails::Engine module Subscriptions class BaseController < Graphql::Dashboard::ApplicationController include Installable def feature_installed? schema_class.subscriptions.is_a?(GraphQL::Pro::Subscriptions) end INSTALLABLE_COMPONENT_HEADER_HTML = "GraphQL-Pro Subscriptions aren't installed on this schema yet.".html_safe INSTALLABLE_COMPONENT_MESSAGE_HTML = <<-HTML.html_safe Deliver live updates over Pusher or Ably with GraphQL-Pro's subscription integrations. HTML end class TopicsController < BaseController def show topic_name = params[:name] all_subscription_ids = [] schema_class.subscriptions.each_subscription_id(topic_name) do |sid| all_subscription_ids << sid end page = params[:page]&.to_i || 1 limit = params[:per_page]&.to_i || 20 offset = limit * (page - 1) subscription_ids = all_subscription_ids[offset, limit] subs = schema_class.subscriptions.read_subscriptions(subscription_ids) show_broadcast_subscribers_count = schema_class.subscriptions.show_broadcast_subscribers_count? subs.each do |sub| sub[:is_broadcast] = is_broadcast = schema_class.subscriptions.broadcast_subscription_id?(sub[:id]) if is_broadcast && show_broadcast_subscribers_count sub[:subscribers_count] = sub_count =schema_class.subscriptions.count_broadcast_subscribed(sub[:id]) sub[:still_subscribed] = sub_count > 0 else sub[:still_subscribed] = schema_class.subscriptions.still_subscribed?(sub[:id]) sub[:subscribers_count] = nil end end @topic_last_triggered_at = schema_class.subscriptions.topic_last_triggered_at(topic_name) @subscriptions = subs @subscriptions_count = all_subscription_ids.size @show_broadcast_subscribers_count = show_broadcast_subscribers_count @has_next_page = all_subscription_ids.size > offset + limit ? page + 1 : false end def index page = params[:page]&.to_i || 1 per_page = params[:per_page]&.to_i || 20 offset = per_page * (page - 1) limit = per_page topics, all_topics_count, has_next_page = schema_class.subscriptions.topics(offset: offset, limit: limit) @topics = topics @all_topics_count = all_topics_count @has_next_page = has_next_page @page = page end end class SubscriptionsController < BaseController def show subscription_id = params[:id] subscriptions = schema_class.subscriptions query_data = subscriptions.read_subscription(subscription_id) is_broadcast = subscriptions.broadcast_subscription_id?(subscription_id) if is_broadcast && subscriptions.show_broadcast_subscribers_count? subscribers_count = subscriptions.count_broadcast_subscribed(subscription_id) is_still_subscribed = subscribers_count > 0 else subscribers_count = nil is_still_subscribed = subscriptions.still_subscribed?(subscription_id) end @query_data = query_data @still_subscribed = is_still_subscribed @is_broadcast = is_broadcast @subscribers_count = subscribers_count end def clear_all schema_class.subscriptions.clear flash[:success] = "All subscription data cleared." head :no_content end end end end end graphql-ruby-2.5.19/lib/graphql/dashboard/views/000077500000000000000000000000001514115062600215165ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/000077500000000000000000000000001514115062600231545ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/000077500000000000000000000000001514115062600251035ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/detailed_traces/000077500000000000000000000000001514115062600302175ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/detailed_traces/traces/000077500000000000000000000000001514115062600315005ustar00rootroot00000000000000index.html.erb000066400000000000000000000034371514115062600341740ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/detailed_traces/traces<% content_for(:title, "Profiles") %>

Detailed Profiles

<%= button_tag "Delete All Traces", class: "btn btn-sm btn-outline-danger", data: { perfetto_delete_all: graphql_dashboard.delete_all_detailed_traces_traces_path } %>
<% if @traces.empty? %> <% end %> <% @traces.each do |trace| %> <% end %>
Operation Duration (ms) Timestamp Open in Perfetto UI
No traces saved yet. Read about saving traces <%= link_to "in the docs", "https://graphql-ruby.org/queries/tracing#detailed-profiles" %>.
<%= trace.operation_name %> <%= trace.duration_ms.round(2) %> <%= Time.at(trace.begin_ms / 1000.0).strftime("%Y-%m-%d %H:%M:%S.%L") %> <%= link_to "View ↗", "#", data: { perfetto_open: trace.operation_name, perfetto_path: graphql_dashboard.detailed_traces_trace_path(trace.id) } %> <%= link_to "Delete", "#", data: { perfetto_delete: graphql_dashboard.detailed_traces_trace_path(trace.id) }, class: "text-danger" %>
<% if @last && @traces.size >= @last %> <%= link_to("Previous >", graphql_dashboard.detailed_traces_traces_path(last: @last, before: @traces.last.begin_ms), class: "btn btn-outline-primary") %> <% end %>
graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/landings/000077500000000000000000000000001514115062600267025ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/landings/show.html.erb000066400000000000000000000007251514115062600313230ustar00rootroot00000000000000<% content_for(:title, "Landing") %>

Welcome to the GraphQL-Ruby Dashboard

Click the links above to see data about your schema (<%= schema_class %>).

graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/limiters/000077500000000000000000000000001514115062600267335ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/limiters/limiters/000077500000000000000000000000001514115062600305635ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/limiters/limiters/show.html.erb000066400000000000000000000054021514115062600332010ustar00rootroot00000000000000<% content_for(:title, @title) %> <% if @install_path %>

<%= @title %>

It looks like this limiter isn't installed yet. Install it now.

<% else %>

<%= @title %>

<%= link_to("This Hour", graphql_dashboard.limiters_limiter_path(params[:name], chart: "hour"), class: "btn btn-sm btn-outline-primary #{@chart_mode == "hour" ? "active" : "inactive"}", params: { chart: "hour" }) %> <%= link_to("Today", graphql_dashboard.limiters_limiter_path(params[:name], chart: "day"), class: "btn btn-sm btn-outline-primary #{@chart_mode == "day" ? "active" : "inactive"}", params: { chart: "day" }) %> <%= link_to("This Month", graphql_dashboard.limiters_limiter_path(params[:name], chart: "month"), class: "btn btn-sm btn-outline-primary #{@chart_mode == "month" ? "active" : "inactive"}", params: { chart: "month" }) %>
<%= form_tag graphql_dashboard.limiters_limiter_path(params[:name], chart: @chart_mode), method: "patch" do %> <%= submit_tag "#{@current_soft ? "Disable" : "Enable"} Soft Limiting", class: "btn btn-sm btn-outline-warning" %> <% end %>
<% @histogram.columns.each_with_index do |col, col_idx| %> <% col.values.each_with_index do |value, val_idx| %> <% end %> <% end %>
Date Limited Requests Unlimited Requests
<%= col.label %> <%= value.formatted_value %> <%= value.label %>: <%= value.formatted_value %>
<%= col.label %>
<%= content_tag "style", nonce: @csp_nonce do %> <% @histogram.columns.each_with_index do |col, col_idx| %> <% col_max = @histogram.max_column_value.to_f %> <% col.values.each_with_index do |val, val_idx| %> #data-<%= col_idx %>-<%= val_idx %> { --size: <%= val.value / col_max %>} <% end %> <% end %> <% end %> <% end %> graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/not_installed.html.erb000066400000000000000000000006341514115062600314020ustar00rootroot00000000000000<% content_for(:title, "Operation Store") %>

<%= @component_header_html %>

<%= @component_message_html %>

graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/operation_store/000077500000000000000000000000001514115062600303175ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/000077500000000000000000000000001514115062600317605ustar00rootroot00000000000000_form.html.erb000066400000000000000000000021311514115062600344350ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients<%= form_tag((@client.persisted? ? graphql_dashboard.operation_store_client_path(name: @client.name) : graphql_dashboard.operation_store_clients_path), method: (@client.persisted? ? "patch" : "post")) do %>
<%= text_field_tag "client[name]", @client.name, class: "form-control", disabled: @client.persisted? %>
a unique identifier for this owner of persisted operations
<%= textarea_tag "client[secret]", @client.secret, class: "form-control" %>
authentication credential for sync transactions
<%= submit_tag "Save", class: "btn btn-outline-primary" %>
<%= link_to "Back", graphql_dashboard.operation_store_clients_path, class: "btn btn-outline-secondary" %>
<% end %> edit.html.erb000066400000000000000000000013621514115062600342650ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients<% content_for(:title, "Edit #{@client.name}") %>

Edit <%= @client.name %>

<%= render partial: "graphql/dashboard/operation_store/clients/form" %>

Delete <%= @client.name %>

If you delete this client, it will no longer be able to use stored operations.

There is no way to undo this action.

<%= form_tag(graphql_dashboard.operation_store_client_path(name: @client.name), method: "delete") do %> <%= submit_tag "Permanently Delete #{@client.name.inspect}", class: "btn btn-outline-danger" %> <% end %>
index.html.erb000066400000000000000000000055431514115062600344540ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients<% content_for(:title, "Clients") %>

<%= pluralize(@clients_page.total_count, "Client") %>

<%= link_to("New Client", graphql_dashboard.new_operation_store_client_path, class: "btn btn-outline-primary") %>
<% if @clients_page.total_count == 0 %> <% else %> <% @clients_page.items.each do |client| %> <% end %> <% end %>
<%= link_to("Name", graphql_dashboard.operation_store_clients_path, params: { order_by: "name", order_dir: ((@order_by == "name" && @order_dir != :desc) ? "desc" : "asc" )}) %> Operations Created At Last Updated <%= link_to("Last Used At", graphql_dashboard.operation_store_clients_path, params: { order_by: "last_used_at", order_dir: ((@order_by == "last_used_at" && @order_dir != :desc) ? "desc": "asc")}) %>
To get started, create a <%= link_to "new client", graphql_dashboard.new_operation_store_client_path %>, then <%= link_to "sync operations", "https://graphql-ruby.org/operation_store/client_workflow.html" %> to your schema.
<%= link_to(client.name, graphql_dashboard.edit_operation_store_client_path(name: client.name)) %> <%= link_to(graphql_dashboard.operation_store_client_operations_path(client_name: client.name)) do %> <%= client.operations_count %><% if client.archived_operations_count > 0 %> (<%= client.archived_operations_count %> archived)<% end %> <% end %> <%= client.created_at %> <% if client.operations_count == 0 %> — <% else %> <%= client.last_synced_at %> <% end %> <%= client.last_used_at || "—" %>
<% if @clients_page.prev_page %> <%= link_to("« prev", graphql_dashboard.operation_store_clients_path(per_page: params[:per_page], page: @clients_page.prev_page), class: "btn btn-outline-secondary") %> <% else %> <% end %>
<% if @clients_page.next_page %> <%= link_to("next »", graphql_dashboard.operation_store_clients_path(per_page: params[:per_page], page: @clients_page.next_page), class: "btn btn-outline-secondary") %> <% else %> <% end %>
new.html.erb000066400000000000000000000002761514115062600341340ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients<% content_for(:title, "New Client") %>

New Client

<%= render partial: "graphql/dashboard/operation_store/clients/form" %> graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/000077500000000000000000000000001514115062600331575ustar00rootroot00000000000000index.html.erb000066400000000000000000000027501514115062600356500ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries<% content_for(:title, "Index#{@search_term ? " - #{@search_term}" : ""}") %>

Schema Index

<%= pluralize(@index_entries_page.total_count, @search_term ? "result" : "entry") %>

<%= text_field_tag "q", @search_term, class: "form-control", placeholder: "Find types, fields, arguments, or enum values" %>
<% @index_entries_page.items.each do |entry| %> <% end %>
Name # Usages Last Used At
<%= link_to(entry.name, graphql_dashboard.operation_store_index_entry_path(name: entry.name)) %> <%= entry.references_count %><% if entry.archived_references_count.nil? %>(missing data - call `YourSchema.operation_store.reindex` to repair index)<% elsif entry.archived_references_count > 0 %> (<%= entry.archived_references_count %> archived)<% end %> <%= entry.last_used_at %>
<%= # render_partial("_pagination") %> show.html.erb000066400000000000000000000017341514115062600355220ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries<% name = @chain.pop %> <% content_for(:title, "Index - #{@entry.name}") %>
<%= link_to("Index", graphql_dashboard.operation_store_index_entries_path) %> <% @chain.each do |c| %> > <%= link_to(c.split(".").last, graphql_dashboard.operation_store_index_entry_path(name: c)) %> <% end %> > <%= name.split(".").last %>

<%= name %>

Used By: <% if @operations.any? %>

    <% @operations.each do |operation| %>
  • <%= link_to(operation.name, graphql_dashboard.operation_store_operation_path(digest: operation.digest)) %><% if operation.is_archived %> (archived)<% end %>
  • <% end %>
<% else %> none <% end %>

Last used at: <%= @entry.last_used_at || "—" %>

graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/000077500000000000000000000000001514115062600325025ustar00rootroot00000000000000index.html.erb000066400000000000000000000072001514115062600351660ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations
<% if @client_operations %> <%= content_for(:title, "#{params[:client_name]} Operations") %>

<%= params[:client_name] %>

<% else %> <%= content_for(:title, "Operations") %>
<% end %>
<% if @client_operations %> <% else %> <% end %> <% if @operations_page.total_count == 0 %> <% else %> <% @operations_page.items.each do |operation| %> <% if @client_operations %> <% else %> <% end %> <% end %> <% end %>
<%= link_to "Name", graphql_dashboard.operation_store_operations_path({ order_by: "name", order_dir: params[:order_dir] == "asc" ? "desc" : "asc" }) %>Alias# ClientsDigest <%= link_to "Last Used At", graphql_dashboard.operation_store_operations_path({ order_by: "last_used_at", order_dir: params[:order_dir] == "asc" ? "desc" : "asc" }) %>
<% if @is_archived %> <%= link_to "Archived operations", "https://graphql-ruby.org/operation_store/server_management.html#archiving-and-deleting-data" %> will appear here. <% else %> Add your first stored operations with <%= link_to "sync", "https://graphql-ruby.org/operation_store/client_workflow.html" %>. <% end %>
<%= link_to(operation.name, graphql_dashboard.operation_store_operation_path(digest: operation.digest)) %><%= operation.operation_alias %><%= operation.clients_count %><%= operation.digest %> <%= operation.last_used_at %> <%= check_box_tag("value", (@client_operations ? operation.operation_alias : operation.digest), class: "archive-check form-check-input") %>
show.html.erb000066400000000000000000000040401514115062600350360ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations<% content_for(:title, "View #{params[:digest]}") %> <% if @operation.nil? %>

No stored operation found for <%= params[:digest] %>

<% else %>

<%= @operation.name %> <% if @operation.is_archived %> (archived)<% end %>

Aliases

<% if @client_operations.empty? %>

None

<% else %>
    <% @client_operations.each do |cl_op| %>
  • <%= cl_op.operation_alias %> <%= link_to(cl_op.client_name, graphql_dashboard.operation_store_client_operations_path(client_name: cl_op.client_name)) %> <%= cl_op.is_archived ? " (archived)" : "" %>
  • <% end %>
<% end %>

Last Used At

<%= @operation.last_used_at %>

Source

<%= textarea_tag "_source", @graphql_source, class: "graphql-highlight form-control", disabled: true, rows: @graphql_source.count("\n") + 1 %>

References

    <% @entries.each do |entry| %>
  • <%= link_to(entry.name, graphql_dashboard.operation_store_index_entry_path(name: entry.name)) %>
  • <% end %>

Digest

<%= @operation.digest %>

Minified Source

<%= textarea_tag "_source", @operation.body, class: "graphql-highlight form-control", disabled: true, rows: @operation.body.count("\n") + 1 %>
<% end %> graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/000077500000000000000000000000001514115062600300125ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/subscriptions/000077500000000000000000000000001514115062600327215ustar00rootroot00000000000000show.html.erb000066400000000000000000000026751514115062600352710ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/subscriptions<% content_for(:title, "Subscription #{params[:id]}") %>

Subscription: <%= params[:id] %>

<% if @query_data.nil? %>

This subscription was not found or is no longer active.

<% else %>

Created at <%= @query_data[:created_at] %>, last triggered at <%= @query_data[:last_triggered_at] || "--" %>

Subscribed? <%= @still_subscribed ? "YES" : "NO" %>

Broadcast? <%= @is_broadcast ? "YES" : "NO" %> <% if @is_broadcast %> <% if @subscribers_count.nil? %> This subscription may have multiple subscribers. <% else %> (<%= pluralize(@subscribers_count, "subscriber") %>) <% end %> <% end %>

Context:

<%= @query_data[:context].inspect %>

Variables:

<%= @query_data[:variables].inspect %>

Operation Name:

<%= @query_data[:operation_name].inspect %>

Query String:

<%= textarea_tag "_source", @query_data[:query_string], class: "graphql-highlight form-control", disabled: true, rows: @query_data[:query_string].count("\n") + 1 %>
<% end %> graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/000077500000000000000000000000001514115062600313135ustar00rootroot00000000000000index.html.erb000066400000000000000000000033551514115062600340060ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics<% content_for(:title, "Subscriptions - Topics") %>

<%= pluralize(@all_topics_count, "Subscription Topic") %>

<%= button_tag "Clear All", class: "btn btn-outline-danger", data: { subscriptions_delete_all: graphql_dashboard.subscriptions_clear_all_path } %>
<% if @all_topics_count == 0 %> <% else %> <% @topics.each do |topic| %> <% end %> <% end %>
Name # Subscriptions Last Triggered At
There aren't any subscriptions right now.
<%= link_to(topic.name, graphql_dashboard.subscriptions_topic_path(name: topic.name)) %> <%= topic.subscriptions_count %> <%= topic.last_triggered_at || "--" %>
<% if @page > 1 %> <%= link_to("« prev", graphql_dashboard.subscriptions_topics_path(per_page: params[:per_page], page: @page - 1), class: "btn btn-outline-secondary") %> <% else %> <% end %>
<% if @has_next_page %> <%= link_to("next »", graphql_dashboard.subscriptions_topics_path(per_page: params[:per_page], page: @page + 1), class: "btn btn-outline-secondary") %> <% else %> <% end %>
graphql-ruby-2.5.19/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb000066400000000000000000000024631514115062600337350ustar00rootroot00000000000000<%= content_for(:title, "Subscriptions - #{params[:name]}") %>

Topic: <%= params[:name] %>

Last triggered: <%= @topic_last_triggered_at || "none" %>

<%= pluralize(@subscriptions_count, "Subscription") %>

<% if @show_broadcast_subscribers_count %><% end %> <% @subscriptions.each do |subscription| %> <% if @show_broadcast_subscribers_count %><% end %> <% end %>
Subscription ID Created At Subscribed? Broadcast?Subscribers
<%= link_to(subscription[:id], graphql_dashboard.subscriptions_subscription_path(subscription[:id])) %> <%= subscription[:created_at] %> <%= subscription[:still_subscribed] ? "YES" : "NO" %> <%= subscription[:is_broadcast] ? "YES" : "NO" %><%= subscription[:subscribers_count] %>
graphql-ruby-2.5.19/lib/graphql/dashboard/views/layouts/000077500000000000000000000000001514115062600232165ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/layouts/graphql/000077500000000000000000000000001514115062600246545ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/layouts/graphql/dashboard/000077500000000000000000000000001514115062600266035ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb000066400000000000000000000133521514115062600325470ustar00rootroot00000000000000 "> GraphQL Dashboard <%= content_for?(:title) ? " · #{content_for(:title)}" : "" %> " media="screen" /> " media="screen" /> " media="screen" /> <%= csrf_meta_tags %>
<% flash.each do |flash_type, flash_message| %>
<% end %>
<%= yield %>

GraphQL-Ruby v<%= GraphQL::VERSION %> · <%= schema_class %>

graphql-ruby-2.5.19/lib/graphql/dataloader.rb000066400000000000000000000307631514115062600210700ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/dataloader/null_dataloader" require "graphql/dataloader/request" require "graphql/dataloader/request_all" require "graphql/dataloader/source" require "graphql/dataloader/active_record_association_source" require "graphql/dataloader/active_record_source" module GraphQL # This plugin supports Fiber-based concurrency, along with {GraphQL::Dataloader::Source}. # # @example Installing Dataloader # # class MySchema < GraphQL::Schema # use GraphQL::Dataloader # end # # @example Waiting for batch-loaded data in a GraphQL field # # field :team, Types::Team, null: true # # def team # dataloader.with(Sources::Record, Team).load(object.team_id) # end # class Dataloader class << self attr_accessor :default_nonblocking, :default_fiber_limit end def self.use(schema, nonblocking: nil, fiber_limit: nil) dataloader_class = if nonblocking warn("`nonblocking: true` is deprecated from `GraphQL::Dataloader`, please use `GraphQL::Dataloader::AsyncDataloader` instead. Docs: https://graphql-ruby.org/dataloader/async_dataloader.") Class.new(self) { self.default_nonblocking = true } else self end if fiber_limit dataloader_class = Class.new(dataloader_class) dataloader_class.default_fiber_limit = fiber_limit end schema.dataloader_class = dataloader_class end # Call the block with a Dataloader instance, # then run all enqueued jobs and return the result of the block. def self.with_dataloading(&block) dataloader = self.new result = nil dataloader.append_job { result = block.call(dataloader) } dataloader.run result end def initialize(nonblocking: self.class.default_nonblocking, fiber_limit: self.class.default_fiber_limit) @source_cache = Hash.new { |h, k| h[k] = {} } @pending_jobs = [] if !nonblocking.nil? @nonblocking = nonblocking end @fiber_limit = fiber_limit @lazies_at_depth = Hash.new { |h, k| h[k] = [] } end # @return [Integer, nil] attr_reader :fiber_limit def nonblocking? @nonblocking end # This is called before the fiber is spawned, from the parent context (i.e. from # the thread or fiber that it is scheduled from). # # @return [Hash] Current fiber-local variables def get_fiber_variables fiber_vars = {} Thread.current.keys.each do |fiber_var_key| fiber_vars[fiber_var_key] = Thread.current[fiber_var_key] end fiber_vars end # Set up the fiber variables in a new fiber. # # This is called within the fiber, right after it is spawned. # # @param vars [Hash] Fiber-local variables from {get_fiber_variables} # @return [void] def set_fiber_variables(vars) vars.each { |k, v| Thread.current[k] = v } nil end # This method is called when Dataloader is finished using a fiber. # Use it to perform any cleanup, such as releasing database connections (if required manually) def cleanup_fiber end # Get a Source instance from this dataloader, for calling `.load(...)` or `.request(...)` on. # # @param source_class [Class] # @return [GraphQL::Dataloader::Source] An instance of {source_class}, initialized with `self, *batch_parameters`, # and cached for the lifetime of this {Multiplex}. if RUBY_VERSION < "3" || RUBY_ENGINE != "ruby" # truffle-ruby wasn't doing well with the implementation below def with(source_class, *batch_args) batch_key = source_class.batch_key_for(*batch_args) @source_cache[source_class][batch_key] ||= begin source = source_class.new(*batch_args) source.setup(self) source end end else def with(source_class, *batch_args, **batch_kwargs) batch_key = source_class.batch_key_for(*batch_args, **batch_kwargs) @source_cache[source_class][batch_key] ||= begin source = source_class.new(*batch_args, **batch_kwargs) source.setup(self) source end end end # Tell the dataloader that this fiber is waiting for data. # # Dataloader will resume the fiber after the requested data has been loaded (by another Fiber). # # @return [void] def yield(source = Fiber[:__graphql_current_dataloader_source]) trace = Fiber[:__graphql_current_multiplex]&.current_trace trace&.dataloader_fiber_yield(source) Fiber.yield trace&.dataloader_fiber_resume(source) nil end # @api private Nothing to see here def append_job(callable = nil, &job) # Given a block, queue it up to be worked through when `#run` is called. # (If the dataloader is already running, then a Fiber will pick this up later.) @pending_jobs.push(callable || job) nil end # Clear any already-loaded objects from {Source} caches # @return [void] def clear_cache @source_cache.each do |_source_class, batched_sources| batched_sources.each_value(&:clear_cache) end nil end # Use a self-contained queue for the work in the block. def run_isolated prev_queue = @pending_jobs prev_pending_keys = {} prev_lazies_at_depth = @lazies_at_depth @lazies_at_depth = @lazies_at_depth.dup.clear # Clear pending loads but keep already-cached records # in case they are useful to the given block. @source_cache.each do |source_class, batched_sources| batched_sources.each do |batch_args, batched_source_instance| if batched_source_instance.pending? prev_pending_keys[batched_source_instance] = batched_source_instance.pending.dup batched_source_instance.pending.clear end end end @pending_jobs = [] res = nil # Make sure the block is inside a Fiber, so it can `Fiber.yield` append_job { res = yield } run res ensure @pending_jobs = prev_queue @lazies_at_depth = prev_lazies_at_depth prev_pending_keys.each do |source_instance, pending| pending.each do |key, value| if !source_instance.results.key?(key) source_instance.pending[key] = value end end end end # @param trace_query_lazy [nil, Execution::Multiplex] def run(trace_query_lazy: nil) trace = Fiber[:__graphql_current_multiplex]&.current_trace jobs_fiber_limit, total_fiber_limit = calculate_fiber_limit job_fibers = [] next_job_fibers = [] source_fibers = [] next_source_fibers = [] first_pass = true manager = spawn_fiber do trace&.begin_dataloader(self) while first_pass || !job_fibers.empty? first_pass = false run_pending_steps(trace, job_fibers, next_job_fibers, jobs_fiber_limit, source_fibers, next_source_fibers, total_fiber_limit) if !@lazies_at_depth.empty? with_trace_query_lazy(trace_query_lazy) do run_next_pending_lazies(job_fibers, trace) run_pending_steps(trace, job_fibers, next_job_fibers, jobs_fiber_limit, source_fibers, next_source_fibers, total_fiber_limit) end end end trace&.end_dataloader(self) end run_fiber(manager) if manager.alive? raise "Invariant: Manager fiber didn't terminate properly." end if !job_fibers.empty? raise "Invariant: job fibers should have exited but #{job_fibers.size} remained" end if !source_fibers.empty? raise "Invariant: source fibers should have exited but #{source_fibers.size} remained" end rescue UncaughtThrowError => e throw e.tag, e.value end def run_fiber(f) f.resume end # @api private def lazy_at_depth(depth, lazy) @lazies_at_depth[depth] << lazy end def spawn_fiber fiber_vars = get_fiber_variables Fiber.new(blocking: !@nonblocking) { set_fiber_variables(fiber_vars) yield cleanup_fiber } end # Pre-warm the Dataloader cache with ActiveRecord objects which were loaded elsewhere. # These will be used by {Dataloader::ActiveRecordSource}, {Dataloader::ActiveRecordAssociationSource} and their helper # methods, `dataload_record` and `dataload_association`. # @param records [Array] Already-loaded records to warm the cache with # @param index_by [Symbol] The attribute to use as the cache key. (Should match `find_by:` when using {ActiveRecordSource}) # @return [void] def merge_records(records, index_by: :id) records_by_class = Hash.new { |h, k| h[k] = {} } records.each do |r| records_by_class[r.class][r.public_send(index_by)] = r end records_by_class.each do |r_class, records| with(ActiveRecordSource, r_class).merge(records) end end private def run_next_pending_lazies(job_fibers, trace) smallest_depth = nil @lazies_at_depth.each_key do |depth_key| smallest_depth ||= depth_key if depth_key < smallest_depth smallest_depth = depth_key end end if smallest_depth lazies = @lazies_at_depth.delete(smallest_depth) if !lazies.empty? lazies.each_with_index do |l, idx| append_job { l.value } end job_fibers.unshift(spawn_job_fiber(trace)) end end end def run_pending_steps(trace, job_fibers, next_job_fibers, jobs_fiber_limit, source_fibers, next_source_fibers, total_fiber_limit) while (f = (job_fibers.shift || (((next_job_fibers.size + job_fibers.size) < jobs_fiber_limit) && spawn_job_fiber(trace)))) if f.alive? finished = run_fiber(f) if !finished next_job_fibers << f end end end join_queues(job_fibers, next_job_fibers) while (!source_fibers.empty? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) }) while (f = source_fibers.shift || (((job_fibers.size + source_fibers.size + next_source_fibers.size + next_job_fibers.size) < total_fiber_limit) && spawn_source_fiber(trace))) if f.alive? finished = run_fiber(f) if !finished next_source_fibers << f end end end join_queues(source_fibers, next_source_fibers) end end def with_trace_query_lazy(multiplex_or_nil, &block) if (multiplex = multiplex_or_nil) query = multiplex.queries.length == 1 ? multiplex.queries[0] : nil multiplex.current_trace.execute_query_lazy(query: query, multiplex: multiplex, &block) else yield end end def calculate_fiber_limit total_fiber_limit = @fiber_limit || Float::INFINITY if total_fiber_limit < 4 raise ArgumentError, "Dataloader fiber limit is too low (#{total_fiber_limit}), it must be at least 4" end total_fiber_limit -= 1 # deduct one fiber for `manager` # Deduct at least one fiber for sources jobs_fiber_limit = total_fiber_limit - 2 return jobs_fiber_limit, total_fiber_limit end def join_queues(prev_queue, new_queue) @nonblocking && Fiber.scheduler.run prev_queue.concat(new_queue) new_queue.clear end def spawn_job_fiber(trace) if !@pending_jobs.empty? spawn_fiber do trace&.dataloader_spawn_execution_fiber(@pending_jobs) while job = @pending_jobs.shift job.call end trace&.dataloader_fiber_exit end end end def spawn_source_fiber(trace) pending_sources = nil @source_cache.each_value do |source_by_batch_params| source_by_batch_params.each_value do |source| if source.pending? pending_sources ||= [] pending_sources << source end end end if pending_sources spawn_fiber do trace&.dataloader_spawn_source_fiber(pending_sources) pending_sources.each do |source| Fiber[:__graphql_current_dataloader_source] = source trace&.begin_dataloader_source(source) source.run_pending_keys trace&.end_dataloader_source(source) end trace&.dataloader_fiber_exit end end end end end require "graphql/dataloader/async_dataloader" graphql-ruby-2.5.19/lib/graphql/dataloader/000077500000000000000000000000001514115062600205325ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/dataloader/active_record_association_source.rb000066400000000000000000000050211514115062600276420ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/dataloader/source" require "graphql/dataloader/active_record_source" module GraphQL class Dataloader class ActiveRecordAssociationSource < GraphQL::Dataloader::Source RECORD_SOURCE_CLASS = ActiveRecordSource def initialize(association, scope = nil) @association = association @scope = scope end def self.batch_key_for(association, scope = nil) if scope [association, scope.to_sql] else [association] end end def load(record) if (assoc = record.association(@association)).loaded? assoc.target else super end end def fetch(records) record_classes = Set.new.compare_by_identity associated_classes = Set.new.compare_by_identity scoped_fetch = !@scope.nil? records.each do |record| if scoped_fetch assoc = record.association(@association) assoc.reset end if record_classes.add?(record.class) reflection = record.class.reflect_on_association(@association) if !reflection.polymorphic? && reflection.klass associated_classes.add(reflection.klass) end end end available_records = [] associated_classes.each do |assoc_class| already_loaded_records = dataloader.with(RECORD_SOURCE_CLASS, assoc_class).results.values available_records.concat(already_loaded_records) end ::ActiveRecord::Associations::Preloader.new(records: records, associations: @association, available_records: available_records, scope: @scope).call loaded_associated_records = records.map { |r| assoc = r.association(@association) lar = assoc.target if scoped_fetch assoc.reset end lar } if !scoped_fetch # Don't cache records loaded via scope because they might have reduced `SELECT`s # Could check .select_values here? records_by_model = {} loaded_associated_records.flatten.each do |record| if record updates = records_by_model[record.class] ||= {} updates[record.id] = record end end records_by_model.each do |model_class, updates| dataloader.with(RECORD_SOURCE_CLASS, model_class).merge(updates) end end loaded_associated_records end end end end graphql-ruby-2.5.19/lib/graphql/dataloader/active_record_source.rb000066400000000000000000000025301514115062600252500ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/dataloader/source" module GraphQL class Dataloader class ActiveRecordSource < GraphQL::Dataloader::Source def initialize(model_class, find_by: model_class.primary_key) @model_class = model_class @find_by = find_by @find_by_many = find_by.is_a?(Array) if @find_by_many @type_for_column = @find_by.map { |fb| @model_class.type_for_attribute(fb) } else @type_for_column = @model_class.type_for_attribute(@find_by) end end def result_key_for(requested_key) normalize_fetch_key(requested_key) end def normalize_fetch_key(requested_key) if @find_by_many requested_key.each_with_index.map do |k, idx| @type_for_column[idx].cast(k) end else @type_for_column.cast(requested_key) end end def fetch(record_ids) records = @model_class.where(@find_by => record_ids) record_lookup = {} if @find_by_many records.each do |r| key = @find_by.map { |fb| r.public_send(fb) } record_lookup[key] = r end else records.each { |r| record_lookup[r.public_send(@find_by)] = r } end record_ids.map { |id| record_lookup[id] } end end end end graphql-ruby-2.5.19/lib/graphql/dataloader/async_dataloader.rb000066400000000000000000000075741514115062600243710ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Dataloader class AsyncDataloader < Dataloader def yield(source = Fiber[:__graphql_current_dataloader_source]) trace = Fiber[:__graphql_current_multiplex]&.current_trace trace&.dataloader_fiber_yield(source) if (condition = Fiber[:graphql_dataloader_next_tick]) condition.wait else Fiber.yield end trace&.dataloader_fiber_resume(source) nil end def run(trace_query_lazy: nil) trace = Fiber[:__graphql_current_multiplex]&.current_trace jobs_fiber_limit, total_fiber_limit = calculate_fiber_limit job_fibers = [] next_job_fibers = [] source_tasks = [] next_source_tasks = [] first_pass = true sources_condition = Async::Condition.new manager = spawn_fiber do trace&.begin_dataloader(self) while first_pass || !job_fibers.empty? first_pass = false fiber_vars = get_fiber_variables run_pending_steps(job_fibers, next_job_fibers, source_tasks, jobs_fiber_limit, trace) Sync do |root_task| set_fiber_variables(fiber_vars) while !source_tasks.empty? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) } while (task = (source_tasks.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size + next_source_tasks.size) < total_fiber_limit) && spawn_source_task(root_task, sources_condition, trace)))) if task.alive? root_task.yield # give the source task a chance to run next_source_tasks << task end end sources_condition.signal source_tasks.concat(next_source_tasks) next_source_tasks.clear end end if !@lazies_at_depth.empty? with_trace_query_lazy(trace_query_lazy) do run_next_pending_lazies(job_fibers, trace) run_pending_steps(job_fibers, next_job_fibers, source_tasks, jobs_fiber_limit, trace) end end end trace&.end_dataloader(self) end manager.resume if manager.alive? raise "Invariant: Manager didn't terminate successfully: #{manager}" end rescue UncaughtThrowError => e throw e.tag, e.value end private def run_pending_steps(job_fibers, next_job_fibers, source_tasks, jobs_fiber_limit, trace) while (f = (job_fibers.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size) < jobs_fiber_limit) && spawn_job_fiber(trace)))) if f.alive? finished = run_fiber(f) if !finished next_job_fibers << f end end end job_fibers.concat(next_job_fibers) next_job_fibers.clear end def spawn_source_task(parent_task, condition, trace) pending_sources = nil @source_cache.each_value do |source_by_batch_params| source_by_batch_params.each_value do |source| if source.pending? pending_sources ||= [] pending_sources << source end end end if pending_sources fiber_vars = get_fiber_variables parent_task.async do trace&.dataloader_spawn_source_fiber(pending_sources) set_fiber_variables(fiber_vars) Fiber[:graphql_dataloader_next_tick] = condition pending_sources.each do |s| trace&.begin_dataloader_source(s) s.run_pending_keys trace&.end_dataloader_source(s) end cleanup_fiber trace&.dataloader_fiber_exit end end end end end end graphql-ruby-2.5.19/lib/graphql/dataloader/null_dataloader.rb000066400000000000000000000033331514115062600242130ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Dataloader # GraphQL-Ruby uses this when Dataloader isn't enabled. # # It runs execution code inline and gathers lazy objects (eg. Promises) # and resolves them during {#run}. class NullDataloader < Dataloader def initialize(*) @lazies_at_depth = Hash.new { |h,k| h[k] = [] } end def freeze @lazies_at_depth.default_proc = nil @lazies_at_depth.freeze super end def run(trace_query_lazy: nil) with_trace_query_lazy(trace_query_lazy) do while !@lazies_at_depth.empty? smallest_depth = nil @lazies_at_depth.each_key do |depth_key| smallest_depth ||= depth_key if depth_key < smallest_depth smallest_depth = depth_key end end if smallest_depth lazies = @lazies_at_depth.delete(smallest_depth) lazies.each(&:value) # resolve these Lazy instances end end end end def run_isolated new_dl = self.class.new res = nil new_dl.append_job { res = yield } new_dl.run res end def clear_cache; end def yield(_source) raise GraphQL::Error, "GraphQL::Dataloader is not running -- add `use GraphQL::Dataloader` to your schema to use Dataloader sources." end def append_job(callable = nil) callable ? callable.call : yield nil end def with(*) raise GraphQL::Error, "GraphQL::Dataloader is not running -- add `use GraphQL::Dataloader` to your schema to use Dataloader sources." end end end end graphql-ruby-2.5.19/lib/graphql/dataloader/request.rb000066400000000000000000000012431514115062600225470ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Dataloader # @see Source#request which returns an instance of this class Request def initialize(source, key) @source = source @key = key end # Call this method to cause the current Fiber to wait for the results of this request. # # @return [Object] the object loaded for `key` def load @source.load(@key) end def load_with_deprecation_warning warn("Returning `.request(...)` from GraphQL::Dataloader is deprecated, use `.load(...)` instead. (See usage of #{@source} with #{@key.inspect}).") load end end end end graphql-ruby-2.5.19/lib/graphql/dataloader/request_all.rb000066400000000000000000000007521514115062600234030ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Dataloader # @see Source#request_all which returns an instance of this. class RequestAll < Request def initialize(source, keys) @source = source @keys = keys end # Call this method to cause the current Fiber to wait for the results of this request. # # @return [Array] One object for each of `keys` def load @source.load_all(@keys) end end end end graphql-ruby-2.5.19/lib/graphql/dataloader/source.rb000066400000000000000000000173721514115062600223710ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Dataloader class Source # Called by {Dataloader} to prepare the {Source}'s internal state # @api private def setup(dataloader) # These keys have been requested but haven't been fetched yet @pending = {} # These keys have been passed to `fetch` but haven't been finished yet @fetching = {} # { key => result } @results = {} @dataloader = dataloader end attr_reader :dataloader # @return [Dataloader::Request] a pending request for a value from `key`. Call `.load` on that object to wait for the result. def request(value) res_key = result_key_for(value) if !@results.key?(res_key) @pending[res_key] ||= normalize_fetch_key(value) end Dataloader::Request.new(self, value) end # Implement this method to return a stable identifier if different # key objects should load the same data value. # # @param value [Object] A value passed to `.request` or `.load`, for which a value will be loaded # @return [Object] The key for tracking this pending data def result_key_for(value) value end # Implement this method if varying values given to {load} (etc) should be consolidated # or normalized before being handed off to your {fetch} implementation. # # This is different than {result_key_for} because _that_ method handles unification inside Dataloader's cache, # but this method changes the value passed into {fetch}. # # @param value [Object] The value passed to {load}, {load_all}, {request}, or {request_all} # @return [Object] The value given to {fetch} def normalize_fetch_key(value) value end # @return [Dataloader::Request] a pending request for a values from `keys`. Call `.load` on that object to wait for the results. def request_all(values) values.each do |v| res_key = result_key_for(v) if !@results.key?(res_key) @pending[res_key] ||= normalize_fetch_key(v) end end Dataloader::RequestAll.new(self, values) end # @param value [Object] A loading value which will be passed to {#fetch} if it isn't already in the internal cache. # @return [Object] The result from {#fetch} for `key`. If `key` hasn't been loaded yet, the Fiber will yield until it's loaded. def load(value) result_key = result_key_for(value) if @results.key?(result_key) result_for(result_key) else @pending[result_key] ||= normalize_fetch_key(value) sync([result_key]) result_for(result_key) end end # @param values [Array] Loading keys which will be passed to `#fetch` (or read from the internal cache). # @return [Object] The result from {#fetch} for `keys`. If `keys` haven't been loaded yet, the Fiber will yield until they're loaded. def load_all(values) result_keys = [] pending_keys = [] values.each { |v| k = result_key_for(v) result_keys << k if !@results.key?(k) @pending[k] ||= normalize_fetch_key(v) pending_keys << k end } if !pending_keys.empty? sync(pending_keys) end result_keys.map { |k| result_for(k) } end # Subclasses must implement this method to return a value for each of `keys` # @param keys [Array] keys passed to {#load}, {#load_all}, {#request}, or {#request_all} # @return [Array] A loaded value for each of `keys`. The array must match one-for-one to the list of `keys`. def fetch(keys) # somehow retrieve these from the backend raise "Implement `#{self.class}#fetch(#{keys.inspect}) to return a record for each of the keys" end MAX_ITERATIONS = 1000 # Wait for a batch, if there's anything to batch. # Then run the batch and update the cache. # @return [void] def sync(pending_result_keys) @dataloader.yield(self) iterations = 0 while pending_result_keys.any? { |key| !@results.key?(key) } iterations += 1 if iterations > MAX_ITERATIONS raise "#{self.class}#sync tried #{MAX_ITERATIONS} times to load pending keys (#{pending_result_keys}), but they still weren't loaded. There is likely a circular dependency#{@dataloader.fiber_limit ? " or `fiber_limit: #{@dataloader.fiber_limit}` is set too low" : ""}." end @dataloader.yield(self) end nil end # @return [Boolean] True if this source has any pending requests for data. def pending? !@pending.empty? end # Add these key-value pairs to this source's cache # (future loads will use these merged values). # @param new_results [Hash Object>] key-value pairs to cache in this source # @return [void] def merge(new_results) new_results.each do |new_k, new_v| key = result_key_for(new_k) @results[key] = new_v end nil end # Called by {GraphQL::Dataloader} to resolve and pending requests to this source. # @api private # @return [void] def run_pending_keys if !@fetching.empty? @fetching.each_key { |k| @pending.delete(k) } end return if @pending.empty? fetch_h = @pending @pending = {} @fetching.merge!(fetch_h) results = fetch(fetch_h.values) fetch_h.each_with_index do |(key, _value), idx| @results[key] = results[idx] end nil rescue StandardError => error fetch_h.each_key { |key| @results[key] = error } ensure fetch_h && fetch_h.each_key { |k| @fetching.delete(k) } end # These arguments are given to `dataloader.with(source_class, ...)`. The object # returned from this method is used to de-duplicate batch loads under the hood # by using it as a Hash key. # # By default, the arguments are all put in an Array. To customize how this source's # batches are merged, override this method to return something else. # # For example, if you pass `ActiveRecord::Relation`s to `.with(...)`, you could override # this method to call `.to_sql` on them, thus merging `.load(...)` calls when they apply # to equivalent relations. # # @param batch_args [Array] # @param batch_kwargs [Hash] # @return [Object] def self.batch_key_for(*batch_args, **batch_kwargs) [*batch_args, **batch_kwargs] end # Clear any already-loaded objects for this source # @return [void] def clear_cache @results.clear nil end attr_reader :pending, :results private # Reads and returns the result for the key from the internal cache, or raises an error if the result was an error # @param key [Object] key passed to {#load} or {#load_all} # @return [Object] The result from {#fetch} for `key`. # @api private def result_for(key) if !@results.key?(key) raise GraphQL::InvariantError, <<-ERR Fetching result for a key on #{self.class} that hasn't been loaded yet (#{key.inspect}, loaded: #{@results.keys}) This key should have been loaded already. This is a bug in GraphQL::Dataloader, please report it on GitHub: https://github.com/rmosolgo/graphql-ruby/issues/new. ERR end result = @results[key] if result.is_a?(StandardError) # Dup it because the rescuer may modify it. # (This happens for GraphQL::ExecutionErrors, at least) raise result.dup end result end end end end graphql-ruby-2.5.19/lib/graphql/date_encoding_error.rb000066400000000000000000000010211514115062600227450ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL # This error is raised when `Types::ISO8601Date` is asked to return a value # that cannot be parsed to a Ruby Date. # # @see GraphQL::Types::ISO8601Date which raises this error class DateEncodingError < GraphQL::RuntimeTypeError # The value which couldn't be encoded attr_reader :date_value def initialize(value) @date_value = value super("Date cannot be parsed: #{value}. \nDate must be able to be parsed as a Ruby Date object.") end end end graphql-ruby-2.5.19/lib/graphql/dig.rb000066400000000000000000000012201514115062600175150ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Dig # implemented using the old activesupport #dig instead of the ruby built-in # so we can use some of the magic in Schema::InputObject and Interpreter::Arguments # to handle stringified/symbolized keys. # # @param own_key [String, Symbol] A key to retrieve # @param rest_keys [Array<[String, Symbol>] Retrieves the value object corresponding to the each key objects repeatedly # @return [Object] def dig(own_key, *rest_keys) val = self[own_key] if val.nil? || rest_keys.empty? val else val.dig(*rest_keys) end end end end graphql-ruby-2.5.19/lib/graphql/duration_encoding_error.rb000066400000000000000000000011201514115062600236550ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL # This error is raised when `Types::ISO8601Duration` is asked to return a value # that cannot be parsed as an ISO8601-formatted duration by ActiveSupport::Duration. # # @see GraphQL::Types::ISO8601Duration which raises this error class DurationEncodingError < GraphQL::RuntimeTypeError # The value which couldn't be encoded attr_reader :duration_value def initialize(value) @duration_value = value super("Duration cannot be parsed: #{value}. \nDuration must be an ISO8601-formatted duration.") end end end graphql-ruby-2.5.19/lib/graphql/execution.rb000066400000000000000000000007161514115062600207660ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/execution/directive_checks" require "graphql/execution/interpreter" require "graphql/execution/lazy" require "graphql/execution/lookahead" require "graphql/execution/multiplex" require "graphql/execution/errors" module GraphQL module Execution # @api private class Skip < GraphQL::Error; end # Just a singleton for implementing {Query::Context#skip} # @api private SKIP = Skip.new end end graphql-ruby-2.5.19/lib/graphql/execution/000077500000000000000000000000001514115062600204355ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/execution/directive_checks.rb000066400000000000000000000020341514115062600242570ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Execution # Boolean checks for how an AST node's directives should # influence its execution # @api private module DirectiveChecks SKIP = "skip" INCLUDE = "include" module_function # @return [Boolean] Should this node be included in the query? def include?(directive_ast_nodes, query) directive_ast_nodes.each do |directive_ast_node| name = directive_ast_node.name directive_defn = query.schema.directives[name] case name when SKIP args = query.arguments_for(directive_ast_node, directive_defn) if args[:if] == true return false end when INCLUDE args = query.arguments_for(directive_ast_node, directive_defn) if args[:if] == false return false end else # Undefined directive, or one we don't care about end end true end end end end graphql-ruby-2.5.19/lib/graphql/execution/errors.rb000066400000000000000000000073441514115062600223060ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Execution class Errors # Register this handler, updating the # internal handler index to maintain least-to-most specific. # # @param error_class [Class] # @param error_handlers [Hash] # @param error_handler [Proc] # @return [void] def self.register_rescue_from(error_class, error_handlers, error_handler) subclasses_handlers = {} this_level_subclasses = [] # During this traversal, do two things: # - Identify any already-registered subclasses of this error class # and gather them up to be inserted _under_ this class # - Find the point in the index where this handler should be inserted # (That is, _under_ any superclasses, or at top-level, if there are no superclasses registered) while (error_handlers) do this_level_subclasses.clear # First, identify already-loaded handlers that belong # _under_ this one. (That is, they're handlers # for subclasses of `error_class`.) error_handlers.each do |err_class, handler| if err_class < error_class subclasses_handlers[err_class] = handler this_level_subclasses << err_class end end # Any handlers that we'll be moving, delete them from this point in the index this_level_subclasses.each do |err_class| error_handlers.delete(err_class) end # See if any keys in this hash are superclasses of this new class: next_index_point = error_handlers.find { |err_class, handler| error_class < err_class } if next_index_point error_handlers = next_index_point[1][:subclass_handlers] else # this new handler doesn't belong to any sub-handlers, # so insert it in the current set of `handlers` break end end # Having found the point at which to insert this handler, # register it and merge any subclass handlers back in at this point. this_class_handlers = error_handlers[error_class] this_class_handlers[:handler] = error_handler this_class_handlers[:subclass_handlers].merge!(subclasses_handlers) nil end # @return [Proc, nil] The handler for `error_class`, if one was registered on this schema or inherited def self.find_handler_for(schema, error_class) handlers = schema.error_handlers[:subclass_handlers] handler = nil while (handlers) do _err_class, next_handler = handlers.find { |err_class, handler| error_class <= err_class } if next_handler handlers = next_handler[:subclass_handlers] handler = next_handler else # Don't reassign `handler` -- # let the previous assignment carry over outside this block. break end end # check for a handler from a parent class: if schema.superclass.respond_to?(:error_handlers) parent_handler = find_handler_for(schema.superclass, error_class) end # If the inherited handler is more specific than the one defined here, # use it. # If it's a tie (or there is no parent handler), use the one defined here. # If there's an inherited one, but not one defined here, use the inherited one. # Otherwise, there's no handler for this error, return `nil`. if parent_handler && handler && parent_handler[:class] < handler[:class] parent_handler elsif handler handler elsif parent_handler parent_handler else nil end end end end end graphql-ruby-2.5.19/lib/graphql/execution/interpreter.rb000066400000000000000000000153331514115062600233320ustar00rootroot00000000000000# frozen_string_literal: true require "fiber" require "graphql/execution/interpreter/argument_value" require "graphql/execution/interpreter/arguments" require "graphql/execution/interpreter/arguments_cache" require "graphql/execution/interpreter/execution_errors" require "graphql/execution/interpreter/runtime" require "graphql/execution/interpreter/resolve" require "graphql/execution/interpreter/handles_raw_value" module GraphQL module Execution class Interpreter class << self # Used internally to signal that the query shouldn't be executed # @api private NO_OPERATION = GraphQL::EmptyObjects::EMPTY_HASH # @param schema [GraphQL::Schema] # @param queries [Array] # @param context [Hash] # @param max_complexity [Integer, nil] # @return [Array] One result per query def run_all(schema, query_options, context: {}, max_complexity: schema.max_complexity) queries = query_options.map do |opts| query = case opts when Hash schema.query_class.new(schema, nil, **opts) when GraphQL::Query, GraphQL::Query::Partial opts else raise "Expected Hash or GraphQL::Query, not #{opts.class} (#{opts.inspect})" end query end return GraphQL::EmptyObjects::EMPTY_ARRAY if queries.empty? multiplex = Execution::Multiplex.new(schema: schema, queries: queries, context: context, max_complexity: max_complexity) trace = multiplex.current_trace Fiber[:__graphql_current_multiplex] = multiplex trace.execute_multiplex(multiplex: multiplex) do schema = multiplex.schema queries = multiplex.queries multiplex_analyzers = schema.multiplex_analyzers if multiplex.max_complexity multiplex_analyzers += [GraphQL::Analysis::MaxQueryComplexity] end trace.begin_analyze_multiplex(multiplex, multiplex_analyzers) schema.analysis_engine.analyze_multiplex(multiplex, multiplex_analyzers) trace.end_analyze_multiplex(multiplex, multiplex_analyzers) begin # Since this is basically the batching context, # share it for a whole multiplex multiplex.context[:interpreter_instance] ||= multiplex.schema.query_execution_strategy(deprecation_warning: false).new # Do as much eager evaluation of the query as possible results = [] queries.each_with_index do |query, idx| if query.subscription? && !query.subscription_update? subs_namespace = query.context.namespace(:subscriptions) subs_namespace[:events] = [] subs_namespace[:subscriptions] = {} end multiplex.dataloader.append_job { operation = query.selected_operation result = if operation.nil? || !query.valid? || !query.context.errors.empty? NO_OPERATION else begin # Although queries in a multiplex _share_ an Interpreter instance, # they also have another item of state, which is private to that query # in particular, assign it here: runtime = Runtime.new(query: query) query.context.namespace(:interpreter_runtime)[:runtime] = runtime query.current_trace.execute_query(query: query) do runtime.run_eager end rescue GraphQL::ExecutionError => err query.context.errors << err end end results[idx] = result } end multiplex.dataloader.run(trace_query_lazy: multiplex) # Then, find all errors and assign the result to the query object results.each_with_index do |data_result, idx| query = queries[idx] if (events = query.context.namespace(:subscriptions)[:events]) && !events.empty? schema.subscriptions.write_subscription(query, events) end # Assign the result so that it can be accessed in instrumentation query.result_values = if data_result.equal?(NO_OPERATION) if !query.valid? || !query.context.errors.empty? # A bit weird, but `Query#static_errors` _includes_ `query.context.errors` { "errors" => query.static_errors.map(&:to_h) } else data_result end else result = {} if !query.context.errors.empty? error_result = query.context.errors.map(&:to_h) result["errors"] = error_result end result["data"] = query.context.namespace(:interpreter_runtime)[:runtime].final_result result end if query.context.namespace?(:__query_result_extensions__) query.result_values["extensions"] = query.context.namespace(:__query_result_extensions__) end # Get the Query::Result, not the Hash results[idx] = query.result end results rescue Exception # TODO rescue at a higher level so it will catch errors in analysis, too # Assign values here so that the query's `@executed` becomes true queries.map { |q| q.result_values ||= {} } raise ensure Fiber[:__graphql_current_multiplex] = nil queries.map { |query| runtime = query.context.namespace(:interpreter_runtime)[:runtime] if runtime runtime.delete_all_interpreter_context end } end end end end class ListResultFailedError < GraphQL::Error def initialize(value:, path:, field:) message = "Failed to build a GraphQL list result for field `#{field.path}` at path `#{path.join(".")}`.\n".dup message << "Expected `#{value.inspect}` (#{value.class}) to implement `.each` to satisfy the GraphQL return type `#{field.type.to_type_signature}`.\n" if field.connection? message << "\nThis field was treated as a Relay-style connection; add `connection: false` to the `field(...)` to disable this behavior." end super(message) end end end end end graphql-ruby-2.5.19/lib/graphql/execution/interpreter/000077500000000000000000000000001514115062600230005ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/execution/interpreter/argument_value.rb000066400000000000000000000020611514115062600263420ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Execution class Interpreter # A container for metadata regarding arguments present in a GraphQL query. # @see Interpreter::Arguments#argument_values for a hash of these objects. class ArgumentValue def initialize(definition:, value:, original_value:, default_used:) @definition = definition @value = value @original_value = original_value @default_used = default_used end # @return [Object] The Ruby-ready value for this Argument attr_reader :value # @return [Object] The value of this argument _before_ `prepare` is applied. attr_reader :original_value # @return [GraphQL::Schema::Argument] The definition instance for this argument attr_reader :definition # @return [Boolean] `true` if the schema-defined `default_value:` was applied in this case. (No client-provided value was present.) def default_used? @default_used end end end end end graphql-ruby-2.5.19/lib/graphql/execution/interpreter/arguments.rb000066400000000000000000000064301514115062600253350ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Execution class Interpreter # A wrapper for argument hashes in GraphQL queries. # # This object is immutable so that the runtime code can be sure that # modifications don't leak from one use to another # # @see GraphQL::Query#arguments_for to get access to these objects. class Arguments extend Forwardable include GraphQL::Dig # The Ruby-style arguments hash, ready for a resolver. # This hash is the one used at runtime. # # @return [Hash] attr_reader :keyword_arguments # @param argument_values [nil, Hash{Symbol => ArgumentValue}] # @param keyword_arguments [nil, Hash{Symbol => Object}] def initialize(keyword_arguments: nil, argument_values:) @empty = argument_values.nil? || argument_values.empty? # This is only present when `extras` have been merged in: if keyword_arguments # This is a little crazy. We expect the `:argument_details` extra to _include extras_, # but the object isn't created until _after_ extras are put together. # So, we have to use a special flag here to say, "at the last minute, add yourself to the keyword args." # # Otherwise: # - We can't access the final Arguments instance _while_ we're preparing extras # - After we _can_ access it, it's frozen, so we can't add anything. # # So, this flag gives us a chance to sneak it in before freezing, _and_ while we have access # to the new Arguments instance itself. if keyword_arguments[:argument_details] == :__arguments_add_self keyword_arguments[:argument_details] = self end @keyword_arguments = keyword_arguments.freeze elsif !@empty @keyword_arguments = {} argument_values.each do |name, arg_val| @keyword_arguments[name] = arg_val.value end @keyword_arguments.freeze else @keyword_arguments = NO_ARGS end @argument_values = argument_values ? argument_values.freeze : NO_ARGS freeze end # @return [Hash{Symbol => ArgumentValue}] attr_reader :argument_values def empty? @empty end def_delegators :keyword_arguments, :key?, :[], :fetch, :keys, :each, :values, :size, :to_h def_delegators :argument_values, :each_value def inspect "#<#{self.class} @keyword_arguments=#{keyword_arguments.inspect}>" end # Create a new arguments instance which includes these extras. # # This is called by the runtime to implement field `extras: [...]` # # @param extra_args [Hash Object>] # @return [Interpreter::Arguments] # @api private def merge_extras(extra_args) self.class.new( argument_values: argument_values, keyword_arguments: keyword_arguments.merge(extra_args) ) end NO_ARGS = GraphQL::EmptyObjects::EMPTY_HASH EMPTY = self.new(argument_values: nil, keyword_arguments: NO_ARGS).freeze end end end end graphql-ruby-2.5.19/lib/graphql/execution/interpreter/arguments_cache.rb000066400000000000000000000074231514115062600264630ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Execution class Interpreter class ArgumentsCache def initialize(query) @query = query @dataloader = query.context.dataloader @storage = Hash.new do |h, argument_owner| h[argument_owner] = if argument_owner.arguments_statically_coercible? shared_values_cache = {} Hash.new do |h2, ignored_parent_object| h2[ignored_parent_object] = shared_values_cache end.compare_by_identity else Hash.new do |h2, parent_object| h2[parent_object] = {}.compare_by_identity end.compare_by_identity end end.compare_by_identity end def fetch(ast_node, argument_owner, parent_object) # This runs eagerly if no block is given @storage[argument_owner][parent_object][ast_node] ||= begin args_hash = self.class.prepare_args_hash(@query, ast_node) kwarg_arguments = argument_owner.coerce_arguments(parent_object, args_hash, @query.context) @query.after_lazy(kwarg_arguments) do |resolved_args| @storage[argument_owner][parent_object][ast_node] = resolved_args end end end # @yield [Interpreter::Arguments, Lazy] The finally-loaded arguments def dataload_for(ast_node, argument_owner, parent_object, &block) # First, normalize all AST or Ruby values to a plain Ruby hash arg_storage = @storage[argument_owner][parent_object] if (args = arg_storage[ast_node]) yield(args) else args_hash = self.class.prepare_args_hash(@query, ast_node) argument_owner.coerce_arguments(parent_object, args_hash, @query.context) do |resolved_args| arg_storage[ast_node] = resolved_args yield(resolved_args) end end nil end private NO_ARGUMENTS = GraphQL::EmptyObjects::EMPTY_HASH NO_VALUE_GIVEN = NOT_CONFIGURED def self.prepare_args_hash(query, ast_arg_or_hash_or_value) case ast_arg_or_hash_or_value when Hash if ast_arg_or_hash_or_value.empty? return NO_ARGUMENTS end args_hash = {} ast_arg_or_hash_or_value.each do |k, v| args_hash[k] = prepare_args_hash(query, v) end args_hash when Array ast_arg_or_hash_or_value.map { |v| prepare_args_hash(query, v) } when GraphQL::Language::Nodes::Field, GraphQL::Language::Nodes::InputObject, GraphQL::Language::Nodes::Directive if ast_arg_or_hash_or_value.arguments.empty? # rubocop:disable Development/ContextIsPassedCop -- AST-related return NO_ARGUMENTS end args_hash = {} ast_arg_or_hash_or_value.arguments.each do |arg| # rubocop:disable Development/ContextIsPassedCop -- AST-related v = prepare_args_hash(query, arg.value) if v != NO_VALUE_GIVEN args_hash[arg.name] = v end end args_hash when GraphQL::Language::Nodes::VariableIdentifier if query.variables.key?(ast_arg_or_hash_or_value.name) variable_value = query.variables[ast_arg_or_hash_or_value.name] prepare_args_hash(query, variable_value) else NO_VALUE_GIVEN end when GraphQL::Language::Nodes::Enum ast_arg_or_hash_or_value.name when GraphQL::Language::Nodes::NullValue nil else ast_arg_or_hash_or_value end end end end end end graphql-ruby-2.5.19/lib/graphql/execution/interpreter/execution_errors.rb000066400000000000000000000013471514115062600267310ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Execution class Interpreter class ExecutionErrors def initialize(ctx, ast_node, path) @context = ctx @ast_node = ast_node @path = path end def add(err_or_msg) err = case err_or_msg when String GraphQL::ExecutionError.new(err_or_msg) when GraphQL::ExecutionError err_or_msg else raise ArgumentError, "expected String or GraphQL::ExecutionError, not #{err_or_msg.class} (#{err_or_msg.inspect})" end err.ast_node ||= @ast_node err.path ||= @path @context.add_error(err) end end end end end graphql-ruby-2.5.19/lib/graphql/execution/interpreter/handles_raw_value.rb000066400000000000000000000004401514115062600270060ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Execution class Interpreter # Wrapper for raw values class RawValue def initialize(obj = nil) @object = obj end def resolve @object end end end end end graphql-ruby-2.5.19/lib/graphql/execution/interpreter/resolve.rb000066400000000000000000000072571514115062600250170ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Execution class Interpreter module Resolve # Continue field results in `results` until there's nothing else to continue. # @return [void] # @deprecated Call `dataloader.run` instead def self.resolve_all(results, dataloader) warn "#{self}.#{__method__} is deprecated; Use `dataloader.run` instead.#{caller(1, 5).map { |l| "\n #{l}"}.join}" dataloader.append_job { resolve(results, dataloader) } nil end # @deprecated Call `dataloader.run` instead def self.resolve_each_depth(lazies_at_depth, dataloader) warn "#{self}.#{__method__} is deprecated; Use `dataloader.run` instead.#{caller(1, 5).map { |l| "\n #{l}"}.join}" smallest_depth = nil lazies_at_depth.each_key do |depth_key| smallest_depth ||= depth_key if depth_key < smallest_depth smallest_depth = depth_key end end if smallest_depth lazies = lazies_at_depth.delete(smallest_depth) if !lazies.empty? lazies.each do |l| dataloader.append_job { l.value } end # Run lazies _and_ dataloader, see if more are enqueued dataloader.run resolve_each_depth(lazies_at_depth, dataloader) end end nil end # @deprecated Call `dataloader.run` instead def self.resolve(results, dataloader) warn "#{self}.#{__method__} is deprecated; Use `dataloader.run` instead.#{caller(1, 5).map { |l| "\n #{l}"}.join}" # There might be pending jobs here that _will_ write lazies # into the result hash. We should run them out, so we # can be sure that all lazies will be present in the result hashes. # A better implementation would somehow interleave (or unify) # these approaches. dataloader.run next_results = [] while !results.empty? result_value = results.shift if result_value.is_a?(Runtime::GraphQLResultHash) || result_value.is_a?(Hash) results.concat(result_value.values) next elsif result_value.is_a?(Runtime::GraphQLResultArray) results.concat(result_value.values) next elsif result_value.is_a?(Array) results.concat(result_value) next elsif result_value.is_a?(Lazy) loaded_value = result_value.value if loaded_value.is_a?(Lazy) # Since this field returned another lazy, # add it to the same queue results << loaded_value elsif loaded_value.is_a?(Runtime::GraphQLResultHash) || loaded_value.is_a?(Runtime::GraphQLResultArray) || loaded_value.is_a?(Hash) || loaded_value.is_a?(Array) # Add these values in wholesale -- # they might be modified by later work in the dataloader. next_results << loaded_value end end end if !next_results.empty? # Any pending data loader jobs may populate the # resutl arrays or result hashes accumulated in # `next_results``. Run those **to completion** # before continuing to resolve `next_results`. # (Just `.append_job` doesn't work if any pending # jobs require multiple passes.) dataloader.run dataloader.append_job { resolve(next_results, dataloader) } end nil end end end end end graphql-ruby-2.5.19/lib/graphql/execution/interpreter/runtime.rb000066400000000000000000001350771514115062600250250ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/execution/interpreter/runtime/graphql_result" module GraphQL module Execution class Interpreter # I think it would be even better if we could somehow make # `continue_field` not recursive. "Trampolining" it somehow. # # @api private class Runtime class CurrentState def initialize @current_field = nil @current_arguments = nil @current_result_name = nil @current_result = nil @was_authorized_by_scope_items = nil end def current_object @current_result.graphql_application_value end attr_accessor :current_result, :current_result_name, :current_arguments, :current_field, :was_authorized_by_scope_items end # @return [GraphQL::Query] attr_reader :query # @return [Class] attr_reader :schema # @return [GraphQL::Query::Context] attr_reader :context def initialize(query:) @query = query @current_trace = query.current_trace @dataloader = query.multiplex.dataloader @schema = query.schema @context = query.context @response = nil # Identify runtime directives by checking which of this schema's directives have overridden `def self.resolve` @runtime_directive_names = [] noop_resolve_owner = GraphQL::Schema::Directive.singleton_class @schema_directives = schema.directives @schema_directives.each do |name, dir_defn| if dir_defn.method(:resolve).owner != noop_resolve_owner @runtime_directive_names << name end end # { Class => Boolean } @lazy_cache = {}.compare_by_identity end def final_result @response.respond_to?(:graphql_result_data) ? @response.graphql_result_data : @response end def inspect "#<#{self.class.name} response=#{@response.inspect}>" end # @return [void] def run_eager root_type = query.root_type case query when GraphQL::Query ast_node = query.selected_operation selections = ast_node.selections object = query.root_value is_eager = ast_node.operation_type == "mutation" base_path = nil when GraphQL::Query::Partial ast_node = query.ast_nodes.first selections = query.ast_nodes.map(&:selections).inject(&:+) object = query.object is_eager = false base_path = query.path else raise ArgumentError, "Unexpected Runnable, can't execute: #{query.class} (#{query.inspect})" end object = schema.sync_lazy(object) # TODO test query partial with lazy root object runtime_state = get_current_runtime_state case root_type.kind.name when "OBJECT" object_proxy = root_type.wrap(object, context) object_proxy = schema.sync_lazy(object_proxy) if object_proxy.nil? @response = nil else @response = GraphQLResultHash.new(nil, root_type, object_proxy, nil, false, selections, is_eager, ast_node, nil, nil) @response.base_path = base_path runtime_state.current_result = @response call_method_on_directives(:resolve, object, ast_node.directives) do each_gathered_selections(@response) do |selections, is_selection_array, ordered_result_keys| @response.ordered_result_keys ||= ordered_result_keys if is_selection_array selection_response = GraphQLResultHash.new(nil, root_type, object_proxy, nil, false, selections, is_eager, ast_node, nil, nil) selection_response.ordered_result_keys = ordered_result_keys final_response = @response else selection_response = @response final_response = nil end @dataloader.append_job { evaluate_selections( selections, selection_response, final_response, nil, ) } end end end when "LIST" inner_type = root_type.unwrap case inner_type.kind.name when "SCALAR", "ENUM" result_name = ast_node.alias || ast_node.name field_defn = query.field_definition owner_type = field_defn.owner selection_result = GraphQLResultHash.new(nil, owner_type, nil, nil, false, EmptyObjects::EMPTY_ARRAY, false, ast_node, nil, nil) selection_result.base_path = base_path selection_result.ordered_result_keys = [result_name] runtime_state = get_current_runtime_state runtime_state.current_result = selection_result runtime_state.current_result_name = result_name continue_value = continue_value(object, field_defn, false, ast_node, result_name, selection_result) if HALT != continue_value continue_field(continue_value, owner_type, field_defn, root_type, ast_node, nil, false, nil, nil, result_name, selection_result, false, runtime_state) # rubocop:disable Metrics/ParameterLists end @response = selection_result[result_name] else @response = GraphQLResultArray.new(nil, root_type, nil, nil, false, selections, false, ast_node, nil, nil) @response.base_path = base_path idx = nil object.each do |inner_value| idx ||= 0 this_idx = idx idx += 1 @dataloader.append_job do runtime_state.current_result_name = this_idx runtime_state.current_result = @response continue_field( inner_value, root_type, nil, inner_type, nil, @response.graphql_selections, false, object_proxy, nil, this_idx, @response, false, runtime_state ) end end end when "SCALAR", "ENUM" result_name = ast_node.alias || ast_node.name field_defn = query.field_definition owner_type = field_defn.owner selection_result = GraphQLResultHash.new(nil, owner_type, nil, nil, false, EmptyObjects::EMPTY_ARRAY, false, ast_node, nil, nil) selection_result.ordered_result_keys = [result_name] selection_result.base_path = base_path runtime_state = get_current_runtime_state runtime_state.current_result = selection_result runtime_state.current_result_name = result_name continue_value = continue_value(object, field_defn, false, ast_node, result_name, selection_result) if HALT != continue_value continue_field(continue_value, owner_type, field_defn, query.root_type, ast_node, nil, false, nil, nil, result_name, selection_result, false, runtime_state) # rubocop:disable Metrics/ParameterLists end @response = selection_result[result_name] when "UNION", "INTERFACE" resolved_type, _resolved_obj = resolve_type(root_type, object) resolved_type = schema.sync_lazy(resolved_type) object_proxy = resolved_type.wrap(object, context) object_proxy = schema.sync_lazy(object_proxy) @response = GraphQLResultHash.new(nil, resolved_type, object_proxy, nil, false, selections, false, query.ast_nodes.first, nil, nil) @response.base_path = base_path each_gathered_selections(@response) do |selections, is_selection_array, ordered_result_keys| @response.ordered_result_keys ||= ordered_result_keys if is_selection_array == true raise "This isn't supported yet" end @dataloader.append_job { evaluate_selections( selections, @response, nil, runtime_state, ) } end else raise "Invariant: unsupported type kind for partial execution: #{root_type.kind.inspect} (#{root_type})" end nil end def each_gathered_selections(response_hash) ordered_result_keys = [] gathered_selections = gather_selections(response_hash.graphql_application_value, response_hash.graphql_result_type, response_hash.graphql_selections, nil, {}, ordered_result_keys) ordered_result_keys.uniq! if gathered_selections.is_a?(Array) gathered_selections.each do |item| yield(item, true, ordered_result_keys) end else yield(gathered_selections, false, ordered_result_keys) end end def gather_selections(owner_object, owner_type, selections, selections_to_run, selections_by_name, ordered_result_keys) selections.each do |node| # Skip gathering this if the directive says so if !directives_include?(node, owner_object, owner_type) next end if node.is_a?(GraphQL::Language::Nodes::Field) response_key = node.alias || node.name ordered_result_keys << response_key selections = selections_by_name[response_key] # if there was already a selection of this field, # use an array to hold all selections, # otherwise, use the single node to represent the selection if selections # This field was already selected at least once, # add this node to the list of selections s = Array(selections) s << node selections_by_name[response_key] = s else # No selection was found for this field yet selections_by_name[response_key] = node end else # This is an InlineFragment or a FragmentSpread if !@runtime_directive_names.empty? && node.directives.any? { |d| @runtime_directive_names.include?(d.name) } next_selections = {} next_selections[:graphql_directives] = node.directives if selections_to_run selections_to_run << next_selections else selections_to_run = [] selections_to_run << selections_by_name selections_to_run << next_selections end else next_selections = selections_by_name end case node when GraphQL::Language::Nodes::InlineFragment if node.type type_defn = query.types.type(node.type.name) if query.types.possible_types(type_defn).include?(owner_type) result = gather_selections(owner_object, owner_type, node.selections, selections_to_run, next_selections, ordered_result_keys) if !result.equal?(next_selections) selections_to_run = result end end else # it's an untyped fragment, definitely continue result = gather_selections(owner_object, owner_type, node.selections, selections_to_run, next_selections, ordered_result_keys) if !result.equal?(next_selections) selections_to_run = result end end when GraphQL::Language::Nodes::FragmentSpread fragment_def = query.fragments[node.name] type_defn = query.types.type(fragment_def.type.name) if query.types.possible_types(type_defn).include?(owner_type) result = gather_selections(owner_object, owner_type, fragment_def.selections, selections_to_run, next_selections, ordered_result_keys) if !result.equal?(next_selections) selections_to_run = result end end else raise "Invariant: unexpected selection class: #{node.class}" end end end selections_to_run || selections_by_name end NO_ARGS = GraphQL::EmptyObjects::EMPTY_HASH # @return [void] def evaluate_selections(gathered_selections, selections_result, target_result, runtime_state) # rubocop:disable Metrics/ParameterLists runtime_state ||= get_current_runtime_state runtime_state.current_result_name = nil runtime_state.current_result = selections_result # This is a less-frequent case; use a fast check since it's often not there. if (directives = gathered_selections[:graphql_directives]) gathered_selections.delete(:graphql_directives) end call_method_on_directives(:resolve, selections_result.graphql_application_value, directives) do gathered_selections.each do |result_name, field_ast_nodes_or_ast_node| # Field resolution may pause the fiber, # so it wouldn't get to the `Resolve` call that happens below. # So instead trigger a run from this outer context. if selections_result.graphql_is_eager @dataloader.clear_cache @dataloader.run_isolated { evaluate_selection( result_name, field_ast_nodes_or_ast_node, selections_result ) @dataloader.clear_cache } else @dataloader.append_job { evaluate_selection( result_name, field_ast_nodes_or_ast_node, selections_result ) } end end if target_result selections_result.merge_into(target_result) end selections_result end end # @return [void] def evaluate_selection(result_name, field_ast_nodes_or_ast_node, selections_result) # rubocop:disable Metrics/ParameterLists return if selections_result.graphql_dead # As a performance optimization, the hash key will be a `Node` if # there's only one selection of the field. But if there are multiple # selections of the field, it will be an Array of nodes if field_ast_nodes_or_ast_node.is_a?(Array) field_ast_nodes = field_ast_nodes_or_ast_node ast_node = field_ast_nodes.first else field_ast_nodes = nil ast_node = field_ast_nodes_or_ast_node end field_name = ast_node.name owner_type = selections_result.graphql_result_type field_defn = query.types.field(owner_type, field_name) # Set this before calling `run_with_directives`, so that the directive can have the latest path runtime_state = get_current_runtime_state runtime_state.current_field = field_defn runtime_state.current_result = selections_result runtime_state.current_result_name = result_name owner_object = selections_result.graphql_application_value if field_defn.dynamic_introspection owner_object = field_defn.owner.wrap(owner_object, context) end if !field_defn.any_arguments? resolved_arguments = GraphQL::Execution::Interpreter::Arguments::EMPTY if field_defn.extras.size == 0 evaluate_selection_with_resolved_keyword_args( NO_ARGS, resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_object, result_name, selections_result, runtime_state ) else evaluate_selection_with_args(resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_object, result_name, selections_result, runtime_state) end else @query.arguments_cache.dataload_for(ast_node, field_defn, owner_object) do |resolved_arguments| runtime_state = get_current_runtime_state # This might be in a different fiber runtime_state.current_field = field_defn runtime_state.current_arguments = resolved_arguments runtime_state.current_result_name = result_name runtime_state.current_result = selections_result evaluate_selection_with_args(resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_object, result_name, selections_result, runtime_state) end end end def evaluate_selection_with_args(arguments, field_defn, ast_node, field_ast_nodes, object, result_name, selection_result, runtime_state) # rubocop:disable Metrics/ParameterLists after_lazy(arguments, field: field_defn, ast_node: ast_node, owner_object: object, arguments: arguments, result_name: result_name, result: selection_result, runtime_state: runtime_state) do |resolved_arguments, runtime_state| if resolved_arguments.is_a?(GraphQL::ExecutionError) || resolved_arguments.is_a?(GraphQL::UnauthorizedError) next if selection_result.collect_result(result_name, resolved_arguments) return_type_non_null = field_defn.type.non_null? continue_value(resolved_arguments, field_defn, return_type_non_null, ast_node, result_name, selection_result) next end kwarg_arguments = if field_defn.extras.empty? if resolved_arguments.empty? # We can avoid allocating the `{ Symbol => Object }` hash in this case NO_ARGS else resolved_arguments.keyword_arguments end else # Bundle up the extras, then make a new arguments instance # that includes the extras, too. extra_args = {} field_defn.extras.each do |extra| case extra when :ast_node extra_args[:ast_node] = ast_node when :execution_errors extra_args[:execution_errors] = ExecutionErrors.new(context, ast_node, current_path) when :path extra_args[:path] = current_path when :lookahead if !field_ast_nodes field_ast_nodes = [ast_node] end extra_args[:lookahead] = Execution::Lookahead.new( query: query, ast_nodes: field_ast_nodes, field: field_defn, ) when :argument_details # Use this flag to tell Interpreter::Arguments to add itself # to the keyword args hash _before_ freezing everything. extra_args[:argument_details] = :__arguments_add_self when :parent parent_result = selection_result.graphql_parent extra_args[:parent] = parent_result&.graphql_application_value&.object else extra_args[extra] = field_defn.fetch_extra(extra, context) end end if !extra_args.empty? resolved_arguments = resolved_arguments.merge_extras(extra_args) end resolved_arguments.keyword_arguments end evaluate_selection_with_resolved_keyword_args(kwarg_arguments, resolved_arguments, field_defn, ast_node, field_ast_nodes, object, result_name, selection_result, runtime_state) end end def evaluate_selection_with_resolved_keyword_args(kwarg_arguments, resolved_arguments, field_defn, ast_node, field_ast_nodes, object, result_name, selection_result, runtime_state) # rubocop:disable Metrics/ParameterLists runtime_state.current_field = field_defn runtime_state.current_arguments = resolved_arguments runtime_state.current_result_name = result_name runtime_state.current_result = selection_result # Optimize for the case that field is selected only once if field_ast_nodes.nil? || field_ast_nodes.size == 1 next_selections = ast_node.selections directives = ast_node.directives else next_selections = [] directives = [] field_ast_nodes.each { |f| next_selections.concat(f.selections) directives.concat(f.directives) } end call_method_on_directives(:resolve, object, directives) do if !directives.empty? # This might be executed in a different context; reset this info runtime_state = get_current_runtime_state runtime_state.current_field = field_defn runtime_state.current_arguments = resolved_arguments runtime_state.current_result_name = result_name runtime_state.current_result = selection_result end # Actually call the field resolver and capture the result app_result = begin @current_trace.begin_execute_field(field_defn, object, kwarg_arguments, query) @current_trace.execute_field(field: field_defn, ast_node: ast_node, query: query, object: object, arguments: kwarg_arguments) do field_defn.resolve(object, kwarg_arguments, context) end rescue GraphQL::ExecutionError => err err rescue StandardError => err begin query.handle_or_reraise(err) rescue GraphQL::ExecutionError => ex_err ex_err end end @current_trace.end_execute_field(field_defn, object, kwarg_arguments, query, app_result) after_lazy(app_result, field: field_defn, ast_node: ast_node, owner_object: object, arguments: resolved_arguments, result_name: result_name, result: selection_result, runtime_state: runtime_state) do |inner_result, runtime_state| next if selection_result.collect_result(result_name, inner_result) owner_type = selection_result.graphql_result_type return_type = field_defn.type continue_value = continue_value(inner_result, field_defn, return_type.non_null?, ast_node, result_name, selection_result) if HALT != continue_value was_scoped = runtime_state.was_authorized_by_scope_items runtime_state.was_authorized_by_scope_items = nil continue_field(continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, resolved_arguments, result_name, selection_result, was_scoped, runtime_state) else nil end end end # If this field is a root mutation field, immediately resolve # all of its child fields before moving on to the next root mutation field. # (Subselections of this mutation will still be resolved level-by-level.) if selection_result.graphql_is_eager @dataloader.run end end def set_result(selection_result, result_name, value, is_child_result, is_non_null) if !selection_result.graphql_dead if value.nil? && is_non_null # This is an invalid nil that should be propagated # One caller of this method passes a block, # namely when application code returns a `nil` to GraphQL and it doesn't belong there. # The other possibility for reaching here is when a field returns an ExecutionError, so we write # `nil` to the response, not knowing whether it's an invalid `nil` or not. # (And in that case, we don't have to call the schema's handler, since it's not a bug in the application.) # TODO the code is trying to tell me something. yield if block_given? parent = selection_result.graphql_parent if parent.nil? # This is a top-level result hash @response = nil else name_in_parent = selection_result.graphql_result_name is_non_null_in_parent = selection_result.graphql_is_non_null_in_parent set_result(parent, name_in_parent, nil, false, is_non_null_in_parent) set_graphql_dead(selection_result) end elsif is_child_result selection_result.set_child_result(result_name, value) else selection_result.set_leaf(result_name, value) end end end # Mark this node and any already-registered children as dead, # so that it accepts no more writes. def set_graphql_dead(selection_result) case selection_result when GraphQLResultArray selection_result.graphql_dead = true selection_result.values.each { |v| set_graphql_dead(v) } when GraphQLResultHash selection_result.graphql_dead = true selection_result.each { |k, v| set_graphql_dead(v) } else # It's a scalar, no way to mark it dead. end end def current_path st = get_current_runtime_state result = st.current_result path = result && result.path if path && (rn = st.current_result_name) path = path.dup path.push(rn) end path end HALT = Object.new.freeze def continue_value(value, field, is_non_null, ast_node, result_name, selection_result) # rubocop:disable Metrics/ParameterLists case value when nil if is_non_null set_result(selection_result, result_name, nil, false, is_non_null) do # When this comes from a list item, use the parent object: is_from_array = selection_result.is_a?(GraphQLResultArray) parent_type = is_from_array ? selection_result.graphql_parent.graphql_result_type : selection_result.graphql_result_type # This block is called if `result_name` is not dead. (Maybe a previous invalid nil caused it be marked dead.) err = parent_type::InvalidNullError.new(parent_type, field, ast_node, is_from_array: is_from_array) schema.type_error(err, context) end else set_result(selection_result, result_name, nil, false, is_non_null) end HALT when GraphQL::Error # Handle these cases inside a single `when` # to avoid the overhead of checking three different classes # every time. if value.is_a?(GraphQL::ExecutionError) if selection_result.nil? || !selection_result.graphql_dead value.path ||= current_path value.ast_node ||= ast_node context.errors << value if selection_result set_result(selection_result, result_name, nil, false, is_non_null) end end HALT elsif value.is_a?(GraphQL::UnauthorizedFieldError) value.field ||= field # this hook might raise & crash, or it might return # a replacement value next_value = begin schema.unauthorized_field(value) rescue GraphQL::ExecutionError => err err end continue_value(next_value, field, is_non_null, ast_node, result_name, selection_result) elsif value.is_a?(GraphQL::UnauthorizedError) # this hook might raise & crash, or it might return # a replacement value next_value = begin schema.unauthorized_object(value) rescue GraphQL::ExecutionError => err err end continue_value(next_value, field, is_non_null, ast_node, result_name, selection_result) elsif GraphQL::Execution::SKIP == value # It's possible a lazy was already written here case selection_result when GraphQLResultHash selection_result.delete(result_name) when GraphQLResultArray selection_result.graphql_skip_at(result_name) when nil # this can happen with directives else raise "Invariant: unexpected result class #{selection_result.class} (#{selection_result.inspect})" end HALT else # What could this actually _be_? Anyhow, # preserve the default behavior of doing nothing with it. value end when Array # It's an array full of execution errors; add them all. if !value.empty? && value.all?(GraphQL::ExecutionError) list_type_at_all = (field && (field.type.list?)) if selection_result.nil? || !selection_result.graphql_dead value.each_with_index do |error, index| error.ast_node ||= ast_node error.path ||= current_path + (list_type_at_all ? [index] : []) context.errors << error end if selection_result if list_type_at_all result_without_errors = value.map { |v| v.is_a?(GraphQL::ExecutionError) ? nil : v } set_result(selection_result, result_name, result_without_errors, false, is_non_null) else set_result(selection_result, result_name, nil, false, is_non_null) end end end HALT else value end when GraphQL::Execution::Interpreter::RawValue # Write raw value directly to the response without resolving nested objects set_result(selection_result, result_name, value.resolve, false, is_non_null) HALT else value end end # The resolver for `field` returned `value`. Continue to execute the query, # treating `value` as `type` (probably the return type of the field). # # Use `next_selections` to resolve object fields, if there are any. # # Location information from `path` and `ast_node`. # # @return [Lazy, Array, Hash, Object] Lazy, Array, and Hash are all traversed to resolve lazy values later def continue_field(value, owner_type, field, current_type, ast_node, next_selections, is_non_null, owner_object, arguments, result_name, selection_result, was_scoped, runtime_state) # rubocop:disable Metrics/ParameterLists if current_type.non_null? current_type = current_type.of_type is_non_null = true end case current_type.kind.name when "SCALAR", "ENUM" r = begin current_type.coerce_result(value, context) rescue GraphQL::ExecutionError => ex_err return continue_value(ex_err, field, is_non_null, ast_node, result_name, selection_result) rescue StandardError => err begin query.handle_or_reraise(err) rescue GraphQL::ExecutionError => ex_err return continue_value(ex_err, field, is_non_null, ast_node, result_name, selection_result) end end set_result(selection_result, result_name, r, false, is_non_null) r when "UNION", "INTERFACE" resolved_type_or_lazy = begin resolve_type(current_type, value) rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => ex_err return continue_value(ex_err, field, is_non_null, ast_node, result_name, selection_result) rescue StandardError => err begin query.handle_or_reraise(err) rescue GraphQL::ExecutionError => ex_err return continue_value(ex_err, field, is_non_null, ast_node, result_name, selection_result) end end after_lazy(resolved_type_or_lazy, ast_node: ast_node, field: field, owner_object: owner_object, arguments: arguments, trace: false, result_name: result_name, result: selection_result, runtime_state: runtime_state) do |resolved_type_result, runtime_state| if resolved_type_result.is_a?(Array) && resolved_type_result.length == 2 resolved_type, resolved_value = resolved_type_result else resolved_type = resolved_type_result resolved_value = value end possible_types = query.types.possible_types(current_type) if !possible_types.include?(resolved_type) parent_type = field.owner_type err_class = current_type::UnresolvedTypeError type_error = err_class.new(resolved_value, field, parent_type, resolved_type, possible_types) schema.type_error(type_error, context) set_result(selection_result, result_name, nil, false, is_non_null) nil else continue_field(resolved_value, owner_type, field, resolved_type, ast_node, next_selections, is_non_null, owner_object, arguments, result_name, selection_result, was_scoped, runtime_state) end end when "OBJECT" object_proxy = begin was_scoped ? current_type.wrap_scoped(value, context) : current_type.wrap(value, context) rescue GraphQL::ExecutionError => err err end after_lazy(object_proxy, ast_node: ast_node, field: field, owner_object: owner_object, arguments: arguments, trace: false, result_name: result_name, result: selection_result, runtime_state: runtime_state) do |inner_object, runtime_state| continue_value = continue_value(inner_object, field, is_non_null, ast_node, result_name, selection_result) if HALT != continue_value response_hash = GraphQLResultHash.new(result_name, current_type, continue_value, selection_result, is_non_null, next_selections, false, ast_node, arguments, field) set_result(selection_result, result_name, response_hash, true, is_non_null) each_gathered_selections(response_hash) do |selections, is_selection_array, ordered_result_keys| response_hash.ordered_result_keys ||= ordered_result_keys if is_selection_array this_result = GraphQLResultHash.new(result_name, current_type, continue_value, selection_result, is_non_null, selections, false, ast_node, arguments, field) this_result.ordered_result_keys = ordered_result_keys final_result = response_hash else this_result = response_hash final_result = nil end evaluate_selections( selections, this_result, final_result, runtime_state, ) end end end when "LIST" inner_type = current_type.of_type # This is true for objects, unions, and interfaces use_dataloader_job = !inner_type.unwrap.kind.input? inner_type_non_null = inner_type.non_null? response_list = GraphQLResultArray.new(result_name, current_type, owner_object, selection_result, is_non_null, next_selections, false, ast_node, arguments, field) set_result(selection_result, result_name, response_list, true, is_non_null) idx = nil list_value = begin begin value.each do |inner_value| idx ||= 0 this_idx = idx idx += 1 if use_dataloader_job @dataloader.append_job do resolve_list_item(inner_value, inner_type, inner_type_non_null, ast_node, field, owner_object, arguments, this_idx, response_list, owner_type, was_scoped, runtime_state) end else resolve_list_item(inner_value, inner_type, inner_type_non_null, ast_node, field, owner_object, arguments, this_idx, response_list, owner_type, was_scoped, runtime_state) end end response_list rescue NoMethodError => err # Ruby 2.2 doesn't have NoMethodError#receiver, can't check that one in this case. (It's been EOL since 2017.) if err.name == :each && (err.respond_to?(:receiver) ? err.receiver == value : true) # This happens when the GraphQL schema doesn't match the implementation. Help the dev debug. raise ListResultFailedError.new(value: value, field: field, path: current_path) else # This was some other NoMethodError -- let it bubble to reveal the real error. raise end rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => ex_err ex_err rescue StandardError => err begin query.handle_or_reraise(err) rescue GraphQL::ExecutionError => ex_err ex_err end end rescue StandardError => err begin query.handle_or_reraise(err) rescue GraphQL::ExecutionError => ex_err ex_err end end # Detect whether this error came while calling `.each` (before `idx` is set) or while running list *items* (after `idx` is set) error_is_non_null = idx.nil? ? is_non_null : inner_type.non_null? continue_value(list_value, field, error_is_non_null, ast_node, result_name, selection_result) else raise "Invariant: Unhandled type kind #{current_type.kind} (#{current_type})" end end def resolve_list_item(inner_value, inner_type, inner_type_non_null, ast_node, field, owner_object, arguments, this_idx, response_list, owner_type, was_scoped, runtime_state) # rubocop:disable Metrics/ParameterLists runtime_state.current_result_name = this_idx runtime_state.current_result = response_list call_method_on_directives(:resolve_each, owner_object, ast_node.directives) do # This will update `response_list` with the lazy after_lazy(inner_value, ast_node: ast_node, field: field, owner_object: owner_object, arguments: arguments, result_name: this_idx, result: response_list, runtime_state: runtime_state) do |inner_inner_value, runtime_state| continue_value = continue_value(inner_inner_value, field, inner_type_non_null, ast_node, this_idx, response_list) if HALT != continue_value continue_field(continue_value, owner_type, field, inner_type, ast_node, response_list.graphql_selections, false, owner_object, arguments, this_idx, response_list, was_scoped, runtime_state) end end end end def call_method_on_directives(method_name, object, directives, &block) return yield if directives.nil? || directives.empty? run_directive(method_name, object, directives, 0, &block) end def run_directive(method_name, object, directives, idx, &block) dir_node = directives[idx] if !dir_node yield else dir_defn = @schema_directives.fetch(dir_node.name) raw_dir_args = arguments(nil, dir_defn, dir_node) if !raw_dir_args.is_a?(GraphQL::ExecutionError) begin dir_defn.validate!(raw_dir_args, context) rescue GraphQL::ExecutionError => err raw_dir_args = err end end dir_args = continue_value( raw_dir_args, # value nil, # field false, # is_non_null dir_node, # ast_node nil, # result_name nil, # selection_result ) if dir_args == HALT nil else dir_defn.public_send(method_name, object, dir_args, context) do run_directive(method_name, object, directives, idx + 1, &block) end end end end # Check {Schema::Directive.include?} for each directive that's present def directives_include?(node, graphql_object, parent_type) node.directives.each do |dir_node| dir_defn = @schema_directives.fetch(dir_node.name) args = arguments(graphql_object, dir_defn, dir_node) if !dir_defn.include?(graphql_object, args, context) return false end end true end def get_current_runtime_state current_state = Fiber[:__graphql_runtime_info] ||= {}.compare_by_identity current_state[@query] ||= CurrentState.new end def minimal_after_lazy(value, &block) if lazy?(value) GraphQL::Execution::Lazy.new do result = @schema.sync_lazy(value) # The returned result might also be lazy, so check it, too minimal_after_lazy(result, &block) end else yield(value) end end # @param obj [Object] Some user-returned value that may want to be batched # @param field [GraphQL::Schema::Field] # @param eager [Boolean] Set to `true` for mutation root fields only # @param trace [Boolean] If `false`, don't wrap this with field tracing # @return [GraphQL::Execution::Lazy, Object] If loading `object` will be deferred, it's a wrapper over it. def after_lazy(lazy_obj, field:, owner_object:, arguments:, ast_node:, result:, result_name:, eager: false, runtime_state:, trace: true, &block) if lazy?(lazy_obj) was_authorized_by_scope_items = runtime_state.was_authorized_by_scope_items lazy = GraphQL::Execution::Lazy.new(field: field) do # This block might be called in a new fiber; # In that case, this will initialize a new state # to avoid conflicting with the parent fiber. runtime_state = get_current_runtime_state runtime_state.current_field = field runtime_state.current_arguments = arguments runtime_state.current_result_name = result_name runtime_state.current_result = result runtime_state.was_authorized_by_scope_items = was_authorized_by_scope_items # Wrap the execution of _this_ method with tracing, # but don't wrap the continuation below sync_result = nil inner_obj = begin sync_result = if trace @current_trace.begin_execute_field(field, owner_object, arguments, query) @current_trace.execute_field_lazy(field: field, query: query, object: owner_object, arguments: arguments, ast_node: ast_node) do schema.sync_lazy(lazy_obj) end else schema.sync_lazy(lazy_obj) end rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => ex_err ex_err rescue StandardError => err begin query.handle_or_reraise(err) rescue GraphQL::ExecutionError => ex_err ex_err end ensure if trace @current_trace.end_execute_field(field, owner_object, arguments, query, sync_result) end end yield(inner_obj, runtime_state) end if eager lazy.value else set_result(result, result_name, lazy, false, false) # is_non_null is irrelevant here @dataloader.lazy_at_depth(result.depth, lazy) lazy end else # Don't need to reset state here because it _wasn't_ lazy. yield(lazy_obj, runtime_state) end end def arguments(graphql_object, arg_owner, ast_node) if arg_owner.arguments_statically_coercible? query.arguments_for(ast_node, arg_owner) else # The arguments must be prepared in the context of the given object query.arguments_for(ast_node, arg_owner, parent_object: graphql_object) end end def delete_all_interpreter_context per_query_state = Fiber[:__graphql_runtime_info] if per_query_state per_query_state.delete(@query) if per_query_state.size == 0 Fiber[:__graphql_runtime_info] = nil end end nil end def resolve_type(type, value) @current_trace.begin_resolve_type(type, value, context) resolved_type, resolved_value = @current_trace.resolve_type(query: query, type: type, object: value) do query.resolve_type(type, value) end @current_trace.end_resolve_type(type, value, context, resolved_type) if lazy?(resolved_type) GraphQL::Execution::Lazy.new do @current_trace.begin_resolve_type(type, value, context) @current_trace.resolve_type_lazy(query: query, type: type, object: value) do rt = schema.sync_lazy(resolved_type) @current_trace.end_resolve_type(type, value, context, rt) rt end end else [resolved_type, resolved_value] end end def lazy?(object) obj_class = object.class is_lazy = @lazy_cache[obj_class] if is_lazy.nil? is_lazy = @lazy_cache[obj_class] = @schema.lazy?(object) end is_lazy end end end end end graphql-ruby-2.5.19/lib/graphql/execution/interpreter/runtime/000077500000000000000000000000001514115062600244635ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/execution/interpreter/runtime/graphql_result.rb000066400000000000000000000207101514115062600300440ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Execution class Interpreter class Runtime module GraphQLResult def initialize(result_name, result_type, application_value, parent_result, is_non_null_in_parent, selections, is_eager, ast_node, graphql_arguments, graphql_field) # rubocop:disable Metrics/ParameterLists @ast_node = ast_node @graphql_arguments = graphql_arguments @graphql_field = graphql_field @graphql_parent = parent_result @graphql_application_value = application_value @graphql_result_type = result_type if parent_result && parent_result.graphql_dead @graphql_dead = true end @graphql_result_name = result_name @graphql_is_non_null_in_parent = is_non_null_in_parent # Jump through some hoops to avoid creating this duplicate storage if at all possible. @graphql_metadata = nil @graphql_selections = selections @graphql_is_eager = is_eager @base_path = nil end # TODO test full path in Partial attr_writer :base_path def path @path ||= build_path([]) end def build_path(path_array) graphql_result_name && path_array.unshift(graphql_result_name) if @graphql_parent @graphql_parent.build_path(path_array) elsif @base_path @base_path + path_array else path_array end end def depth @depth ||= if @graphql_parent @graphql_parent.depth + 1 else 1 end end attr_accessor :graphql_dead attr_reader :graphql_parent, :graphql_result_name, :graphql_is_non_null_in_parent, :graphql_application_value, :graphql_result_type, :graphql_selections, :graphql_is_eager, :ast_node, :graphql_arguments, :graphql_field # @return [Hash] Plain-Ruby result data (`@graphql_metadata` contains Result wrapper objects) attr_accessor :graphql_result_data end class GraphQLResultHash def initialize(_result_name, _result_type, _application_value, _parent_result, _is_non_null_in_parent, _selections, _is_eager, _ast_node, _graphql_arguments, graphql_field) # rubocop:disable Metrics/ParameterLists super @graphql_result_data = {} @ordered_result_keys = nil end attr_accessor :ordered_result_keys include GraphQLResult attr_accessor :graphql_merged_into def set_leaf(key, value) # This is a hack. # Basically, this object is merged into the root-level result at some point. # But the problem is, some lazies are created whose closures retain reference to _this_ # object. When those lazies are resolved, they cause an update to this object. # # In order to return a proper top-level result, we have to update that top-level result object. # In order to return a proper partial result (eg, for a directive), we have to update this object, too. # Yowza. if (t = @graphql_merged_into) t.set_leaf(key, value) end before_size = @graphql_result_data.size @graphql_result_data[key] = value after_size = @graphql_result_data.size if after_size > before_size && @ordered_result_keys[before_size] != key fix_result_order end # keep this up-to-date if it's been initialized @graphql_metadata && @graphql_metadata[key] = value value end def set_child_result(key, value) if (t = @graphql_merged_into) t.set_child_result(key, value) end before_size = @graphql_result_data.size @graphql_result_data[key] = value.graphql_result_data after_size = @graphql_result_data.size if after_size > before_size && @ordered_result_keys[before_size] != key fix_result_order end # If we encounter some part of this response that requires metadata tracking, # then create the metadata hash if necessary. It will be kept up-to-date after this. (@graphql_metadata ||= @graphql_result_data.dup)[key] = value value end def delete(key) @graphql_metadata && @graphql_metadata.delete(key) @graphql_result_data.delete(key) end def each (@graphql_metadata || @graphql_result_data).each { |k, v| yield(k, v) } end def values (@graphql_metadata || @graphql_result_data).values end def key?(k) @graphql_result_data.key?(k) end def [](k) (@graphql_metadata || @graphql_result_data)[k] end def merge_into(into_result) self.each do |key, value| case value when GraphQLResultHash next_into = into_result[key] if next_into value.merge_into(next_into) else into_result.set_child_result(key, value) end when GraphQLResultArray # There's no special handling of arrays because currently, there's no way to split the execution # of a list over several concurrent flows. into_result.set_child_result(key, value) else # We have to assume that, since this passed the `fields_will_merge` selection, # that the old and new values are the same. into_result.set_leaf(key, value) end end @graphql_merged_into = into_result end def fix_result_order @ordered_result_keys.each do |k| if @graphql_result_data.key?(k) @graphql_result_data[k] = @graphql_result_data.delete(k) end end end # hook for breadth-first implementations to signal when collecting results. def collect_result(result_name, result_value) false end end class GraphQLResultArray include GraphQLResult def initialize(_result_name, _result_type, _application_value, _parent_result, _is_non_null_in_parent, _selections, _is_eager, _ast_node, _graphql_arguments, graphql_field) # rubocop:disable Metrics/ParameterLists super @graphql_result_data = [] end def graphql_skip_at(index) # Mark this index as dead. It's tricky because some indices may already be storing # `Lazy`s. So the runtime is still holding indexes _before_ skipping, # this object has to coordinate incoming writes to account for any already-skipped indices. @skip_indices ||= [] @skip_indices << index offset_by = @skip_indices.count { |skipped_idx| skipped_idx < index} delete_at_index = index - offset_by @graphql_metadata && @graphql_metadata.delete_at(delete_at_index) @graphql_result_data.delete_at(delete_at_index) end def set_leaf(idx, value) if @skip_indices offset_by = @skip_indices.count { |skipped_idx| skipped_idx < idx } idx -= offset_by end @graphql_result_data[idx] = value @graphql_metadata && @graphql_metadata[idx] = value value end def set_child_result(idx, value) if @skip_indices offset_by = @skip_indices.count { |skipped_idx| skipped_idx < idx } idx -= offset_by end @graphql_result_data[idx] = value.graphql_result_data # If we encounter some part of this response that requires metadata tracking, # then create the metadata hash if necessary. It will be kept up-to-date after this. (@graphql_metadata ||= @graphql_result_data.dup)[idx] = value value end def values (@graphql_metadata || @graphql_result_data) end def [](idx) (@graphql_metadata || @graphql_result_data)[idx] end end end end end end graphql-ruby-2.5.19/lib/graphql/execution/lazy.rb000066400000000000000000000043071514115062600217450ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/execution/lazy/lazy_method_map" module GraphQL module Execution # This wraps a value which is available, but not yet calculated, like a promise or future. # # Calling `#value` will trigger calculation & return the "lazy" value. # # This is an itty-bitty promise-like object, with key differences: # - It has only two states, not-resolved and resolved # - It has no error-catching functionality # @api private class Lazy attr_reader :field # Create a {Lazy} which will get its inner value by calling the block # @param field [GraphQL::Schema::Field] # @param get_value_func [Proc] a block to get the inner value (later) def initialize(field: nil, &get_value_func) @get_value_func = get_value_func @resolved = false @field = field end # @return [Object] The wrapped value, calling the lazy block if necessary def value if !@resolved @resolved = true v = @get_value_func.call if v.is_a?(Lazy) v = v.value end @value = v end # `SKIP` was made into a subclass of `GraphQL::Error` to improve runtime performance # (fewer clauses in a hot `case` block), but now it requires special handling here. # I think it's still worth it for the performance win, but if the number of special # cases grows, then maybe it's worth rethinking somehow. if @value.is_a?(StandardError) && @value != GraphQL::Execution::SKIP raise @value else @value end end # @return [Lazy] A {Lazy} whose value depends on another {Lazy}, plus any transformations in `block` def then self.class.new { yield(value) } end # @param lazies [Array] Maybe-lazy objects # @return [Lazy] A lazy which will sync all of `lazies` def self.all(lazies) self.new { lazies.map { |l| l.is_a?(Lazy) ? l.value : l } } end # This can be used for fields which _had no_ lazy results # @api private NullResult = Lazy.new(){} NullResult.value end end end graphql-ruby-2.5.19/lib/graphql/execution/lazy/000077500000000000000000000000001514115062600214145ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/execution/lazy/lazy_method_map.rb000066400000000000000000000055771514115062600251330ustar00rootroot00000000000000# frozen_string_literal: true require 'thread' begin require 'concurrent' rescue LoadError # no problem, we'll fallback to our own map end module GraphQL module Execution class Lazy # {GraphQL::Schema} uses this to match returned values to lazy resolution methods. # Methods may be registered for classes, they apply to its subclasses also. # The result of this lookup is cached for future resolutions. # Instances of this class are thread-safe. # @api private # @see {Schema#lazy?} looks up values from this map class LazyMethodMap def initialize(use_concurrent: defined?(Concurrent::Map)) @storage = use_concurrent ? Concurrent::Map.new : ConcurrentishMap.new end def initialize_copy(other) @storage = other.storage.dup end # @param lazy_class [Class] A class which represents a lazy value (subclasses may also be used) # @param lazy_value_method [Symbol] The method to call on this class to get its value def set(lazy_class, lazy_value_method) @storage[lazy_class] = lazy_value_method end # @param value [Object] an object which may have a `lazy_value_method` registered for its class or superclasses # @return [Symbol, nil] The `lazy_value_method` for this object, or nil def get(value) @storage.compute_if_absent(value.class) { find_superclass_method(value.class) } end def each @storage.each_pair { |k, v| yield(k, v) } end protected attr_reader :storage private def find_superclass_method(value_class) @storage.each_pair { |lazy_class, lazy_value_method| return lazy_value_method if value_class < lazy_class } nil end # Mock the Concurrent::Map API class ConcurrentishMap extend Forwardable # Technically this should be under the mutex too, # but I know it's only used when the lock is already acquired. def_delegators :@storage, :each_pair, :size def initialize @semaphore = Mutex.new # Access to this hash must always be managed by the mutex # since it may be modified at runtime @storage = {} end def []=(key, value) @semaphore.synchronize { @storage[key] = value } end def compute_if_absent(key) @semaphore.synchronize { @storage.fetch(key) { @storage[key] = yield } } end def initialize_copy(other) @semaphore = Mutex.new @storage = other.copy_storage end protected def copy_storage @semaphore.synchronize { @storage.dup } end end end end end end graphql-ruby-2.5.19/lib/graphql/execution/lookahead.rb000066400000000000000000000362511514115062600227200ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Execution # Lookahead creates a uniform interface to inspect the forthcoming selections. # # It assumes that the AST it's working with is valid. (So, it's safe to use # during execution, but if you're using it directly, be sure to validate first.) # # A field may get access to its lookahead by adding `extras: [:lookahead]` # to its configuration. # # @example looking ahead in a field # field :articles, [Types::Article], null: false, # extras: [:lookahead] # # # For example, imagine a faster database call # # may be issued when only some fields are requested. # # # # Imagine that _full_ fetch must be made to satisfy `fullContent`, # # we can look ahead to see if we need that field. If we do, # # we make the expensive database call instead of the cheap one. # def articles(lookahead:) # if lookahead.selects?(:full_content) # fetch_full_articles(object) # else # fetch_preview_articles(object) # end # end class Lookahead # @param query [GraphQL::Query] # @param ast_nodes [Array, Array] # @param field [GraphQL::Schema::Field] if `ast_nodes` are fields, this is the field definition matching those nodes # @param root_type [Class] if `ast_nodes` are operation definition, this is the root type for that operation def initialize(query:, ast_nodes:, field: nil, root_type: nil, owner_type: nil) @ast_nodes = ast_nodes.freeze @field = field @root_type = root_type @query = query @selected_type = @field ? @field.type.unwrap : root_type @owner_type = owner_type end # @return [Array] attr_reader :ast_nodes # @return [GraphQL::Schema::Field] attr_reader :field # @return [GraphQL::Schema::Object, GraphQL::Schema::Union, GraphQL::Schema::Interface] attr_reader :owner_type # @return [Hash] def arguments if defined?(@arguments) @arguments else @arguments = if @field @query.after_lazy(@query.arguments_for(@ast_nodes.first, @field)) do |args| case args when Execution::Interpreter::Arguments args.keyword_arguments when GraphQL::ExecutionError EmptyObjects::EMPTY_HASH else args end end else nil end end end # True if this node has a selection on `field_name`. # If `field_name` is a String, it is treated as a GraphQL-style (camelized) # field name and used verbatim. If `field_name` is a Symbol, it is # treated as a Ruby-style (underscored) name and camelized before comparing. # # If `arguments:` is provided, each provided key/value will be matched # against the arguments in the next selection. This method will return false # if any of the given `arguments:` are not present and matching in the next selection. # (But, the next selection may contain _more_ than the given arguments.) # @param field_name [String, Symbol] # @param arguments [Hash] Arguments which must match in the selection # @return [Boolean] def selects?(field_name, selected_type: @selected_type, arguments: nil) selection(field_name, selected_type: selected_type, arguments: arguments).selected? end # True if this node has a selection with alias matching `alias_name`. # If `alias_name` is a String, it is treated as a GraphQL-style (camelized) # field name and used verbatim. If `alias_name` is a Symbol, it is # treated as a Ruby-style (underscored) name and camelized before comparing. # # If `arguments:` is provided, each provided key/value will be matched # against the arguments in the next selection. This method will return false # if any of the given `arguments:` are not present and matching in the next selection. # (But, the next selection may contain _more_ than the given arguments.) # @param alias_name [String, Symbol] # @param arguments [Hash] Arguments which must match in the selection # @return [Boolean] def selects_alias?(alias_name, arguments: nil) alias_selection(alias_name, arguments: arguments).selected? end # @return [Boolean] True if this lookahead represents a field that was requested def selected? true end # Like {#selects?}, but can be used for chaining. # It returns a null object (check with {#selected?}) # @param field_name [String, Symbol] # @return [GraphQL::Execution::Lookahead] def selection(field_name, selected_type: @selected_type, arguments: nil) next_field_defn = case field_name when String @query.types.field(selected_type, field_name) when Symbol # Try to avoid the `.to_s` below, if possible all_fields = if selected_type.kind.fields? @query.types.fields(selected_type) else # Handle unions by checking possible @query.types .possible_types(selected_type) .map { |t| @query.types.fields(t) } .tap(&:flatten!) end if (match_by_orig_name = all_fields.find { |f| f.original_name == field_name }) match_by_orig_name else # Symbol#name is only present on 3.0+ sym_s = field_name.respond_to?(:name) ? field_name.name : field_name.to_s guessed_name = Schema::Member::BuildType.camelize(sym_s) @query.types.field(selected_type, guessed_name) end end lookahead_for_selection(next_field_defn, selected_type, arguments) end # Like {#selection}, but for aliases. # It returns a null object (check with {#selected?}) # @return [GraphQL::Execution::Lookahead] def alias_selection(alias_name, selected_type: @selected_type, arguments: nil) alias_cache_key = [alias_name, arguments] return alias_selections[key] if alias_selections.key?(alias_name) alias_node = lookup_alias_node(ast_nodes, alias_name) return NULL_LOOKAHEAD unless alias_node next_field_defn = @query.types.field(selected_type, alias_node.name) alias_arguments = @query.arguments_for(alias_node, next_field_defn) if alias_arguments.is_a?(::GraphQL::Execution::Interpreter::Arguments) alias_arguments = alias_arguments.keyword_arguments end return NULL_LOOKAHEAD if arguments && arguments != alias_arguments alias_selections[alias_cache_key] = lookahead_for_selection(next_field_defn, selected_type, alias_arguments, alias_name) end # Like {#selection}, but for all nodes. # It returns a list of Lookaheads for all Selections # # If `arguments:` is provided, each provided key/value will be matched # against the arguments in each selection. This method will filter the selections # if any of the given `arguments:` do not match the given selection. # # @example getting the name of a selection # def articles(lookahead:) # next_lookaheads = lookahead.selections # => [#, ...] # next_lookaheads.map(&:name) #=> [:full_content, :title] # end # # @param arguments [Hash] Arguments which must match in the selection # @return [Array] def selections(arguments: nil) subselections_by_type = {} subselections_on_type = subselections_by_type[@selected_type] = {} @ast_nodes.each do |node| find_selections(subselections_by_type, subselections_on_type, @selected_type, node.selections, arguments) end subselections = [] subselections_by_type.each do |type, ast_nodes_by_response_key| ast_nodes_by_response_key.each do |response_key, ast_nodes| field_defn = @query.types.field(type, ast_nodes.first.name) lookahead = Lookahead.new(query: @query, ast_nodes: ast_nodes, field: field_defn, owner_type: type) subselections.push(lookahead) end end subselections end # The method name of the field. # It returns the method_sym of the Lookahead's field. # # @example getting the name of a selection # def articles(lookahead:) # article.selection(:full_content).name # => :full_content # # ... # end # # @return [Symbol] def name @field && @field.original_name end def inspect "#" end # This is returned for {Lookahead#selection} when a non-existent field is passed class NullLookahead < Lookahead # No inputs required here. def initialize end def selected? false end def selects?(*) false end def selection(*) NULL_LOOKAHEAD end def selections(*) [] end def inspect "#" end end # A singleton, so that misses don't come with overhead. NULL_LOOKAHEAD = NullLookahead.new private def skipped_by_directive?(ast_selection) ast_selection.directives.each do |directive| dir_defn = @query.schema.directives.fetch(directive.name) directive_class = dir_defn if directive_class dir_args = @query.arguments_for(directive, dir_defn) return true unless directive_class.static_include?(dir_args, @query.context) end end false end def find_selections(subselections_by_type, selections_on_type, selected_type, ast_selections, arguments) ast_selections.each do |ast_selection| next if skipped_by_directive?(ast_selection) case ast_selection when GraphQL::Language::Nodes::Field response_key = ast_selection.alias || ast_selection.name if selections_on_type.key?(response_key) selections_on_type[response_key] << ast_selection elsif arguments.nil? || arguments.empty? selections_on_type[response_key] = [ast_selection] else field_defn = @query.types.field(selected_type, ast_selection.name) if arguments_match?(arguments, field_defn, ast_selection) selections_on_type[response_key] = [ast_selection] end end when GraphQL::Language::Nodes::InlineFragment on_type = selected_type subselections_on_type = selections_on_type if (t = ast_selection.type) # Assuming this is valid, that `t` will be found. on_type = @query.types.type(t.name) subselections_on_type = subselections_by_type[on_type] ||= {} end find_selections(subselections_by_type, subselections_on_type, on_type, ast_selection.selections, arguments) when GraphQL::Language::Nodes::FragmentSpread frag_defn = lookup_fragment(ast_selection) # Again, assuming a valid AST on_type = @query.types.type(frag_defn.type.name) subselections_on_type = subselections_by_type[on_type] ||= {} find_selections(subselections_by_type, subselections_on_type, on_type, frag_defn.selections, arguments) else raise "Invariant: Unexpected selection type: #{ast_selection.class}" end end end # If a selection on `node` matches `field_name` (which is backed by `field_defn`) # and matches the `arguments:` constraints, then add that node to `matches` def find_selected_nodes(node, field_name, field_defn, arguments:, matches:, alias_name: NOT_CONFIGURED) return if skipped_by_directive?(node) case node when GraphQL::Language::Nodes::Field if node.name == field_name && (NOT_CONFIGURED.equal?(alias_name) || node.alias == alias_name) if arguments.nil? || arguments.empty? # No constraint applied matches << node elsif arguments_match?(arguments, field_defn, node) matches << node end end when GraphQL::Language::Nodes::InlineFragment node.selections.each { |s| find_selected_nodes(s, field_name, field_defn, arguments: arguments, matches: matches, alias_name: alias_name) } when GraphQL::Language::Nodes::FragmentSpread frag_defn = lookup_fragment(node) frag_defn.selections.each { |s| find_selected_nodes(s, field_name, field_defn, arguments: arguments, matches: matches, alias_name: alias_name) } else raise "Unexpected selection comparison on #{node.class.name} (#{node})" end end def arguments_match?(arguments, field_defn, field_node) query_kwargs = @query.arguments_for(field_node, field_defn) arguments.all? do |arg_name, arg_value| arg_name_sym = if arg_name.is_a?(String) Schema::Member::BuildType.underscore(arg_name).to_sym else arg_name end # Make sure the constraint is present with a matching value query_kwargs.key?(arg_name_sym) && query_kwargs[arg_name_sym] == arg_value end end def lookahead_for_selection(field_defn, selected_type, arguments, alias_name = NOT_CONFIGURED) return NULL_LOOKAHEAD unless field_defn next_nodes = [] field_name = field_defn.name @ast_nodes.each do |ast_node| ast_node.selections.each do |selection| find_selected_nodes(selection, field_name, field_defn, arguments: arguments, matches: next_nodes, alias_name: alias_name) end end return NULL_LOOKAHEAD if next_nodes.empty? Lookahead.new(query: @query, ast_nodes: next_nodes, field: field_defn, owner_type: selected_type) end def alias_selections return @alias_selections if defined?(@alias_selections) @alias_selections ||= {} end def lookup_alias_node(nodes, name) return if nodes.empty? nodes.flat_map(&:children) .flat_map { |child| unwrap_fragments(child) } .find { |child| child.is_a?(GraphQL::Language::Nodes::Field) && child.alias == name } end def unwrap_fragments(node) case node when GraphQL::Language::Nodes::InlineFragment node.children when GraphQL::Language::Nodes::FragmentSpread lookup_fragment(node).children else [node] end end def lookup_fragment(ast_selection) @query.fragments[ast_selection.name] || raise("Invariant: Can't look ahead to nonexistent fragment #{ast_selection.name} (found: #{@query.fragments.keys})") end end end end graphql-ruby-2.5.19/lib/graphql/execution/multiplex.rb000066400000000000000000000031271514115062600230100ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Execution # Execute multiple queries under the same multiplex "umbrella". # They can share a batching context and reduce redundant database hits. # # The flow is: # # - Multiplex instrumentation setup # - Query instrumentation setup # - Analyze the multiplex + each query # - Begin each query # - Resolve lazy values, breadth-first across all queries # - Finish each query (eg, get errors) # - Query instrumentation teardown # - Multiplex instrumentation teardown # # If one query raises an application error, all queries will be in undefined states. # # Validation errors and {GraphQL::ExecutionError}s are handled in isolation: # one of these errors in one query will not affect the other queries. # # @see {Schema#multiplex} for public API # @api private class Multiplex include Tracing::Traceable attr_reader :context, :queries, :schema, :max_complexity, :dataloader, :current_trace def initialize(schema:, queries:, context:, max_complexity:) @schema = schema @queries = queries @queries.each { |q| q.multiplex = self } @context = context @dataloader = @context[:dataloader] ||= @schema.dataloader_class.new @tracers = schema.tracers + (context[:tracers] || []) @max_complexity = max_complexity @current_trace = context[:trace] ||= schema.new_trace(multiplex: self) @logger = nil end def logger @logger ||= @schema.logger_for(context) end end end end graphql-ruby-2.5.19/lib/graphql/execution_error.rb000066400000000000000000000032171514115062600221760ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL # If a field's resolve function returns a {ExecutionError}, # the error will be inserted into the response's `"errors"` key # and the field will resolve to `nil`. class ExecutionError < GraphQL::Error # @return [GraphQL::Language::Nodes::Field] the field where the error occurred attr_accessor :ast_node # @return [String] an array describing the JSON-path into the execution # response which corresponds to this error. attr_accessor :path # @return [Hash] Optional data for error objects # @deprecated Use `extensions` instead of `options`. The GraphQL spec # recommends that any custom entries in an error be under the # `extensions` key. attr_accessor :options # @return [Hash] Optional custom data for error objects which will be added # under the `extensions` key. attr_accessor :extensions def initialize(message, ast_node: nil, options: nil, extensions: nil) @ast_node = ast_node @options = options @extensions = extensions super(message) end # @return [Hash] An entry for the response's "errors" key def to_h hash = { "message" => message, } if ast_node hash["locations"] = [ { "line" => ast_node.line, "column" => ast_node.col, } ] end if path hash["path"] = path end if options hash.merge!(options) end if extensions hash["extensions"] = extensions.each_with_object({}) { |(key, value), ext| ext[key.to_s] = value } end hash end end end graphql-ruby-2.5.19/lib/graphql/integer_decoding_error.rb000066400000000000000000000011001514115062600234510ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL # This error is raised when `Types::Int` is given an input value outside of 32-bit integer range. # # For really big integer values, consider `GraphQL::Types::BigInt` # # @see GraphQL::Types::Int which raises this error class IntegerDecodingError < GraphQL::RuntimeTypeError # The value which couldn't be decoded attr_reader :integer_value def initialize(value) @integer_value = value super("Integer out of bounds: #{value}. \nConsider using GraphQL::Types::BigInt instead.") end end end graphql-ruby-2.5.19/lib/graphql/integer_encoding_error.rb000066400000000000000000000022151514115062600234730ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL # This error is raised when `Types::Int` is asked to return a value outside of 32-bit integer range. # # For values outside that range, consider: # # - `ID` for database primary keys or other identifiers # - `GraphQL::Types::BigInt` for really big integer values # # @see GraphQL::Types::Int which raises this error class IntegerEncodingError < GraphQL::RuntimeTypeError # The value which couldn't be encoded attr_reader :integer_value # @return [GraphQL::Schema::Field] The field that returned a too-big integer attr_reader :field # @return [Array] Where the field appeared in the GraphQL response attr_reader :path def initialize(value, context:) @integer_value = value @field = context[:current_field] @path = context[:current_path] message = "Integer out of bounds: #{value}".dup if @path message << " @ #{@path.join(".")}" end if @field message << " (#{@field.path})" end message << ". Consider using ID or GraphQL::Types::BigInt instead." super(message) end end end graphql-ruby-2.5.19/lib/graphql/introspection.rb000066400000000000000000000053641514115062600216670ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Introspection def self.query(include_deprecated_args: false, include_schema_description: false, include_is_repeatable: false, include_specified_by_url: false, include_is_one_of: false) # The introspection query to end all introspection queries, copied from # https://github.com/graphql/graphql-js/blob/master/src/utilities/introspectionQuery.js <<-QUERY.gsub(/\n{2,}/, "\n") query IntrospectionQuery { __schema { #{include_schema_description ? "description" : ""} queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description locations #{include_is_repeatable ? "isRepeatable" : ""} args#{include_deprecated_args ? '(includeDeprecated: true)' : ''} { ...InputValue } } } } fragment FullType on __Type { kind name description #{include_specified_by_url ? "specifiedByURL" : ""} #{include_is_one_of ? "isOneOf" : ""} fields(includeDeprecated: true) { name description args#{include_deprecated_args ? '(includeDeprecated: true)' : ''} { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields#{include_deprecated_args ? '(includeDeprecated: true)' : ''} { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue #{include_deprecated_args ? 'isDeprecated' : ''} #{include_deprecated_args ? 'deprecationReason' : ''} } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } } } QUERY end end end require "graphql/introspection/base_object" require "graphql/introspection/input_value_type" require "graphql/introspection/enum_value_type" require "graphql/introspection/type_kind_enum" require "graphql/introspection/type_type" require "graphql/introspection/field_type" require "graphql/introspection/directive_location_enum" require "graphql/introspection/directive_type" require "graphql/introspection/schema_type" require "graphql/introspection/introspection_query" require "graphql/introspection/dynamic_fields" require "graphql/introspection/entry_points" graphql-ruby-2.5.19/lib/graphql/introspection/000077500000000000000000000000001514115062600213325ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/introspection/base_object.rb000066400000000000000000000004451514115062600241220ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Introspection class BaseObject < GraphQL::Schema::Object introspection(true) def self.field(*args, **kwargs, &block) kwargs[:introspection] = true super(*args, **kwargs, &block) end end end end graphql-ruby-2.5.19/lib/graphql/introspection/directive_location_enum.rb000066400000000000000000000011061514115062600265470ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Introspection class DirectiveLocationEnum < GraphQL::Schema::Enum graphql_name "__DirectiveLocation" description "A Directive can be adjacent to many parts of the GraphQL language, "\ "a __DirectiveLocation describes one such possible adjacencies." GraphQL::Schema::Directive::LOCATIONS.each do |location| value(location.to_s, GraphQL::Schema::Directive::LOCATION_DESCRIPTIONS[location], value: location, value_method: false) end introspection true end end end graphql-ruby-2.5.19/lib/graphql/introspection/directive_type.rb000066400000000000000000000031631514115062600247010ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Introspection class DirectiveType < Introspection::BaseObject graphql_name "__Directive" description "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document."\ "\n\n"\ "In some cases, you need to provide options to alter GraphQL's execution behavior "\ "in ways field arguments will not suffice, such as conditionally including or "\ "skipping a field. Directives provide this by describing additional information "\ "to the executor." field :name, String, null: false, method: :graphql_name field :description, String field :locations, [GraphQL::Schema::LateBoundType.new("__DirectiveLocation")], null: false, scope: false field :args, [GraphQL::Schema::LateBoundType.new("__InputValue")], null: false, scope: false do argument :include_deprecated, Boolean, required: false, default_value: false end field :on_operation, Boolean, null: false, deprecation_reason: "Use `locations`.", method: :on_operation? field :on_fragment, Boolean, null: false, deprecation_reason: "Use `locations`.", method: :on_fragment? field :on_field, Boolean, null: false, deprecation_reason: "Use `locations`.", method: :on_field? field :is_repeatable, Boolean, method: :repeatable? def args(include_deprecated:) args = @context.types.arguments(@object) args = args.reject(&:deprecation_reason) unless include_deprecated args end end end end graphql-ruby-2.5.19/lib/graphql/introspection/dynamic_fields.rb000066400000000000000000000004571514115062600246370ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Introspection class DynamicFields < Introspection::BaseObject field :__typename, String, "The name of this type", null: false, dynamic_introspection: true def __typename object.class.graphql_name end end end end graphql-ruby-2.5.19/lib/graphql/introspection/entry_points.rb000066400000000000000000000017141514115062600244170ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Introspection class EntryPoints < Introspection::BaseObject field :__schema, GraphQL::Schema::LateBoundType.new("__Schema"), "This GraphQL schema", null: false, dynamic_introspection: true field :__type, GraphQL::Schema::LateBoundType.new("__Type"), "A type in the GraphQL system", dynamic_introspection: true do argument :name, String end def __schema # Apply wrapping manually since this field isn't wrapped by instrumentation schema = context.schema schema_type = schema.introspection_system.types["__Schema"] schema_type.wrap(schema, context) end def __type(name:) if context.types.reachable_type?(name) && (type = context.types.type(name)) type elsif (type = context.schema.extra_types.find { |t| t.graphql_name == name }) type else nil end end end end end graphql-ruby-2.5.19/lib/graphql/introspection/enum_value_type.rb000066400000000000000000000013011514115062600250530ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Introspection class EnumValueType < Introspection::BaseObject graphql_name "__EnumValue" description "One possible value for a given Enum. Enum values are unique values, not a "\ "placeholder for a string or numeric value. However an Enum value is returned in "\ "a JSON response as a string." field :name, String, null: false field :description, String field :is_deprecated, Boolean, null: false field :deprecation_reason, String def name object.graphql_name end def is_deprecated !!@object.deprecation_reason end end end end graphql-ruby-2.5.19/lib/graphql/introspection/field_type.rb000066400000000000000000000020141514115062600240000ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Introspection class FieldType < Introspection::BaseObject graphql_name "__Field" description "Object and Interface types are described by a list of Fields, each of which has "\ "a name, potentially a list of arguments, and a return type." field :name, String, null: false field :description, String field :args, [GraphQL::Schema::LateBoundType.new("__InputValue")], null: false, scope: false do argument :include_deprecated, Boolean, required: false, default_value: false end field :type, GraphQL::Schema::LateBoundType.new("__Type"), null: false field :is_deprecated, Boolean, null: false field :deprecation_reason, String def is_deprecated !!@object.deprecation_reason end def args(include_deprecated:) args = @context.types.arguments(@object) args = args.reject(&:deprecation_reason) unless include_deprecated args end end end end graphql-ruby-2.5.19/lib/graphql/introspection/input_value_type.rb000066400000000000000000000046651514115062600252660ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Introspection class InputValueType < Introspection::BaseObject graphql_name "__InputValue" description "Arguments provided to Fields or Directives and the input fields of an "\ "InputObject are represented as Input Values which describe their type and "\ "optionally a default value." field :name, String, null: false field :description, String field :type, GraphQL::Schema::LateBoundType.new("__Type"), null: false field :default_value, String, "A GraphQL-formatted string representing the default value for this input value." field :is_deprecated, Boolean, null: false field :deprecation_reason, String def is_deprecated !!@object.deprecation_reason end def default_value if @object.default_value? value = @object.default_value if value.nil? 'null' else if (@object.type.kind.list? || (@object.type.kind.non_null? && @object.type.of_type.kind.list?)) && !value.respond_to?(:map) # This is a bit odd -- we expect the default value to be an application-style value, so we use coerce result below. # But coerce_result doesn't wrap single-item lists, which are valid inputs to list types. # So, apply that wrapper here if needed. value = [value] end coerced_default_value = @object.type.coerce_result(value, @context) serialize_default_value(coerced_default_value, @object.type) end else nil end end private # Recursively serialize, taking care not to add quotes to enum values def serialize_default_value(value, type) if value.nil? 'null' elsif type.kind.list? inner_type = type.of_type "[" + value.map { |v| serialize_default_value(v, inner_type) }.join(", ") + "]" elsif type.kind.non_null? serialize_default_value(value, type.of_type) elsif type.kind.enum? value elsif type.kind.input_object? "{" + value.map do |k, v| arg_defn = type.get_argument(k, context) "#{k}: #{serialize_default_value(v, arg_defn.type)}" end.join(", ") + "}" else GraphQL::Language.serialize(value) end end end end end graphql-ruby-2.5.19/lib/graphql/introspection/introspection_query.rb000066400000000000000000000005031514115062600260020ustar00rootroot00000000000000# frozen_string_literal: true # This query is used by graphql-client so don't add the includeDeprecated # argument for inputFields since the server may not support it. Two stage # introspection queries will be required to handle this in clients. GraphQL::Introspection::INTROSPECTION_QUERY = GraphQL::Introspection.query graphql-ruby-2.5.19/lib/graphql/introspection/schema_type.rb000066400000000000000000000034641514115062600241670ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Introspection class SchemaType < Introspection::BaseObject graphql_name "__Schema" description "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all "\ "available types and directives on the server, as well as the entry points for "\ "query, mutation, and subscription operations." field :types, [GraphQL::Schema::LateBoundType.new("__Type")], "A list of all types supported by this server.", null: false, scope: false field :query_type, GraphQL::Schema::LateBoundType.new("__Type"), "The type that query operations will be rooted at.", null: false field :mutation_type, GraphQL::Schema::LateBoundType.new("__Type"), "If this server supports mutation, the type that mutation operations will be rooted at." field :subscription_type, GraphQL::Schema::LateBoundType.new("__Type"), "If this server support subscription, the type that subscription operations will be rooted at." field :directives, [GraphQL::Schema::LateBoundType.new("__Directive")], "A list of all directives supported by this server.", null: false, scope: false field :description, String, resolver_method: :schema_description def schema_description context.schema.description end def types query_types = context.types.all_types types = query_types + context.schema.extra_types types.sort_by!(&:graphql_name) types end def query_type @context.types.query_root end def mutation_type @context.types.mutation_root end def subscription_type @context.types.subscription_root end def directives @context.types.directives.sort_by(&:graphql_name) end end end end graphql-ruby-2.5.19/lib/graphql/introspection/type_kind_enum.rb000066400000000000000000000006041514115062600246710ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Introspection class TypeKindEnum < GraphQL::Schema::Enum graphql_name "__TypeKind" description "An enum describing what kind of type a given `__Type` is." GraphQL::TypeKinds::TYPE_KINDS.each do |type_kind| value(type_kind.name, type_kind.description) end introspection true end end end graphql-ruby-2.5.19/lib/graphql/introspection/type_type.rb000066400000000000000000000070621514115062600237060ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Introspection class TypeType < Introspection::BaseObject graphql_name "__Type" description "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in "\ "GraphQL as represented by the `__TypeKind` enum.\n\n"\ "Depending on the kind of a type, certain fields describe information about that type. "\ "Scalar types provide no information beyond a name and description, while "\ "Enum types provide their values. Object and Interface types provide the fields "\ "they describe. Abstract types, Union and Interface, provide the Object types "\ "possible at runtime. List and NonNull types compose other types." field :kind, GraphQL::Schema::LateBoundType.new("__TypeKind"), null: false field :name, String, method: :graphql_name field :description, String field :fields, [GraphQL::Schema::LateBoundType.new("__Field")], scope: false do argument :include_deprecated, Boolean, required: false, default_value: false end field :interfaces, [GraphQL::Schema::LateBoundType.new("__Type")], scope: false field :possible_types, [GraphQL::Schema::LateBoundType.new("__Type")], scope: false field :enum_values, [GraphQL::Schema::LateBoundType.new("__EnumValue")], scope: false do argument :include_deprecated, Boolean, required: false, default_value: false end field :input_fields, [GraphQL::Schema::LateBoundType.new("__InputValue")], scope: false do argument :include_deprecated, Boolean, required: false, default_value: false end field :of_type, GraphQL::Schema::LateBoundType.new("__Type") field :specifiedByURL, String, resolver_method: :specified_by_url field :is_one_of, Boolean, null: false def is_one_of object.kind.input_object? && object.directives.any? { |d| d.graphql_name == "oneOf" } end def specified_by_url if object.kind.scalar? object.specified_by_url else nil end end def kind @object.kind.name end def enum_values(include_deprecated:) if !@object.kind.enum? nil else enum_values = @context.types.enum_values(@object) if !include_deprecated enum_values = enum_values.select {|f| !f.deprecation_reason } end enum_values end end def interfaces if @object.kind.object? || @object.kind.interface? @context.types.interfaces(@object).sort_by(&:graphql_name) else nil end end def input_fields(include_deprecated:) if @object.kind.input_object? args = @context.types.arguments(@object) args = args.reject(&:deprecation_reason) unless include_deprecated args else nil end end def possible_types if @object.kind.abstract? @context.types.possible_types(@object).sort_by(&:graphql_name) else nil end end def fields(include_deprecated:) if !@object.kind.fields? nil else fields = @context.types.fields(@object) if !include_deprecated fields = fields.select {|f| !f.deprecation_reason } end fields.sort_by(&:name) end end def of_type @object.kind.wraps? ? @object.of_type : nil end end end end graphql-ruby-2.5.19/lib/graphql/invalid_name_error.rb000066400000000000000000000004671514115062600226250ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class InvalidNameError < GraphQL::Error attr_reader :name, :valid_regex def initialize(name, valid_regex) @name = name @valid_regex = valid_regex super("Names must match #{@valid_regex.inspect} but '#{@name}' does not") end end end graphql-ruby-2.5.19/lib/graphql/invalid_null_error.rb000066400000000000000000000034321514115062600226520ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL # Raised automatically when a field's resolve function returns `nil` # for a non-null field. class InvalidNullError < GraphQL::Error # @return [GraphQL::BaseType] The owner of {#field} attr_reader :parent_type # @return [GraphQL::Field] The field which failed to return a value attr_reader :field # @return [GraphQL::Language::Nodes::Field] the field where the error occurred attr_reader :ast_node # @return [Boolean] indicates an array result caused the error attr_reader :is_from_array def initialize(parent_type, field, ast_node, is_from_array: false) @parent_type = parent_type @field = field @ast_node = ast_node @is_from_array = is_from_array # For List elements, identify the non-null error is for an # element and the required element type so it's not ambiguous # whether it was caused by a null instead of the list or a # null element. if @is_from_array super("Cannot return null for non-nullable element of type '#{@field.type.of_type.of_type.to_type_signature}' for #{@parent_type.graphql_name}.#{@field.graphql_name}") else super("Cannot return null for non-nullable field #{@parent_type.graphql_name}.#{@field.graphql_name}") end end class << self attr_accessor :parent_class def subclass_for(parent_class) subclass = Class.new(self) subclass.parent_class = parent_class subclass end def inspect if (name.nil? || parent_class&.name.nil?) && parent_class.respond_to?(:mutation) && (mutation = parent_class.mutation) "#{mutation.inspect}::#{parent_class.graphql_name}::InvalidNullError" else super end end end end end graphql-ruby-2.5.19/lib/graphql/language.rb000066400000000000000000000071461514115062600205520ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/language/block_string" require "graphql/language/comment" require "graphql/language/printer" require "graphql/language/sanitized_printer" require "graphql/language/document_from_schema_definition" require "graphql/language/generation" require "graphql/language/lexer" require "graphql/language/nodes" require "graphql/language/cache" require "graphql/language/parser" require "graphql/language/static_visitor" require "graphql/language/visitor" require "graphql/language/definition_slice" require "strscan" module GraphQL module Language # @api private def self.serialize(value) if value.is_a?(Hash) serialized_hash = value.map do |k, v| "#{k}:#{serialize v}" end.join(",") "{#{serialized_hash}}" elsif value.is_a?(Array) serialized_array = value.map do |v| serialize v end.join(",") "[#{serialized_array}]" else JSON.generate(value, quirks_mode: true) end rescue JSON::GeneratorError if Float::INFINITY == value "Infinity" else raise end end # Returns a new string if any single-quoted newlines were escaped. # Otherwise, returns `query_str` unchanged. # @return [String] def self.escape_single_quoted_newlines(query_str) scanner = StringScanner.new(query_str) inside_single_quoted_string = false new_query_str = nil while !scanner.eos? if scanner.skip(/(?:\\"|[^"\n\r]|""")+/m) new_query_str && (new_query_str << scanner.matched) elsif scanner.skip('"') new_query_str && (new_query_str << '"') inside_single_quoted_string = !inside_single_quoted_string elsif scanner.skip("\n") if inside_single_quoted_string new_query_str ||= query_str[0, scanner.pos - 1] new_query_str << '\\n' else new_query_str && (new_query_str << "\n") end elsif scanner.skip("\r") if inside_single_quoted_string new_query_str ||= query_str[0, scanner.pos - 1] new_query_str << '\\r' else new_query_str && (new_query_str << "\r") end elsif scanner.eos? break else raise ArgumentError, "Unmatchable string scanner segment: #{scanner.rest.inspect}" end end new_query_str || query_str end LEADING_REGEX = Regexp.union(" ", *Lexer::Punctuation.constants.map { |const| Lexer::Punctuation.const_get(const) }) # Optimized pattern using: # - Possessive quantifiers (*+, ++) to prevent backtracking in number patterns # - Atomic group (?>...) for IGNORE to prevent backtracking # - Single unified number pattern instead of three alternatives EFFICIENT_NUMBER_REGEXP = /-?(?:0|[1-9][0-9]*+)(?:\.[0-9]++)?(?:[eE][+-]?[0-9]++)?/ EFFICIENT_IGNORE_REGEXP = /(?>[, \r\n\t]+|\#[^\n]*$)*/ MAYBE_INVALID_NUMBER = /\d[_a-zA-Z]/ INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP = %r{ (?#{LEADING_REGEX}) (?#{EFFICIENT_NUMBER_REGEXP}) (?#{Lexer::IDENTIFIER_REGEXP}) #{EFFICIENT_IGNORE_REGEXP} : }x def self.add_space_between_numbers_and_names(query_str) # Fast check for digit followed by identifier char. If this doesn't match, skip the more expensive regexp entirely. return query_str unless query_str.match?(MAYBE_INVALID_NUMBER) return query_str unless query_str.match?(INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP) query_str.gsub(INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP, "\\k\\k \\k:") end end end graphql-ruby-2.5.19/lib/graphql/language/000077500000000000000000000000001514115062600202155ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/language/block_string.rb000066400000000000000000000061251514115062600232260ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Language module BlockString # Remove leading and trailing whitespace from a block string. # See "Block Strings" in https://github.com/facebook/graphql/blob/master/spec/Section%202%20--%20Language.md def self.trim_whitespace(str) # Early return for the most common cases: if str == "" return "".dup elsif !(has_newline = str.include?("\n")) && !(str.start_with?(" ")) return str end lines = has_newline ? str.split("\n") : [str] common_indent = nil # find the common whitespace lines.each_with_index do |line, idx| if idx == 0 next end line_length = line.size line_indent = if line.match?(/\A [^ ]/) 2 elsif line.match?(/\A [^ ]/) 4 elsif line.match?(/\A[^ ]/) 0 else line[/\A */].size end if line_indent < line_length && (common_indent.nil? || line_indent < common_indent) common_indent = line_indent end end # Remove the common whitespace if common_indent && common_indent > 0 lines.each_with_index do |line, idx| if idx == 0 next else line.slice!(0, common_indent) end end end # Remove leading & trailing blank lines while lines.size > 0 && contains_only_whitespace?(lines.first) lines.shift end while lines.size > 0 && contains_only_whitespace?(lines.last) lines.pop end # Rebuild the string lines.size > 1 ? lines.join("\n") : (lines.first || "".dup) end def self.print(str, indent: '') line_length = 120 - indent.length block_str = "".dup triple_quotes = "\"\"\"\n" block_str << indent block_str << triple_quotes if str.include?("\n") str.split("\n") do |line| if line == '' block_str << "\n" else break_line(line, line_length) do |subline| block_str << indent block_str << subline block_str << "\n" end end end else break_line(str, line_length) do |subline| block_str << indent block_str << subline block_str << "\n" end end block_str << indent block_str << triple_quotes end private def self.break_line(line, length) return yield(line) if line.length < length + 5 parts = line.split(Regexp.new("((?: |^).{15,#{length - 40}}(?= |$))")) return yield(line) if parts.length < 4 yield(parts.slice!(0, 3).join) parts.each_with_index do |part, i| next if i % 2 == 1 yield "#{part[1..-1]}#{parts[i + 1]}" end nil end def self.contains_only_whitespace?(line) line.match?(/^\s*$/) end end end end graphql-ruby-2.5.19/lib/graphql/language/cache.rb000066400000000000000000000032021514115062600216020ustar00rootroot00000000000000# frozen_string_literal: true require 'graphql/version' require 'digest/sha2' module GraphQL module Language # This cache is used by {GraphQL::Language::Parser.parse_file} when it's enabled. # # With Rails, parser caching may enabled by setting `config.graphql.parser_cache = true` in your Rails application. # # The cache may be manually built by assigning `GraphQL::Language::Parser.cache = GraphQL::Language::Cache.new("some_dir")`. # This will create a directory (`tmp/cache/graphql` by default) that stores a cache of parsed files. # # Much like [bootsnap](https://github.com/Shopify/bootsnap), the parser cache needs to be cleaned up manually. # You will need to clear the cache directory for each new deployment of your application. # Also note that the parser cache will grow as your schema is loaded, so the cache directory must be writable. # # @see GraphQL::Railtie for simple Rails integration class Cache def initialize(path) @path = path end DIGEST = Digest::SHA256.new << GraphQL::VERSION def fetch(filename) hash = DIGEST.dup << filename begin hash << File.mtime(filename).to_i.to_s rescue SystemCallError return yield end cache_path = @path.join(hash.to_s) if cache_path.exist? Marshal.load(cache_path.read) else payload = yield tmp_path = "#{cache_path}.#{rand}" @path.mkpath File.binwrite(tmp_path, Marshal.dump(payload)) File.rename(tmp_path, cache_path.to_s) payload end end end end end graphql-ruby-2.5.19/lib/graphql/language/comment.rb000066400000000000000000000006071514115062600222070ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Language module Comment def self.print(str, indent: '') lines = str.split("\n").map do |line| comment_str = "".dup comment_str << indent comment_str << "# " comment_str << line comment_str.rstrip end lines.join("\n") + "\n" end end end end graphql-ruby-2.5.19/lib/graphql/language/definition_slice.rb000066400000000000000000000022301514115062600240460ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Language module DefinitionSlice extend self def slice(document, name) definitions = {} document.definitions.each { |d| definitions[d.name] = d } names = Set.new DependencyVisitor.find_definition_dependencies(definitions, name, names) definitions = document.definitions.select { |d| names.include?(d.name) } Nodes::Document.new(definitions: definitions) end private class DependencyVisitor < GraphQL::Language::StaticVisitor def initialize(doc, definitions, names) @names = names @definitions = definitions super(doc) end def on_fragment_spread(node, parent) if fragment = @definitions[node.name] self.class.find_definition_dependencies(@definitions, fragment.name, @names) end super end def self.find_definition_dependencies(definitions, name, names) names.add(name) visitor = self.new(definitions[name], definitions, names) visitor.visit nil end end end end end graphql-ruby-2.5.19/lib/graphql/language/document_from_schema_definition.rb000066400000000000000000000336501514115062600271420ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Language # @api private # # {GraphQL::Language::DocumentFromSchemaDefinition} is used to convert a {GraphQL::Schema} object # To a {GraphQL::Language::Document} AST node. # # @param context [Hash] # @param only [<#call(member, ctx)>] # @param except [<#call(member, ctx)>] # @param include_introspection_types [Boolean] Whether or not to include introspection types in the AST # @param include_built_in_scalars [Boolean] Whether or not to include built in scalars in the AST # @param include_built_in_directives [Boolean] Whether or not to include built in directives in the AST class DocumentFromSchemaDefinition def initialize( schema, context: nil, include_introspection_types: false, include_built_in_directives: false, include_built_in_scalars: false, always_include_schema: false ) @schema = schema @context = context @always_include_schema = always_include_schema @include_introspection_types = include_introspection_types @include_built_in_scalars = include_built_in_scalars @include_built_in_directives = include_built_in_directives @include_one_of = false dummy_query = @schema.query_class.new(@schema, "{ __typename }", validate: false, context: context) @types = dummy_query.types # rubocop:disable Development/ContextIsPassedCop end def document GraphQL::Language::Nodes::Document.new( definitions: build_definition_nodes ) end def build_schema_node if !schema_respects_root_name_conventions?(@schema) GraphQL::Language::Nodes::SchemaDefinition.new( query: @types.query_root&.graphql_name, mutation: @types.mutation_root&.graphql_name, subscription: @types.subscription_root&.graphql_name, directives: definition_directives(@schema, :schema_directives) ) else # A plain `schema ...` _must_ include root type definitions. # If the only difference is directives, then you have to use `extend schema` GraphQL::Language::Nodes::SchemaExtension.new(directives: definition_directives(@schema, :schema_directives)) end end def build_object_type_node(object_type) ints = @types.interfaces(object_type) if !ints.empty? ints = ints.sort_by(&:graphql_name) ints.map! { |iface| build_type_name_node(iface) } end GraphQL::Language::Nodes::ObjectTypeDefinition.new( name: object_type.graphql_name, comment: object_type.comment, interfaces: ints, fields: build_field_nodes(@types.fields(object_type)), description: object_type.description, directives: directives(object_type), ) end def build_field_node(field) GraphQL::Language::Nodes::FieldDefinition.new( name: field.graphql_name, comment: field.comment, arguments: build_argument_nodes(@types.arguments(field)), type: build_type_name_node(field.type), description: field.description, directives: directives(field), ) end def build_union_type_node(union_type) GraphQL::Language::Nodes::UnionTypeDefinition.new( name: union_type.graphql_name, comment: union_type.comment, description: union_type.description, types: @types.possible_types(union_type).sort_by(&:graphql_name).map { |type| build_type_name_node(type) }, directives: directives(union_type), ) end def build_interface_type_node(interface_type) GraphQL::Language::Nodes::InterfaceTypeDefinition.new( name: interface_type.graphql_name, comment: interface_type.comment, interfaces: @types.interfaces(interface_type).sort_by(&:graphql_name).map { |type| build_type_name_node(type) }, description: interface_type.description, fields: build_field_nodes(@types.fields(interface_type)), directives: directives(interface_type), ) end def build_enum_type_node(enum_type) GraphQL::Language::Nodes::EnumTypeDefinition.new( name: enum_type.graphql_name, comment: enum_type.comment, values: @types.enum_values(enum_type).sort_by(&:graphql_name).map do |enum_value| build_enum_value_node(enum_value) end, description: enum_type.description, directives: directives(enum_type), ) end def build_enum_value_node(enum_value) GraphQL::Language::Nodes::EnumValueDefinition.new( name: enum_value.graphql_name, comment: enum_value.comment, description: enum_value.description, directives: directives(enum_value), ) end def build_scalar_type_node(scalar_type) GraphQL::Language::Nodes::ScalarTypeDefinition.new( name: scalar_type.graphql_name, comment: scalar_type.comment, description: scalar_type.description, directives: directives(scalar_type), ) end def build_argument_node(argument) if argument.default_value? default_value = build_default_value(argument.default_value, argument.type) else default_value = nil end argument_node = GraphQL::Language::Nodes::InputValueDefinition.new( name: argument.graphql_name, comment: argument.comment, description: argument.description, type: build_type_name_node(argument.type), default_value: default_value, directives: directives(argument), ) argument_node end def build_input_object_node(input_object) GraphQL::Language::Nodes::InputObjectTypeDefinition.new( name: input_object.graphql_name, comment: input_object.comment, fields: build_argument_nodes(@types.arguments(input_object)), description: input_object.description, directives: directives(input_object), ) end def build_directive_node(directive) GraphQL::Language::Nodes::DirectiveDefinition.new( name: directive.graphql_name, repeatable: directive.repeatable?, arguments: build_argument_nodes(@types.arguments(directive)), locations: build_directive_location_nodes(directive.locations), description: directive.description, ) end def build_directive_location_nodes(locations) locations.sort.map { |location| build_directive_location_node(location) } end def build_directive_location_node(location) GraphQL::Language::Nodes::DirectiveLocation.new( name: location.to_s ) end def build_type_name_node(type) case type.kind.name when "LIST" GraphQL::Language::Nodes::ListType.new( of_type: build_type_name_node(type.of_type) ) when "NON_NULL" GraphQL::Language::Nodes::NonNullType.new( of_type: build_type_name_node(type.of_type) ) else @cached_type_name_nodes ||= {} @cached_type_name_nodes[type.graphql_name] ||= GraphQL::Language::Nodes::TypeName.new(name: type.graphql_name) end end def build_default_value(default_value, type) if default_value.nil? return GraphQL::Language::Nodes::NullValue.new(name: "null") end case type.kind.name when "SCALAR" type.coerce_isolated_result(default_value) when "ENUM" GraphQL::Language::Nodes::Enum.new(name: type.coerce_isolated_result(default_value)) when "INPUT_OBJECT" GraphQL::Language::Nodes::InputObject.new( arguments: default_value.to_h.map do |arg_name, arg_value| args = @types.arguments(type) arg = args.find { |a| a.keyword.to_s == arg_name.to_s } if arg.nil? raise ArgumentError, "No argument definition on #{type.graphql_name} for argument: #{arg_name.inspect} (expected one of: #{args.map(&:keyword)})" end GraphQL::Language::Nodes::Argument.new( name: arg.graphql_name.to_s, value: build_default_value(arg_value, arg.type) ) end ) when "NON_NULL" build_default_value(default_value, type.of_type) when "LIST" default_value.to_a.map { |v| build_default_value(v, type.of_type) } else raise GraphQL::RequiredImplementationMissingError, "Unexpected default value type #{type.inspect}" end end def build_type_definition_node(type) case type.kind.name when "OBJECT" build_object_type_node(type) when "UNION" build_union_type_node(type) when "INTERFACE" build_interface_type_node(type) when "SCALAR" build_scalar_type_node(type) when "ENUM" build_enum_type_node(type) when "INPUT_OBJECT" build_input_object_node(type) else raise TypeError end end def build_argument_nodes(arguments) if !arguments.empty? nodes = arguments.map { |arg| build_argument_node(arg) } nodes.sort_by!(&:name) nodes else arguments end end def build_directive_nodes(directives) directives .map { |directive| build_directive_node(directive) } .sort_by(&:name) end def build_definition_nodes dirs_to_build = @types.directives if !include_built_in_directives dirs_to_build = dirs_to_build.reject { |directive| directive.default_directive? } end definitions = build_directive_nodes(dirs_to_build) all_types = @types.all_types type_nodes = build_type_definition_nodes(all_types) if !(ex_t = schema.extra_types).empty? dummy_query = Class.new(GraphQL::Schema::Object) do graphql_name "DummyQuery" (all_types + ex_t).each_with_index do |type, idx| if !type.kind.input_object? && !type.introspection? field "f#{idx}", type end end end extra_types_schema = Class.new(GraphQL::Schema) do query(dummy_query) end extra_types_types = GraphQL::Query.new(extra_types_schema, "{ __typename }", context: @context).types # rubocop:disable Development/ContextIsPassedCop # Temporarily replace `@types` with something from this example schema. # It'd be much nicer to pass this in, but that would be a big refactor :S prev_types = @types @types = extra_types_types type_nodes += build_type_definition_nodes(ex_t) @types = prev_types end type_nodes.sort_by!(&:name) if @include_one_of # This may have been set to true when iterating over all types definitions.concat(build_directive_nodes([GraphQL::Schema::Directive::OneOf])) end definitions.concat(type_nodes) if include_schema_node? definitions.unshift(build_schema_node) end definitions end def build_type_definition_nodes(types) if !include_introspection_types types = types.reject { |type| type.introspection? } end if !include_built_in_scalars types = types.reject { |type| type.kind.scalar? && type.default_scalar? } end types.map { |type| build_type_definition_node(type) } end def build_field_nodes(fields) f_nodes = fields.map { |field| build_field_node(field) } f_nodes.sort_by!(&:name) f_nodes end private def include_schema_node? always_include_schema || !schema_respects_root_name_conventions?(schema) || !schema.schema_directives.empty? end def schema_respects_root_name_conventions?(schema) (schema.query.nil? || schema.query.graphql_name == 'Query') && (schema.mutation.nil? || schema.mutation.graphql_name == 'Mutation') && (schema.subscription.nil? || schema.subscription.graphql_name == 'Subscription') end def directives(member) definition_directives(member, :directives) end def definition_directives(member, directives_method) if !member.respond_to?(directives_method) || member.directives.empty? EmptyObjects::EMPTY_ARRAY else visible_directives = member.public_send(directives_method).select { |dir| @types.directive_exists?(dir.graphql_name) } visible_directives.map! do |dir| args = [] dir.arguments.argument_values.each_value do |arg_value| # rubocop:disable Development/ContextIsPassedCop -- directive instance method arg_defn = arg_value.definition if arg_defn.default_value? && arg_value.value == arg_defn.default_value next else value_node = build_default_value(arg_value.value, arg_value.definition.type) args << GraphQL::Language::Nodes::Argument.new( name: arg_value.definition.name, value: value_node, ) end end # If this schema uses this built-in directive definition, # include it in the print-out since it's not part of the spec yet. @include_one_of ||= dir.class == GraphQL::Schema::Directive::OneOf GraphQL::Language::Nodes::Directive.new( name: dir.class.graphql_name, arguments: args ) end visible_directives end end attr_reader :schema, :always_include_schema, :include_introspection_types, :include_built_in_directives, :include_built_in_scalars end end end graphql-ruby-2.5.19/lib/graphql/language/generation.rb000066400000000000000000000016461514115062600227040ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Language # Exposes {.generate}, which turns AST nodes back into query strings. module Generation extend self # Turn an AST node back into a string. # # @example Turning a document into a query # document = GraphQL.parse(query_string) # GraphQL::Language::Generation.generate(document) # # => "{ ... }" # # @param node [GraphQL::Language::Nodes::AbstractNode] an AST node to recursively stringify # @param indent [String] Whitespace to add to each printed node # @param printer [GraphQL::Language::Printer] An optional custom printer for printing AST nodes. Defaults to GraphQL::Language::Printer # @return [String] Valid GraphQL for `node` def generate(node, indent: "", printer: GraphQL::Language::Printer.new) printer.print(node, indent: indent) end end end end graphql-ruby-2.5.19/lib/graphql/language/lexer.rb000066400000000000000000000274741514115062600216770ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Language class Lexer def initialize(graphql_str, filename: nil, max_tokens: nil) if !(graphql_str.encoding == Encoding::UTF_8 || graphql_str.ascii_only?) graphql_str = graphql_str.dup.force_encoding(Encoding::UTF_8) end @string = graphql_str @filename = filename @scanner = StringScanner.new(graphql_str) @pos = nil @max_tokens = max_tokens || Float::INFINITY @tokens_count = 0 @finished = false end def finished? @finished end def freeze @scanner = nil super end attr_reader :pos, :tokens_count def advance @scanner.skip(IGNORE_REGEXP) if @scanner.eos? @finished = true return false end @tokens_count += 1 if @tokens_count > @max_tokens raise_parse_error("This query is too large to execute.") end @pos = @scanner.pos next_byte = @string.getbyte(@pos) next_byte_is_for = FIRST_BYTES[next_byte] case next_byte_is_for when ByteFor::PUNCTUATION @scanner.pos += 1 PUNCTUATION_NAME_FOR_BYTE[next_byte] when ByteFor::NAME if len = @scanner.skip(KEYWORD_REGEXP) case len when 2 :ON when 12 :SUBSCRIPTION else pos = @pos # Use bytes 2 and 3 as a unique identifier for this keyword bytes = (@string.getbyte(pos + 2) << 8) | @string.getbyte(pos + 1) KEYWORD_BY_TWO_BYTES[_hash(bytes)] end else @scanner.skip(IDENTIFIER_REGEXP) :IDENTIFIER end when ByteFor::IDENTIFIER @scanner.skip(IDENTIFIER_REGEXP) :IDENTIFIER when ByteFor::NUMBER if len = @scanner.skip(NUMERIC_REGEXP) if GraphQL.reject_numbers_followed_by_names new_pos = @scanner.pos peek_byte = @string.getbyte(new_pos) next_first_byte = FIRST_BYTES[peek_byte] if next_first_byte == ByteFor::NAME || next_first_byte == ByteFor::IDENTIFIER number_part = token_value name_part = @scanner.scan(IDENTIFIER_REGEXP) raise_parse_error("Name after number is not allowed (in `#{number_part}#{name_part}`)") end end # Check for a matched decimal: @scanner[1] ? :FLOAT : :INT else # Attempt to find the part after the `-` value = @scanner.scan(/-\s?[a-z0-9]*/i) invalid_byte_for_number_error_message = "Expected type 'number', but it was malformed#{value.nil? ? "" : ": #{value.inspect}"}." raise_parse_error(invalid_byte_for_number_error_message) end when ByteFor::ELLIPSIS if @string.getbyte(@pos + 1) != 46 || @string.getbyte(@pos + 2) != 46 raise_parse_error("Expected `...`, actual: #{@string[@pos..@pos + 2].inspect}") end @scanner.pos += 3 :ELLIPSIS when ByteFor::STRING if @scanner.skip(BLOCK_STRING_REGEXP) || @scanner.skip(QUOTED_STRING_REGEXP) :STRING else raise_parse_error("Expected string or block string, but it was malformed") end else @scanner.pos += 1 :UNKNOWN_CHAR end rescue ArgumentError => err if err.message == "invalid byte sequence in UTF-8" raise_parse_error("Parse error on bad Unicode escape sequence", nil, nil) end end def token_value @string.byteslice(@scanner.pos - @scanner.matched_size, @scanner.matched_size) rescue StandardError => err raise GraphQL::Error, "(token_value failed: #{err.class}: #{err.message})" end def debug_token_value(token_name) if token_name && Lexer::Punctuation.const_defined?(token_name) Lexer::Punctuation.const_get(token_name) elsif token_name == :ELLIPSIS "..." elsif token_name == :STRING string_value elsif @scanner.matched_size.nil? @scanner.peek(1) else token_value end end ESCAPES = /\\["\\\/bfnrt]/ ESCAPES_REPLACE = { '\\"' => '"', "\\\\" => "\\", "\\/" => '/', "\\b" => "\b", "\\f" => "\f", "\\n" => "\n", "\\r" => "\r", "\\t" => "\t", } UTF_8 = /\\u(?:([\dAa-f]{4})|\{([\da-f]{4,})\})(?:\\u([\dAa-f]{4}))?/i VALID_STRING = /\A(?:[^\\]|#{ESCAPES}|#{UTF_8})*\z/o ESCAPED = /(?:#{ESCAPES}|#{UTF_8})/o def string_value str = token_value is_block = str.start_with?('"""') if is_block str.gsub!(/\A"""|"""\z/, '') return Language::BlockString.trim_whitespace(str) else str.gsub!(/\A"|"\z/, '') if !str.valid_encoding? || !str.match?(VALID_STRING) raise_parse_error("Bad unicode escape in #{str.inspect}") else Lexer.replace_escaped_characters_in_place(str) if !str.valid_encoding? raise_parse_error("Bad unicode escape in #{str.inspect}") else str end end end end def line_number @scanner.string[0..@pos].count("\n") + 1 end def column_number @scanner.string[0..@pos].split("\n").last.length end def raise_parse_error(message, line = line_number, col = column_number) raise GraphQL::ParseError.new(message, line, col, @string, filename: @filename) end IGNORE_REGEXP = %r{ (?: [, \c\r\n\t]+ | \#.*$ )* }x IDENTIFIER_REGEXP = /[_A-Za-z][_0-9A-Za-z]*/ INT_REGEXP = /-?(?:[0]|[1-9][0-9]*)/ FLOAT_DECIMAL_REGEXP = /[.][0-9]+/ FLOAT_EXP_REGEXP = /[eE][+-]?[0-9]+/ # TODO: FLOAT_EXP_REGEXP should not be allowed to follow INT_REGEXP, integers are not allowed to have exponent parts. NUMERIC_REGEXP = /#{INT_REGEXP}(#{FLOAT_DECIMAL_REGEXP}#{FLOAT_EXP_REGEXP}|#{FLOAT_DECIMAL_REGEXP}|#{FLOAT_EXP_REGEXP})?/ KEYWORDS = [ "on", "fragment", "true", "false", "null", "query", "mutation", "subscription", "schema", "scalar", "type", "extend", "implements", "interface", "union", "enum", "input", "directive", "repeatable" ].freeze KEYWORD_REGEXP = /#{Regexp.union(KEYWORDS.sort)}\b/ KEYWORD_BY_TWO_BYTES = [ :INTERFACE, :MUTATION, :EXTEND, :FALSE, :ENUM, :TRUE, :NULL, nil, nil, nil, nil, nil, nil, nil, :QUERY, nil, nil, :REPEATABLE, :IMPLEMENTS, :INPUT, :TYPE, :SCHEMA, nil, nil, nil, :DIRECTIVE, :UNION, nil, nil, :SCALAR, nil, :FRAGMENT ].freeze # This produces a unique integer for bytes 2 and 3 of each keyword string # See https://tenderlovemaking.com/2023/09/02/fast-tokenizers-with-stringscanner.html def _hash key (key * 18592990) >> 27 & 0x1f end module Punctuation LCURLY = '{' RCURLY = '}' LPAREN = '(' RPAREN = ')' LBRACKET = '[' RBRACKET = ']' COLON = ':' VAR_SIGN = '$' DIR_SIGN = '@' EQUALS = '=' BANG = '!' PIPE = '|' AMP = '&' end # A sparse array mapping the bytes for each punctuation # to a symbol name for that punctuation PUNCTUATION_NAME_FOR_BYTE = Punctuation.constants.each_with_object([]) { |name, arr| punct = Punctuation.const_get(name) arr[punct.ord] = name }.freeze QUOTE = '"' UNICODE_DIGIT = /[0-9A-Za-z]/ FOUR_DIGIT_UNICODE = /#{UNICODE_DIGIT}{4}/ N_DIGIT_UNICODE = %r{#{Punctuation::LCURLY}#{UNICODE_DIGIT}{4,}#{Punctuation::RCURLY}}x UNICODE_ESCAPE = %r{\\u(?:#{FOUR_DIGIT_UNICODE}|#{N_DIGIT_UNICODE})} STRING_ESCAPE = %r{[\\][\\/bfnrt]} BLOCK_QUOTE = '"""' ESCAPED_QUOTE = /\\"/; STRING_CHAR = /#{ESCAPED_QUOTE}|[^"\\\n\r]|#{UNICODE_ESCAPE}|#{STRING_ESCAPE}/ QUOTED_STRING_REGEXP = %r{#{QUOTE} (?:#{STRING_CHAR})* #{QUOTE}}x BLOCK_STRING_REGEXP = %r{ #{BLOCK_QUOTE} (?: [^"\\] | # Any characters that aren't a quote or slash (?= 0xD800 && codepoint_1 <= 0xDBFF) && # leading surrogate (codepoint_2 >= 0xDC00 && codepoint_2 <= 0xDFFF) # trailing surrogate # A surrogate pair combined = ((codepoint_1 - 0xD800) * 0x400) + (codepoint_2 - 0xDC00) + 0x10000 [combined].pack('U'.freeze) else # Two separate code points [codepoint_1].pack('U'.freeze) + [codepoint_2].pack('U'.freeze) end else [codepoint_1].pack('U'.freeze) end else ESCAPES_REPLACE[matched_str] end end nil end # This is not used during parsing because the parser # doesn't actually need tokens. def self.tokenize(string) lexer = GraphQL::Language::Lexer.new(string) tokens = [] while (token_name = lexer.advance) new_token = [ token_name, lexer.line_number, lexer.column_number, lexer.debug_token_value(token_name), ] tokens << new_token end tokens end end end end graphql-ruby-2.5.19/lib/graphql/language/nodes.rb000066400000000000000000000655721514115062600216710ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Language module Nodes NONE = GraphQL::EmptyObjects::EMPTY_ARRAY # {AbstractNode} is the base class for all nodes in a GraphQL AST. # # It provides some APIs for working with ASTs: # - `children` returns all AST nodes attached to this one. Used for tree traversal. # - `scalars` returns all scalar (Ruby) values attached to this one. Used for comparing nodes. # - `to_query_string` turns an AST node into a GraphQL string class AbstractNode module DefinitionNode # This AST node's {#line} returns the first line, which may be the description. # @return [Integer] The first line of the definition (not the description) attr_reader :definition_line def initialize(definition_line: nil, **_rest) @definition_line = definition_line super(**_rest) end def marshal_dump super << @definition_line end def marshal_load(values) @definition_line = values.pop super end end attr_reader :filename def line @line ||= @source&.line_at(@pos) end def col @col ||= @source&.column_at(@pos) end def definition_line @definition_line ||= (@source && @definition_pos) ? @source.line_at(@definition_pos) : nil end # Value equality # @return [Boolean] True if `self` is equivalent to `other` def ==(other) return true if equal?(other) other.kind_of?(self.class) && other.scalars == self.scalars && other.children == self.children end NO_CHILDREN = GraphQL::EmptyObjects::EMPTY_ARRAY # @return [Array] all nodes in the tree below this one def children NO_CHILDREN end # @return [Array] Scalar values attached to this node def scalars NO_CHILDREN end # This might be unnecessary, but its easiest to add it here. def initialize_copy(other) @children = nil @scalars = nil @query_string = nil end def children_method_name self.class.children_method_name end def position [line, col] end def to_query_string(printer: GraphQL::Language::Printer.new) if printer.is_a?(GraphQL::Language::Printer) if frozen? @query_string || printer.print(self) else @query_string ||= printer.print(self) end else printer.print(self) end end # This creates a copy of `self`, with `new_options` applied. # @param new_options [Hash] # @return [AbstractNode] a shallow copy of `self` def merge(new_options) dup.merge!(new_options) end # Copy `self`, but modify the copy so that `previous_child` is replaced by `new_child` def replace_child(previous_child, new_child) # Figure out which list `previous_child` may be found in method_name = previous_child.children_method_name # Get the value from this (original) node prev_children = public_send(method_name) if prev_children.is_a?(Array) # Copy that list, and replace `previous_child` with `new_child` # in the list. new_children = prev_children.dup prev_idx = new_children.index(previous_child) new_children[prev_idx] = new_child else # Use the new value for the given attribute new_children = new_child end # Copy this node, but with the new child value copy_of_self = merge(method_name => new_children) # Return the copy: copy_of_self end # TODO DRY with `replace_child` def delete_child(previous_child) # Figure out which list `previous_child` may be found in method_name = previous_child.children_method_name # Copy that list, and delete previous_child new_children = public_send(method_name).dup new_children.delete(previous_child) # Copy this node, but with the new list of children: copy_of_self = merge(method_name => new_children) # Return the copy: copy_of_self end protected def merge!(new_options) new_options.each do |key, value| instance_variable_set(:"@#{key}", value) end self end class << self # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time # Add a default `#visit_method` and `#children_method_name` using the class name def inherited(child_class) super name_underscored = child_class.name .split("::").last .gsub(/([a-z])([A-Z])/,'\1_\2') # insert underscores .downcase # remove caps child_class.module_eval <<-RUBY, __FILE__, __LINE__ def visit_method :on_#{name_underscored} end class << self attr_accessor :children_method_name def visit_method :on_#{name_underscored} end end self.children_method_name = :#{name_underscored}s RUBY end def children_of_type @children_methods end private # Name accessors which return lists of nodes, # along with the kind of node they return, if possible. # - Add a reader for these children # - Add a persistent update method to add a child # - Generate a `#children` method def children_methods(children_of_type) if defined?(@children_methods) raise "Can't re-call .children_methods for #{self} (already have: #{@children_methods})" else @children_methods = children_of_type end if children_of_type == false @children_methods = {} # skip else children_of_type.each do |method_name, node_type| module_eval <<-RUBY, __FILE__, __LINE__ # A reader for these children attr_reader :#{method_name} RUBY if node_type # Only generate a method if we know what kind of node to make module_eval <<-RUBY, __FILE__, __LINE__ # Singular method: create a node with these options # and return a new `self` which includes that node in this list. def merge_#{method_name.to_s.sub(/s$/, "")}(**node_opts) merge(#{method_name}: #{method_name} + [#{node_type.name}.new(**node_opts)]) end RUBY end end if children_of_type.size == 1 module_eval <<-RUBY, __FILE__, __LINE__ alias :children #{children_of_type.keys.first} RUBY else module_eval <<-RUBY, __FILE__, __LINE__ def children @children ||= begin if #{children_of_type.keys.map { |k| "@#{k}.any?" }.join(" || ")} new_children = [] #{children_of_type.keys.map { |k| "new_children.concat(@#{k})" }.join("; ")} new_children.freeze new_children else NO_CHILDREN end end end RUBY end end if defined?(@scalar_methods) if !@initialize_was_generated @initialize_was_generated = true generate_initialize else # This method was defined manually end else raise "Can't generate_initialize because scalar_methods wasn't called; call it before children_methods" end end # These methods return a plain Ruby value, not another node # - Add reader methods # - Add a `#scalars` method def scalar_methods(*method_names) if defined?(@scalar_methods) raise "Can't re-call .scalar_methods for #{self} (already have: #{@scalar_methods})" else @scalar_methods = method_names end if method_names == [false] @scalar_methods = [] # skip it else module_eval <<-RUBY, __FILE__, __LINE__ # add readers for each scalar attr_reader #{method_names.map { |m| ":#{m}"}.join(", ")} def scalars @scalars ||= [#{method_names.map { |k| "@#{k}" }.join(", ")}].freeze end RUBY end end DEFAULT_INITIALIZE_OPTIONS = [ "line: nil", "col: nil", "pos: nil", "filename: nil", "source: nil" ] IGNORED_MARSHALLING_KEYWORDS = [:comment] def generate_initialize return if method_defined?(:marshal_load, false) # checking for `:initialize` doesn't work right scalar_method_names = @scalar_methods # TODO: These probably should be scalar methods, but `types` returns an array [:types, :description, :comment].each do |extra_method| if method_defined?(extra_method) scalar_method_names += [extra_method] end end children_method_names = @children_methods.keys all_method_names = scalar_method_names + children_method_names if all_method_names.include?(:alias) # Rather than complicating this special case, # let it be overridden (in field) return else arguments = scalar_method_names.map { |m| "#{m}: nil"} + children_method_names.map { |m| "#{m}: NO_CHILDREN" } + DEFAULT_INITIALIZE_OPTIONS assignments = scalar_method_names.map { |m| "@#{m} = #{m}"} + children_method_names.map { |m| "@#{m} = #{m}.freeze" } if name.end_with?("Definition") && name != "FragmentDefinition" arguments << "definition_pos: nil" assignments << "@definition_pos = definition_pos" end keywords = scalar_method_names.map { |m| "#{m}: #{m}"} + children_method_names.map { |m| "#{m}: #{m}" } ignored_keywords = IGNORED_MARSHALLING_KEYWORDS.map do |keyword| "#{keyword.to_s}: nil" end marshalling_method_names = all_method_names - IGNORED_MARSHALLING_KEYWORDS module_eval <<-RUBY, __FILE__, __LINE__ def initialize(#{arguments.join(", ")}) @line = line @col = col @pos = pos @filename = filename @source = source #{assignments.join("\n")} end def self.from_a(filename, line, col, #{marshalling_method_names.join(", ")}, #{ignored_keywords.join(", ")}) self.new(filename: filename, line: line, col: col, #{keywords.join(", ")}) end def marshal_dump [ line, col, # use methods here to force them to be calculated @filename, #{marshalling_method_names.map { |n| "@#{n}," }.join} ] end def marshal_load(values) @line, @col, @filename #{marshalling_method_names.map { |n| ", @#{n}"}.join} = values end RUBY end end # rubocop:enable Development/NoEvalCop end end # Base class for non-null type names and list type names class WrapperType < AbstractNode scalar_methods :of_type children_methods(false) end # Base class for nodes whose only value is a name (no child nodes or other scalars) class NameOnlyNode < AbstractNode scalar_methods :name children_methods(false) end # A key-value pair for a field's inputs class Argument < AbstractNode scalar_methods :name, :value children_methods(false) # @!attribute name # @return [String] the key for this argument # @!attribute value # @return [String, Float, Integer, Boolean, Array, InputObject, VariableIdentifier] The value passed for this key def children @children ||= Array(value).flatten.tap { _1.select! { |v| v.is_a?(AbstractNode) } } end end class Directive < AbstractNode scalar_methods :name children_methods(arguments: GraphQL::Language::Nodes::Argument) end class DirectiveLocation < NameOnlyNode end class DirectiveDefinition < AbstractNode attr_reader :description scalar_methods :name, :repeatable children_methods( arguments: Nodes::Argument, locations: Nodes::DirectiveLocation, ) self.children_method_name = :definitions end # An enum value. The string is available as {#name}. class Enum < NameOnlyNode end # A null value literal. class NullValue < NameOnlyNode end # A single selection in a GraphQL query. class Field < AbstractNode def initialize(name: nil, arguments: NONE, directives: NONE, selections: NONE, field_alias: nil, line: nil, col: nil, pos: nil, filename: nil, source: nil) @name = name @arguments = arguments || NONE @directives = directives || NONE @selections = selections || NONE # oops, alias is a keyword: @alias = field_alias @line = line @col = col @pos = pos @filename = filename @source = source end def self.from_a(filename, line, col, field_alias, name, arguments, directives, selections) # rubocop:disable Metrics/ParameterLists self.new(filename: filename, line: line, col: col, field_alias: field_alias, name: name, arguments: arguments, directives: directives, selections: selections) end def marshal_dump [line, col, @filename, @name, @arguments, @directives, @selections, @alias] end def marshal_load(values) @line, @col, @filename, @name, @arguments, @directives, @selections, @alias = values end scalar_methods :name, :alias children_methods({ arguments: GraphQL::Language::Nodes::Argument, selections: GraphQL::Language::Nodes::Field, directives: GraphQL::Language::Nodes::Directive, }) # Override this because default is `:fields` self.children_method_name = :selections end # A reusable fragment, defined at document-level. class FragmentDefinition < AbstractNode def initialize(name: nil, type: nil, directives: NONE, selections: NONE, filename: nil, pos: nil, source: nil, line: nil, col: nil) @name = name @type = type @directives = directives @selections = selections @filename = filename @pos = pos @source = source @line = line @col = col end def self.from_a(filename, line, col, name, type, directives, selections) self.new(filename: filename, line: line, col: col, name: name, type: type, directives: directives, selections: selections) end def marshal_dump [line, col, @filename, @name, @type, @directives, @selections] end def marshal_load(values) @line, @col, @filename, @name, @type, @directives, @selections = values end scalar_methods :name, :type children_methods({ selections: GraphQL::Language::Nodes::Field, directives: GraphQL::Language::Nodes::Directive, }) self.children_method_name = :definitions end # Application of a named fragment in a selection class FragmentSpread < AbstractNode scalar_methods :name children_methods(directives: GraphQL::Language::Nodes::Directive) self.children_method_name = :selections # @!attribute name # @return [String] The identifier of the fragment to apply, corresponds with {FragmentDefinition#name} end # An unnamed fragment, defined directly in the query with `... { }` class InlineFragment < AbstractNode scalar_methods :type children_methods({ directives: GraphQL::Language::Nodes::Directive, selections: GraphQL::Language::Nodes::Field, }) self.children_method_name = :selections # @!attribute type # @return [String, nil] Name of the type this fragment applies to, or `nil` if this fragment applies to any type end # A collection of key-value inputs which may be a field argument class InputObject < AbstractNode scalar_methods(false) children_methods(arguments: GraphQL::Language::Nodes::Argument) # @!attribute arguments # @return [Array] A list of key-value pairs inside this input object # @return [Hash] Recursively turn this input object into a Ruby Hash def to_h(options={}) arguments.inject({}) do |memo, pair| v = pair.value memo[pair.name] = serialize_value_for_hash v memo end end self.children_method_name = :value private def serialize_value_for_hash(value) case value when InputObject value.to_h when Array value.map do |v| serialize_value_for_hash v end when Enum value.name when NullValue nil else value end end end # A list type definition, denoted with `[...]` (used for variable type definitions) class ListType < WrapperType end # A non-null type definition, denoted with `...!` (used for variable type definitions) class NonNullType < WrapperType end # An operation-level query variable class VariableDefinition < AbstractNode scalar_methods :name, :type, :default_value children_methods(directives: Directive) # @!attribute default_value # @return [String, Integer, Float, Boolean, Array, NullValue] A Ruby value to use if no other value is provided # @!attribute type # @return [TypeName, NonNullType, ListType] The expected type of this value # @!attribute name # @return [String] The identifier for this variable, _without_ `$` self.children_method_name = :variables end # A query, mutation or subscription. # May be anonymous or named. # May be explicitly typed (eg `mutation { ... }`) or implicitly a query (eg `{ ... }`). class OperationDefinition < AbstractNode scalar_methods :operation_type, :name children_methods({ variables: GraphQL::Language::Nodes::VariableDefinition, directives: GraphQL::Language::Nodes::Directive, selections: GraphQL::Language::Nodes::Field, }) # @!attribute variables # @return [Array] Variable $definitions for this operation # @!attribute selections # @return [Array] Root-level fields on this operation # @!attribute operation_type # @return [String, nil] The root type for this operation, or `nil` for implicit `"query"` # @!attribute name # @return [String, nil] The name for this operation, or `nil` if unnamed self.children_method_name = :definitions end # This is the AST root for normal queries # # @example Deriving a document by parsing a string # document = GraphQL.parse(query_string) # # @example Creating a string from a document # document.to_query_string # # { ... } # # @example Creating a custom string from a document # class VariableScrubber < GraphQL::Language::Printer # def print_argument(arg) # print_string("#{arg.name}: ") # end # end # # document.to_query_string(printer: VariableScrubber.new) # class Document < AbstractNode scalar_methods false children_methods(definitions: nil) # @!attribute definitions # @return [Array] top-level GraphQL units: operations or fragments def slice_definition(name) GraphQL::Language::DefinitionSlice.slice(self, name) end end # A type name, used for variable definitions class TypeName < NameOnlyNode end # Usage of a variable in a query. Name does _not_ include `$`. class VariableIdentifier < NameOnlyNode self.children_method_name = :value end class SchemaDefinition < AbstractNode scalar_methods :query, :mutation, :subscription children_methods({ directives: GraphQL::Language::Nodes::Directive, }) self.children_method_name = :definitions end class SchemaExtension < AbstractNode scalar_methods :query, :mutation, :subscription children_methods({ directives: GraphQL::Language::Nodes::Directive, }) self.children_method_name = :definitions end class ScalarTypeDefinition < AbstractNode attr_reader :description, :comment scalar_methods :name children_methods({ directives: GraphQL::Language::Nodes::Directive, }) self.children_method_name = :definitions end class ScalarTypeExtension < AbstractNode scalar_methods :name children_methods({ directives: GraphQL::Language::Nodes::Directive, }) self.children_method_name = :definitions end class InputValueDefinition < AbstractNode attr_reader :description, :comment scalar_methods :name, :type, :default_value children_methods({ directives: GraphQL::Language::Nodes::Directive, }) self.children_method_name = :fields end class FieldDefinition < AbstractNode attr_reader :description, :comment scalar_methods :name, :type children_methods({ arguments: GraphQL::Language::Nodes::InputValueDefinition, directives: GraphQL::Language::Nodes::Directive, }) self.children_method_name = :fields # this is so that `children_method_name` of `InputValueDefinition` works properly # with `#replace_child` alias :fields :arguments def merge(new_options) if (f = new_options.delete(:fields)) new_options[:arguments] = f end super end end class ObjectTypeDefinition < AbstractNode attr_reader :description, :comment scalar_methods :name, :interfaces children_methods({ directives: GraphQL::Language::Nodes::Directive, fields: GraphQL::Language::Nodes::FieldDefinition, }) self.children_method_name = :definitions end class ObjectTypeExtension < AbstractNode scalar_methods :name, :interfaces children_methods({ directives: GraphQL::Language::Nodes::Directive, fields: GraphQL::Language::Nodes::FieldDefinition, }) self.children_method_name = :definitions end class InterfaceTypeDefinition < AbstractNode attr_reader :description, :comment scalar_methods :name children_methods({ interfaces: GraphQL::Language::Nodes::TypeName, directives: GraphQL::Language::Nodes::Directive, fields: GraphQL::Language::Nodes::FieldDefinition, }) self.children_method_name = :definitions end class InterfaceTypeExtension < AbstractNode scalar_methods :name children_methods({ interfaces: GraphQL::Language::Nodes::TypeName, directives: GraphQL::Language::Nodes::Directive, fields: GraphQL::Language::Nodes::FieldDefinition, }) self.children_method_name = :definitions end class UnionTypeDefinition < AbstractNode attr_reader :description, :comment, :types scalar_methods :name children_methods({ directives: GraphQL::Language::Nodes::Directive, }) self.children_method_name = :definitions end class UnionTypeExtension < AbstractNode attr_reader :types scalar_methods :name children_methods({ directives: GraphQL::Language::Nodes::Directive, }) self.children_method_name = :definitions end class EnumValueDefinition < AbstractNode attr_reader :description, :comment scalar_methods :name children_methods({ directives: GraphQL::Language::Nodes::Directive, }) self.children_method_name = :values end class EnumTypeDefinition < AbstractNode attr_reader :description, :comment scalar_methods :name children_methods({ directives: GraphQL::Language::Nodes::Directive, values: GraphQL::Language::Nodes::EnumValueDefinition, }) self.children_method_name = :definitions end class EnumTypeExtension < AbstractNode scalar_methods :name children_methods({ directives: GraphQL::Language::Nodes::Directive, values: GraphQL::Language::Nodes::EnumValueDefinition, }) self.children_method_name = :definitions end class InputObjectTypeDefinition < AbstractNode attr_reader :description, :comment scalar_methods :name children_methods({ directives: GraphQL::Language::Nodes::Directive, fields: GraphQL::Language::Nodes::InputValueDefinition, }) self.children_method_name = :definitions end class InputObjectTypeExtension < AbstractNode scalar_methods :name children_methods({ directives: GraphQL::Language::Nodes::Directive, fields: GraphQL::Language::Nodes::InputValueDefinition, }) self.children_method_name = :definitions end end end end graphql-ruby-2.5.19/lib/graphql/language/parser.rb000066400000000000000000000652001514115062600220410ustar00rootroot00000000000000# frozen_string_literal: true require "strscan" require "graphql/language/nodes" require "graphql/tracing/null_trace" module GraphQL module Language class Parser include GraphQL::Language::Nodes include EmptyObjects class << self attr_accessor :cache def parse(graphql_str, filename: nil, trace: Tracing::NullTrace, max_tokens: nil) self.new(graphql_str, filename: filename, trace: trace, max_tokens: max_tokens).parse end def parse_file(filename, trace: Tracing::NullTrace) if cache cache.fetch(filename) do parse(File.read(filename), filename: filename, trace: trace) end else parse(File.read(filename), filename: filename, trace: trace) end end end def initialize(graphql_str, filename: nil, trace: Tracing::NullTrace, max_tokens: nil) if graphql_str.nil? raise GraphQL::ParseError.new("No query string was present", nil, nil, nil) end @lexer = Lexer.new(graphql_str, filename: filename, max_tokens: max_tokens) @graphql_str = graphql_str @filename = filename @trace = trace @dedup_identifiers = false @lines_at = nil end def parse @document ||= begin @trace.parse(query_string: @graphql_str) do document end rescue SystemStackError raise GraphQL::ParseError.new("This query is too large to execute.", nil, nil, @query_str, filename: @filename) end end def tokens_count parse @lexer.tokens_count end def line_at(pos) line = lines_at.bsearch_index { |l| l >= pos } if line.nil? @lines_at.size + 1 else line + 1 end end def column_at(pos) next_line_idx = lines_at.bsearch_index { |l| l >= pos } || 0 if next_line_idx > 0 line_pos = @lines_at[next_line_idx - 1] pos - line_pos else pos + 1 end end private # @return [Array] Positions of each line break in the original string def lines_at @lines_at ||= begin la = [] idx = 0 while idx idx = @graphql_str.index("\n", idx) if idx la << idx idx += 1 end end la end end attr_reader :token_name def advance_token @token_name = @lexer.advance end def pos @lexer.pos end def document any_tokens = advance_token defns = [] if any_tokens defns << definition else # Only ignored characters is not a valid document raise GraphQL::ParseError.new("Unexpected end of document", nil, nil, @graphql_str) end while !@lexer.finished? defns << definition end Document.new(pos: 0, definitions: defns, filename: @filename, source: self) end def definition case token_name when :FRAGMENT loc = pos expect_token :FRAGMENT f_name = if !at?(:ON) parse_name end expect_token :ON f_type = parse_type_name directives = parse_directives selections = selection_set Nodes::FragmentDefinition.new( pos: loc, name: f_name, type: f_type, directives: directives, selections: selections, filename: @filename, source: self ) when :QUERY, :MUTATION, :SUBSCRIPTION, :LCURLY op_loc = pos op_type = case token_name when :LCURLY "query" else parse_operation_type end op_name = case token_name when :LPAREN, :LCURLY, :DIR_SIGN nil else parse_name end variable_definitions = if at?(:LPAREN) expect_token(:LPAREN) defs = [] while !at?(:RPAREN) loc = pos expect_token(:VAR_SIGN) var_name = parse_name expect_token(:COLON) var_type = self.type || raise_parse_error("Missing type definition for variable: $#{var_name}") default_value = if at?(:EQUALS) advance_token value end directives = parse_directives defs << Nodes::VariableDefinition.new( pos: loc, name: var_name, type: var_type, default_value: default_value, directives: directives, filename: @filename, source: self ) end expect_token(:RPAREN) defs else EmptyObjects::EMPTY_ARRAY end directives = parse_directives OperationDefinition.new( pos: op_loc, operation_type: op_type, name: op_name, variables: variable_definitions, directives: directives, selections: selection_set, filename: @filename, source: self ) when :EXTEND loc = pos advance_token case token_name when :SCALAR advance_token name = parse_name directives = parse_directives ScalarTypeExtension.new(pos: loc, name: name, directives: directives, filename: @filename, source: self) when :TYPE advance_token name = parse_name implements_interfaces = parse_implements directives = parse_directives field_defns = at?(:LCURLY) ? parse_field_definitions : EMPTY_ARRAY ObjectTypeExtension.new(pos: loc, name: name, interfaces: implements_interfaces, directives: directives, fields: field_defns, filename: @filename, source: self) when :INTERFACE advance_token name = parse_name directives = parse_directives interfaces = parse_implements fields_definition = at?(:LCURLY) ? parse_field_definitions : EMPTY_ARRAY InterfaceTypeExtension.new(pos: loc, name: name, directives: directives, fields: fields_definition, interfaces: interfaces, filename: @filename, source: self) when :UNION advance_token name = parse_name directives = parse_directives union_member_types = parse_union_members UnionTypeExtension.new(pos: loc, name: name, directives: directives, types: union_member_types, filename: @filename, source: self) when :ENUM advance_token name = parse_name directives = parse_directives enum_values_definition = parse_enum_value_definitions Nodes::EnumTypeExtension.new(pos: loc, name: name, directives: directives, values: enum_values_definition, filename: @filename, source: self) when :INPUT advance_token name = parse_name directives = parse_directives input_fields_definition = parse_input_object_field_definitions InputObjectTypeExtension.new(pos: loc, name: name, directives: directives, fields: input_fields_definition, filename: @filename, source: self) when :SCHEMA advance_token directives = parse_directives query = mutation = subscription = nil if at?(:LCURLY) advance_token while !at?(:RCURLY) if at?(:QUERY) advance_token expect_token(:COLON) query = parse_name elsif at?(:MUTATION) advance_token expect_token(:COLON) mutation = parse_name elsif at?(:SUBSCRIPTION) advance_token expect_token(:COLON) subscription = parse_name else expect_one_of([:QUERY, :MUTATION, :SUBSCRIPTION]) end end expect_token :RCURLY end SchemaExtension.new( subscription: subscription, mutation: mutation, query: query, directives: directives, pos: loc, filename: @filename, source: self, ) else expect_one_of([:SCHEMA, :SCALAR, :TYPE, :ENUM, :INPUT, :UNION, :INTERFACE]) end else loc = pos desc = at?(:STRING) ? string_value : nil defn_loc = pos case token_name when :SCHEMA advance_token directives = parse_directives query = mutation = subscription = nil expect_token :LCURLY while !at?(:RCURLY) if at?(:QUERY) advance_token expect_token(:COLON) query = parse_name elsif at?(:MUTATION) advance_token expect_token(:COLON) mutation = parse_name elsif at?(:SUBSCRIPTION) advance_token expect_token(:COLON) subscription = parse_name else expect_one_of([:QUERY, :MUTATION, :SUBSCRIPTION]) end end expect_token :RCURLY SchemaDefinition.new(pos: loc, definition_pos: defn_loc, query: query, mutation: mutation, subscription: subscription, directives: directives, filename: @filename, source: self) when :DIRECTIVE advance_token expect_token :DIR_SIGN name = parse_name arguments_definition = parse_argument_definitions repeatable = if at?(:REPEATABLE) advance_token true else false end expect_token :ON directive_locations = [DirectiveLocation.new(pos: pos, name: parse_name, filename: @filename, source: self)] while at?(:PIPE) advance_token directive_locations << DirectiveLocation.new(pos: pos, name: parse_name, filename: @filename, source: self) end DirectiveDefinition.new(pos: loc, definition_pos: defn_loc, description: desc, name: name, arguments: arguments_definition, locations: directive_locations, repeatable: repeatable, filename: @filename, source: self) when :TYPE advance_token name = parse_name implements_interfaces = parse_implements directives = parse_directives field_defns = at?(:LCURLY) ? parse_field_definitions : EMPTY_ARRAY ObjectTypeDefinition.new(pos: loc, definition_pos: defn_loc, description: desc, name: name, interfaces: implements_interfaces, directives: directives, fields: field_defns, filename: @filename, source: self) when :INTERFACE advance_token name = parse_name interfaces = parse_implements directives = parse_directives fields_definition = parse_field_definitions InterfaceTypeDefinition.new(pos: loc, definition_pos: defn_loc, description: desc, name: name, directives: directives, fields: fields_definition, interfaces: interfaces, filename: @filename, source: self) when :UNION advance_token name = parse_name directives = parse_directives union_member_types = parse_union_members UnionTypeDefinition.new(pos: loc, definition_pos: defn_loc, description: desc, name: name, directives: directives, types: union_member_types, filename: @filename, source: self) when :SCALAR advance_token name = parse_name directives = parse_directives ScalarTypeDefinition.new(pos: loc, definition_pos: defn_loc, description: desc, name: name, directives: directives, filename: @filename, source: self) when :ENUM advance_token name = parse_name directives = parse_directives enum_values_definition = parse_enum_value_definitions Nodes::EnumTypeDefinition.new(pos: loc, definition_pos: defn_loc, description: desc, name: name, directives: directives, values: enum_values_definition, filename: @filename, source: self) when :INPUT advance_token name = parse_name directives = parse_directives input_fields_definition = parse_input_object_field_definitions InputObjectTypeDefinition.new(pos: loc, definition_pos: defn_loc, description: desc, name: name, directives: directives, fields: input_fields_definition, filename: @filename, source: self) else expect_one_of([:SCHEMA, :SCALAR, :TYPE, :ENUM, :INPUT, :UNION, :INTERFACE]) end end end def parse_input_object_field_definitions if at?(:LCURLY) expect_token :LCURLY list = [] while !at?(:RCURLY) list << parse_input_value_definition end expect_token :RCURLY list else EMPTY_ARRAY end end def parse_enum_value_definitions if at?(:LCURLY) expect_token :LCURLY list = [] while !at?(:RCURLY) v_loc = pos description = if at?(:STRING); string_value; end defn_loc = pos # Any identifier, but not true, false, or null enum_value = if at?(:TRUE) || at?(:FALSE) || at?(:NULL) expect_token(:IDENTIFIER) else parse_name end v_directives = parse_directives list << EnumValueDefinition.new(pos: v_loc, definition_pos: defn_loc, description: description, name: enum_value, directives: v_directives, filename: @filename, source: self) end expect_token :RCURLY list else EMPTY_ARRAY end end def parse_union_members if at?(:EQUALS) expect_token :EQUALS if at?(:PIPE) advance_token end list = [parse_type_name] while at?(:PIPE) advance_token list << parse_type_name end list else EMPTY_ARRAY end end def parse_implements if at?(:IMPLEMENTS) advance_token list = [] while true advance_token if at?(:AMP) break unless at?(:IDENTIFIER) list << parse_type_name end list else EMPTY_ARRAY end end def parse_field_definitions expect_token :LCURLY list = [] while !at?(:RCURLY) loc = pos description = if at?(:STRING); string_value; end defn_loc = pos name = parse_name arguments_definition = parse_argument_definitions expect_token :COLON type = self.type directives = parse_directives list << FieldDefinition.new(pos: loc, definition_pos: defn_loc, description: description, name: name, arguments: arguments_definition, type: type, directives: directives, filename: @filename, source: self) end expect_token :RCURLY list end def parse_argument_definitions if at?(:LPAREN) advance_token list = [] while !at?(:RPAREN) list << parse_input_value_definition end expect_token :RPAREN list else EMPTY_ARRAY end end def parse_input_value_definition loc = pos description = if at?(:STRING); string_value; end defn_loc = pos name = parse_name expect_token :COLON type = self.type default_value = if at?(:EQUALS) advance_token value else nil end directives = parse_directives InputValueDefinition.new(pos: loc, definition_pos: defn_loc, description: description, name: name, type: type, default_value: default_value, directives: directives, filename: @filename, source: self) end def type parsed_type = case token_name when :IDENTIFIER parse_type_name when :LBRACKET list_type else nil end if at?(:BANG) && parsed_type parsed_type = Nodes::NonNullType.new(pos: pos, of_type: parsed_type, source: self) expect_token(:BANG) end parsed_type end def list_type loc = pos expect_token(:LBRACKET) inner_type = self.type parsed_list_type = if inner_type Nodes::ListType.new(pos: loc, of_type: inner_type, source: self) else nil end expect_token(:RBRACKET) parsed_list_type end def parse_operation_type val = if at?(:QUERY) "query" elsif at?(:MUTATION) "mutation" elsif at?(:SUBSCRIPTION) "subscription" else expect_one_of([:QUERY, :MUTATION, :SUBSCRIPTION]) end advance_token val end def selection_set expect_token(:LCURLY) selections = [] while @token_name != :RCURLY selections << if at?(:ELLIPSIS) loc = pos advance_token case token_name when :ON, :DIR_SIGN, :LCURLY if_type = if at?(:ON) advance_token parse_type_name else nil end directives = parse_directives Nodes::InlineFragment.new(pos: loc, type: if_type, directives: directives, selections: selection_set, filename: @filename, source: self) else name = parse_name_without_on directives = parse_directives # Can this ever happen? # expect_token(:IDENTIFIER) if at?(:ON) FragmentSpread.new(pos: loc, name: name, directives: directives, filename: @filename, source: self) end else loc = pos name = parse_name field_alias = nil if at?(:COLON) advance_token field_alias = name name = parse_name end arguments = at?(:LPAREN) ? parse_arguments : nil directives = at?(:DIR_SIGN) ? parse_directives : nil selection_set = at?(:LCURLY) ? self.selection_set : nil Nodes::Field.new(pos: loc, field_alias: field_alias, name: name, arguments: arguments, directives: directives, selections: selection_set, filename: @filename, source: self) end end expect_token(:RCURLY) selections end def parse_name case token_name when :IDENTIFIER expect_token_value(:IDENTIFIER) when :SCHEMA advance_token "schema" when :SCALAR advance_token "scalar" when :IMPLEMENTS advance_token "implements" when :INTERFACE advance_token "interface" when :UNION advance_token "union" when :ENUM advance_token "enum" when :INPUT advance_token "input" when :DIRECTIVE advance_token "directive" when :TYPE advance_token "type" when :QUERY advance_token "query" when :MUTATION advance_token "mutation" when :SUBSCRIPTION advance_token "subscription" when :TRUE advance_token "true" when :FALSE advance_token "false" when :FRAGMENT advance_token "fragment" when :REPEATABLE advance_token "repeatable" when :NULL advance_token "null" when :ON advance_token "on" when :EXTEND advance_token "extend" else expect_token(:NAME) end end def parse_name_without_on if at?(:ON) expect_token(:IDENTIFIER) else parse_name end end def parse_type_name TypeName.new(pos: pos, name: parse_name, filename: @filename, source: self) end def parse_directives if at?(:DIR_SIGN) dirs = [] while at?(:DIR_SIGN) loc = pos advance_token name = parse_name arguments = parse_arguments dirs << Nodes::Directive.new(pos: loc, name: name, arguments: arguments, filename: @filename, source: self) end dirs else EMPTY_ARRAY end end def parse_arguments if at?(:LPAREN) advance_token args = [] while !at?(:RPAREN) loc = pos name = parse_name expect_token(:COLON) args << Nodes::Argument.new(pos: loc, name: name, value: value, filename: @filename, source: self) end if args.empty? expect_token(:ARGUMENT_NAME) # At least one argument is required end expect_token(:RPAREN) args else EMPTY_ARRAY end end def string_value token_value = @lexer.string_value expect_token :STRING token_value end def value case token_name when :INT expect_token_value(:INT).to_i when :FLOAT expect_token_value(:FLOAT).to_f when :STRING string_value when :TRUE advance_token true when :FALSE advance_token false when :NULL advance_token NullValue.new(pos: pos, name: "null", filename: @filename, source: self) when :IDENTIFIER Nodes::Enum.new(pos: pos, name: expect_token_value(:IDENTIFIER), filename: @filename, source: self) when :LBRACKET advance_token list = [] while !at?(:RBRACKET) list << value end expect_token(:RBRACKET) list when :LCURLY start = pos advance_token args = [] while !at?(:RCURLY) loc = pos n = parse_name expect_token(:COLON) args << Argument.new(pos: loc, name: n, value: value, filename: @filename, source: self) end expect_token(:RCURLY) InputObject.new(pos: start, arguments: args, filename: @filename, source: self) when :VAR_SIGN loc = pos advance_token VariableIdentifier.new(pos: loc, name: parse_name, filename: @filename, source: self) when :SCHEMA advance_token Nodes::Enum.new(pos: pos, name: "schema", filename: @filename, source: self) when :SCALAR advance_token Nodes::Enum.new(pos: pos, name: "scalar", filename: @filename, source: self) when :IMPLEMENTS advance_token Nodes::Enum.new(pos: pos, name: "implements", filename: @filename, source: self) when :INTERFACE advance_token Nodes::Enum.new(pos: pos, name: "interface", filename: @filename, source: self) when :UNION advance_token Nodes::Enum.new(pos: pos, name: "union", filename: @filename, source: self) when :ENUM advance_token Nodes::Enum.new(pos: pos, name: "enum", filename: @filename, source: self) when :INPUT advance_token Nodes::Enum.new(pos: pos, name: "input", filename: @filename, source: self) when :DIRECTIVE advance_token Nodes::Enum.new(pos: pos, name: "directive", filename: @filename, source: self) when :TYPE advance_token Nodes::Enum.new(pos: pos, name: "type", filename: @filename, source: self) when :QUERY advance_token Nodes::Enum.new(pos: pos, name: "query", filename: @filename, source: self) when :MUTATION advance_token Nodes::Enum.new(pos: pos, name: "mutation", filename: @filename, source: self) when :SUBSCRIPTION advance_token Nodes::Enum.new(pos: pos, name: "subscription", filename: @filename, source: self) when :FRAGMENT advance_token Nodes::Enum.new(pos: pos, name: "fragment", filename: @filename, source: self) when :REPEATABLE advance_token Nodes::Enum.new(pos: pos, name: "repeatable", filename: @filename, source: self) when :ON advance_token Nodes::Enum.new(pos: pos, name: "on", filename: @filename, source: self) when :EXTEND advance_token Nodes::Enum.new(pos: pos, name: "extend", filename: @filename, source: self) else expect_token(:VALUE) end end def at?(expected_token_name) @token_name == expected_token_name end def expect_token(expected_token_name) unless @token_name == expected_token_name raise_parse_error("Expected #{expected_token_name}, actual: #{token_name || "(none)"} (#{debug_token_value.inspect})") end advance_token end def expect_one_of(token_names) raise_parse_error("Expected one of #{token_names.join(", ")}, actual: #{token_name || "NOTHING"} (#{debug_token_value.inspect})") end def raise_parse_error(message) message += " at [#{@lexer.line_number}, #{@lexer.column_number}]" raise GraphQL::ParseError.new( message, @lexer.line_number, @lexer.column_number, @graphql_str, filename: @filename, ) end # Only use when we care about the expected token's value def expect_token_value(tok) token_value = @lexer.token_value if @dedup_identifiers token_value = -token_value end expect_token(tok) token_value end # token_value works for when the scanner matched something # which is usually fine and it's good for it to be fast at that. def debug_token_value @lexer.debug_token_value(token_name) end class SchemaParser < Parser def initialize(*args, **kwargs) super @dedup_identifiers = true end end end end end graphql-ruby-2.5.19/lib/graphql/language/printer.rb000066400000000000000000000442111514115062600222270ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Language class Printer OMISSION = "... (truncated)" class TruncatableBuffer class TruncateSizeReached < StandardError; end DEFAULT_INIT_CAPACITY = 500 def initialize(truncate_size: nil) @out = String.new(capacity: truncate_size || DEFAULT_INIT_CAPACITY) @truncate_size = truncate_size end def append(other) if @truncate_size && (@out.size + other.size) > @truncate_size @out << other.slice(0, @truncate_size - @out.size) raise(TruncateSizeReached, "Truncate size reached") else @out << other end end def to_string @out end end # Turn an arbitrary AST node back into a string. # # @example Turning a document into a query string # document = GraphQL.parse(query_string) # GraphQL::Language::Printer.new.print(document) # # => "{ ... }" # # # @example Building a custom printer # # class MyPrinter < GraphQL::Language::Printer # def print_argument(arg) # print_string("#{arg.name}: ") # end # end # # MyPrinter.new.print(document) # # => "mutation { pay(creditCard: ) { success } }" # # @param node [Nodes::AbstractNode] # @param indent [String] Whitespace to add to the printed node # @param truncate_size [Integer, nil] The size to truncate to. # @return [String] Valid GraphQL for `node` def print(node, indent: "", truncate_size: nil) truncate_size = truncate_size ? [truncate_size - OMISSION.size, 0].max : nil @out = TruncatableBuffer.new(truncate_size: truncate_size) print_node(node, indent: indent) @out.to_string rescue TruncatableBuffer::TruncateSizeReached @out.to_string << OMISSION end protected def print_string(str) @out.append(str) end def print_document(document) document.definitions.each_with_index do |d, i| print_node(d) print_string("\n\n") if i < document.definitions.size - 1 end end def print_argument(argument) print_string(argument.name) print_string(": ") print_node(argument.value) end def print_input_object(input_object) print_string("{") input_object.arguments.each_with_index do |a, i| print_argument(a) print_string(", ") if i < input_object.arguments.size - 1 end print_string("}") end def print_directive(directive) print_string("@") print_string(directive.name) if !directive.arguments.empty? print_string("(") directive.arguments.each_with_index do |a, i| print_argument(a) print_string(", ") if i < directive.arguments.size - 1 end print_string(")") end end def print_enum(enum) print_string(enum.name) end def print_null_value print_string("null") end def print_field(field, indent: "") print_string(indent) if field.alias print_string(field.alias) print_string(": ") end print_string(field.name) if !field.arguments.empty? print_string("(") field.arguments.each_with_index do |a, i| print_argument(a) print_string(", ") if i < field.arguments.size - 1 end print_string(")") end print_directives(field.directives) print_selections(field.selections, indent: indent) end def print_fragment_definition(fragment_def, indent: "") print_string(indent) print_string("fragment") if fragment_def.name print_string(" ") print_string(fragment_def.name) end if fragment_def.type print_string(" on ") print_node(fragment_def.type) end print_directives(fragment_def.directives) print_selections(fragment_def.selections, indent: indent) end def print_fragment_spread(fragment_spread, indent: "") print_string(indent) print_string("...") print_string(fragment_spread.name) print_directives(fragment_spread.directives) end def print_inline_fragment(inline_fragment, indent: "") print_string(indent) print_string("...") if inline_fragment.type print_string(" on ") print_node(inline_fragment.type) end print_directives(inline_fragment.directives) print_selections(inline_fragment.selections, indent: indent) end def print_list_type(list_type) print_string("[") print_node(list_type.of_type) print_string("]") end def print_non_null_type(non_null_type) print_node(non_null_type.of_type) print_string("!") end def print_operation_definition(operation_definition, indent: "") print_string(indent) print_string(operation_definition.operation_type) if operation_definition.name print_string(" ") print_string(operation_definition.name) end if !operation_definition.variables.empty? print_string("(") operation_definition.variables.each_with_index do |v, i| print_variable_definition(v) print_string(", ") if i < operation_definition.variables.size - 1 end print_string(")") end print_directives(operation_definition.directives) print_selections(operation_definition.selections, indent: indent) end def print_type_name(type_name) print_string(type_name.name) end def print_variable_definition(variable_definition) print_string("$") print_string(variable_definition.name) print_string(": ") print_node(variable_definition.type) unless variable_definition.default_value.nil? print_string(" = ") print_node(variable_definition.default_value) end variable_definition.directives.each do |dir| print_string(" ") print_directive(dir) end end def print_variable_identifier(variable_identifier) print_string("$") print_string(variable_identifier.name) end def print_schema_definition(schema, extension: false) has_conventional_names = (schema.query.nil? || schema.query == 'Query') && (schema.mutation.nil? || schema.mutation == 'Mutation') && (schema.subscription.nil? || schema.subscription == 'Subscription') if has_conventional_names && schema.directives.empty? return end extension ? print_string("extend schema") : print_string("schema") if !schema.directives.empty? schema.directives.each do |dir| print_string("\n ") print_node(dir) end if !has_conventional_names print_string("\n") end end if !has_conventional_names if schema.directives.empty? print_string(" ") end print_string("{\n") print_string(" query: #{schema.query}\n") if schema.query print_string(" mutation: #{schema.mutation}\n") if schema.mutation print_string(" subscription: #{schema.subscription}\n") if schema.subscription print_string("}") end end def print_scalar_type_definition(scalar_type, extension: false) extension ? print_string("extend ") : print_description_and_comment(scalar_type) print_string("scalar ") print_string(scalar_type.name) print_directives(scalar_type.directives) end def print_object_type_definition(object_type, extension: false) extension ? print_string("extend ") : print_description_and_comment(object_type) print_string("type ") print_string(object_type.name) print_implements(object_type) unless object_type.interfaces.empty? print_directives(object_type.directives) print_field_definitions(object_type.fields) end def print_implements(type) print_string(" implements ") i = 0 type.interfaces.each do |int| if i > 0 print_string(" & ") end print_string(int.name) i += 1 end end def print_input_value_definition(input_value) print_string(input_value.name) print_string(": ") print_node(input_value.type) unless input_value.default_value.nil? print_string(" = ") print_node(input_value.default_value) end print_directives(input_value.directives) end def print_arguments(arguments, indent: "") if arguments.all? { |arg| !arg.description && !arg.comment } print_string("(") arguments.each_with_index do |arg, i| print_input_value_definition(arg) print_string(", ") if i < arguments.size - 1 end print_string(")") return end print_string("(\n") arguments.each_with_index do |arg, i| print_comment(arg, indent: " " + indent, first_in_block: i == 0) print_description(arg, indent: " " + indent, first_in_block: i == 0) print_string(" ") print_string(indent) print_input_value_definition(arg) print_string("\n") if i < arguments.size - 1 end print_string("\n") print_string(indent) print_string(")") end def print_field_definition(field) print_string(field.name) unless field.arguments.empty? print_arguments(field.arguments, indent: " ") end print_string(": ") print_node(field.type) print_directives(field.directives) end def print_interface_type_definition(interface_type, extension: false) extension ? print_string("extend ") : print_description_and_comment(interface_type) print_string("interface ") print_string(interface_type.name) print_implements(interface_type) if !interface_type.interfaces.empty? print_directives(interface_type.directives) print_field_definitions(interface_type.fields) end def print_union_type_definition(union_type, extension: false) extension ? print_string("extend ") : print_description_and_comment(union_type) print_string("union ") print_string(union_type.name) print_directives(union_type.directives) if !union_type.types.empty? print_string(" = ") i = 0 union_type.types.each do |t| if i > 0 print_string(" | ") end print_string(t.name) i += 1 end end end def print_enum_type_definition(enum_type, extension: false) extension ? print_string("extend ") : print_description_and_comment(enum_type) print_string("enum ") print_string(enum_type.name) print_directives(enum_type.directives) if !enum_type.values.empty? print_string(" {\n") enum_type.values.each.with_index do |value, i| print_description(value, indent: " ", first_in_block: i == 0) print_comment(value, indent: " ", first_in_block: i == 0) print_enum_value_definition(value) end print_string("}") end end def print_enum_value_definition(enum_value) print_string(" ") print_string(enum_value.name) print_directives(enum_value.directives) print_string("\n") end def print_input_object_type_definition(input_object_type, extension: false) extension ? print_string("extend ") : print_description_and_comment(input_object_type) print_string("input ") print_string(input_object_type.name) print_directives(input_object_type.directives) if !input_object_type.fields.empty? print_string(" {\n") input_object_type.fields.each.with_index do |field, i| print_description(field, indent: " ", first_in_block: i == 0) print_comment(field, indent: " ", first_in_block: i == 0) print_string(" ") print_input_value_definition(field) print_string("\n") end print_string("}") end end def print_directive_definition(directive) print_description(directive) print_string("directive @") print_string(directive.name) if !directive.arguments.empty? print_arguments(directive.arguments) end if directive.repeatable print_string(" repeatable") end print_string(" on ") i = 0 directive.locations.each do |loc| if i > 0 print_string(" | ") end print_string(loc.name) i += 1 end end def print_description(node, indent: "", first_in_block: true) return unless node.description print_string("\n") if indent != "" && !first_in_block print_string(GraphQL::Language::BlockString.print(node.description, indent: indent)) end def print_comment(node, indent: "", first_in_block: true) return unless node.comment print_string("\n") if indent != "" && !first_in_block print_string(GraphQL::Language::Comment.print(node.comment, indent: indent)) end def print_description_and_comment(node) print_description(node) print_comment(node) end def print_field_definitions(fields) return if fields.empty? print_string(" {\n") i = 0 fields.each do |field| print_description(field, indent: " ", first_in_block: i == 0) print_comment(field, indent: " ", first_in_block: i == 0) print_string(" ") print_field_definition(field) print_string("\n") i += 1 end print_string("}") end def print_directives(directives) return if directives.empty? directives.each do |d| print_string(" ") print_directive(d) end end def print_selections(selections, indent: "") return if selections.empty? print_string(" {\n") selections.each do |selection| print_node(selection, indent: indent + " ") print_string("\n") end print_string(indent) print_string("}") end def print_node(node, indent: "") case node when Nodes::Document print_document(node) when Nodes::Argument print_argument(node) when Nodes::Directive print_directive(node) when Nodes::Enum print_enum(node) when Nodes::NullValue print_null_value when Nodes::Field print_field(node, indent: indent) when Nodes::FragmentDefinition print_fragment_definition(node, indent: indent) when Nodes::FragmentSpread print_fragment_spread(node, indent: indent) when Nodes::InlineFragment print_inline_fragment(node, indent: indent) when Nodes::InputObject print_input_object(node) when Nodes::ListType print_list_type(node) when Nodes::NonNullType print_non_null_type(node) when Nodes::OperationDefinition print_operation_definition(node, indent: indent) when Nodes::TypeName print_type_name(node) when Nodes::VariableDefinition print_variable_definition(node) when Nodes::VariableIdentifier print_variable_identifier(node) when Nodes::SchemaDefinition print_schema_definition(node) when Nodes::SchemaExtension print_schema_definition(node, extension: true) when Nodes::ScalarTypeDefinition print_scalar_type_definition(node) when Nodes::ScalarTypeExtension print_scalar_type_definition(node, extension: true) when Nodes::ObjectTypeDefinition print_object_type_definition(node) when Nodes::ObjectTypeExtension print_object_type_definition(node, extension: true) when Nodes::InputValueDefinition print_input_value_definition(node) when Nodes::FieldDefinition print_field_definition(node) when Nodes::InterfaceTypeDefinition print_interface_type_definition(node) when Nodes::InterfaceTypeExtension print_interface_type_definition(node, extension: true) when Nodes::UnionTypeDefinition print_union_type_definition(node) when Nodes::UnionTypeExtension print_union_type_definition(node, extension: true) when Nodes::EnumTypeDefinition print_enum_type_definition(node) when Nodes::EnumTypeExtension print_enum_type_definition(node, extension: true) when Nodes::EnumValueDefinition print_enum_value_definition(node) when Nodes::InputObjectTypeDefinition print_input_object_type_definition(node) when Nodes::InputObjectTypeExtension print_input_object_type_definition(node, extension: true) when Nodes::DirectiveDefinition print_directive_definition(node) when FalseClass, Float, Integer, NilClass, String, TrueClass, Symbol print_string(GraphQL::Language.serialize(node)) when Array print_string("[") node.each_with_index do |v, i| print_node(v) print_string(", ") if i < node.length - 1 end print_string("]") when Hash print_string("{") node.each_with_index do |(k, v), i| print_string(k) print_string(": ") print_node(v) print_string(", ") if i < node.length - 1 end print_string("}") else print_string(GraphQL::Language.serialize(node.to_s)) end end end end end graphql-ruby-2.5.19/lib/graphql/language/sanitized_printer.rb000066400000000000000000000155511514115062600243060ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Language # A custom printer used to print sanitized queries. It inlines provided variables # within the query for facilitate logging and analysis of queries. # # The printer returns `nil` if the query is invalid. # # Since the GraphQL Ruby AST for a GraphQL query doesnt contain any reference # on the type of fields or arguments, we have to track the current object, field # and input type while printing the query. # # @example Printing a scrubbed string # printer = QueryPrinter.new(query) # puts printer.sanitized_query_string # # @see {Query#sanitized_query_string} class SanitizedPrinter < GraphQL::Language::Printer REDACTED = "\"\"" def initialize(query, inline_variables: true) @query = query @current_type = nil @current_field = nil @current_input_type = nil @inline_variables = inline_variables end # @return [String, nil] A scrubbed query string, if the query was valid. def sanitized_query_string if query.valid? print(query.document) else nil end end def print_node(node, indent: "") case node when FalseClass, Float, Integer, String, TrueClass if @current_argument && redact_argument_value?(@current_argument, node) print_string(redacted_argument_value(@current_argument)) else super end when Array old_input_type = @current_input_type if @current_input_type && @current_input_type.list? @current_input_type = @current_input_type.of_type @current_input_type = @current_input_type.of_type if @current_input_type.non_null? end super @current_input_type = old_input_type else super end end # Indicates whether or not to redact non-null values for the given argument. Defaults to redacting all strings # arguments but this can be customized by subclasses. def redact_argument_value?(argument, value) # Default to redacting any strings or custom scalars encoded as strings type = argument.type.unwrap value.is_a?(String) && type.kind.scalar? && (type.graphql_name == "String" || !type.default_scalar?) end # Returns the value to use for redacted versions of the given argument. Defaults to the # string "". def redacted_argument_value(argument) REDACTED end def print_argument(argument) # We won't have type information if we're recursing into a custom scalar return super if @current_input_type && @current_input_type.kind.scalar? arg_owner = @current_input_type || @current_directive || @current_field old_current_argument = @current_argument @current_argument = arg_owner.get_argument(argument.name, @query.context) old_input_type = @current_input_type @current_input_type = @current_argument.type.non_null? ? @current_argument.type.of_type : @current_argument.type argument_value = if coerce_argument_value_to_list?(@current_input_type, argument.value) [argument.value] else argument.value end print_string("#{argument.name}: ") print_node(argument_value) @current_input_type = old_input_type @current_argument = old_current_argument end def coerce_argument_value_to_list?(type, value) type.list? && !value.is_a?(Array) && !value.nil? && !value.is_a?(GraphQL::Language::Nodes::VariableIdentifier) end def print_variable_identifier(variable_id) if @inline_variables variable_value = query.variables[variable_id.name] print_node(value_to_ast(variable_value, @current_input_type)) else super end end def print_field(field, indent: "") @current_field = query.types.field(@current_type, field.name) old_type = @current_type @current_type = @current_field.type.unwrap super @current_type = old_type end def print_inline_fragment(inline_fragment, indent: "") old_type = @current_type if inline_fragment.type @current_type = query.get_type(inline_fragment.type.name) end super @current_type = old_type end def print_fragment_definition(fragment_def, indent: "") old_type = @current_type @current_type = query.get_type(fragment_def.type.name) super @current_type = old_type end def print_directive(directive) @current_directive = query.schema.directives[directive.name] super @current_directive = nil end # Print the operation definition but do not include the variable # definitions since we will inline them within the query def print_operation_definition(operation_definition, indent: "") old_type = @current_type @current_type = query.schema.public_send(operation_definition.operation_type) if @inline_variables print_string("#{indent}#{operation_definition.operation_type}") print_string(" #{operation_definition.name}") if operation_definition.name print_directives(operation_definition.directives) print_selections(operation_definition.selections, indent: indent) else super end @current_type = old_type end private def value_to_ast(value, type) type = type.of_type if type.non_null? if value.nil? return GraphQL::Language::Nodes::NullValue.new(name: "null") end case type.kind.name when "INPUT_OBJECT" value = if value.respond_to?(:to_unsafe_h) # for ActionController::Parameters value.to_unsafe_h else value.to_h end arguments = value.map do |key, val| sub_type = type.get_argument(key.to_s, @query.context).type GraphQL::Language::Nodes::Argument.new( name: key.to_s, value: value_to_ast(val, sub_type) ) end GraphQL::Language::Nodes::InputObject.new( arguments: arguments ) when "LIST" if value.is_a?(Array) value.map { |v| value_to_ast(v, type.of_type) } else [value].map { |v| value_to_ast(v, type.of_type) } end when "ENUM" if value.is_a?(GraphQL::Language::Nodes::Enum) # if it was a default value, it's already wrapped value else GraphQL::Language::Nodes::Enum.new(name: value) end else value end end attr_reader :query end end end graphql-ruby-2.5.19/lib/graphql/language/static_visitor.rb000066400000000000000000000140571514115062600236170ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Language # Like `GraphQL::Language::Visitor` except it doesn't support # making changes to the document -- only visiting it as-is. class StaticVisitor def initialize(document) @document = document end # Visit `document` and all children # @return [void] def visit # `@document` may be any kind of node: visit_method = @document.visit_method result = public_send(visit_method, @document, nil) @result = if result.is_a?(Array) result.first else # The node wasn't modified @document end end def on_document_children(document_node) document_node.children.each do |child_node| visit_method = child_node.visit_method public_send(visit_method, child_node, document_node) end end def on_field_children(new_node) new_node.arguments.each do |arg_node| # rubocop:disable Development/ContextIsPassedCop on_argument(arg_node, new_node) end visit_directives(new_node) visit_selections(new_node) end def visit_directives(new_node) new_node.directives.each do |dir_node| on_directive(dir_node, new_node) end end def visit_selections(new_node) new_node.selections.each do |selection| case selection when GraphQL::Language::Nodes::Field on_field(selection, new_node) when GraphQL::Language::Nodes::InlineFragment on_inline_fragment(selection, new_node) when GraphQL::Language::Nodes::FragmentSpread on_fragment_spread(selection, new_node) else raise ArgumentError, "Invariant: unexpected field selection #{selection.class} (#{selection.inspect})" end end end def on_fragment_definition_children(new_node) visit_directives(new_node) visit_selections(new_node) end alias :on_inline_fragment_children :on_fragment_definition_children def on_operation_definition_children(new_node) new_node.variables.each do |arg_node| on_variable_definition(arg_node, new_node) end visit_directives(new_node) visit_selections(new_node) end def on_argument_children(new_node) new_node.children.each do |value_node| case value_node when Language::Nodes::VariableIdentifier on_variable_identifier(value_node, new_node) when Language::Nodes::InputObject on_input_object(value_node, new_node) when Language::Nodes::Enum on_enum(value_node, new_node) when Language::Nodes::NullValue on_null_value(value_node, new_node) else raise ArgumentError, "Invariant: unexpected argument value node #{value_node.class} (#{value_node.inspect})" end end end # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time # We don't use `alias` here because it breaks `super` def self.make_visit_methods(ast_node_class) node_method = ast_node_class.visit_method children_of_type = ast_node_class.children_of_type child_visit_method = :"#{node_method}_children" class_eval(<<-RUBY, __FILE__, __LINE__ + 1) # The default implementation for visiting an AST node. # It doesn't _do_ anything, but it continues to visiting the node's children. # To customize this hook, override one of its make_visit_methods (or the base method?) # in your subclasses. # # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node. # @return [void] def #{node_method}(node, parent) #{ if method_defined?(child_visit_method) "#{child_visit_method}(node)" elsif children_of_type children_of_type.map do |child_accessor, child_class| "node.#{child_accessor}.each do |child_node| #{child_class.visit_method}(child_node, node) end" end.join("\n") else "" end } end RUBY end [ Language::Nodes::Argument, Language::Nodes::Directive, Language::Nodes::DirectiveDefinition, Language::Nodes::DirectiveLocation, Language::Nodes::Document, Language::Nodes::Enum, Language::Nodes::EnumTypeDefinition, Language::Nodes::EnumTypeExtension, Language::Nodes::EnumValueDefinition, Language::Nodes::Field, Language::Nodes::FieldDefinition, Language::Nodes::FragmentDefinition, Language::Nodes::FragmentSpread, Language::Nodes::InlineFragment, Language::Nodes::InputObject, Language::Nodes::InputObjectTypeDefinition, Language::Nodes::InputObjectTypeExtension, Language::Nodes::InputValueDefinition, Language::Nodes::InterfaceTypeDefinition, Language::Nodes::InterfaceTypeExtension, Language::Nodes::ListType, Language::Nodes::NonNullType, Language::Nodes::NullValue, Language::Nodes::ObjectTypeDefinition, Language::Nodes::ObjectTypeExtension, Language::Nodes::OperationDefinition, Language::Nodes::ScalarTypeDefinition, Language::Nodes::ScalarTypeExtension, Language::Nodes::SchemaDefinition, Language::Nodes::SchemaExtension, Language::Nodes::TypeName, Language::Nodes::UnionTypeDefinition, Language::Nodes::UnionTypeExtension, Language::Nodes::VariableDefinition, Language::Nodes::VariableIdentifier, ].each do |ast_node_class| make_visit_methods(ast_node_class) end # rubocop:disable Development/NoEvalCop end end end graphql-ruby-2.5.19/lib/graphql/language/visitor.rb000066400000000000000000000261061514115062600222460ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Language # Depth-first traversal through the tree, calling hooks at each stop. # # @example Create a visitor counting certain field names # class NameCounter < GraphQL::Language::Visitor # def initialize(document, field_name) # super(document) # @field_name = field_name # @count = 0 # end # # attr_reader :count # # def on_field(node, parent) # # if this field matches our search, increment the counter # if node.name == @field_name # @count += 1 # end # # Continue visiting subfields: # super # end # end # # # Initialize a visitor # visitor = NameCounter.new(document, "name") # # Run it # visitor.visit # # Check the result # visitor.count # # => 3 # # @see GraphQL::Language::StaticVisitor for a faster visitor that doesn't support modifying the document class Visitor class DeleteNode; end # When this is returned from a visitor method, # Then the `node` passed into the method is removed from `parent`'s children. DELETE_NODE = DeleteNode.new def initialize(document) @document = document @result = nil end # @return [GraphQL::Language::Nodes::Document] The document with any modifications applied attr_reader :result # Visit `document` and all children # @return [void] def visit # `@document` may be any kind of node: visit_method = :"#{@document.visit_method}_with_modifications" result = public_send(visit_method, @document, nil) @result = if result.is_a?(Array) result.first else # The node wasn't modified @document end end def on_document_children(document_node) new_node = document_node document_node.children.each do |child_node| visit_method = :"#{child_node.visit_method}_with_modifications" new_child_and_node = public_send(visit_method, child_node, new_node) # Reassign `node` in case the child hook makes a modification if new_child_and_node.is_a?(Array) new_node = new_child_and_node[1] end end new_node end def on_field_children(new_node) new_node.arguments.each do |arg_node| # rubocop:disable Development/ContextIsPassedCop new_child_and_node = on_argument_with_modifications(arg_node, new_node) # Reassign `node` in case the child hook makes a modification if new_child_and_node.is_a?(Array) new_node = new_child_and_node[1] end end new_node = visit_directives(new_node) new_node = visit_selections(new_node) new_node end def visit_directives(new_node) new_node.directives.each do |dir_node| new_child_and_node = on_directive_with_modifications(dir_node, new_node) # Reassign `node` in case the child hook makes a modification if new_child_and_node.is_a?(Array) new_node = new_child_and_node[1] end end new_node end def visit_selections(new_node) new_node.selections.each do |selection| new_child_and_node = case selection when GraphQL::Language::Nodes::Field on_field_with_modifications(selection, new_node) when GraphQL::Language::Nodes::InlineFragment on_inline_fragment_with_modifications(selection, new_node) when GraphQL::Language::Nodes::FragmentSpread on_fragment_spread_with_modifications(selection, new_node) else raise ArgumentError, "Invariant: unexpected field selection #{selection.class} (#{selection.inspect})" end # Reassign `node` in case the child hook makes a modification if new_child_and_node.is_a?(Array) new_node = new_child_and_node[1] end end new_node end def on_fragment_definition_children(new_node) new_node = visit_directives(new_node) new_node = visit_selections(new_node) new_node end alias :on_inline_fragment_children :on_fragment_definition_children def on_operation_definition_children(new_node) new_node.variables.each do |arg_node| new_child_and_node = on_variable_definition_with_modifications(arg_node, new_node) # Reassign `node` in case the child hook makes a modification if new_child_and_node.is_a?(Array) new_node = new_child_and_node[1] end end new_node = visit_directives(new_node) new_node = visit_selections(new_node) new_node end def on_argument_children(new_node) new_node.children.each do |value_node| new_child_and_node = case value_node when Language::Nodes::VariableIdentifier on_variable_identifier_with_modifications(value_node, new_node) when Language::Nodes::InputObject on_input_object_with_modifications(value_node, new_node) when Language::Nodes::Enum on_enum_with_modifications(value_node, new_node) when Language::Nodes::NullValue on_null_value_with_modifications(value_node, new_node) else raise ArgumentError, "Invariant: unexpected argument value node #{value_node.class} (#{value_node.inspect})" end # Reassign `node` in case the child hook makes a modification if new_child_and_node.is_a?(Array) new_node = new_child_and_node[1] end end new_node end # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time # We don't use `alias` here because it breaks `super` def self.make_visit_methods(ast_node_class) node_method = ast_node_class.visit_method children_of_type = ast_node_class.children_of_type child_visit_method = :"#{node_method}_children" class_eval(<<-RUBY, __FILE__, __LINE__ + 1) # The default implementation for visiting an AST node. # It doesn't _do_ anything, but it continues to visiting the node's children. # To customize this hook, override one of its make_visit_methods (or the base method?) # in your subclasses. # # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node. # @return [Array, nil] If there were modifications, it returns an array of new nodes, otherwise, it returns `nil`. def #{node_method}(node, parent) if node.equal?(DELETE_NODE) # This might be passed to `super(DELETE_NODE, ...)` # by a user hook, don't want to keep visiting in that case. [node, parent] else new_node = node #{ if method_defined?(child_visit_method) "new_node = #{child_visit_method}(new_node)" elsif children_of_type children_of_type.map do |child_accessor, child_class| "node.#{child_accessor}.each do |child_node| new_child_and_node = #{child_class.visit_method}_with_modifications(child_node, new_node) # Reassign `node` in case the child hook makes a modification if new_child_and_node.is_a?(Array) new_node = new_child_and_node[1] end end" end.join("\n") else "" end } if new_node.equal?(node) [node, parent] else [new_node, parent] end end end def #{node_method}_with_modifications(node, parent) new_node_and_new_parent = #{node_method}(node, parent) apply_modifications(node, parent, new_node_and_new_parent) end RUBY end [ Language::Nodes::Argument, Language::Nodes::Directive, Language::Nodes::DirectiveDefinition, Language::Nodes::DirectiveLocation, Language::Nodes::Document, Language::Nodes::Enum, Language::Nodes::EnumTypeDefinition, Language::Nodes::EnumTypeExtension, Language::Nodes::EnumValueDefinition, Language::Nodes::Field, Language::Nodes::FieldDefinition, Language::Nodes::FragmentDefinition, Language::Nodes::FragmentSpread, Language::Nodes::InlineFragment, Language::Nodes::InputObject, Language::Nodes::InputObjectTypeDefinition, Language::Nodes::InputObjectTypeExtension, Language::Nodes::InputValueDefinition, Language::Nodes::InterfaceTypeDefinition, Language::Nodes::InterfaceTypeExtension, Language::Nodes::ListType, Language::Nodes::NonNullType, Language::Nodes::NullValue, Language::Nodes::ObjectTypeDefinition, Language::Nodes::ObjectTypeExtension, Language::Nodes::OperationDefinition, Language::Nodes::ScalarTypeDefinition, Language::Nodes::ScalarTypeExtension, Language::Nodes::SchemaDefinition, Language::Nodes::SchemaExtension, Language::Nodes::TypeName, Language::Nodes::UnionTypeDefinition, Language::Nodes::UnionTypeExtension, Language::Nodes::VariableDefinition, Language::Nodes::VariableIdentifier, ].each do |ast_node_class| make_visit_methods(ast_node_class) end # rubocop:enable Development/NoEvalCop private def apply_modifications(node, parent, new_node_and_new_parent) if new_node_and_new_parent.is_a?(Array) new_node = new_node_and_new_parent[0] new_parent = new_node_and_new_parent[1] if new_node.is_a?(Nodes::AbstractNode) && !node.equal?(new_node) # The user-provided hook returned a new node. new_parent = new_parent && new_parent.replace_child(node, new_node) return new_node, new_parent elsif new_node.equal?(DELETE_NODE) # The user-provided hook requested to remove this node new_parent = new_parent && new_parent.delete_child(node) return nil, new_parent elsif new_node_and_new_parent.none? { |n| n == nil || n.class < Nodes::AbstractNode } # The user-provided hook returned an array of who-knows-what # return nil here to signify that no changes should be made nil else new_node_and_new_parent end else # The user-provided hook didn't make any modifications. # In fact, the hook might have returned who-knows-what, so # ignore the return value and use the original values. new_node_and_new_parent end end end end end graphql-ruby-2.5.19/lib/graphql/load_application_object_failed_error.rb000066400000000000000000000017521514115062600263310ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL # Raised when a argument is configured with `loads:` and the client provides an `ID`, # but no object is loaded for that ID. # # @see GraphQL::Schema::Member::HasArguments::ArgumentObjectLoader#load_application_object_failed, A hook which you can override in resolvers, mutations and input objects. class LoadApplicationObjectFailedError < GraphQL::ExecutionError # @return [GraphQL::Schema::Argument] the argument definition for the argument that was looked up attr_reader :argument # @return [String] The ID provided by the client attr_reader :id # @return [Object] The value found with this ID attr_reader :object # @return [GraphQL::Query::Context] attr_reader :context def initialize(argument:, id:, object:, context:) @id = id @argument = argument @object = object @context = context super("No object found for `#{argument.graphql_name}: #{id.inspect}`") end end end graphql-ruby-2.5.19/lib/graphql/name_validator.rb000066400000000000000000000004771514115062600217540ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class NameValidator VALID_NAME_REGEX = /^[_a-zA-Z][_a-zA-Z0-9]*$/ def self.validate!(name) name = name.is_a?(String) ? name : name.to_s raise GraphQL::InvalidNameError.new(name, VALID_NAME_REGEX) unless name.match?(VALID_NAME_REGEX) end end end graphql-ruby-2.5.19/lib/graphql/pagination.rb000066400000000000000000000004441514115062600211120ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/pagination/array_connection" require "graphql/pagination/active_record_relation_connection" require "graphql/pagination/connections" require "graphql/pagination/mongoid_relation_connection" require "graphql/pagination/sequel_dataset_connection" graphql-ruby-2.5.19/lib/graphql/pagination/000077500000000000000000000000001514115062600205635ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/pagination/active_record_relation_connection.rb000066400000000000000000000034331514115062600300400ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/pagination/relation_connection" module GraphQL module Pagination # Customizes `RelationConnection` to work with `ActiveRecord::Relation`s. class ActiveRecordRelationConnection < Pagination::RelationConnection private def relation_count(relation) int_or_hash = if already_loaded?(relation) relation.size elsif relation.respond_to?(:unscope) relation.unscope(:order).count(:all) else # Rails 3 relation.count end if int_or_hash.is_a?(Integer) int_or_hash else # Grouped relations return count-by-group hashes int_or_hash.length end end def relation_limit(relation) if relation.is_a?(Array) nil else relation.limit_value end end def relation_offset(relation) if relation.is_a?(Array) nil else relation.offset_value end end def null_relation(relation) if relation.respond_to?(:none) relation.none else # Rails 3 relation.where("1=2") end end def set_limit(nodes, limit) if already_loaded?(nodes) nodes.take(limit) else super end end def set_offset(nodes, offset) if already_loaded?(nodes) # If the client sent a bogus cursor beyond the size of the relation, # it might get `nil` from `#[...]`, so return an empty array in that case nodes[offset..-1] || [] else super end end private def already_loaded?(relation) relation.is_a?(Array) || relation.loaded? end end end end graphql-ruby-2.5.19/lib/graphql/pagination/array_connection.rb000066400000000000000000000037531514115062600244550ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/pagination/connection" module GraphQL module Pagination class ArrayConnection < Pagination::Connection def nodes load_nodes @nodes end def has_previous_page load_nodes @has_previous_page end def has_next_page load_nodes @has_next_page end def cursor_for(item) idx = items.find_index(item) + 1 encode(idx.to_s) end private def index_from_cursor(cursor) decode(cursor).to_i end # Populate all the pagination info _once_, # It doesn't do anything on subsequent calls. def load_nodes @nodes ||= begin sliced_nodes = if before && after end_idx = index_from_cursor(before) - 2 end_idx < 0 ? [] : items[index_from_cursor(after)..end_idx] || [] elsif before end_idx = index_from_cursor(before) - 2 end_idx < 0 ? [] : items[0..end_idx] || [] elsif after items[index_from_cursor(after)..-1] || [] else items end @has_previous_page = if last # There are items preceding the ones in this result sliced_nodes.count > last elsif after # We've paginated into the Array a bit, there are some behind us index_from_cursor(after) > 0 else false end @has_next_page = if before # The original array is longer than the `before` index index_from_cursor(before) < items.length + 1 elsif first # There are more items after these items sliced_nodes.count > first else false end limited_nodes = sliced_nodes limited_nodes = limited_nodes.first(first) if first limited_nodes = limited_nodes.last(last) if last limited_nodes end end end end end graphql-ruby-2.5.19/lib/graphql/pagination/connection.rb000066400000000000000000000234221514115062600232520ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Pagination # A Connection wraps a list of items and provides cursor-based pagination over it. # # Connections were introduced by Facebook's `Relay` front-end framework, but # proved to be generally useful for GraphQL APIs. When in doubt, use connections # to serve lists (like Arrays, ActiveRecord::Relations) via GraphQL. # # Unlike the previous connection implementation, these default to bidirectional pagination. # # Pagination arguments and context may be provided at initialization or assigned later (see {Schema::Field::ConnectionExtension}). class Connection class PaginationImplementationMissingError < GraphQL::Error end # @return [Object] A list object, from the application. This is the unpaginated value passed into the connection. attr_reader :items # @return [GraphQL::Query::Context] attr_reader :context def context=(new_ctx) @context = new_ctx if @was_authorized_by_scope_items.nil? @was_authorized_by_scope_items = detect_was_authorized_by_scope_items end @context end # @return [Object] the object this collection belongs to attr_accessor :parent # Raw access to client-provided values. (`max_page_size` not applied to first or last.) attr_accessor :before_value, :after_value, :first_value, :last_value # @return [String, nil] the client-provided cursor. `""` is treated as `nil`. def before if defined?(@before) @before else @before = @before_value == "" ? nil : @before_value end end # @return [String, nil] the client-provided cursor. `""` is treated as `nil`. def after if defined?(@after) @after else @after = @after_value == "" ? nil : @after_value end end # @return [Hash Object>] The field arguments from the field that returned this connection attr_accessor :arguments # @param items [Object] some unpaginated collection item, like an `Array` or `ActiveRecord::Relation` # @param context [Query::Context] # @param parent [Object] The object this collection belongs to # @param first [Integer, nil] The limit parameter from the client, if it provided one # @param after [String, nil] A cursor for pagination, if the client provided one # @param last [Integer, nil] Limit parameter from the client, if provided # @param before [String, nil] A cursor for pagination, if the client provided one. # @param arguments [Hash] The arguments to the field that returned the collection wrapped by this connection # @param max_page_size [Integer, nil] A configured value to cap the result size. Applied as `first` if neither first or last are given and no `default_page_size` is set. # @param default_page_size [Integer, nil] A configured value to determine the result size when neither first or last are given. def initialize(items, parent: nil, field: nil, context: nil, first: nil, after: nil, max_page_size: NOT_CONFIGURED, default_page_size: NOT_CONFIGURED, last: nil, before: nil, edge_class: nil, arguments: nil) @items = items @parent = parent @context = context @field = field @first_value = first @after_value = after @last_value = last @before_value = before @arguments = arguments @edge_class = edge_class || self.class::Edge # This is only true if the object was _initialized_ with an override # or if one is assigned later. @has_max_page_size_override = max_page_size != NOT_CONFIGURED @max_page_size = if max_page_size == NOT_CONFIGURED nil else max_page_size end @has_default_page_size_override = default_page_size != NOT_CONFIGURED @default_page_size = if default_page_size == NOT_CONFIGURED nil else default_page_size end @was_authorized_by_scope_items = detect_was_authorized_by_scope_items end def was_authorized_by_scope_items? @was_authorized_by_scope_items end def max_page_size=(new_value) @has_max_page_size_override = true @max_page_size = new_value end def max_page_size if @has_max_page_size_override @max_page_size else context.schema.default_max_page_size end end def has_max_page_size_override? @has_max_page_size_override end def default_page_size=(new_value) @has_default_page_size_override = true @default_page_size = new_value end def default_page_size if @has_default_page_size_override @default_page_size else context.schema.default_page_size end end def has_default_page_size_override? @has_default_page_size_override end attr_writer :first # @return [Integer, nil] # A clamped `first` value. # (The underlying instance variable doesn't have limits on it.) # If neither `first` nor `last` is given, but `default_page_size` is # present, default_page_size is used for first. If `default_page_size` # is greater than `max_page_size``, it'll be clamped down to # `max_page_size`. If `default_page_size` is nil, use `max_page_size`. def first @first ||= begin capped = limit_pagination_argument(@first_value, max_page_size) if capped.nil? && last.nil? capped = limit_pagination_argument(default_page_size, max_page_size) || max_page_size end capped end end # This is called by `Relay::RangeAdd` -- it can be overridden # when `item` needs some modifications based on this connection's state. # # @param item [Object] An item newly added to `items` # @return [Edge] def range_add_edge(item) edge_class.new(item, self) end attr_writer :last # @return [Integer, nil] A clamped `last` value. (The underlying instance variable doesn't have limits on it) def last @last ||= limit_pagination_argument(@last_value, max_page_size) end # @return [Array] {nodes}, but wrapped with Edge instances def edges @edges ||= nodes.map { |n| @edge_class.new(n, self) } end # @return [Class] A wrapper class for edges of this connection attr_accessor :edge_class # @return [GraphQL::Schema::Field] The field this connection was returned by attr_accessor :field # @return [Array] A slice of {items}, constrained by {@first_value}/{@after_value}/{@last_value}/{@before_value} def nodes raise PaginationImplementationMissingError, "Implement #{self.class}#nodes to paginate `@items`" end # A dynamic alias for compatibility with {Relay::BaseConnection}. # @deprecated use {#nodes} instead def edge_nodes nodes end # The connection object itself implements `PageInfo` fields def page_info self end # @return [Boolean] True if there are more items after this page def has_next_page raise PaginationImplementationMissingError, "Implement #{self.class}#has_next_page to return the next-page check" end # @return [Boolean] True if there were items before these items def has_previous_page raise PaginationImplementationMissingError, "Implement #{self.class}#has_previous_page to return the previous-page check" end # @return [String] The cursor of the first item in {nodes} def start_cursor nodes.first && cursor_for(nodes.first) end # @return [String] The cursor of the last item in {nodes} def end_cursor nodes.last && cursor_for(nodes.last) end # Return a cursor for this item. # @param item [Object] one of the passed in {items}, taken from {nodes} # @return [String] def cursor_for(item) raise PaginationImplementationMissingError, "Implement #{self.class}#cursor_for(item) to return the cursor for #{item.inspect}" end private def detect_was_authorized_by_scope_items if @context && (current_runtime_state = Fiber[:__graphql_runtime_info]) && (query_runtime_state = current_runtime_state[@context.query]) query_runtime_state.was_authorized_by_scope_items else nil end end # @param argument [nil, Integer] `first` or `last`, as provided by the client # @param max_page_size [nil, Integer] # @return [nil, Integer] `nil` if the input was `nil`, otherwise a value between `0` and `max_page_size` def limit_pagination_argument(argument, max_page_size) if argument if argument < 0 argument = 0 elsif max_page_size && argument > max_page_size argument = max_page_size end end argument end def decode(cursor) context.schema.cursor_encoder.decode(cursor, nonce: true) end def encode(cursor) context.schema.cursor_encoder.encode(cursor, nonce: true) end # A wrapper around paginated items. It includes a {cursor} for pagination # and could be extended with custom relationship-level data. class Edge attr_reader :node def initialize(node, connection) @connection = connection @node = node end def parent @connection.parent end def cursor @cursor ||= @connection.cursor_for(@node) end def was_authorized_by_scope_items? @connection.was_authorized_by_scope_items? end end end end end graphql-ruby-2.5.19/lib/graphql/pagination/connections.rb000066400000000000000000000101271514115062600234330ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Pagination # A schema-level connection wrapper manager. # # Attach as a plugin. # # @example Adding a custom wrapper # class MySchema < GraphQL::Schema # connections.add(MyApp::SearchResults, MyApp::SearchResultsConnection) # end # # @example Removing default connection support for arrays (they can still be manually wrapped) # class MySchema < GraphQL::Schema # connections.delete(Array) # end # # @see {Schema.connections} class Connections class ImplementationMissingError < GraphQL::Error end def initialize(schema:) @schema = schema @wrappers = {} add_default end def add(nodes_class, implementation) @wrappers[nodes_class] = implementation end def delete(nodes_class) @wrappers.delete(nodes_class) end def all_wrappers all_wrappers = {} @schema.ancestors.reverse_each do |schema_class| if schema_class.respond_to?(:connections) && (c = schema_class.connections) all_wrappers.merge!(c.wrappers) end end all_wrappers end def wrapper_for(items, wrappers: all_wrappers) impl = nil items.class.ancestors.each { |cls| impl = wrappers[cls] break if impl } impl end # Used by the runtime to wrap values in connection wrappers. # @api Private def wrap(field, parent, items, arguments, context) return items if GraphQL::Execution::Interpreter::RawValue === items wrappers = context ? context.namespace(:connections)[:all_wrappers] : all_wrappers impl = wrapper_for(items, wrappers: wrappers) if impl impl.new( items, context: context, parent: parent, field: field, max_page_size: field.has_max_page_size? ? field.max_page_size : context.schema.default_max_page_size, default_page_size: field.has_default_page_size? ? field.default_page_size : context.schema.default_page_size, first: arguments[:first], after: arguments[:after], last: arguments[:last], before: arguments[:before], arguments: arguments, edge_class: edge_class_for_field(field), ) else raise ImplementationMissingError, "Couldn't find a connection wrapper for #{items.class} during #{field.path} (#{items.inspect})" end end # use an override if there is one # @api private def edge_class_for_field(field) conn_type = field.type.unwrap conn_type_edge_type = conn_type.respond_to?(:edge_class) && conn_type.edge_class if conn_type_edge_type && conn_type_edge_type != Pagination::Connection::Edge conn_type_edge_type else nil end end protected attr_reader :wrappers private def add_default add(Array, Pagination::ArrayConnection) if defined?(ActiveRecord::Relation) add(ActiveRecord::Relation, Pagination::ActiveRecordRelationConnection) end if defined?(Sequel::Dataset) add(Sequel::Dataset, Pagination::SequelDatasetConnection) end if defined?(Mongoid::Criteria) add(Mongoid::Criteria, Pagination::MongoidRelationConnection) end # Mongoid 5 and 6 if defined?(Mongoid::Relations::Targets::Enumerable) add(Mongoid::Relations::Targets::Enumerable, Pagination::MongoidRelationConnection) end # Mongoid 7 if defined?(Mongoid::Association::Referenced::HasMany::Targets::Enumerable) add(Mongoid::Association::Referenced::HasMany::Targets::Enumerable, Pagination::MongoidRelationConnection) end # Mongoid 7.3+ if defined?(Mongoid::Association::Referenced::HasMany::Enumerable) add(Mongoid::Association::Referenced::HasMany::Enumerable, Pagination::MongoidRelationConnection) end end end end end graphql-ruby-2.5.19/lib/graphql/pagination/mongoid_relation_connection.rb000066400000000000000000000010461514115062600266610ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/pagination/relation_connection" module GraphQL module Pagination class MongoidRelationConnection < Pagination::RelationConnection def relation_offset(relation) relation.options.skip end def relation_limit(relation) relation.options.limit end def relation_count(relation) relation.all.count(relation.options.slice(:limit, :skip)) end def null_relation(relation) relation.without_options.none end end end end graphql-ruby-2.5.19/lib/graphql/pagination/relation_connection.rb000066400000000000000000000171601514115062600251510ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/pagination/connection" module GraphQL module Pagination # A generic class for working with database query objects. class RelationConnection < Pagination::Connection def nodes load_nodes @nodes end def has_previous_page if @has_previous_page.nil? @has_previous_page = if after_offset && after_offset > 0 true elsif last # See whether there are any nodes _before_ the current offset. # If there _is no_ current offset, then there can't be any nodes before it. # Assume that if the offset is positive, there are nodes before the offset. limited_nodes !(@paged_nodes_offset.nil? || @paged_nodes_offset == 0) else false end end @has_previous_page end def has_next_page if @has_next_page.nil? @has_next_page = if before_offset && before_offset > 0 true elsif first if @nodes && @nodes.count < first false else relation_larger_than(sliced_nodes, @sliced_nodes_offset, first) end else false end end @has_next_page end def cursor_for(item) load_nodes # index in nodes + existing offset + 1 (because it's offset, not index) offset = nodes.index(item) + 1 + (@paged_nodes_offset || 0) - (relation_offset(items) || 0) encode(offset.to_s) end private # @param relation [Object] A database query object # @param _initial_offset [Integer] The number of items already excluded from the relation # @param size [Integer] The value against which we check the relation size # @return [Boolean] True if the number of items in this relation is larger than `size` def relation_larger_than(relation, _initial_offset, size) relation_count(set_limit(relation, size + 1)) == size + 1 end # @param relation [Object] A database query object # @return [Integer, nil] The offset value, or nil if there isn't one def relation_offset(relation) raise "#{self.class}#relation_offset(relation) must return the offset value for a #{relation.class} (#{relation.inspect})" end # @param relation [Object] A database query object # @return [Integer, nil] The limit value, or nil if there isn't one def relation_limit(relation) raise "#{self.class}#relation_limit(relation) must return the limit value for a #{relation.class} (#{relation.inspect})" end # @param relation [Object] A database query object # @return [Integer, nil] The number of items in this relation (hopefully determined without loading all records into memory!) def relation_count(relation) raise "#{self.class}#relation_count(relation) must return the count of records for a #{relation.class} (#{relation.inspect})" end # @param relation [Object] A database query object # @return [Object] A modified query object which will return no records def null_relation(relation) raise "#{self.class}#null_relation(relation) must return an empty relation for a #{relation.class} (#{relation.inspect})" end # @return [Integer] def offset_from_cursor(cursor) decode(cursor).to_i end # Abstract this operation so we can always ignore inputs less than zero. # (Sequel doesn't like it, understandably.) def set_offset(relation, offset_value) if offset_value >= 0 relation.offset(offset_value) else relation.offset(0) end end # Abstract this operation so we can always ignore inputs less than zero. # (Sequel doesn't like it, understandably.) def set_limit(relation, limit_value) if limit_value > 0 relation.limit(limit_value) elsif limit_value == 0 null_relation(relation) else relation end end def calculate_sliced_nodes_parameters if defined?(@sliced_nodes_limit) return else next_offset = relation_offset(items) || 0 relation_limit = relation_limit(items) if after_offset next_offset += after_offset end if before_offset && after_offset if after_offset < before_offset # Get the number of items between the two cursors space_between = before_offset - after_offset - 1 relation_limit = space_between else # The cursors overextend one another to an empty set @sliced_nodes_null_relation = true end elsif before_offset # Use limit to cut off the tail of the relation relation_limit = before_offset - 1 end @sliced_nodes_limit = relation_limit @sliced_nodes_offset = next_offset end end # Apply `before` and `after` to the underlying `items`, # returning a new relation. def sliced_nodes @sliced_nodes ||= begin calculate_sliced_nodes_parameters paginated_nodes = items if @sliced_nodes_null_relation paginated_nodes = null_relation(paginated_nodes) else if @sliced_nodes_limit paginated_nodes = set_limit(paginated_nodes, @sliced_nodes_limit) end if @sliced_nodes_offset paginated_nodes = set_offset(paginated_nodes, @sliced_nodes_offset) end end paginated_nodes end end # @return [Integer, nil] def before_offset @before_offset ||= before && offset_from_cursor(before) end # @return [Integer, nil] def after_offset @after_offset ||= after && offset_from_cursor(after) end # Apply `first` and `last` to `sliced_nodes`, # returning a new relation def limited_nodes @limited_nodes ||= begin calculate_sliced_nodes_parameters if @sliced_nodes_null_relation # it's an empty set return sliced_nodes end relation_limit = @sliced_nodes_limit relation_offset = @sliced_nodes_offset if first && (relation_limit.nil? || relation_limit > first) # `first` would create a stricter limit that the one already applied, so add it relation_limit = first end if last if relation_limit if last <= relation_limit # `last` is a smaller slice than the current limit, so apply it relation_offset += (relation_limit - last) relation_limit = last end else # No limit, so get the last items sliced_nodes_count = relation_count(sliced_nodes) relation_offset += (sliced_nodes_count - [last, sliced_nodes_count].min) relation_limit = last end end @paged_nodes_offset = relation_offset paginated_nodes = items paginated_nodes = set_offset(paginated_nodes, relation_offset) if relation_limit paginated_nodes = set_limit(paginated_nodes, relation_limit) end paginated_nodes end end # Load nodes after applying first/last/before/after, # returns an array of nodes def load_nodes # Return an array so we can consistently use `.index(node)` on it @nodes ||= limited_nodes.to_a end end end end graphql-ruby-2.5.19/lib/graphql/pagination/sequel_dataset_connection.rb000066400000000000000000000011731514115062600263340ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/pagination/relation_connection" module GraphQL module Pagination # Customizes `RelationConnection` to work with `Sequel::Dataset`s. class SequelDatasetConnection < Pagination::RelationConnection private def relation_offset(relation) relation.opts[:offset] end def relation_limit(relation) relation.opts[:limit] end def relation_count(relation) # Remove order to make it faster relation.order(nil).count end def null_relation(relation) relation.where(false) end end end end graphql-ruby-2.5.19/lib/graphql/parse_error.rb000066400000000000000000000007751514115062600213130ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class ParseError < GraphQL::Error attr_reader :line, :col, :query def initialize(message, line, col, query, filename: nil) if filename message += " (#{filename})" end super(message) @line = line @col = col @query = query end def to_h locations = line ? [{ "line" => line, "column" => col }] : [] { "message" => message, "locations" => locations, } end end end graphql-ruby-2.5.19/lib/graphql/query.rb000066400000000000000000000436251514115062600201360ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL # A combination of query string and {Schema} instance which can be reduced to a {#result}. class Query extend Autoload include Tracing::Traceable extend Forwardable autoload :Context, "graphql/query/context" autoload :Fingerprint, "graphql/query/fingerprint" autoload :NullContext, "graphql/query/null_context" autoload :Partial, "graphql/query/partial" autoload :Result, "graphql/query/result" autoload :Variables, "graphql/query/variables" autoload :InputValidationResult, "graphql/query/input_validation_result" autoload :VariableValidationError, "graphql/query/variable_validation_error" autoload :ValidationPipeline, "graphql/query/validation_pipeline" # Code shared with {Partial} module Runnable def after_lazy(value, &block) if !defined?(@runtime_instance) @runtime_instance = context.namespace(:interpreter_runtime)[:runtime] end if @runtime_instance @runtime_instance.minimal_after_lazy(value, &block) else @schema.after_lazy(value, &block) end end # Node-level cache for calculating arguments. Used during execution and query analysis. # @param ast_node [GraphQL::Language::Nodes::AbstractNode] # @param definition [GraphQL::Schema::Field] # @param parent_object [GraphQL::Schema::Object] # @return [Hash{Symbol => Object}] def arguments_for(ast_node, definition, parent_object: nil) arguments_cache.fetch(ast_node, definition, parent_object) end def arguments_cache @arguments_cache ||= Execution::Interpreter::ArgumentsCache.new(self) end # @api private def handle_or_reraise(err) @schema.handle_or_reraise(context, err) end end include Runnable class OperationNameMissingError < GraphQL::ExecutionError def initialize(name) msg = if name.nil? %|An operation name is required| else %|No operation named "#{name}"| end super(msg) end end attr_reader :schema, :context, :provided_variables # The value for root types attr_accessor :root_value # @return [nil, String] The operation name provided by client or the one inferred from the document. Used to determine which operation to run. attr_accessor :operation_name # @return [Boolean] if false, static validation is skipped (execution behavior for invalid queries is undefined) attr_reader :validate # @param new_validate [Boolean] if false, static validation is skipped. This can't be reasssigned after validation. def validate=(new_validate) if defined?(@validation_pipeline) && @validation_pipeline && @validation_pipeline.has_validated? raise ArgumentError, "Can't reassign Query#validate= after validation has run, remove this assignment." else @validate = new_validate end end # @return [GraphQL::StaticValidation::Validator] if present, the query will validate with these rules. attr_reader :static_validator # @param new_validator [GraphQL::StaticValidation::Validator] if present, the query will validate with these rules. This can't be reasssigned after validation. def static_validator=(new_validator) if defined?(@validation_pipeline) && @validation_pipeline && @validation_pipeline.has_validated? raise ArgumentError, "Can't reassign Query#static_validator= after validation has run, remove this assignment." elsif !new_validator.is_a?(GraphQL::StaticValidation::Validator) raise ArgumentError, "Expected a `GraphQL::StaticValidation::Validator` instance." else @static_validator = new_validator end end attr_writer :query_string # @return [GraphQL::Language::Nodes::Document] def document # It's ok if this hasn't been assigned yet if @query_string || @document with_prepared_ast { @document } else nil end end def inspect "query ..." end # @return [String, nil] The name of the operation to run (may be inferred) def selected_operation_name return nil unless selected_operation selected_operation.name end # @return [String, nil] the triggered event, if this query is a subscription update attr_reader :subscription_topic attr_reader :tracers # Prepare query `query_string` on `schema` # @param schema [GraphQL::Schema] # @param query_string [String] # @param context [#[]] an arbitrary hash of values which you can access in {GraphQL::Field#resolve} # @param variables [Hash] values for `$variables` in the query # @param operation_name [String] if the query string contains many operations, this is the one which should be executed # @param root_value [Object] the object used to resolve fields on the root type # @param max_depth [Numeric] the maximum number of nested selections allowed for this query (falls back to schema-level value) # @param max_complexity [Numeric] the maximum field complexity for this query (falls back to schema-level value) # @param visibility_profile [Symbol] Another way to assign `context[:visibility_profile]` def initialize(schema, query_string = nil, query: nil, document: nil, context: nil, variables: nil, multiplex: nil, validate: true, static_validator: nil, visibility_profile: nil, subscription_topic: nil, operation_name: nil, root_value: nil, max_depth: schema.max_depth, max_complexity: schema.max_complexity, warden: nil, use_visibility_profile: nil) # Even if `variables: nil` is passed, use an empty hash for simpler logic variables ||= {} @multiplex = multiplex @schema = schema @context = schema.context_class.new(query: self, values: context) if visibility_profile @context[:visibility_profile] ||= visibility_profile end if use_visibility_profile.nil? use_visibility_profile = warden ? false : schema.use_visibility_profile? end if use_visibility_profile @visibility_profile = @schema.visibility.profile_for(@context) @warden = Schema::Warden::NullWarden.new(context: @context, schema: @schema) else @visibility_profile = nil @warden = warden end @subscription_topic = subscription_topic @root_value = root_value @fragments = nil @operations = nil @validate = validate self.static_validator = static_validator if static_validator context_tracers = (context ? context.fetch(:tracers, []) : []) @tracers = schema.tracers + context_tracers if !context_tracers.empty? && !(schema.trace_class <= GraphQL::Tracing::CallLegacyTracers) raise ArgumentError, "context[:tracers] are not supported without `trace_with(GraphQL::Tracing::CallLegacyTracers)` in the schema configuration, please add it." end @analysis_errors = [] if variables.is_a?(String) raise ArgumentError, "Query variables should be a Hash, not a String. Try JSON.parse to prepare variables." else @provided_variables = variables || {} end @query_string = query_string || query @document = document if @query_string && @document raise ArgumentError, "Query should only be provided a query string or a document, not both." end if @query_string && !@query_string.is_a?(String) raise ArgumentError, "Query string argument should be a String, got #{@query_string.class.name} instead." end # A two-layer cache of type resolution: # { abstract_type => { value => resolved_type } } @resolved_types_cache = Hash.new do |h1, k1| h1[k1] = Hash.new do |h2, k2| h2[k2] = @schema.resolve_type(k1, k2, @context) end end # Trying to execute a document # with no operations returns an empty hash @ast_variables = [] @mutation = false @operation_name = operation_name @prepared_ast = false @validation_pipeline = nil @max_depth = max_depth @max_complexity = max_complexity @result_values = nil @executed = false @logger = schema.logger_for(context) end # If a document was provided to `GraphQL::Schema#execute` instead of the raw query string, we will need to get it from the document def query_string @query_string ||= (document ? document.to_query_string : nil) end # @return [Symbol, nil] attr_reader :visibility_profile attr_accessor :multiplex # @return [GraphQL::Tracing::Trace] def current_trace @current_trace ||= context[:trace] || (multiplex ? multiplex.current_trace : schema.new_trace(multiplex: multiplex, query: self)) end def subscription_update? @subscription_topic && subscription? end # A lookahead for the root selections of this query # @return [GraphQL::Execution::Lookahead] def lookahead @lookahead ||= begin if selected_operation.nil? GraphQL::Execution::Lookahead::NULL_LOOKAHEAD else GraphQL::Execution::Lookahead.new(query: self, root_type: root_type, ast_nodes: [selected_operation]) end end end # @api private def result_values=(result_hash) if @executed raise "Invariant: Can't reassign result" else @executed = true @result_values = result_hash end end # @api private attr_reader :result_values def fragments with_prepared_ast { @fragments } end def operations with_prepared_ast { @operations } end # Run subtree partials of this query and return their results. # Each partial is identified with a `path:` and `object:` # where the path references a field in the AST and the object will be treated # as the return value from that field. Subfields of the field named by `path` # will be executed with `object` as the starting point # @param partials_hashes [Array Object}>] Hashes with `path:` and `object:` keys # @return [Array] def run_partials(partials_hashes) partials = partials_hashes.map { |partial_options| Partial.new(query: self, **partial_options) } Execution::Interpreter.run_all(@schema, partials, context: @context) end # Get the result for this query, executing it once # @return [GraphQL::Query::Result] A Hash-like GraphQL response, with `"data"` and/or `"errors"` keys def result if !@executed Execution::Interpreter.run_all(@schema, [self], context: @context) end @result ||= Query::Result.new(query: self, values: @result_values) end def executed? @executed end def static_errors validation_errors + analysis_errors + context.errors end # This is the operation to run for this query. # If more than one operation is present, it must be named at runtime. # @return [GraphQL::Language::Nodes::OperationDefinition, nil] def selected_operation with_prepared_ast { @selected_operation } end # Determine the values for variables of this query, using default values # if a value isn't provided at runtime. # # If some variable is invalid, errors are added to {#validation_errors}. # # @return [GraphQL::Query::Variables] Variables to apply to this query def variables @variables ||= begin with_prepared_ast { GraphQL::Query::Variables.new( @context, @ast_variables, @provided_variables, ) } end end # A version of the given query string, with: # - Variables inlined to the query # - Strings replaced with `` # @return [String, nil] Returns nil if the query is invalid. def sanitized_query_string(inline_variables: true) with_prepared_ast { schema.sanitized_printer.new(self, inline_variables: inline_variables).sanitized_query_string } end # This contains a few components: # # - The selected operation name (or `anonymous`) # - The fingerprint of the query string # - The number of given variables (for readability) # - The fingerprint of the given variables # # This fingerprint can be used to track runs of the same operation-variables combination over time. # # @see operation_fingerprint # @see variables_fingerprint # @return [String] An opaque hash identifying this operation-variables combination def fingerprint @fingerprint ||= "#{operation_fingerprint}/#{variables_fingerprint}" end # @return [String] An opaque hash for identifying this query's given query string and selected operation def operation_fingerprint @operation_fingerprint ||= "#{selected_operation_name || "anonymous"}/#{Fingerprint.generate(query_string || "")}" end # @return [String] An opaque hash for identifying this query's given a variable values (not including defaults) def variables_fingerprint @variables_fingerprint ||= "#{provided_variables.size}/#{Fingerprint.generate(provided_variables.to_json)}" end def validation_pipeline with_prepared_ast { @validation_pipeline } end def_delegators :validation_pipeline, :validation_errors, :analyzers, :ast_analyzers, :max_depth, :max_complexity, :validate_timeout_remaining attr_accessor :analysis_errors def valid? validation_pipeline.valid? && analysis_errors.empty? end def warden with_prepared_ast { @warden } end def get_type(type_name) types.type(type_name) # rubocop:disable Development/ContextIsPassedCop end def get_field(owner, field_name) types.field(owner, field_name) # rubocop:disable Development/ContextIsPassedCop end def possible_types(type) types.possible_types(type) # rubocop:disable Development/ContextIsPassedCop end def root_type_for_operation(op_type) case op_type when "query", nil types.query_root # rubocop:disable Development/ContextIsPassedCop when "mutation" types.mutation_root # rubocop:disable Development/ContextIsPassedCop when "subscription" types.subscription_root # rubocop:disable Development/ContextIsPassedCop else raise ArgumentError, "unexpected root type name: #{op_type.inspect}; expected nil, 'query', 'mutation', or 'subscription'" end end def root_type root_type_for_operation(selected_operation.operation_type) end def types @visibility_profile || warden.visibility_profile end # @param abstract_type [GraphQL::UnionType, GraphQL::InterfaceType] # @param value [Object] Any runtime value # @return [GraphQL::ObjectType, nil] The runtime type of `value` from {Schema#resolve_type} # @see {#possible_types} to apply filtering from `only` / `except` def resolve_type(abstract_type, value = NOT_CONFIGURED) if value.is_a?(Symbol) && value == NOT_CONFIGURED # Old method signature value = abstract_type abstract_type = nil end if value.is_a?(GraphQL::Schema::Object) value = value.object end @resolved_types_cache[abstract_type][value] end def mutation? with_prepared_ast { @mutation } end def query? with_prepared_ast { @query } end def subscription? with_prepared_ast { @subscription } end attr_reader :logger private def find_operation(operations, operation_name) if operation_name.nil? && operations.length == 1 operations.values.first elsif !operations.key?(operation_name) nil else operations.fetch(operation_name) end end def prepare_ast @prepared_ast = true @warden ||= @schema.warden_class.new(schema: @schema, context: @context) parse_error = nil @document ||= begin if query_string GraphQL.parse(query_string, trace: self.current_trace, max_tokens: @schema.max_query_string_tokens) end rescue GraphQL::ParseError => err parse_error = err @schema.parse_error(err, @context) nil end @fragments = {} @operations = {} if @document @document.definitions.each do |part| case part when GraphQL::Language::Nodes::FragmentDefinition @fragments[part.name] = part when GraphQL::Language::Nodes::OperationDefinition @operations[part.name] = part end end elsif parse_error # This will be handled later else parse_error = GraphQL::ExecutionError.new("No query string was present") @context.add_error(parse_error) end # Trying to execute a document # with no operations returns an empty hash @ast_variables = [] @mutation = false @subscription = false operation_name_error = nil if !@operations.empty? @selected_operation = find_operation(@operations, @operation_name) if @selected_operation.nil? operation_name_error = GraphQL::Query::OperationNameMissingError.new(@operation_name) else if @operation_name.nil? @operation_name = @selected_operation.name end @ast_variables = @selected_operation.variables @mutation = @selected_operation.operation_type == "mutation" @query = @selected_operation.operation_type == "query" @subscription = @selected_operation.operation_type == "subscription" end end @validation_pipeline = GraphQL::Query::ValidationPipeline.new( query: self, parse_error: parse_error, operation_name_error: operation_name_error, max_depth: @max_depth, max_complexity: @max_complexity ) end # Since the query string is processed at the last possible moment, # any internal values which depend on it should be accessed within this wrapper. def with_prepared_ast if !@prepared_ast prepare_ast end yield end end end graphql-ruby-2.5.19/lib/graphql/query/000077500000000000000000000000001514115062600175775ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/query/context.rb000066400000000000000000000200441514115062600216100ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Query # Expose some query-specific info to field resolve functions. # It delegates `[]` to the hash that's passed to `GraphQL::Query#initialize`. class Context class ExecutionErrors def initialize(ctx) @context = ctx end def add(err_or_msg) err = case err_or_msg when String GraphQL::ExecutionError.new(err_or_msg) when GraphQL::ExecutionError err_or_msg else raise ArgumentError, "expected String or GraphQL::ExecutionError, not #{err_or_msg.class} (#{err_or_msg.inspect})" end # This will assign ast_node and path @context.add_error(err) end alias :>> :add alias :push :add end extend Forwardable # @return [Array] errors returned during execution attr_reader :errors # @return [GraphQL::Query] The query whose context this is attr_reader :query # @return [GraphQL::Schema] attr_reader :schema # Make a new context which delegates key lookup to `values` # @param query [GraphQL::Query] the query who owns this context # @param values [Hash] A hash of arbitrary values which will be accessible at query-time def initialize(query:, schema: query.schema, values:) @query = query @schema = schema @provided_values = values || {} # Namespaced storage, where user-provided values are in `nil` namespace: @storage = Hash.new { |h, k| h[k] = {} } @storage[nil] = @provided_values @errors = [] @scoped_context = ScopedContext.new(self) end # Modify this hash to return extensions to client. # @return [Hash] A hash that will be added verbatim to the result hash, as `"extensions" => { ... }` def response_extensions namespace(:__query_result_extensions__) end def dataloader @dataloader ||= self[:dataloader] || (query.multiplex ? query.multiplex.dataloader : schema.dataloader_class.new) end # @api private attr_writer :interpreter # @api private attr_writer :value # @api private attr_reader :scoped_context def []=(key, value) @provided_values[key] = value end def_delegators :@query, :trace def types @types ||= @query.types end attr_writer :types RUNTIME_METADATA_KEYS = Set.new([:current_object, :current_arguments, :current_field, :current_path]).freeze # @!method []=(key, value) # Reassign `key` to the hash passed to {Schema#execute} as `context:` # Lookup `key` from the hash passed to {Schema#execute} as `context:` def [](key) if @scoped_context.key?(key) @scoped_context[key] elsif @provided_values.key?(key) @provided_values[key] elsif RUNTIME_METADATA_KEYS.include?(key) if key == :current_path current_path else (current_runtime_state = Fiber[:__graphql_runtime_info]) && (query_runtime_state = current_runtime_state[@query]) && (query_runtime_state.public_send(key)) end else # not found nil end end # Return this value to tell the runtime # to exclude this field from the response altogether def skip GraphQL::Execution::SKIP end # Add error at query-level. # @param error [GraphQL::ExecutionError] an execution error # @return [void] def add_error(error) if !error.is_a?(ExecutionError) raise TypeError, "expected error to be a ExecutionError, but was #{error.class}" end errors << error nil end # @example Print the GraphQL backtrace during field resolution # puts ctx.backtrace # # @return [GraphQL::Backtrace] The backtrace for this point in query execution def backtrace GraphQL::Backtrace.new(self) end def execution_errors @execution_errors ||= ExecutionErrors.new(self) end def current_path current_runtime_state = Fiber[:__graphql_runtime_info] query_runtime_state = current_runtime_state && current_runtime_state[@query] path = query_runtime_state && (result = query_runtime_state.current_result) && (result.path) if path && (rn = query_runtime_state.current_result_name) path = path.dup path.push(rn) end path end def delete(key) if @scoped_context.key?(key) @scoped_context.delete(key) else @provided_values.delete(key) end end UNSPECIFIED_FETCH_DEFAULT = Object.new def fetch(key, default = UNSPECIFIED_FETCH_DEFAULT) if RUNTIME_METADATA_KEYS.include?(key) (runtime = Fiber[:__graphql_runtime_info]) && (query_runtime_state = runtime[@query]) && (query_runtime_state.public_send(key)) elsif @scoped_context.key?(key) scoped_context[key] elsif @provided_values.key?(key) @provided_values[key] elsif default != UNSPECIFIED_FETCH_DEFAULT default elsif block_given? yield(self, key) else raise KeyError.new(key: key) end end def dig(key, *other_keys) if RUNTIME_METADATA_KEYS.include?(key) (current_runtime_state = Fiber[:__graphql_runtime_info]) && (query_runtime_state = current_runtime_state[@query]) && (obj = query_runtime_state.public_send(key)) && if other_keys.empty? obj else obj.dig(*other_keys) end elsif @scoped_context.key?(key) @scoped_context.dig(key, *other_keys) else @provided_values.dig(key, *other_keys) end end def to_h if (current_scoped_context = @scoped_context.merged_context) @provided_values.merge(current_scoped_context) else @provided_values end end alias :to_hash :to_h def key?(key) @scoped_context.key?(key) || @provided_values.key?(key) end # @return [GraphQL::Schema::Warden] def warden @warden ||= (@query && @query.warden) end # @api private attr_writer :warden # Get an isolated hash for `ns`. Doesn't affect user-provided storage. # @param ns [Object] a usage-specific namespace identifier # @return [Hash] namespaced storage def namespace(ns) if ns == :interpreter self else @storage[ns] end end # @return [Boolean] true if this namespace was accessed before def namespace?(ns) @storage.key?(ns) end def logger @query && @query.logger end def inspect "#<#{self.class} ...>" end def scoped_merge!(hash) @scoped_context.merge!(hash) end def scoped_set!(key, value) scoped_merge!(key => value) nil end # Use this when you need to do a scoped set _inside_ a lazy-loaded (or batch-loaded) # block of code. # # @example using scoped context inside a promise # scoped_ctx = context.scoped # SomeBatchLoader.load(...).then do |thing| # # use a scoped_ctx which was created _before_ dataloading: # scoped_ctx.set!(:thing, thing) # end # @return [Context::Scoped] def scoped Scoped.new(@scoped_context, current_path) end class Scoped def initialize(scoped_context, path) @path = path @scoped_context = scoped_context end def merge!(hash) @scoped_context.merge!(hash, at: @path) end def set!(key, value) @scoped_context.merge!({ key => value }, at: @path) nil end end end end end require "graphql/query/context/scoped_context" graphql-ruby-2.5.19/lib/graphql/query/context/000077500000000000000000000000001514115062600212635ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/query/context/scoped_context.rb000066400000000000000000000047651514115062600246450ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Query class Context class ScopedContext def initialize(query_context) @query_context = query_context @scoped_contexts = nil @all_keys = nil end def merged_context if @scoped_contexts.nil? GraphQL::EmptyObjects::EMPTY_HASH else merged_ctx = {} each_present_path_ctx do |path_ctx| merged_ctx = path_ctx.merge(merged_ctx) end merged_ctx end end def merge!(hash, at: current_path) @all_keys ||= Set.new @all_keys.merge(hash.keys) ctx = @scoped_contexts ||= {} at.each do |path_part| ctx = ctx[path_part] ||= { parent: ctx } end this_scoped_ctx = ctx[:scoped_context] ||= {} this_scoped_ctx.merge!(hash) end def key?(key) if @all_keys && @all_keys.include?(key) each_present_path_ctx do |path_ctx| if path_ctx.key?(key) return true end end end false end def [](key) each_present_path_ctx do |path_ctx| if path_ctx.key?(key) return path_ctx[key] end end nil end def current_path @query_context.current_path || GraphQL::EmptyObjects::EMPTY_ARRAY end def dig(key, *other_keys) each_present_path_ctx do |path_ctx| if path_ctx.key?(key) found_value = path_ctx[key] if !other_keys.empty? return found_value.dig(*other_keys) else return found_value end end end nil end private # Start at the current location, # but look up the tree for previously-assigned scoped values def each_present_path_ctx ctx = @scoped_contexts if ctx.nil? # no-op else current_path.each do |path_part| if ctx.key?(path_part) ctx = ctx[path_part] else break end end while ctx if (scoped_ctx = ctx[:scoped_context]) yield(scoped_ctx) end ctx = ctx[:parent] end end end end end end end graphql-ruby-2.5.19/lib/graphql/query/fingerprint.rb000066400000000000000000000013401514115062600224510ustar00rootroot00000000000000# frozen_string_literal: true require 'digest/sha2' module GraphQL class Query # @api private # @see Query#query_fingerprint # @see Query#variables_fingerprint # @see Query#fingerprint module Fingerprint # Make an obfuscated hash of the given string (either a query string or variables JSON) # @param string [String] # @return [String] A normalized, opaque hash def self.generate(input_str) # Implemented to be: # - Short (and uniform) length # - Stable # - Irreversibly Opaque (don't want to leak variable values) # - URL-friendly bytes = Digest::SHA256.digest(input_str) Base64.urlsafe_encode64(bytes) end end end end graphql-ruby-2.5.19/lib/graphql/query/input_validation_result.rb000066400000000000000000000026211514115062600250740ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Query class InputValidationResult attr_accessor :problems def self.from_problem(explanation, path = nil, extensions: nil, message: nil) result = self.new result.add_problem(explanation, path, extensions: extensions, message: message) result end def initialize(valid: true, problems: nil) @valid = valid @problems = problems end def valid? @valid end def add_problem(explanation, path = nil, extensions: nil, message: nil) @problems ||= [] @valid = false problem = { "path" => path || [], "explanation" => explanation } if extensions problem["extensions"] = extensions end if message problem["message"] = message end @problems.push(problem) end def merge_result!(path, inner_result) return if inner_result.nil? || inner_result.valid? if inner_result.problems inner_result.problems.each do |p| item_path = [path, *p["path"]] add_problem(p["explanation"], item_path, message: p["message"], extensions: p["extensions"]) end end # It could have been explicitly set on inner_result (if it had no problems) @valid = false end VALID = self.new VALID.freeze end end end graphql-ruby-2.5.19/lib/graphql/query/null_context.rb000066400000000000000000000015151514115062600226440ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/query/context" module GraphQL class Query # This object can be `ctx` in places where there is no query class NullContext < Context include Singleton class NullQuery def after_lazy(value) yield(value) end end class NullSchema < GraphQL::Schema end extend Forwardable attr_reader :schema, :query, :warden, :dataloader def_delegators GraphQL::EmptyObjects::EMPTY_HASH, :[], :fetch, :dig, :key?, :to_h def initialize @query = NullQuery.new @dataloader = GraphQL::Dataloader::NullDataloader.new @schema = NullSchema @warden = Schema::Warden::NullWarden.new(context: self, schema: @schema) @types = @warden.visibility_profile freeze end end end end graphql-ruby-2.5.19/lib/graphql/query/partial.rb000066400000000000000000000124541514115062600215660ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Query # This class is _like_ a {GraphQL::Query}, except it can run on an arbitrary path within a query string. # # It depends on a "parent" {Query}. # # During execution, it calls query-related tracing hooks but passes itself as `query:`. # # The {Partial} will use your {Schema.resolve_type} hook to find the right GraphQL type to use for # `object` in some cases. # # @see Query#run_partials Run via {Query#run_partials} class Partial include Query::Runnable # @param path [Array] A path in `query.query_string` to start executing from # @param object [Object] A starting object for execution # @param query [GraphQL::Query] A full query instance that this partial is based on. Caches are shared. # @param context [Hash] Extra context values to merge into `query.context`, if provided # @param fragment_node [GraphQL::Language::Nodes::InlineFragment, GraphQL::Language::Nodes::FragmentDefinition] def initialize(path: nil, object:, query:, context: nil, fragment_node: nil, type: nil) @path = path @object = object @query = query @schema = query.schema context_vals = @query.context.to_h if context context_vals = context_vals.merge(context) end @context = GraphQL::Query::Context.new(query: self, schema: @query.schema, values: context_vals) @multiplex = nil @result_values = nil @result = nil if fragment_node @ast_nodes = [fragment_node] @root_type = type || raise(ArgumentError, "Pass `type:` when using `node:`") # This is only used when `@leaf` @field_definition = nil elsif path.nil? raise ArgumentError, "`path:` is required if `node:` is not given; add `path:`" else set_type_info_from_path end @leaf = @root_type.unwrap.kind.leaf? end def leaf? @leaf end attr_reader :context, :query, :ast_nodes, :root_type, :object, :field_definition, :path, :schema attr_accessor :multiplex, :result_values class Result < GraphQL::Query::Result def path @query.path end # @return [GraphQL::Query::Partial] def partial @query end end def result @result ||= Result.new(query: self, values: result_values) end def current_trace @query.current_trace end def types @query.types end def resolve_type(...) @query.resolve_type(...) end def variables @query.variables end def fragments @query.fragments end def valid? @query.valid? end def analyzers EmptyObjects::EMPTY_ARRAY end def analysis_errors=(_ignored) # pass end def subscription? @query.subscription? end def selected_operation ast_nodes.first end def static_errors @query.static_errors end def selected_operation_name @query.selected_operation_name end private def set_type_info_from_path selections = [@query.selected_operation] type = @query.root_type parent_type = nil field_defn = nil @path.each do |name_in_doc| if name_in_doc.is_a?(Integer) if type.list? type = type.unwrap next else raise ArgumentError, "Received path with index `#{name_in_doc}`, but type wasn't a list. Type: #{type.to_type_signature}, path: #{@path}" end end next_selections = [] selections.each do |selection| selections_to_check = [] selections_to_check.concat(selection.selections) while (sel = selections_to_check.shift) case sel when GraphQL::Language::Nodes::InlineFragment selections_to_check.concat(sel.selections) when GraphQL::Language::Nodes::FragmentSpread fragment = @query.fragments[sel.name] selections_to_check.concat(fragment.selections) when GraphQL::Language::Nodes::Field if sel.alias == name_in_doc || sel.name == name_in_doc next_selections << sel end else raise "Unexpected selection in partial path: #{sel.class}, #{sel.inspect}" end end end if next_selections.empty? raise ArgumentError, "Path `#{@path.inspect}` is not present in this query. `#{name_in_doc.inspect}` was not found. Try a different path or rewrite the query to include it." end field_name = next_selections.first.name field_defn = @schema.get_field(type, field_name, @query.context) || raise("Invariant: no field called #{field_name} on #{type.graphql_name}") parent_type = type type = field_defn.type if type.non_null? type = type.of_type end selections = next_selections end @ast_nodes = selections @root_type = type @field_definition = field_defn end end end end graphql-ruby-2.5.19/lib/graphql/query/result.rb000066400000000000000000000031631514115062600214450ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Query # A result from {Schema#execute}. # It provides the requested data and # access to the {Query} and {Query::Context}. class Result extend Forwardable def initialize(query:, values:) @query = query @to_h = values end # @return [GraphQL::Query] The query that was executed attr_reader :query # @return [Hash] The resulting hash of "data" and/or "errors" attr_reader :to_h def_delegators :@query, :context, :mutation?, :query?, :subscription? def_delegators :@to_h, :[], :keys, :values, :to_json, :as_json # Delegate any hash-like method to the underlying hash. def method_missing(method_name, *args, &block) if @to_h.respond_to?(method_name) @to_h.public_send(method_name, *args, &block) else super end end def respond_to_missing?(method_name, include_private = false) @to_h.respond_to?(method_name) || super end def inspect "#" end # A result is equal to another object when: # # - The other object is a Hash whose value matches `result.to_h` # - The other object is a Result whose value matches `result.to_h` # # (The query is ignored for comparing result equality.) # # @return [Boolean] def ==(other) case other when Hash @to_h == other when Query::Result @to_h == other.to_h else super end end end end end graphql-ruby-2.5.19/lib/graphql/query/validation_pipeline.rb000066400000000000000000000074111514115062600241460ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Query # Contain the validation pipeline and expose the results. # # 0. Checks in {Query#initialize}: # - Rescue a ParseError, halt if there is one # - Check for selected operation, halt if not found # 1. Validate the AST, halt if errors # 2. Validate the variables, halt if errors # 3. Run query analyzers, halt if errors # # {#valid?} is false if any of the above checks halted the pipeline. # # @api private class ValidationPipeline attr_reader :max_depth, :max_complexity, :validate_timeout_remaining def initialize(query:, parse_error:, operation_name_error:, max_depth:, max_complexity:) @validation_errors = [] @parse_error = parse_error @operation_name_error = operation_name_error @query = query @schema = query.schema @max_depth = max_depth @max_complexity = max_complexity @has_validated = false end # @return [Boolean] does this query have errors that should prevent it from running? def valid? ensure_has_validated @valid end # @return [Array] Static validation errors for the query string def validation_errors ensure_has_validated @validation_errors end def analyzers ensure_has_validated @query_analyzers end def has_validated? @has_validated == true end private # If the pipeline wasn't run yet, run it. # If it was already run, do nothing. def ensure_has_validated return if @has_validated @has_validated = true if @parse_error # This is kind of crazy: we push the parse error into `ctx` # in `def self.parse_error` by default so that users can _opt out_ by redefining that hook. # That means we can't _re-add_ the error here (otherwise we'd either # add it twice _or_ override the user's choice to not add it). # So we just have to know that it was invalid and go from there. @valid = false return elsif @operation_name_error @validation_errors << @operation_name_error else validator = @query.static_validator || @schema.static_validator validation_result = validator.validate(@query, validate: @query.validate, timeout: @schema.validate_timeout, max_errors: @schema.validate_max_errors) @validation_errors.concat(validation_result[:errors]) @validate_timeout_remaining = validation_result[:remaining_timeout] if @validation_errors.empty? @validation_errors.concat(@query.variables.errors) end if @validation_errors.empty? @query_analyzers = build_analyzers( @schema, @max_depth, @max_complexity ) end end @valid = @validation_errors.empty? rescue SystemStackError => err @valid = false @schema.query_stack_error(@query, err) end # If there are max_* values, add them, # otherwise reuse the schema's list of analyzers. def build_analyzers(schema, max_depth, max_complexity) qa = schema.query_analyzers.dup if max_depth || max_complexity # Depending on the analysis engine, we must use different analyzers # remove this once everything has switched over to AST analyzers if max_depth qa << GraphQL::Analysis::MaxQueryDepth end if max_complexity qa << GraphQL::Analysis::MaxQueryComplexity end qa else qa end end end end end graphql-ruby-2.5.19/lib/graphql/query/variable_validation_error.rb000066400000000000000000000025301514115062600253340ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Query class VariableValidationError < GraphQL::ExecutionError attr_accessor :value, :validation_result def initialize(variable_ast, type, value, validation_result, msg: nil) @value = value @validation_result = validation_result msg ||= "Variable $#{variable_ast.name} of type #{type.to_type_signature} was provided invalid value" if !problem_fields.empty? msg += " for #{problem_fields.join(", ")}" end super(msg) self.ast_node = variable_ast end def to_h # It is possible there are other extension items in this error, so handle # a one level deep merge explicitly. However beyond that only show the # latest value and problems. super.merge({ "extensions" => { "value" => value, "problems" => validation_result.problems }}) do |key, oldValue, newValue| if oldValue.respond_to?(:merge) oldValue.merge(newValue) else newValue end end end private def problem_fields @problem_fields ||= @validation_result .problems .reject { |problem| problem["path"].empty? } .map { |problem| "#{problem['path'].join('.')} (#{problem['explanation']})" } end end end end graphql-ruby-2.5.19/lib/graphql/query/variables.rb000066400000000000000000000073431514115062600221030ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Query # Read-only access to query variables, applying default values if needed. class Variables extend Forwardable # @return [Array] Any errors encountered when parsing the provided variables and literal values attr_reader :errors attr_reader :context def initialize(ctx, ast_variables, provided_variables) schema = ctx.schema @context = ctx @provided_variables = deep_stringify(provided_variables) @errors = [] @storage = ast_variables.each_with_object({}) do |ast_variable, memo| if schema.validate_max_errors && schema.validate_max_errors <= @errors.count add_max_errors_reached_message break end # Find the right value for this variable: # - First, use the value provided at runtime # - Then, fall back to the default value from the query string # If it's still nil, raise an error if it's required. variable_type = schema.type_from_ast(ast_variable.type, context: ctx) if variable_type.nil? || !variable_type.unwrap.kind.input? # Pass -- it will get handled by a validator else variable_name = ast_variable.name default_value = ast_variable.default_value provided_value = @provided_variables[variable_name] value_was_provided = @provided_variables.key?(variable_name) max_errors = schema.validate_max_errors - @errors.count if schema.validate_max_errors begin validation_result = variable_type.validate_input(provided_value, ctx, max_errors: max_errors) if validation_result.valid? if value_was_provided # Add the variable if a value was provided memo[variable_name] = provided_value elsif default_value != nil memo[variable_name] = if default_value.is_a?(Language::Nodes::NullValue) nil else default_value end end end rescue GraphQL::ExecutionError => ex # TODO: This should really include the path to the problematic node in the variable value # like InputValidationResults generated by validate_non_null_input but unfortunately we don't # have this information available in the coerce_input call chain. Note this path is the path # that appears under errors.extensions.problems.path and NOT the result path under errors.path. validation_result = GraphQL::Query::InputValidationResult.from_problem(ex.message) end if !validation_result.valid? @errors << GraphQL::Query::VariableValidationError.new(ast_variable, variable_type, provided_value, validation_result) end end end end def_delegators :@storage, :length, :key?, :[], :fetch, :to_h private def deep_stringify(val) case val when Array val.map { |v| deep_stringify(v) } when Hash new_val = {} val.each do |k, v| new_val[k.to_s] = deep_stringify(v) end new_val else val end end def add_max_errors_reached_message message = "Too many errors processing variables, max validation error limit reached. Execution aborted" validation_result = GraphQL::Query::InputValidationResult.from_problem(message) errors << GraphQL::Query::VariableValidationError.new(nil, nil, nil, validation_result, msg: message) end end end end graphql-ruby-2.5.19/lib/graphql/railtie.rb000066400000000000000000000011451514115062600204110ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL # Support {GraphQL::Parser::Cache} and {GraphQL.eager_load!} # # @example Enable the parser cache with default directory # # config.graphql.parser_cache = true # class Railtie < Rails::Railtie config.graphql = ActiveSupport::OrderedOptions.new config.graphql.parser_cache = false config.eager_load_namespaces << GraphQL initializer("graphql.cache") do |app| if config.graphql.parser_cache Language::Parser.cache ||= Language::Cache.new( app.root.join("tmp/cache/graphql") ) end end end end graphql-ruby-2.5.19/lib/graphql/rake_task.rb000066400000000000000000000116461514115062600207330ustar00rootroot00000000000000# frozen_string_literal: true require "fileutils" require "rake" require "graphql/rake_task/validate" module GraphQL # A rake task for dumping a schema as IDL or JSON. # # By default, schemas are looked up by name as constants using `schema_name:`. # You can provide a `load_schema` function to return your schema another way. # # Use `load_context:` and `visible?` to dump schemas under certain visibility constraints. # # @example Dump a Schema to .graphql + .json files # require "graphql/rake_task" # GraphQL::RakeTask.new(schema_name: "MySchema") # # # $ rake graphql:schema:dump # # Schema IDL dumped to ./schema.graphql # # Schema JSON dumped to ./schema.json # # @example Invoking the task from Ruby # require "rake" # Rake::Task["graphql:schema:dump"].invoke # # @example Providing arguments to build the introspection query # require "graphql/rake_task" # GraphQL::RakeTask.new(schema_name: "MySchema", include_is_one_of: true) class RakeTask include Rake::DSL DEFAULT_OPTIONS = { namespace: "graphql", dependencies: nil, schema_name: nil, load_schema: ->(task) { Object.const_get(task.schema_name) }, load_context: ->(task) { {} }, directory: ".", idl_outfile: "schema.graphql", json_outfile: "schema.json", include_deprecated_args: true, include_schema_description: false, include_is_repeatable: false, include_specified_by_url: false, include_is_one_of: false } # @return [String] Namespace for generated tasks attr_writer :namespace def rake_namespace @namespace end # @return [Array] attr_accessor :dependencies # @return [String] By default, used to find the schema as a constant. # @see {#load_schema} for loading a schema another way attr_accessor :schema_name # @return [<#call(task)>] A proc for loading the target GraphQL schema attr_accessor :load_schema # @return [<#call(task)>] A callable for loading the query context attr_accessor :load_context # @return [String] target for IDL task attr_accessor :idl_outfile # @return [String] target for JSON task attr_accessor :json_outfile # @return [String] directory for IDL & JSON files attr_accessor :directory # @return [Boolean] Options for additional fields in the introspection query JSON response # @see GraphQL::Schema.as_json attr_accessor :include_deprecated_args, :include_schema_description, :include_is_repeatable, :include_specified_by_url, :include_is_one_of # Set the parameters of this task by passing keyword arguments # or assigning attributes inside the block def initialize(options = {}) all_options = DEFAULT_OPTIONS.merge(options) all_options.each do |k, v| self.public_send("#{k}=", v) end if block_given? yield(self) end define_task end private # Use the provided `method_name` to generate a string from the specified schema # then write it to `file`. def write_outfile(method_name, file) schema = @load_schema.call(self) context = @load_context.call(self) result = case method_name when :to_json schema.to_json( include_is_one_of: include_is_one_of, include_deprecated_args: include_deprecated_args, include_is_repeatable: include_is_repeatable, include_specified_by_url: include_specified_by_url, include_schema_description: include_schema_description, context: context ) when :to_definition schema.to_definition(context: context) else raise ArgumentError, "Unexpected schema dump method: #{method_name.inspect}" end dir = File.dirname(file) FileUtils.mkdir_p(dir) if !result.end_with?("\n") result += "\n" end File.write(file, result) end def idl_path File.join(@directory, @idl_outfile) end def json_path File.join(@directory, @json_outfile) end def load_rails_environment_if_defined if Rake::Task.task_defined?('environment') Rake::Task['environment'].invoke end end # Use the Rake DSL to add tasks def define_task namespace(@namespace) do namespace("schema") do desc("Dump the schema to IDL in #{idl_path}") task :idl => @dependencies do load_rails_environment_if_defined write_outfile(:to_definition, idl_path) puts "Schema IDL dumped into #{idl_path}" end desc("Dump the schema to JSON in #{json_path}") task :json => @dependencies do load_rails_environment_if_defined write_outfile(:to_json, json_path) puts "Schema JSON dumped into #{json_path}" end desc("Dump the schema to JSON and IDL") task :dump => [:idl, :json] end end end end end graphql-ruby-2.5.19/lib/graphql/rake_task/000077500000000000000000000000001514115062600203765ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/rake_task/validate.rb000066400000000000000000000046421514115062600225220ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class RakeTask extend Rake::DSL desc "Get the checksum of a graphql-pro version and compare it to published versions on GitHub and graphql-ruby.org" task "graphql:pro:validate", [:gem_version] do |t, args| version = args[:gem_version] if version.nil? raise ArgumentError, "A specific version is required, eg `rake graphql:pro:validate[1.12.0]`" end check = "\e[32m✓\e[0m" ex = "\e[31m✘\e[0m" puts "Validating graphql-pro v#{version}" puts " - Checking for graphql-pro credentials..." creds = `bundle config gems.graphql.pro --parseable`[/[a-z0-9]{11}:[a-z0-9]{11}/] if creds.nil? puts " #{ex} failed, please set with `bundle config gems.graphql.pro $MY_CREDENTIALS`" exit(1) else puts " #{check} found" end puts " - Fetching the gem..." fetch_result = `gem fetch graphql-pro -v #{version} --source https://#{creds}@gems.graphql.pro` if fetch_result.empty? puts " #{ex} failed to fetch v#{version}" exit(1) else puts " #{check} fetched" end puts " - Validating digest..." require "digest/sha2" gem_digest = Digest::SHA512.new.hexdigest(File.read("graphql-pro-#{version}.gem")) require "net/http" github_uri = URI("https://raw.githubusercontent.com/rmosolgo/graphql-ruby/master/guides/pro/checksums/graphql-pro-#{version}.txt") # Remove final newline from .txt file github_digest = Net::HTTP.get(github_uri).chomp docs_uri = URI("https://graphql-ruby.org/pro/checksums/graphql-pro-#{version}.txt") docs_digest = Net::HTTP.get(docs_uri).chomp if docs_digest == gem_digest && github_digest == gem_digest puts " #{check} validated from GitHub" puts " #{check} validated from graphql-ruby.org" else puts " #{ex} SHA mismatch:" puts " Downloaded: #{gem_digest}" puts " GitHub: #{github_digest}" puts " graphql-ruby.org: #{docs_digest}" puts "" puts " This download of graphql-pro is invalid, please open an issue:" puts " https://github.com/rmosolgo/graphql-ruby/issues/new?title=graphql-pro%20digest%20mismatch%20(#{version})" exit(1) end puts "\e[32m✔\e[0m graphql-pro #{version} validated successfully!" end end end graphql-ruby-2.5.19/lib/graphql/relay.rb000066400000000000000000000001011514115062600200630ustar00rootroot00000000000000# frozen_string_literal: true require 'graphql/relay/range_add' graphql-ruby-2.5.19/lib/graphql/relay/000077500000000000000000000000001514115062600175465ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/relay/range_add.rb000066400000000000000000000040621514115062600220010ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Relay # This provides some isolation from `GraphQL::Relay` internals. # # Given a list of items and a new item, it will provide a connection and an edge. # # The connection doesn't receive outside arguments, so the list of items # should be ordered and paginated before providing it here. # # @example Adding a comment to list of comments # post = Post.find(args[:post_id]) # comments = post.comments # new_comment = comments.build(body: args[:body]) # new_comment.save! # # range_add = GraphQL::Relay::RangeAdd.new( # parent: post, # collection: comments, # item: new_comment, # context: context, # ) # # response = { # post: post, # comments_connection: range_add.connection, # new_comment_edge: range_add.edge, # } class RangeAdd attr_reader :edge, :connection, :parent # @param collection [Object] The list of items to wrap in a connection # @param item [Object] The newly-added item (will be wrapped in `edge_class`) # @param context [GraphQL::Query::Context] The surrounding `ctx`, will be passed to the connection # @param parent [Object] The owner of `collection`, will be passed to the connection if provided # @param edge_class [Class] The class to wrap `item` with (defaults to the connection's edge class) def initialize(collection:, item:, context:, parent: nil, edge_class: nil) conn_class = context.schema.connections.wrapper_for(collection) # The rest will be added by ConnectionExtension @connection = conn_class.new(collection, parent: parent, context: context, edge_class: edge_class) # Check if this connection supports it, to support old versions of GraphQL-Pro @edge = if @connection.respond_to?(:range_add_edge) @connection.range_add_edge(item) else @connection.edge_class.new(item, @connection) end @parent = parent end end end end graphql-ruby-2.5.19/lib/graphql/rubocop.rb000066400000000000000000000003671514115062600204360ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/rubocop/graphql/default_null_true" require "graphql/rubocop/graphql/default_required_true" require "graphql/rubocop/graphql/field_type_in_block" require "graphql/rubocop/graphql/root_types_in_block" graphql-ruby-2.5.19/lib/graphql/rubocop/000077500000000000000000000000001514115062600201035ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/rubocop/graphql/000077500000000000000000000000001514115062600215415ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/rubocop/graphql/base_cop.rb000066400000000000000000000024401514115062600236410ustar00rootroot00000000000000# frozen_string_literal: true require "rubocop" module GraphQL module Rubocop module GraphQL class BaseCop < RuboCop::Cop::Base extend RuboCop::Cop::AutoCorrector # Return the source of `send_node`, but without the keyword argument represented by `pair_node` def source_without_keyword_argument(send_node, pair_node) # work back to the preceding comma first_pos = pair_node.location.expression.begin_pos end_pos = pair_node.location.expression.end_pos node_source = send_node.source_range.source node_first_pos = send_node.location.expression.begin_pos relative_first_pos = first_pos - node_first_pos relative_last_pos = end_pos - node_first_pos begin_removal_pos = relative_first_pos while node_source[begin_removal_pos] != "," begin_removal_pos -= 1 if begin_removal_pos < 1 raise "Invariant: somehow backtracked to beginning of node looking for a comma (node source: #{node_source.inspect})" end end end_removal_pos = relative_last_pos cleaned_node_source = node_source[0...begin_removal_pos] + node_source[end_removal_pos..-1] cleaned_node_source end end end end end graphql-ruby-2.5.19/lib/graphql/rubocop/graphql/default_null_true.rb000066400000000000000000000024521514115062600256060ustar00rootroot00000000000000# frozen_string_literal: true require_relative "base_cop" module GraphQL module Rubocop module GraphQL # Identify (and auto-correct) any field configuration which duplicates # the default `null: true` property. # # `null: true` is default because nullable fields can always be converted # to non-null fields (`null: false`) without a breaking change. (The opposite change, from `null: false` # to `null: true`, change.) # # @example # # Both of these define `name: String` in GraphQL: # # # bad # field :name, String, null: true # # # good # field :name, String # class DefaultNullTrue < BaseCop MSG = "`null: true` is the default and can be removed." def_node_matcher :field_config_with_null_true?, <<-Pattern ( send nil? :field ... (hash $(pair (sym :null) (true)) ...) ) Pattern def on_send(node) field_config_with_null_true?(node) do |null_config| add_offense(null_config) do |corrector| cleaned_node_source = source_without_keyword_argument(node, null_config) corrector.replace(node.source_range, cleaned_node_source) end end end end end end end graphql-ruby-2.5.19/lib/graphql/rubocop/graphql/default_required_true.rb000066400000000000000000000025441514115062600264560ustar00rootroot00000000000000# frozen_string_literal: true require_relative "./base_cop" module GraphQL module Rubocop module GraphQL # Identify (and auto-correct) any argument configuration which duplicates # the default `required: true` property. # # `required: true` is default because required arguments can always be converted # to optional arguments (`required: false`) without a breaking change. (The opposite change, from `required: false` # to `required: true`, change.) # # @example # # Both of these define `id: ID!` in GraphQL: # # # bad # argument :id, ID, required: true # # # good # argument :id, ID # class DefaultRequiredTrue < BaseCop MSG = "`required: true` is the default and can be removed." def_node_matcher :argument_config_with_required_true?, <<-Pattern ( send {nil? _} :argument ... (hash <$(pair (sym :required) (true)) ...>) ) Pattern def on_send(node) argument_config_with_required_true?(node) do |required_config| add_offense(required_config) do |corrector| cleaned_node_source = source_without_keyword_argument(node, required_config) corrector.replace(node, cleaned_node_source) end end end end end end end graphql-ruby-2.5.19/lib/graphql/rubocop/graphql/field_type_in_block.rb000066400000000000000000000123371514115062600260600ustar00rootroot00000000000000# frozen_string_literal: true require_relative "./base_cop" module GraphQL module Rubocop module GraphQL # Identify (and auto-correct) any field whose type configuration isn't given # in the configuration block. # # @example # # bad, immediately causes Rails to load `app/graphql/types/thing.rb` # field :thing, Types::Thing # # # good, defers loading until the file is needed # field :thing do # type(Types::Thing) # end # class FieldTypeInBlock < BaseCop MSG = "type configuration can be moved to a block to defer loading the type's file" BUILT_IN_SCALAR_NAMES = ["Float", "Int", "Integer", "String", "ID", "Boolean"] def_node_matcher :field_config_with_inline_type, <<-Pattern ( send {nil? _} :field sym ${const array} ... ) Pattern def_node_matcher :field_config_with_inline_type_and_block, <<-Pattern ( block (send {nil? _} :field sym ${const array} ...) ... (args) _ ) Pattern def on_block(node) ignore_node(node) field_config_with_inline_type_and_block(node) do |type_const| type_const_str = get_type_argument_str(node, type_const) if ignore_inline_type_str?(type_const_str) # Do nothing ... else add_offense(type_const) do |corrector| cleaned_node_source = delete_type_argument(node, type_const) field_indent = determine_field_indent(node) cleaned_node_source.sub!(/(\{|do)/, "\\1\n#{field_indent} type #{type_const_str}") corrector.replace(node, cleaned_node_source) end end end end def on_send(node) return if part_of_ignored_node?(node) field_config_with_inline_type(node) do |type_const| type_const_str = get_type_argument_str(node, type_const) if ignore_inline_type_str?(type_const_str) # Do nothing -- not loading from another file else add_offense(type_const) do |corrector| cleaned_node_source = delete_type_argument(node, type_const) field_indent = determine_field_indent(node) cleaned_node_source += " do\n#{field_indent} type #{type_const_str}\n#{field_indent}end" corrector.replace(node, cleaned_node_source) end end end end private def ignore_inline_type_str?(type_str) if BUILT_IN_SCALAR_NAMES.include?(type_str) true elsif (inner_type_str = type_str.sub(/\[([A-Za-z]+)(, null: (true|false))?\]/, '\1')) && BUILT_IN_SCALAR_NAMES.include?(inner_type_str) true else false end end def get_type_argument_str(send_node, type_const) first_pos = type_const.location.expression.begin_pos end_pos = type_const.location.expression.end_pos node_source = send_node.source_range.source node_first_pos = send_node.location.expression.begin_pos relative_first_pos = first_pos - node_first_pos end_removal_pos = end_pos - node_first_pos node_source[relative_first_pos...end_removal_pos] end def delete_type_argument(send_node, type_const) first_pos = type_const.location.expression.begin_pos end_pos = type_const.location.expression.end_pos node_source = send_node.source_range.source node_first_pos = send_node.location.expression.begin_pos relative_first_pos = first_pos - node_first_pos end_removal_pos = end_pos - node_first_pos begin_removal_pos = relative_first_pos while node_source[begin_removal_pos] != "," begin_removal_pos -= 1 if begin_removal_pos < 1 raise "Invariant: somehow backtracked to beginning of node looking for a comma (node source: #{node_source.inspect})" end end node_source[0...begin_removal_pos] + node_source[end_removal_pos..-1] end def determine_field_indent(send_node) type_defn_node = send_node while (type_defn_node && !(type_defn_node.class_definition? || type_defn_node.module_definition?)) type_defn_node = type_defn_node.parent end if type_defn_node.nil? raise "Invariant: Something went wrong in GraphQL-Ruby, couldn't find surrounding class definition for field (#{send_node}).\n\nPlease report this error on GitHub." end type_defn_source = type_defn_node.source indent_test_idx = send_node.location.expression.begin_pos - type_defn_node.source_range.begin_pos - 1 field_indent = "".dup while type_defn_source[indent_test_idx] == " " field_indent << " " indent_test_idx -= 1 if indent_test_idx == 0 raise "Invariant: somehow backtracted to beginning of class when looking for field indent (source: #{node_source.inspect})" end end field_indent end end end end end graphql-ruby-2.5.19/lib/graphql/rubocop/graphql/root_types_in_block.rb000066400000000000000000000021621514115062600261360ustar00rootroot00000000000000# frozen_string_literal: true require_relative "./base_cop" module GraphQL module Rubocop module GraphQL # Identify (and auto-correct) any root types in your schema file. # # @example # # bad, immediately causes Rails to load `app/graphql/types/query.rb` # query Types::Query # # # good, defers loading until the file is needed # query { Types::Query } # class RootTypesInBlock < BaseCop MSG = "type configuration can be moved to a block to defer loading the type's file" def_node_matcher :root_type_config_without_block, <<-Pattern ( send nil? {:query :mutation :subscription} const ) Pattern def on_send(node) root_type_config_without_block(node) do add_offense(node) do |corrector| new_node_source = node.source_range.source new_node_source.sub!(/(query|mutation|subscription)/, '\1 {') new_node_source << " }" corrector.replace(node, new_node_source) end end end end end end end graphql-ruby-2.5.19/lib/graphql/runtime_type_error.rb000066400000000000000000000001411514115062600227100ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class RuntimeTypeError < GraphQL::Error end end graphql-ruby-2.5.19/lib/graphql/schema.rb000066400000000000000000002477131514115062600202350ustar00rootroot00000000000000# frozen_string_literal: true require "logger" require "graphql/schema/addition" require "graphql/schema/always_visible" require "graphql/schema/base_64_encoder" require "graphql/schema/find_inherited_value" require "graphql/schema/finder" require "graphql/schema/introspection_system" require "graphql/schema/late_bound_type" require "graphql/schema/ractor_shareable" require "graphql/schema/timeout" require "graphql/schema/type_expression" require "graphql/schema/unique_within_type" require "graphql/schema/warden" require "graphql/schema/build_from_definition" require "graphql/schema/validator" require "graphql/schema/member" require "graphql/schema/wrapper" require "graphql/schema/list" require "graphql/schema/non_null" require "graphql/schema/argument" require "graphql/schema/enum_value" require "graphql/schema/enum" require "graphql/schema/field_extension" require "graphql/schema/field" require "graphql/schema/input_object" require "graphql/schema/interface" require "graphql/schema/scalar" require "graphql/schema/object" require "graphql/schema/union" require "graphql/schema/directive" require "graphql/schema/directive/deprecated" require "graphql/schema/directive/include" require "graphql/schema/directive/one_of" require "graphql/schema/directive/skip" require "graphql/schema/directive/feature" require "graphql/schema/directive/flagged" require "graphql/schema/directive/transform" require "graphql/schema/directive/specified_by" require "graphql/schema/type_membership" require "graphql/schema/resolver" require "graphql/schema/mutation" require "graphql/schema/has_single_input_argument" require "graphql/schema/relay_classic_mutation" require "graphql/schema/subscription" require "graphql/schema/visibility" module GraphQL # A GraphQL schema which may be queried with {GraphQL::Query}. # # The {Schema} contains: # # - types for exposing your application # - query analyzers for assessing incoming queries (including max depth & max complexity restrictions) # - execution strategies for running incoming queries # # Schemas start with root types, {Schema#query}, {Schema#mutation} and {Schema#subscription}. # The schema will traverse the tree of fields & types, using those as starting points. # Any undiscoverable types may be provided with the `types` configuration. # # Schemas can restrict large incoming queries with `max_depth` and `max_complexity` configurations. # (These configurations can be overridden by specific calls to {Schema.execute}) # # @example defining a schema # class MySchema < GraphQL::Schema # query QueryType # # If types are only connected by way of interfaces, they must be added here # orphan_types ImageType, AudioType # end # class Schema extend GraphQL::Schema::Member::HasAstNode extend GraphQL::Schema::FindInheritedValue extend Autoload autoload :BUILT_IN_TYPES, "graphql/schema/built_in_types" class DuplicateNamesError < GraphQL::Error attr_reader :duplicated_name def initialize(duplicated_name:, duplicated_definition_1:, duplicated_definition_2:) @duplicated_name = duplicated_name super( "Found two visible definitions for `#{duplicated_name}`: #{duplicated_definition_1}, #{duplicated_definition_2}" ) end end class UnresolvedLateBoundTypeError < GraphQL::Error attr_reader :type def initialize(type:) @type = type super("Late bound type was never found: #{type.inspect}") end end # Error that is raised when [#Schema#from_definition] is passed an invalid schema definition string. class InvalidDocumentError < Error; end; class << self # Create schema with the result of an introspection query. # @param introspection_result [Hash] A response from {GraphQL::Introspection::INTROSPECTION_QUERY} # @return [Class] the schema described by `input` def from_introspection(introspection_result) GraphQL::Schema::Loader.load(introspection_result) end # Create schema from an IDL schema or file containing an IDL definition. # @param definition_or_path [String] A schema definition string, or a path to a file containing the definition # @param default_resolve [<#call(type, field, obj, args, ctx)>] A callable for handling field resolution # @param parser [Object] An object for handling definition string parsing (must respond to `parse`) # @param using [Hash] Plugins to attach to the created schema with `use(key, value)` # @return [Class] the schema described by `document` def from_definition(definition_or_path, default_resolve: nil, parser: GraphQL.default_parser, using: {}, base_types: {}) # If the file ends in `.graphql` or `.graphqls`, treat it like a filepath if definition_or_path.end_with?(".graphql") || definition_or_path.end_with?(".graphqls") GraphQL::Schema::BuildFromDefinition.from_definition_path( self, definition_or_path, default_resolve: default_resolve, parser: parser, using: using, base_types: base_types, ) else GraphQL::Schema::BuildFromDefinition.from_definition( self, definition_or_path, default_resolve: default_resolve, parser: parser, using: using, base_types: base_types, ) end end def deprecated_graphql_definition graphql_definition(silence_deprecation_warning: true) end # @return [GraphQL::Subscriptions] def subscriptions(inherited: true) defined?(@subscriptions) ? @subscriptions : (inherited ? find_inherited_value(:subscriptions, nil) : nil) end def subscriptions=(new_implementation) @subscriptions = new_implementation end # @param new_mode [Symbol] If configured, this will be used when `context: { trace_mode: ... }` isn't set. def default_trace_mode(new_mode = NOT_CONFIGURED) if !NOT_CONFIGURED.equal?(new_mode) @default_trace_mode = new_mode elsif defined?(@default_trace_mode) && !@default_trace_mode.nil? # This `nil?` check seems necessary because of # Ractors silently initializing @default_trace_mode somehow @default_trace_mode elsif superclass.respond_to?(:default_trace_mode) superclass.default_trace_mode else :default end end def trace_class(new_class = nil) if new_class # If any modules were already added for `:default`, # re-apply them here mods = trace_modules_for(:default) mods.each { |mod| new_class.include(mod) } new_class.include(DefaultTraceClass) trace_mode(:default, new_class) end trace_class_for(:default, build: true) end # @return [Class] Return the trace class to use for this mode, looking one up on the superclass if this Schema doesn't have one defined. def trace_class_for(mode, build: false) if (trace_class = own_trace_modes[mode]) trace_class elsif superclass.respond_to?(:trace_class_for) && (trace_class = superclass.trace_class_for(mode, build: false)) trace_class elsif build own_trace_modes[mode] = build_trace_mode(mode) else nil end end # Configure `trace_class` to be used whenever `context: { trace_mode: mode_name }` is requested. # {default_trace_mode} is used when no `trace_mode: ...` is requested. # # When a `trace_class` is added this way, it will _not_ receive other modules added with `trace_with(...)` # unless `trace_mode` is explicitly given. (This class will not receive any default trace modules.) # # Subclasses of the schema will use `trace_class` as a base class for this mode and those # subclass also will _not_ receive default tracing modules. # # @param mode_name [Symbol] # @param trace_class [Class] subclass of GraphQL::Tracing::Trace # @return void def trace_mode(mode_name, trace_class) own_trace_modes[mode_name] = trace_class nil end def own_trace_modes @own_trace_modes ||= {} end def build_trace_mode(mode) case mode when :default # Use the superclass's default mode if it has one, or else start an inheritance chain at the built-in base class. base_class = (superclass.respond_to?(:trace_class_for) && superclass.trace_class_for(mode, build: true)) || GraphQL::Tracing::Trace const_set(:DefaultTrace, Class.new(base_class) do include DefaultTraceClass end) else # First, see if the superclass has a custom-defined class for this. # Then, if it doesn't, use this class's default trace base_class = (superclass.respond_to?(:trace_class_for) && superclass.trace_class_for(mode)) || trace_class_for(:default, build: true) # Prepare the default trace class if it hasn't been initialized yet base_class ||= (own_trace_modes[:default] = build_trace_mode(:default)) mods = trace_modules_for(mode) if base_class < DefaultTraceClass mods = trace_modules_for(:default) + mods end # Copy the existing default options into this mode's options default_options = trace_options_for(:default) add_trace_options_for(mode, default_options) Class.new(base_class) do !mods.empty? && include(*mods) end end end def own_trace_modules @own_trace_modules ||= Hash.new { |h, k| h[k] = [] } end # @return [Array] Modules added for tracing in `trace_mode`, including inherited ones def trace_modules_for(trace_mode) modules = own_trace_modules[trace_mode] if superclass.respond_to?(:trace_modules_for) modules += superclass.trace_modules_for(trace_mode) end modules end # Returns the JSON response of {Introspection::INTROSPECTION_QUERY}. # @see #as_json Return a Hash representation of the schema # @return [String] def to_json(**args) JSON.pretty_generate(as_json(**args)) end # Return the Hash response of {Introspection::INTROSPECTION_QUERY}. # @param context [Hash] # @param include_deprecated_args [Boolean] If true, deprecated arguments will be included in the JSON response # @param include_schema_description [Boolean] If true, the schema's description will be queried and included in the response # @param include_is_repeatable [Boolean] If true, `isRepeatable: true|false` will be included with the schema's directives # @param include_specified_by_url [Boolean] If true, scalar types' `specifiedByUrl:` will be included in the response # @param include_is_one_of [Boolean] If true, `isOneOf: true|false` will be included with input objects # @return [Hash] GraphQL result def as_json(context: {}, include_deprecated_args: true, include_schema_description: false, include_is_repeatable: false, include_specified_by_url: false, include_is_one_of: false) introspection_query = Introspection.query( include_deprecated_args: include_deprecated_args, include_schema_description: include_schema_description, include_is_repeatable: include_is_repeatable, include_is_one_of: include_is_one_of, include_specified_by_url: include_specified_by_url, ) execute(introspection_query, context: context).to_h end # Return the GraphQL IDL for the schema # @param context [Hash] # @return [String] def to_definition(context: {}) GraphQL::Schema::Printer.print_schema(self, context: context) end # Return the GraphQL::Language::Document IDL AST for the schema # @return [GraphQL::Language::Document] def to_document GraphQL::Language::DocumentFromSchemaDefinition.new(self).document end # @return [String, nil] def description(new_description = nil) if new_description @description = new_description elsif defined?(@description) @description else find_inherited_value(:description, nil) end end def find(path) if !@finder @find_cache = {} @finder ||= GraphQL::Schema::Finder.new(self) end @find_cache[path] ||= @finder.find(path) end def static_validator GraphQL::StaticValidation::Validator.new(schema: self) end # Add `plugin` to this schema # @param plugin [#use] A Schema plugin # @return void def use(plugin, **kwargs) if !kwargs.empty? plugin.use(self, **kwargs) else plugin.use(self) end own_plugins << [plugin, kwargs] end def plugins find_inherited_value(:plugins, EMPTY_ARRAY) + own_plugins end # Build a map of `{ name => type }` and return it # @return [Hash Class>] A dictionary of type classes by their GraphQL name # @see get_type Which is more efficient for finding _one type_ by name, because it doesn't merge hashes. def types(context = GraphQL::Query::NullContext.instance) if use_visibility_profile? types = Visibility::Profile.from_context(context, self) return types.all_types_h end all_types = non_introspection_types.merge(introspection_system.types) visible_types = {} all_types.each do |k, v| visible_types[k] =if v.is_a?(Array) visible_t = nil v.each do |t| if t.visible?(context) if visible_t.nil? visible_t = t else raise DuplicateNamesError.new( duplicated_name: k, duplicated_definition_1: visible_t.inspect, duplicated_definition_2: t.inspect ) end end end visible_t else v end end visible_types end # @param type_name [String] # @param context [GraphQL::Query::Context] Used for filtering definitions at query-time # @param use_visibility_profile Private, for migration to {Schema::Visibility} # @return [Module, nil] A type, or nil if there's no type called `type_name` def get_type(type_name, context = GraphQL::Query::NullContext.instance, use_visibility_profile = use_visibility_profile?) if use_visibility_profile profile = Visibility::Profile.from_context(context, self) return profile.type(type_name) end local_entry = own_types[type_name] type_defn = case local_entry when nil nil when Array if context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Profile) local_entry else visible_t = nil warden = Warden.from_context(context) local_entry.each do |t| if warden.visible_type?(t, context) if visible_t.nil? visible_t = t else raise DuplicateNamesError.new( duplicated_name: type_name, duplicated_definition_1: visible_t.inspect, duplicated_definition_2: t.inspect ) end end end visible_t end when Module local_entry else raise "Invariant: unexpected own_types[#{type_name.inspect}]: #{local_entry.inspect}" end type_defn || introspection_system.types[type_name] || # todo context-specific introspection? (superclass.respond_to?(:get_type) ? superclass.get_type(type_name, context, use_visibility_profile) : nil) end # @return [Boolean] Does this schema have _any_ definition for a type named `type_name`, regardless of visibility? def has_defined_type?(type_name) own_types.key?(type_name) || introspection_system.types.key?(type_name) || (superclass.respond_to?(:has_defined_type?) ? superclass.has_defined_type?(type_name) : false) end # @api private attr_writer :connections # @return [GraphQL::Pagination::Connections] if installed def connections if defined?(@connections) @connections else inherited_connections = find_inherited_value(:connections, nil) # This schema is part of an inheritance chain which is using new connections, # make a new instance, so we don't pollute the upstream one. if inherited_connections @connections = Pagination::Connections.new(schema: self) else nil end end end # Get or set the root `query { ... }` object for this schema. # # @example Using `Types::Query` as the entry-point # query { Types::Query } # # @param new_query_object [Class] The root type to use for queries # @param lazy_load_block If a block is given, then it will be called when GraphQL-Ruby needs the root query type. # @return [Class, nil] The configured query root type, if there is one. def query(new_query_object = nil, &lazy_load_block) if new_query_object || block_given? if @query_object dup_defn = new_query_object || yield raise GraphQL::Error, "Second definition of `query(...)` (#{dup_defn.inspect}) is invalid, already configured with #{@query_object.inspect}" elsif use_visibility_profile? if block_given? if visibility.preload? @query_object = lazy_load_block.call self.visibility.query_configured(@query_object) else @query_object = lazy_load_block end else @query_object = new_query_object self.visibility.query_configured(@query_object) end else @query_object = new_query_object || lazy_load_block.call add_type_and_traverse(@query_object, root: true) end nil elsif @query_object.is_a?(Proc) @query_object = @query_object.call self.visibility&.query_configured(@query_object) @query_object else @query_object || find_inherited_value(:query) end end # Get or set the root `mutation { ... }` object for this schema. # # @example Using `Types::Mutation` as the entry-point # mutation { Types::Mutation } # # @param new_mutation_object [Class] The root type to use for mutations # @param lazy_load_block If a block is given, then it will be called when GraphQL-Ruby needs the root mutation type. # @return [Class, nil] The configured mutation root type, if there is one. def mutation(new_mutation_object = nil, &lazy_load_block) if new_mutation_object || block_given? if @mutation_object dup_defn = new_mutation_object || yield raise GraphQL::Error, "Second definition of `mutation(...)` (#{dup_defn.inspect}) is invalid, already configured with #{@mutation_object.inspect}" elsif use_visibility_profile? if block_given? if visibility.preload? @mutation_object = lazy_load_block.call self.visibility.mutation_configured(@mutation_object) else @mutation_object = lazy_load_block end else @mutation_object = new_mutation_object self.visibility.mutation_configured(@mutation_object) end else @mutation_object = new_mutation_object || lazy_load_block.call add_type_and_traverse(@mutation_object, root: true) end nil elsif @mutation_object.is_a?(Proc) @mutation_object = @mutation_object.call self.visibility&.mutation_configured(@mutation_object) @mutation_object else @mutation_object || find_inherited_value(:mutation) end end # Get or set the root `subscription { ... }` object for this schema. # # @example Using `Types::Subscription` as the entry-point # subscription { Types::Subscription } # # @param new_subscription_object [Class] The root type to use for subscriptions # @param lazy_load_block If a block is given, then it will be called when GraphQL-Ruby needs the root subscription type. # @return [Class, nil] The configured subscription root type, if there is one. def subscription(new_subscription_object = nil, &lazy_load_block) if new_subscription_object || block_given? if @subscription_object dup_defn = new_subscription_object || yield raise GraphQL::Error, "Second definition of `subscription(...)` (#{dup_defn.inspect}) is invalid, already configured with #{@subscription_object.inspect}" elsif use_visibility_profile? if block_given? if visibility.preload? @subscription_object = lazy_load_block.call visibility.subscription_configured(@subscription_object) else @subscription_object = lazy_load_block end else @subscription_object = new_subscription_object self.visibility.subscription_configured(@subscription_object) end add_subscription_extension_if_necessary else @subscription_object = new_subscription_object || lazy_load_block.call add_subscription_extension_if_necessary add_type_and_traverse(@subscription_object, root: true) end nil elsif @subscription_object.is_a?(Proc) @subscription_object = @subscription_object.call add_subscription_extension_if_necessary self.visibility.subscription_configured(@subscription_object) @subscription_object else @subscription_object || find_inherited_value(:subscription) end end # @api private def root_type_for_operation(operation) case operation when "query" query when "mutation" mutation when "subscription" subscription else raise ArgumentError, "unknown operation type: #{operation}" end end # @return [Array] The root types (query, mutation, subscription) defined for this schema def root_types if use_visibility_profile? [query, mutation, subscription].compact else @root_types end end # @api private def warden_class if defined?(@warden_class) @warden_class elsif superclass.respond_to?(:warden_class) superclass.warden_class else GraphQL::Schema::Warden end end # @api private attr_writer :warden_class # @api private def visibility_profile_class if defined?(@visibility_profile_class) @visibility_profile_class elsif superclass.respond_to?(:visibility_profile_class) superclass.visibility_profile_class else GraphQL::Schema::Visibility::Profile end end # @api private attr_writer :visibility_profile_class, :use_visibility_profile # @api private attr_accessor :visibility # @api private def use_visibility_profile? if defined?(@use_visibility_profile) @use_visibility_profile elsif superclass.respond_to?(:use_visibility_profile?) superclass.use_visibility_profile? else false end end # @param type [Module] The type definition whose possible types you want to see # @param context [GraphQL::Query::Context] used for filtering visible possible types at runtime # @param use_visibility_profile Private, for migration to {Schema::Visibility} # @return [Hash] All possible types, if no `type` is given. # @return [Array] Possible types for `type`, if it's given. def possible_types(type = nil, context = GraphQL::Query::NullContext.instance, use_visibility_profile = use_visibility_profile?) if use_visibility_profile if type return Visibility::Profile.from_context(context, self).possible_types(type) else raise "Schema.possible_types is not implemented for `use_visibility_profile?`" end end if type # TODO duck-typing `.possible_types` would probably be nicer here if type.kind.union? type.possible_types(context: context) else stored_possible_types = own_possible_types[type] visible_possible_types = if stored_possible_types && type.kind.interface? stored_possible_types.select do |possible_type| possible_type.interfaces(context).include?(type) end else stored_possible_types end visible_possible_types || introspection_system.possible_types[type] || ( superclass.respond_to?(:possible_types) ? superclass.possible_types(type, context, use_visibility_profile) : EMPTY_ARRAY ) end else find_inherited_value(:possible_types, EMPTY_HASH) .merge(own_possible_types) .merge(introspection_system.possible_types) end end def union_memberships(type = nil) if type own_um = own_union_memberships.fetch(type.graphql_name, EMPTY_ARRAY) inherited_um = find_inherited_value(:union_memberships, EMPTY_HASH).fetch(type.graphql_name, EMPTY_ARRAY) own_um + inherited_um else joined_um = own_union_memberships.dup find_inherited_value(:union_memberhips, EMPTY_HASH).each do |k, v| um = joined_um[k] ||= [] um.concat(v) end joined_um end end # @api private # @see GraphQL::Dataloader def dataloader_class @dataloader_class || GraphQL::Dataloader::NullDataloader end attr_writer :dataloader_class def references_to(to_type = nil, from: nil) if to_type if from refs = own_references_to[to_type] ||= [] refs << from else get_references_to(to_type) || EMPTY_ARRAY end else # `@own_references_to` can be quite large for big schemas, # and generally speaking, we won't inherit any values. # So optimize the most common case -- don't create a duplicate Hash. inherited_value = find_inherited_value(:references_to, EMPTY_HASH) if !inherited_value.empty? inherited_value.merge(own_references_to) else own_references_to end end end def type_from_ast(ast_node, context: self.query_class.new(self, "{ __typename }").context) GraphQL::Schema::TypeExpression.build_type(context.query.types, ast_node) end def get_field(type_or_name, field_name, context = GraphQL::Query::NullContext.instance, use_visibility_profile = use_visibility_profile?) if use_visibility_profile profile = Visibility::Profile.from_context(context, self) parent_type = case type_or_name when String profile.type(type_or_name) when Module type_or_name when LateBoundType profile.type(type_or_name.name) else raise GraphQL::InvariantError, "Unexpected field owner for #{field_name.inspect}: #{type_or_name.inspect} (#{type_or_name.class})" end return profile.field(parent_type, field_name) end parent_type = case type_or_name when LateBoundType get_type(type_or_name.name, context) when String get_type(type_or_name, context) when Module type_or_name else raise GraphQL::InvariantError, "Unexpected field owner for #{field_name.inspect}: #{type_or_name.inspect} (#{type_or_name.class})" end if parent_type.kind.fields? && (field = parent_type.get_field(field_name, context)) field elsif parent_type == query && (entry_point_field = introspection_system.entry_point(name: field_name)) entry_point_field elsif (dynamic_field = introspection_system.dynamic_field(name: field_name)) dynamic_field else nil end end def get_fields(type, context = GraphQL::Query::NullContext.instance) type.fields(context) end # Pass a custom introspection module here to use it for this schema. # @param new_introspection_namespace [Module] If given, use this module for custom introspection on the schema # @return [Module, nil] The configured namespace, if there is one def introspection(new_introspection_namespace = nil) if new_introspection_namespace @introspection = new_introspection_namespace # reset this cached value: @introspection_system = nil introspection_system @introspection else @introspection || find_inherited_value(:introspection) end end # @return [Schema::IntrospectionSystem] Based on {introspection} def introspection_system if !@introspection_system @introspection_system = Schema::IntrospectionSystem.new(self) @introspection_system.resolve_late_bindings self.visibility&.introspection_system_configured(@introspection_system) end @introspection_system end def cursor_encoder(new_encoder = nil) if new_encoder @cursor_encoder = new_encoder end @cursor_encoder || find_inherited_value(:cursor_encoder, Base64Encoder) end def default_max_page_size(new_default_max_page_size = nil) if new_default_max_page_size @default_max_page_size = new_default_max_page_size else @default_max_page_size || find_inherited_value(:default_max_page_size) end end # A limit on the number of tokens to accept on incoming query strings. # Use this to prevent parsing maliciously-large query strings. # @return [nil, Integer] def max_query_string_tokens(new_max_tokens = NOT_CONFIGURED) if NOT_CONFIGURED.equal?(new_max_tokens) defined?(@max_query_string_tokens) ? @max_query_string_tokens : find_inherited_value(:max_query_string_tokens) else @max_query_string_tokens = new_max_tokens end end def default_page_size(new_default_page_size = nil) if new_default_page_size @default_page_size = new_default_page_size else @default_page_size || find_inherited_value(:default_page_size) end end def query_execution_strategy(new_query_execution_strategy = nil, deprecation_warning: true) if deprecation_warning warn "GraphQL::Schema.query_execution_strategy is deprecated without replacement. Use `GraphQL::Query.new` directly to create and execute a custom query instead." warn " #{caller(1, 1).first}" end if new_query_execution_strategy @query_execution_strategy = new_query_execution_strategy else @query_execution_strategy || (superclass.respond_to?(:query_execution_strategy) ? superclass.query_execution_strategy(deprecation_warning: false) : self.default_execution_strategy) end end def mutation_execution_strategy(new_mutation_execution_strategy = nil, deprecation_warning: true) if deprecation_warning warn "GraphQL::Schema.mutation_execution_strategy is deprecated without replacement. Use `GraphQL::Query.new` directly to create and execute a custom query instead." warn " #{caller(1, 1).first}" end if new_mutation_execution_strategy @mutation_execution_strategy = new_mutation_execution_strategy else @mutation_execution_strategy || (superclass.respond_to?(:mutation_execution_strategy) ? superclass.mutation_execution_strategy(deprecation_warning: false) : self.default_execution_strategy) end end def subscription_execution_strategy(new_subscription_execution_strategy = nil, deprecation_warning: true) if deprecation_warning warn "GraphQL::Schema.subscription_execution_strategy is deprecated without replacement. Use `GraphQL::Query.new` directly to create and execute a custom query instead." warn " #{caller(1, 1).first}" end if new_subscription_execution_strategy @subscription_execution_strategy = new_subscription_execution_strategy else @subscription_execution_strategy || (superclass.respond_to?(:subscription_execution_strategy) ? superclass.subscription_execution_strategy(deprecation_warning: false) : self.default_execution_strategy) end end attr_writer :validate_timeout def validate_timeout(new_validate_timeout = NOT_CONFIGURED) if !NOT_CONFIGURED.equal?(new_validate_timeout) @validate_timeout = new_validate_timeout elsif defined?(@validate_timeout) @validate_timeout else find_inherited_value(:validate_timeout) || 3 end end # Validate a query string according to this schema. # @param string_or_document [String, GraphQL::Language::Nodes::Document] # @return [Array] def validate(string_or_document, rules: nil, context: nil) doc = if string_or_document.is_a?(String) GraphQL.parse(string_or_document) else string_or_document end query = query_class.new(self, document: doc, context: context) validator_opts = { schema: self } rules && (validator_opts[:rules] = rules) validator = GraphQL::StaticValidation::Validator.new(**validator_opts) res = validator.validate(query, timeout: validate_timeout, max_errors: validate_max_errors) res[:errors] end # @param new_query_class [Class] A subclass to use when executing queries def query_class(new_query_class = NOT_CONFIGURED) if NOT_CONFIGURED.equal?(new_query_class) @query_class || (superclass.respond_to?(:query_class) ? superclass.query_class : GraphQL::Query) else @query_class = new_query_class end end attr_writer :validate_max_errors def validate_max_errors(new_validate_max_errors = NOT_CONFIGURED) if NOT_CONFIGURED.equal?(new_validate_max_errors) defined?(@validate_max_errors) ? @validate_max_errors : find_inherited_value(:validate_max_errors) else @validate_max_errors = new_validate_max_errors end end attr_writer :max_complexity def max_complexity(max_complexity = nil, count_introspection_fields: true) if max_complexity @max_complexity = max_complexity @max_complexity_count_introspection_fields = count_introspection_fields elsif defined?(@max_complexity) @max_complexity else find_inherited_value(:max_complexity) end end def max_complexity_count_introspection_fields if defined?(@max_complexity_count_introspection_fields) @max_complexity_count_introspection_fields else find_inherited_value(:max_complexity_count_introspection_fields, true) end end attr_writer :analysis_engine def analysis_engine @analysis_engine || find_inherited_value(:analysis_engine, self.default_analysis_engine) end def error_bubbling(new_error_bubbling = nil) if !new_error_bubbling.nil? warn("error_bubbling(#{new_error_bubbling.inspect}) is deprecated; the default value of `false` will be the only option in GraphQL-Ruby 3.0") @error_bubbling = new_error_bubbling else @error_bubbling.nil? ? find_inherited_value(:error_bubbling) : @error_bubbling end end attr_writer :error_bubbling attr_writer :max_depth def max_depth(new_max_depth = nil, count_introspection_fields: true) if new_max_depth @max_depth = new_max_depth @count_introspection_fields = count_introspection_fields elsif defined?(@max_depth) @max_depth else find_inherited_value(:max_depth) end end def count_introspection_fields if defined?(@count_introspection_fields) @count_introspection_fields else find_inherited_value(:count_introspection_fields, true) end end def disable_introspection_entry_points @disable_introspection_entry_points = true # TODO: this clears the cache made in `def types`. But this is not a great solution. @introspection_system = nil end def disable_schema_introspection_entry_point @disable_schema_introspection_entry_point = true # TODO: this clears the cache made in `def types`. But this is not a great solution. @introspection_system = nil end def disable_type_introspection_entry_point @disable_type_introspection_entry_point = true # TODO: this clears the cache made in `def types`. But this is not a great solution. @introspection_system = nil end def disable_introspection_entry_points? if instance_variable_defined?(:@disable_introspection_entry_points) @disable_introspection_entry_points else find_inherited_value(:disable_introspection_entry_points?, false) end end def disable_schema_introspection_entry_point? if instance_variable_defined?(:@disable_schema_introspection_entry_point) @disable_schema_introspection_entry_point else find_inherited_value(:disable_schema_introspection_entry_point?, false) end end def disable_type_introspection_entry_point? if instance_variable_defined?(:@disable_type_introspection_entry_point) @disable_type_introspection_entry_point else find_inherited_value(:disable_type_introspection_entry_point?, false) end end # @param new_extra_types [Module] Type definitions to include in printing and introspection, even though they aren't referenced in the schema # @return [Array] Type definitions added to this schema def extra_types(*new_extra_types) if !new_extra_types.empty? new_extra_types = new_extra_types.flatten @own_extra_types ||= [] @own_extra_types.concat(new_extra_types) end inherited_et = find_inherited_value(:extra_types, nil) if inherited_et if @own_extra_types inherited_et + @own_extra_types else inherited_et end else @own_extra_types || EMPTY_ARRAY end end # Tell the schema about these types so that they can be registered as implementations of interfaces in the schema. # # This method must be used when an object type is connected to the schema as an interface implementor but # not as a return type of a field. In that case, if the object type isn't registered here, GraphQL-Ruby won't be able to find it. # # @param new_orphan_types [Array>] Object types to register as implementations of interfaces in the schema. # @return [Array>] All previously-registered orphan types for this schema def orphan_types(*new_orphan_types) if !new_orphan_types.empty? new_orphan_types = new_orphan_types.flatten non_object_types = new_orphan_types.reject { |ot| ot.is_a?(Class) && ot < GraphQL::Schema::Object } if !non_object_types.empty? raise ArgumentError, <<~ERR Only object type classes should be added as `orphan_types(...)`. - Remove these no-op types from `orphan_types`: #{non_object_types.map { |t| "#{t.inspect} (#{t.kind.name})"}.join(", ")} - See https://graphql-ruby.org/type_definitions/interfaces.html#orphan-types To add other types to your schema, you might want `extra_types`: https://graphql-ruby.org/schema/definition.html#extra-types ERR end add_type_and_traverse(new_orphan_types, root: false) unless use_visibility_profile? own_orphan_types.concat(new_orphan_types.flatten) self.visibility&.orphan_types_configured(new_orphan_types) end inherited_ot = find_inherited_value(:orphan_types, nil) if inherited_ot if !own_orphan_types.empty? inherited_ot + own_orphan_types else inherited_ot end else own_orphan_types end end def default_execution_strategy if superclass <= GraphQL::Schema superclass.default_execution_strategy else @default_execution_strategy ||= GraphQL::Execution::Interpreter end end def default_analysis_engine if superclass <= GraphQL::Schema superclass.default_analysis_engine else @default_analysis_engine ||= GraphQL::Analysis::AST end end # @param new_default_logger [#log] Something to use for logging messages def default_logger(new_default_logger = NOT_CONFIGURED) if NOT_CONFIGURED.equal?(new_default_logger) if defined?(@default_logger) @default_logger elsif superclass.respond_to?(:default_logger) superclass.default_logger elsif defined?(Rails) && Rails.respond_to?(:logger) && (rails_logger = Rails.logger) rails_logger else def_logger = Logger.new($stdout) def_logger.info! # It doesn't output debug info by default def_logger end elsif new_default_logger == nil @default_logger = Logger.new(IO::NULL) else @default_logger = new_default_logger end end # @param context [GraphQL::Query::Context, nil] # @return [Logger] A logger to use for this context configuration, falling back to {.default_logger} def logger_for(context) if context && context[:logger] == false Logger.new(IO::NULL) elsif context && (l = context[:logger]) l else default_logger end end # @param new_context_class [Class] A subclass to use when executing queries def context_class(new_context_class = nil) if new_context_class @context_class = new_context_class else @context_class || find_inherited_value(:context_class, GraphQL::Query::Context) end end # Register a handler for errors raised during execution. The handlers can return a new value or raise a new error. # # @example Handling "not found" with a client-facing error # rescue_from(ActiveRecord::NotFound) { raise GraphQL::ExecutionError, "An object could not be found" } # # @param err_classes [Array] Classes which should be rescued by `handler_block` # @param handler_block The code to run when one of those errors is raised during execution # @yieldparam error [StandardError] An instance of one of the configured `err_classes` # @yieldparam object [Object] The current application object in the query when the error was raised # @yieldparam arguments [GraphQL::Query::Arguments] The current field arguments when the error was raised # @yieldparam context [GraphQL::Query::Context] The context for the currently-running operation # @yieldreturn [Object] Some object to use in the place where this error was raised # @raise [GraphQL::ExecutionError] In the handler, raise to add a client-facing error to the response # @raise [StandardError] In the handler, raise to crash the query with a developer-facing error def rescue_from(*err_classes, &handler_block) err_classes.each do |err_class| Execution::Errors.register_rescue_from(err_class, error_handlers[:subclass_handlers], handler_block) end end def error_handlers @error_handlers ||= begin new_handler_hash = ->(h, k) { h[k] = { class: k, handler: nil, subclass_handlers: Hash.new(&new_handler_hash), } } { class: nil, handler: nil, subclass_handlers: Hash.new(&new_handler_hash), } end end # @api private attr_accessor :using_backtrace # @api private def handle_or_reraise(context, err) handler = Execution::Errors.find_handler_for(self, err.class) if handler obj = context[:current_object] args = context[:current_arguments] args = args && args.respond_to?(:keyword_arguments) ? args.keyword_arguments : nil field = context[:current_field] if obj.is_a?(GraphQL::Schema::Object) obj = obj.object end handler[:handler].call(err, obj, args, context, field) else if (context[:backtrace] || using_backtrace) && !err.is_a?(GraphQL::ExecutionError) err = GraphQL::Backtrace::TracedError.new(err, context) end raise err end end # rubocop:disable Lint/DuplicateMethods module ResolveTypeWithType def resolve_type(type, obj, ctx) maybe_lazy_resolve_type_result = if type.is_a?(Module) && type.respond_to?(:resolve_type) type.resolve_type(obj, ctx) else super end after_lazy(maybe_lazy_resolve_type_result) do |resolve_type_result| if resolve_type_result.is_a?(Array) && resolve_type_result.size == 2 resolved_type = resolve_type_result[0] resolved_value = resolve_type_result[1] else resolved_type = resolve_type_result resolved_value = obj end if resolved_type.nil? || (resolved_type.is_a?(Module) && resolved_type.respond_to?(:kind)) [resolved_type, resolved_value] else raise ".resolve_type should return a type definition, but got #{resolved_type.inspect} (#{resolved_type.class}) from `resolve_type(#{type}, #{obj}, #{ctx})`" end end end end # GraphQL-Ruby calls this method during execution when it needs the application to determine the type to use for an object. # # Usually, this object was returned from a field whose return type is an {GraphQL::Schema::Interface} or a {GraphQL::Schema::Union}. # But this method is called in other cases, too -- for example, when {GraphQL::Schema::Argument#loads} cases an object to be directly loaded from the database. # # @example Returning a GraphQL type based on the object's class name # class MySchema < GraphQL::Schema # def resolve_type(_abs_type, object, _context) # graphql_type_name = "Types::#{object.class.name}Type" # graphql_type_name.constantize # If this raises a NameError, then come implement special cases in this method # end # end # @param abstract_type [Class, Module, nil] The Interface or Union type which is being resolved, if there is one # @param application_object [Object] The object returned from a field whose type must be determined # @param context [GraphQL::Query::Context] The query context for the currently-executing query # @return [Class GraphQL::Schema::Directive::Include, "skip" => GraphQL::Schema::Directive::Skip, "deprecated" => GraphQL::Schema::Directive::Deprecated, "oneOf" => GraphQL::Schema::Directive::OneOf, "specifiedBy" => GraphQL::Schema::Directive::SpecifiedBy, }.freeze end # @return [GraphQL::Tracing::DetailedTrace] if it has been configured for this schema attr_accessor :detailed_trace # @param query [GraphQL::Query, GraphQL::Execution::Multiplex] Called with a multiplex when multiple queries are executed at once (with {.multiplex}) # @return [Boolean] When `true`, save a detailed trace for this query. # @see Tracing::DetailedTrace DetailedTrace saves traces when this method returns true def detailed_trace?(query) raise "#{self} must implement `def.detailed_trace?(query)` to use DetailedTrace. Implement this method in your schema definition." end def tracer(new_tracer, silence_deprecation_warning: false) if !silence_deprecation_warning warn("`Schema.tracer(#{new_tracer.inspect})` is deprecated; use module-based `trace_with` instead. See: https://graphql-ruby.org/queries/tracing.html") warn " #{caller(1, 1).first}" end default_trace = trace_class_for(:default, build: true) if default_trace.nil? || !(default_trace < GraphQL::Tracing::CallLegacyTracers) trace_with(GraphQL::Tracing::CallLegacyTracers) end own_tracers << new_tracer end def tracers find_inherited_value(:tracers, EMPTY_ARRAY) + own_tracers end # Mix `trace_mod` into this schema's `Trace` class so that its methods will be called at runtime. # # You can attach a module to run in only _some_ circumstances by using `mode:`. When a module is added with `mode:`, # it will only run for queries with a matching `context[:trace_mode]`. # # Any custom trace modes _also_ include the default `trace_with ...` modules (that is, those added _without_ any particular `mode: ...` configuration). # # @example Adding a trace in a special mode # # only runs when `query.context[:trace_mode]` is `:special` # trace_with SpecialTrace, mode: :special # # @param trace_mod [Module] A module that implements tracing methods # @param mode [Symbol] Trace module will only be used for this trade mode # @param options [Hash] Keywords that will be passed to the tracing class during `#initialize` # @return [void] # @see GraphQL::Tracing::Trace Tracing::Trace for available tracing methods def trace_with(trace_mod, mode: :default, **options) if mode.is_a?(Array) mode.each { |m| trace_with(trace_mod, mode: m, **options) } else tc = own_trace_modes[mode] ||= build_trace_mode(mode) tc.include(trace_mod) own_trace_modules[mode] << trace_mod add_trace_options_for(mode, options) if mode == :default # This module is being added as a default tracer. If any other mode classes # have already been created, but get their default behavior from a superclass, # Then mix this into this schema's subclass. # (But don't mix it into mode classes that aren't default-based.) own_trace_modes.each do |other_mode_name, other_mode_class| if other_mode_class < DefaultTraceClass # Don't add it back to the inheritance tree if it's already there if !(other_mode_class < trace_mod) other_mode_class.include(trace_mod) end # Add any options so they'll be available add_trace_options_for(other_mode_name, options) end end end end nil end # The options hash for this trace mode # @return [Hash] def trace_options_for(mode) @trace_options_for_mode ||= {} @trace_options_for_mode[mode] ||= begin # It may be time to create an options hash for a mode that wasn't registered yet. # Mix in the default options in that case. default_options = mode == :default ? EMPTY_HASH : trace_options_for(:default) # Make sure this returns a new object so that other hashes aren't modified later if superclass.respond_to?(:trace_options_for) superclass.trace_options_for(mode).merge(default_options) else default_options.dup end end end # Create a trace instance which will include the trace modules specified for the optional mode. # # If no `mode:` is given, then {default_trace_mode} will be used. # # If this schema is using {Tracing::DetailedTrace} and {.detailed_trace?} returns `true`, then # DetailedTrace's mode will override the passed-in `mode`. # # @param mode [Symbol] Trace modules for this trade mode will be included # @param options [Hash] Keywords that will be passed to the tracing class during `#initialize` # @return [Tracing::Trace] def new_trace(mode: nil, **options) should_sample = if detailed_trace if (query = options[:query]) detailed_trace?(query) elsif (multiplex = options[:multiplex]) if multiplex.queries.length == 1 detailed_trace?(multiplex.queries.first) else detailed_trace?(multiplex) end end else false end if should_sample mode = detailed_trace.trace_mode else target = options[:query] || options[:multiplex] mode ||= target && target.context[:trace_mode] end trace_mode = mode || default_trace_mode base_trace_options = trace_options_for(trace_mode) trace_options = base_trace_options.merge(options) trace_class_for_mode = trace_class_for(trace_mode, build: true) trace_class_for_mode.new(**trace_options) end # @param new_analyzer [Class] An analyzer to run on queries to this schema # @see GraphQL::Analysis the analysis system def query_analyzer(new_analyzer) own_query_analyzers << new_analyzer end def query_analyzers find_inherited_value(:query_analyzers, EMPTY_ARRAY) + own_query_analyzers end # @param new_analyzer [Class] An analyzer to run on multiplexes to this schema # @see GraphQL::Analysis the analysis system def multiplex_analyzer(new_analyzer) own_multiplex_analyzers << new_analyzer end def multiplex_analyzers find_inherited_value(:multiplex_analyzers, EMPTY_ARRAY) + own_multiplex_analyzers end def sanitized_printer(new_sanitized_printer = nil) if new_sanitized_printer @own_sanitized_printer = new_sanitized_printer else @own_sanitized_printer || GraphQL::Language::SanitizedPrinter end end # Execute a query on itself. # @see {Query#initialize} for arguments. # @return [GraphQL::Query::Result] query result, ready to be serialized as JSON def execute(query_str = nil, **kwargs) if query_str kwargs[:query] = query_str end # Some of the query context _should_ be passed to the multiplex, too multiplex_context = if (ctx = kwargs[:context]) { backtrace: ctx[:backtrace], tracers: ctx[:tracers], trace: ctx[:trace], dataloader: ctx[:dataloader], trace_mode: ctx[:trace_mode], } else {} end # Since we're running one query, don't run a multiplex-level complexity analyzer all_results = multiplex([kwargs], max_complexity: nil, context: multiplex_context) all_results[0] end # Execute several queries on itself, concurrently. # # @example Run several queries at once # context = { ... } # queries = [ # { query: params[:query_1], variables: params[:variables_1], context: context }, # { query: params[:query_2], variables: params[:variables_2], context: context }, # ] # results = MySchema.multiplex(queries) # render json: { # result_1: results[0], # result_2: results[1], # } # # @see {Query#initialize} for query keyword arguments # @see {Execution::Multiplex#run_all} for multiplex keyword arguments # @param queries [Array] Keyword arguments for each query # @option kwargs [Hash] :context ({}) Multiplex-level context # @option kwargs [nil, Integer] :max_complexity (nil) # @return [Array] One result for each query in the input def multiplex(queries, **kwargs) GraphQL::Execution::Interpreter.run_all(self, queries, **kwargs) end def instrumenters inherited_instrumenters = find_inherited_value(:instrumenters) || Hash.new { |h,k| h[k] = [] } inherited_instrumenters.merge(own_instrumenters) do |_step, inherited, own| inherited + own end end # @api private def add_subscription_extension_if_necessary # TODO: when there's a proper API for extending root types, migrat this to use it. if !defined?(@subscription_extension_added) && @subscription_object.is_a?(Class) && self.subscriptions @subscription_extension_added = true subscription.all_field_definitions.each do |field| if !field.extensions.any? { |ext| ext.is_a?(Subscriptions::DefaultSubscriptionResolveExtension) } field.extension(Subscriptions::DefaultSubscriptionResolveExtension) end end end end # Called when execution encounters a `SystemStackError`. By default, it adds a client-facing error to the response. # You could modify this method to report this error to your bug tracker. # @param query [GraphQL::Query] # @param err [SystemStackError] # @return [void] def query_stack_error(query, err) query.context.errors.push(GraphQL::ExecutionError.new("This query is too large to execute.")) end # Call the given block at the right time, either: # - Right away, if `value` is not registered with `lazy_resolve` # - After resolving `value`, if it's registered with `lazy_resolve` (eg, `Promise`) # @api private def after_lazy(value, &block) if lazy?(value) GraphQL::Execution::Lazy.new do result = sync_lazy(value) # The returned result might also be lazy, so check it, too after_lazy(result, &block) end else yield(value) if block_given? end end # Override this method to handle lazy objects in a custom way. # @param value [Object] an instance of a class registered with {.lazy_resolve} # @return [Object] A GraphQL-ready (non-lazy) object # @api private def sync_lazy(value) lazy_method = lazy_method_name(value) if lazy_method synced_value = value.public_send(lazy_method) sync_lazy(synced_value) else value end end # @return [Symbol, nil] The method name to lazily resolve `obj`, or nil if `obj`'s class wasn't registered with {.lazy_resolve}. def lazy_method_name(obj) lazy_methods.get(obj) end # @return [Boolean] True if this object should be lazily resolved def lazy?(obj) !!lazy_method_name(obj) end # Return a lazy if any of `maybe_lazies` are lazy, # otherwise, call the block eagerly and return the result. # @param maybe_lazies [Array] # @api private def after_any_lazies(maybe_lazies) if maybe_lazies.any? { |l| lazy?(l) } GraphQL::Execution::Lazy.all(maybe_lazies).then do |result| yield result end else yield maybe_lazies end end # Returns `DidYouMean` if it's defined. # Override this to return `nil` if you don't want to use `DidYouMean` def did_you_mean(new_dym = NOT_CONFIGURED) if NOT_CONFIGURED.equal?(new_dym) if defined?(@did_you_mean) @did_you_mean else find_inherited_value(:did_you_mean, defined?(DidYouMean) ? DidYouMean : nil) end else @did_you_mean = new_dym end end # This setting controls how GraphQL-Ruby handles empty selections on Union types. # # To opt into future, spec-compliant behavior where these selections are rejected, set this to `false`. # # If you need to support previous, non-spec behavior which allowed selecting union fields # but *not* selecting any fields on that union, set this to `true` to continue allowing that behavior. # # If this is `true`, then {.legacy_invalid_empty_selections_on_union_with_type} will be called with {Query} objects # with that kind of selections. You must implement that method # @param new_value [Boolean] # @return [true, false, nil] def allow_legacy_invalid_empty_selections_on_union(new_value = NOT_CONFIGURED) if NOT_CONFIGURED.equal?(new_value) if defined?(@allow_legacy_invalid_empty_selections_on_union) @allow_legacy_invalid_empty_selections_on_union else find_inherited_value(:allow_legacy_invalid_empty_selections_on_union) end else @allow_legacy_invalid_empty_selections_on_union = new_value end end # This method is called during validation when a previously-allowed, but non-spec # query is encountered where a union field has no child selections on it. # # If `legacy_invalid_empty_selections_on_union_with_type` is overridden, this method will not be called. # # You should implement this method or `legacy_invalid_empty_selections_on_union_with_type` # to log the violation so that you can contact clients and notify them about changing their queries. # Then return a suitable value to tell GraphQL-Ruby how to continue. # @param query [GraphQL::Query] # @return [:return_validation_error] Let GraphQL-Ruby return the (new) normal validation error for this query # @return [String] A validation error to return for this query # @return [nil] Don't send the client an error, continue the legacy behavior (allow this query to execute) def legacy_invalid_empty_selections_on_union(query) raise "Implement `def self.legacy_invalid_empty_selections_on_union_with_type(query, type)` or `def self.legacy_invalid_empty_selections_on_union(query)` to handle this scenario" end # This method is called during validation when a previously-allowed, but non-spec # query is encountered where a union field has no child selections on it. # # You should implement this method to log the violation so that you can contact clients # and notify them about changing their queries. Then return a suitable value to # tell GraphQL-Ruby how to continue. # @param query [GraphQL::Query] # @param type [Module] A GraphQL type definition # @return [:return_validation_error] Let GraphQL-Ruby return the (new) normal validation error for this query # @return [String] A validation error to return for this query # @return [nil] Don't send the client an error, continue the legacy behavior (allow this query to execute) def legacy_invalid_empty_selections_on_union_with_type(query, type) legacy_invalid_empty_selections_on_union(query) end # This setting controls how GraphQL-Ruby handles overlapping selections on scalar types when the types # don't match. # # When set to `false`, GraphQL-Ruby will reject those queries with a validation error (as per the GraphQL spec). # # When set to `true`, GraphQL-Ruby will call {.legacy_invalid_return_type_conflicts} when the scenario is encountered. # # @param new_value [Boolean] `true` permits the legacy behavior, `false` rejects it. # @return [true, false, nil] def allow_legacy_invalid_return_type_conflicts(new_value = NOT_CONFIGURED) if NOT_CONFIGURED.equal?(new_value) if defined?(@allow_legacy_invalid_return_type_conflicts) @allow_legacy_invalid_return_type_conflicts else find_inherited_value(:allow_legacy_invalid_return_type_conflicts) end else @allow_legacy_invalid_return_type_conflicts = new_value end end # This method is called when the query contains fields which don't contain matching scalar types. # This was previously allowed by GraphQL-Ruby but it's a violation of the GraphQL spec. # # You should implement this method to log the violation so that you observe usage of these fields. # Fixing this scenario might mean adding new fields, and telling clients to use those fields. # (Changing the field return type would be a breaking change, but if it works for your client use cases, # that might work, too.) # # @param query [GraphQL::Query] # @param type1 [Module] A GraphQL type definition # @param type2 [Module] A GraphQL type definition # @param node1 [GraphQL::Language::Nodes::Field] This node is recognized as conflicting. You might call `.line` and `.col` for custom error reporting. # @param node2 [GraphQL::Language::Nodes::Field] The other node recognized as conflicting. # @return [:return_validation_error] Let GraphQL-Ruby return the (new) normal validation error for this query # @return [String] A validation error to return for this query # @return [nil] Don't send the client an error, continue the legacy behavior (allow this query to execute) def legacy_invalid_return_type_conflicts(query, type1, type2, node1, node2) raise "Implement #{self}.legacy_invalid_return_type_conflicts to handle this invalid selection" end # The legacy complexity implementation included several bugs: # # - In some cases, it used the lexically _last_ field to determine a cost, instead of calculating the maximum among selections # - In some cases, it called field complexity hooks repeatedly (when it should have only called them once) # # The future implementation may produce higher total complexity scores, so it's not active by default yet. You can opt into # the future default behavior by configuring `:future` here. Or, you can choose a mode for each query with {.complexity_cost_calculation_mode_for}. # # The legacy mode is currently maintained alongside the future one, but it will be removed in a future GraphQL-Ruby version. # # If you choose `:compare`, you must also implement {.legacy_complexity_cost_calculation_mismatch} to handle the input somehow. # # @example Opting into the future calculation mode # complexity_cost_calculation_mode(:future) # # @example Choosing the legacy mode (which will work until that mode is removed...) # complexity_cost_calculation_mode(:legacy) # # @example Run both modes for every query, call {.legacy_complexity_cost_calculation_mismatch} when they don't match: # complexity_cost_calculation_mode(:compare) def complexity_cost_calculation_mode(new_mode = NOT_CONFIGURED) if NOT_CONFIGURED.equal?(new_mode) if defined?(@complexity_cost_calculation_mode) @complexity_cost_calculation_mode else find_inherited_value(:complexity_cost_calculation_mode) end else @complexity_cost_calculation_mode = new_mode end end # Implement this method to produce a per-query complexity cost calculation mode. (Technically, it's per-multiplex.) # # This is a way to check the compatibility of queries coming to your API without adding overhead of running `:compare` # for every query. You could sample traffic, turn it off/on with feature flags, or anything else. # # @example Sampling traffic # def self.complexity_cost_calculation_mode_for(_context) # if rand < 0.1 # 10% of the time # :compare # else # :legacy # end # end # # @example Using a feature flag to manage future mode # def complexity_cost_calculation_mode_for(context) # current_user = context[:current_user] # if Flipper.enabled?(:future_complexity_cost, current_user) # :future # elsif rand < 0.5 # 50% # :compare # else # :legacy # end # end # # @param multiplex_context [Hash] The context for the currently-running {Execution::Multiplex} (which contains one or more queries) # @return [:future] Use the new calculation algorithm -- may be higher than `:legacy` # @return [:legacy] Use the legacy calculation algorithm, warts and all # @return [:compare] Run both algorithms and call {.legacy_complexity_cost_calculation_mismatch} if they don't match def complexity_cost_calculation_mode_for(multiplex_context) complexity_cost_calculation_mode end # Implement this method in your schema to handle mismatches when `:compare` is used. # # @example Logging the mismatch # def self.legacy_cost_calculation_mismatch(multiplex, future_cost, legacy_cost) # client_id = multiplex.context[:api_client].id # operation_names = multiplex.queries.map { |q| q.selected_operation_name || "anonymous" }.join(", ") # Stats.increment(:complexity_mismatch, tags: { client: client_id, ops: operation_names }) # legacy_cost # end # @see Query::Context#add_error Adding an error to the response to notify the client # @see Query::Context#response_extensions Adding key-value pairs to the response `"extensions" => { ... }` # @param multiplex [GraphQL::Execution::Multiplex] # @param future_complexity_cost [Integer] # @param legacy_complexity_cost [Integer] # @return [Integer] the cost to use for this query (probably one of `future_complexity_cost` or `legacy_complexity_cost`) def legacy_complexity_cost_calculation_mismatch(multiplex, future_complexity_cost, legacy_complexity_cost) raise "Implement #{self}.legacy_complexity_cost(multiplex, future_complexity_cost, legacy_complexity_cost) to handle this mismatch (#{future_complexity_cost} vs. #{legacy_complexity_cost}) and return a value to use" end private def add_trace_options_for(mode, new_options) if mode == :default own_trace_modes.each do |mode_name, t_class| if t_class <= DefaultTraceClass t_opts = trace_options_for(mode_name) t_opts.merge!(new_options) end end else t_opts = trace_options_for(mode) t_opts.merge!(new_options) end nil end # @param t [Module, Array] # @return [void] def add_type_and_traverse(t, root:) if root @root_types ||= [] @root_types << t end new_types = Array(t) addition = Schema::Addition.new(schema: self, own_types: own_types, new_types: new_types) addition.types.each do |name, types_entry| # rubocop:disable Development/ContextIsPassedCop -- build-time, not query-time if (prev_entry = own_types[name]) prev_entries = case prev_entry when Array prev_entry when Module own_types[name] = [prev_entry] else raise "Invariant: unexpected prev_entry at #{name.inspect} when adding #{t.inspect}" end case types_entry when Array prev_entries.concat(types_entry) prev_entries.uniq! # in case any are being re-visited when Module if !prev_entries.include?(types_entry) prev_entries << types_entry end else raise "Invariant: unexpected types_entry at #{name} when adding #{t.inspect}" end else if types_entry.is_a?(Array) types_entry.uniq! end own_types[name] = types_entry end end own_possible_types.merge!(addition.possible_types) { |key, old_val, new_val| old_val + new_val } own_union_memberships.merge!(addition.union_memberships) addition.references.each { |thing, pointers| prev_refs = own_references_to[thing] || [] own_references_to[thing] = prev_refs | pointers.to_a } addition.directives.each { |dir_class| own_directives[dir_class.graphql_name] = dir_class } addition.arguments_with_default_values.each do |arg| arg.validate_default_value end end def lazy_methods if !defined?(@lazy_methods) if inherited_map = find_inherited_value(:lazy_methods) # this isn't _completely_ inherited :S (Things added after `dup` won't work) @lazy_methods = inherited_map.dup else @lazy_methods = GraphQL::Execution::Lazy::LazyMethodMap.new @lazy_methods.set(GraphQL::Execution::Lazy, :value) @lazy_methods.set(GraphQL::Dataloader::Request, :load_with_deprecation_warning) end end @lazy_methods end def own_types @own_types ||= {} end def own_references_to @own_references_to ||= {}.compare_by_identity end def non_introspection_types find_inherited_value(:non_introspection_types, EMPTY_HASH).merge(own_types) end def own_plugins @own_plugins ||= [] end def own_orphan_types @own_orphan_types ||= [] end def own_possible_types @own_possible_types ||= {}.compare_by_identity end def own_union_memberships @own_union_memberships ||= {} end def own_directives @own_directives ||= {} end def own_instrumenters @own_instrumenters ||= Hash.new { |h,k| h[k] = [] } end def own_tracers @own_tracers ||= [] end def own_query_analyzers @defined_query_analyzers ||= [] end def own_multiplex_analyzers @own_multiplex_analyzers ||= [] end # This is overridden in subclasses to check the inheritance chain def get_references_to(type_defn) own_references_to[type_defn] end end module SubclassGetReferencesTo def get_references_to(type_defn) own_refs = own_references_to[type_defn] inherited_refs = superclass.references_to(type_defn) if inherited_refs&.any? if own_refs&.any? own_refs + inherited_refs else inherited_refs end else own_refs end end end # Install these here so that subclasses will also install it. self.connections = GraphQL::Pagination::Connections.new(schema: self) # @api private module DefaultTraceClass end end end require "graphql/schema/loader" require "graphql/schema/printer" graphql-ruby-2.5.19/lib/graphql/schema/000077500000000000000000000000001514115062600176725ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/schema/addition.rb000066400000000000000000000247341514115062600220240ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Addition attr_reader :directives, :possible_types, :types, :union_memberships, :references, :arguments_with_default_values def initialize(schema:, own_types:, new_types:) @schema = schema @own_types = own_types @directives = Set.new @possible_types = {} @types = {} @union_memberships = {} @references = Hash.new { |h, k| h[k] = Set.new } @arguments_with_default_values = [] add_type_and_traverse(new_types) end private def references_to(thing, from:) @references[thing].add(from) end def get_type(name) local_type = @types[name] # This isn't really sophisticated, but # I think it's good enough to support the current usage of LateBoundTypes if local_type.is_a?(Array) local_type = local_type.first end local_type || @schema.get_type(name) end # Lookup using `own_types` here because it's ok to override # inherited types by name def get_local_type(name) @types[name] || @own_types[name] end def add_directives_from(owner) if !(dir_instances = owner.directives).empty? dirs = dir_instances.map(&:class) @directives.merge(dirs) add_type_and_traverse(dirs) end end def add_type_and_traverse(new_types) late_types = [] path = [] new_types.each do |t| path.push(t.graphql_name) add_type(t, owner: nil, late_types: late_types, path: path) path.pop end missed_late_types = 0 while (late_type_vals = late_types.shift) type_owner, lt = late_type_vals if lt.is_a?(String) type = Member::BuildType.constantize(lt) # Reset the counter, since we might succeed next go-round missed_late_types = 0 update_type_owner(type_owner, type) add_type(type, owner: type_owner, late_types: late_types, path: [type.graphql_name]) elsif lt.is_a?(LateBoundType) if (type = get_type(lt.name)) # Reset the counter, since we might succeed next go-round missed_late_types = 0 update_type_owner(type_owner, type) add_type(type, owner: type_owner, late_types: late_types, path: [type.graphql_name]) else missed_late_types += 1 # Add it back to the list, maybe we'll be able to resolve it later. late_types << [type_owner, lt] if missed_late_types == late_types.size # We've looked at all of them and haven't resolved one. raise UnresolvedLateBoundTypeError.new(type: lt) else # Try the next one end end else raise ArgumentError, "Unexpected late type: #{lt.inspect}" end end nil end def update_type_owner(owner, type) case owner when Module if owner.kind.union? # It's a union with possible_types # Replace the item by class name owner.assign_type_membership_object_type(type) @possible_types[owner] = owner.possible_types elsif type.kind.interface? && (owner.kind.object? || owner.kind.interface?) new_interfaces = [] owner.interfaces.each do |int_t| if int_t.is_a?(String) && int_t == type.graphql_name new_interfaces << type elsif int_t.is_a?(LateBoundType) && int_t.graphql_name == type.graphql_name new_interfaces << type else # Don't re-add proper interface definitions, # they were probably already added, maybe with options. end end owner.implements(*new_interfaces) new_interfaces.each do |int| pt = @possible_types[int] ||= [] if !pt.include?(owner) && owner.is_a?(Class) pt << owner end int.interfaces.each do |indirect_int| if indirect_int.is_a?(LateBoundType) && (indirect_int_type = get_type(indirect_int.graphql_name)) update_type_owner(owner, indirect_int_type) end end end end when nil # It's a root type @types[type.graphql_name] = type when GraphQL::Schema::Field, GraphQL::Schema::Argument orig_type = owner.type unwrapped_t = type # Apply list/non-null wrapper as needed if orig_type.respond_to?(:of_type) transforms = [] while (orig_type.respond_to?(:of_type)) if orig_type.kind.non_null? transforms << :to_non_null_type elsif orig_type.kind.list? transforms << :to_list_type else raise "Invariant: :of_type isn't non-null or list" end orig_type = orig_type.of_type end transforms.reverse_each { |t| type = type.public_send(t) } end owner.type = type references_to(unwrapped_t, from: owner) else raise "Unexpected update: #{owner.inspect} #{type.inspect}" end end def add_type(type, owner:, late_types:, path:) if type.is_a?(String) || type.is_a?(GraphQL::Schema::LateBoundType) late_types << [owner, type] return end if owner.is_a?(Class) && owner < GraphQL::Schema::Union um = @union_memberships[type.graphql_name] ||= [] um << owner end if (prev_type = get_local_type(type.graphql_name)) && (prev_type == type || (prev_type.is_a?(Array) && prev_type.include?(type))) # No need to re-visit elsif type.is_a?(Class) && type < GraphQL::Schema::Directive @directives << type type.all_argument_definitions.each do |arg| arg_type = arg.type.unwrap if !arg_type.is_a?(GraphQL::Schema::LateBoundType) references_to(arg_type, from: arg) end path.push(arg.graphql_name) add_type(arg_type, owner: arg, late_types: late_types, path: path) path.pop if arg.default_value? @arguments_with_default_values << arg end end else prev_type = @types[type.graphql_name] if prev_type.nil? @types[type.graphql_name] = type elsif prev_type.is_a?(Array) prev_type << type else @types[type.graphql_name] = [prev_type, type] end add_directives_from(type) if type.kind.fields? type.all_field_definitions.each do |field| field.ensure_loaded name = field.graphql_name field_type = field.type.unwrap if !field_type.is_a?(GraphQL::Schema::LateBoundType) references_to(field_type, from: field) end path.push(name) add_type(field_type, owner: field, late_types: late_types, path: path) add_directives_from(field) field.all_argument_definitions.each do |arg| add_directives_from(arg) arg_type = arg.type.unwrap if !arg_type.is_a?(GraphQL::Schema::LateBoundType) references_to(arg_type, from: arg) end path.push(arg.graphql_name) add_type(arg_type, owner: arg, late_types: late_types, path: path) path.pop if arg.default_value? @arguments_with_default_values << arg end end path.pop end end if type.kind.input_object? type.all_argument_definitions.each do |arg| add_directives_from(arg) arg_type = arg.type.unwrap if !arg_type.is_a?(GraphQL::Schema::LateBoundType) references_to(arg_type, from: arg) end path.push(arg.graphql_name) add_type(arg_type, owner: arg, late_types: late_types, path: path) path.pop if arg.default_value? @arguments_with_default_values << arg end end end if type.kind.union? @possible_types[type] = type.all_possible_types path.push("possible_types") type.all_possible_types.each do |t| add_type(t, owner: type, late_types: late_types, path: path) end path.pop end if type.kind.interface? path.push("orphan_types") type.orphan_types.each do |t| add_type(t, owner: type, late_types: late_types, path: path) end path.pop end if type.kind.object? possible_types_for_this_name = @possible_types[type] ||= [] possible_types_for_this_name << type end if type.kind.object? || type.kind.interface? path.push("implements") type.interface_type_memberships.each do |interface_type_membership| case interface_type_membership when Schema::TypeMembership interface_type = interface_type_membership.abstract_type # We can get these now; we'll have to get late-bound types later if interface_type.is_a?(Module) && type.is_a?(Class) implementers = @possible_types[interface_type] ||= [] if !implementers.include?(type) implementers << type end end when String, Schema::LateBoundType interface_type = interface_type_membership else raise ArgumentError, "Invariant: unexpected type membership for #{type.graphql_name}: #{interface_type_membership.class} (#{interface_type_membership.inspect})" end add_type(interface_type, owner: type, late_types: late_types, path: path) end path.pop end if type.kind.enum? type.all_enum_value_definitions.each do |value_definition| add_directives_from(value_definition) end end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/always_visible.rb000066400000000000000000000004671514115062600232430ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema module AlwaysVisible def self.use(schema, **opts) schema.use(GraphQL::Schema::Visibility, profiles: { nil => {} }) schema.extend(self) end def visible?(_member, _context) true end end end end graphql-ruby-2.5.19/lib/graphql/schema/argument.rb000066400000000000000000000415671514115062600220560ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Argument include GraphQL::Schema::Member::HasPath include GraphQL::Schema::Member::HasAstNode include GraphQL::Schema::Member::HasDirectives include GraphQL::Schema::Member::HasDeprecationReason include GraphQL::Schema::Member::HasValidators include GraphQL::EmptyObjects # @return [String] the GraphQL name for this argument, camelized unless `camelize: false` is provided attr_reader :name alias :graphql_name :name # @return [GraphQL::Schema::Field, Class] The field or input object this argument belongs to attr_reader :owner # @param new_prepare [Method, Proc] # @return [Symbol] A method or proc to call to transform this value before sending it to field resolution method def prepare(new_prepare = NOT_CONFIGURED) if new_prepare != NOT_CONFIGURED @prepare = new_prepare end @prepare end # @return [Symbol] This argument's name in Ruby keyword arguments attr_reader :keyword # @return [Class, Module, nil] If this argument should load an application object, this is the type of object to load attr_reader :loads # @return [Boolean] true if a resolver defined this argument def from_resolver? @from_resolver end # @param arg_name [Symbol] # @param type_expr # @param desc [String] # @param type [Class, Array] Input type; positional argument also accepted # @param name [Symbol] positional argument also accepted # @param loads [Class, Array] A GraphQL type to load for the given ID when one is present # @param definition_block [Proc] Called with the newly-created {Argument} # @param owner [Class] Private, used by GraphQL-Ruby during schema definition # @param required [Boolean, :nullable] if true, this argument is non-null; if false, this argument is nullable. If `:nullable`, then the argument must be provided, though it may be `null`. # @param description [String] # @param default_value [Object] # @param loads [Class, Array] A GraphQL type to load for the given ID when one is present # @param as [Symbol] Override the keyword name when passed to a method # @param prepare [Symbol] A method to call to transform this argument's valuebefore sending it to field resolution # @param camelize [Boolean] if true, the name will be camelized when building the schema # @param from_resolver [Boolean] if true, a Resolver class defined this argument # @param directives [Hash{Class => Hash}] # @param deprecation_reason [String] # @param validates [Hash, nil] Options for building validators, if any should be applied # @param replace_null_with_default [Boolean] if `true`, incoming values of `null` will be replaced with the configured `default_value` # @param comment [String] Private, used by GraphQL-Ruby when parsing GraphQL schema files # @param ast_node [GraphQL::Language::Nodes::InputValueDefinition] Private, used by GraphQL-Ruby when parsing schema files def initialize(arg_name = nil, type_expr = nil, desc = nil, required: true, type: nil, name: nil, loads: nil, description: nil, comment: nil, ast_node: nil, default_value: NOT_CONFIGURED, as: nil, from_resolver: false, camelize: true, prepare: nil, owner:, validates: nil, directives: nil, deprecation_reason: nil, replace_null_with_default: false, &definition_block) arg_name ||= name @name = -(camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s) NameValidator.validate!(@name) @type_expr = type_expr || type @description = desc || description @comment = comment @null = required != true @default_value = default_value if replace_null_with_default if !default_value? raise ArgumentError, "`replace_null_with_default: true` requires a default value, please provide one with `default_value: ...`" end @replace_null_with_default = true end @owner = owner @as = as @loads = loads @keyword = as || (arg_name.is_a?(Symbol) ? arg_name : Schema::Member::BuildType.underscore(@name).to_sym) @prepare = prepare @ast_node = ast_node @from_resolver = from_resolver self.deprecation_reason = deprecation_reason if directives directives.each do |dir_class, dir_options| directive(dir_class, **dir_options) end end if validates && !validates.empty? self.validates(validates) end if required == :nullable self.owner.validates(required: { argument: arg_name }) end if definition_block # `self` will still be self, it will also be the first argument to the block: instance_exec(self, &definition_block) end end def inspect "#<#{self.class} #{path}: #{type.to_type_signature}#{description ? " @description=#{description.inspect}" : ""}>" end # @param default_value [Object] The value to use when the client doesn't provide one # @return [Object] the value used when the client doesn't provide a value for this argument def default_value(new_default_value = NOT_CONFIGURED) if new_default_value != NOT_CONFIGURED @default_value = new_default_value end @default_value end # @return [Boolean] True if this argument has a default value def default_value? @default_value != NOT_CONFIGURED end def replace_null_with_default? @replace_null_with_default end attr_writer :description # @return [String] Documentation for this argument def description(text = nil) if text @description = text else @description end end attr_writer :comment # @return [String] Comment for this argument def comment(text = nil) if text @comment = text else @comment end end # @return [String] Deprecation reason for this argument def deprecation_reason(text = nil) if text self.deprecation_reason = text else super() end end def deprecation_reason=(new_reason) validate_deprecated_or_optional(null: @null, deprecation_reason: new_reason) super end def visible?(context) true end def authorized?(obj, value, ctx) authorized_as_type?(obj, value, ctx, as_type: type) end def authorized_as_type?(obj, value, ctx, as_type:) if value.nil? return true end if as_type.kind.non_null? as_type = as_type.of_type end if as_type.kind.list? value.each do |v| if !authorized_as_type?(obj, v, ctx, as_type: as_type.of_type) return false end end elsif as_type.kind.input_object? return as_type.authorized?(obj, value, ctx) end # None of the early-return conditions were activated, # so this is authorized. true end def type=(new_type) validate_input_type(new_type) # This isn't true for LateBoundTypes, but we can assume those will # be updated via this codepath later in schema setup. if new_type.respond_to?(:non_null?) validate_deprecated_or_optional(null: !new_type.non_null?, deprecation_reason: deprecation_reason) end @type = new_type end def type @type ||= begin parsed_type = begin Member::BuildType.parse_type(@type_expr, null: @null) rescue StandardError => err raise ArgumentError, "Couldn't build type for Argument #{@owner.name}.#{name}: #{err.class.name}: #{err.message}", err.backtrace end # Use the setter method to get validations self.type = parsed_type end end def statically_coercible? return @statically_coercible if defined?(@statically_coercible) requires_parent_object = @prepare.is_a?(String) || @prepare.is_a?(Symbol) || @own_validators @statically_coercible = !requires_parent_object end def freeze statically_coercible? super end # Apply the {prepare} configuration to `value`, using methods from `obj`. # Used by the runtime. # @api private def prepare_value(obj, value, context: nil) if type.unwrap.kind.input_object? value = recursively_prepare_input_object(value, type, context) end Schema::Validator.validate!(validators, obj, context, value) if @prepare.nil? value elsif @prepare.is_a?(String) || @prepare.is_a?(Symbol) if obj.nil? # The problem here is, we _used to_ prepare while building variables. # But now we don't have the runtime object there. # # This will have to be called later, when the runtime object _is_ available. value elsif obj.respond_to?(@prepare) obj.public_send(@prepare, value) elsif owner.respond_to?(@prepare) owner.public_send(@prepare, value, context || obj.context) else raise "Invalid prepare for #{@owner.name}.name: #{@prepare.inspect}. "\ "Could not find prepare method #{@prepare} on #{obj.class} or #{owner}." end elsif @prepare.respond_to?(:call) @prepare.call(value, context || obj.context) else raise "Invalid prepare for #{@owner.name}.name: #{@prepare.inspect}" end end # @api private def coerce_into_values(parent_object, values, context, argument_values) arg_name = graphql_name arg_key = keyword default_used = false if values.key?(arg_name) value = values[arg_name] elsif values.key?(arg_key) value = values[arg_key] elsif default_value? value = default_value default_used = true else # no value at all owner.validate_directive_argument(self, nil) return end if value.nil? && replace_null_with_default? value = default_value default_used = true end loaded_value = nil coerced_value = begin type.coerce_input(value, context) rescue StandardError => err context.schema.handle_or_reraise(context, err) end # If this isn't lazy, then the block returns eagerly and assigns the result here # If it _is_ lazy, then we write the lazy to the hash, then update it later argument_values[arg_key] = context.query.after_lazy(coerced_value) do |resolved_coerced_value| owner.validate_directive_argument(self, resolved_coerced_value) prepared_value = begin prepare_value(parent_object, resolved_coerced_value, context: context) rescue StandardError => err context.schema.handle_or_reraise(context, err) end if loads && !from_resolver? loaded_value = begin load_and_authorize_value(owner, prepared_value, context) rescue StandardError => err context.schema.handle_or_reraise(context, err) end end maybe_loaded_value = loaded_value || prepared_value context.query.after_lazy(maybe_loaded_value) do |resolved_loaded_value| # TODO code smell to access such a deeply-nested constant in a distant module argument_values[arg_key] = GraphQL::Execution::Interpreter::ArgumentValue.new( value: resolved_loaded_value, original_value: resolved_coerced_value, definition: self, default_used: default_used, ) end end end def load_and_authorize_value(load_method_owner, coerced_value, context) if coerced_value.nil? return nil end arg_load_method = "load_#{keyword}" if load_method_owner.respond_to?(arg_load_method) custom_loaded_value = if load_method_owner.is_a?(Class) load_method_owner.public_send(arg_load_method, coerced_value, context) else load_method_owner.public_send(arg_load_method, coerced_value) end context.query.after_lazy(custom_loaded_value) do |custom_value| if loads if type.list? loaded_values = [] context.dataloader.run_isolated do custom_value.each_with_index.map { |custom_val, idx| id = coerced_value[idx] context.dataloader.append_job do loaded_values[idx] = load_method_owner.authorize_application_object(self, id, context, custom_val) end } end context.schema.after_any_lazies(loaded_values, &:itself) else load_method_owner.authorize_application_object(self, coerced_value, context, custom_loaded_value) end else custom_value end end elsif loads if type.list? loaded_values = [] # We want to run these list items all together, # but we also need to wait for the result so we can return it :S context.dataloader.run_isolated do coerced_value.each_with_index { |val, idx| context.dataloader.append_job do loaded_values[idx] = load_method_owner.load_and_authorize_application_object(self, val, context) end } end context.schema.after_any_lazies(loaded_values, &:itself) else load_method_owner.load_and_authorize_application_object(self, coerced_value, context) end else coerced_value end end # @api private def validate_default_value return unless default_value? coerced_default_value = begin # This is weird, but we should accept single-item default values for list-type arguments. # If we used `coerce_isolated_input` below, it would do this for us, but it's not really # the right thing here because we expect default values in application format (Ruby values) # not GraphQL format (scalar values). # # But I don't think Schema::List#coerce_result should apply wrapping to single-item lists. prepped_default_value = if default_value.nil? nil elsif (type.kind.list? || (type.kind.non_null? && type.of_type.list?)) && !default_value.respond_to?(:map) [default_value] else default_value end type.coerce_isolated_result(prepped_default_value) unless prepped_default_value.nil? rescue GraphQL::Schema::Enum::UnresolvedValueError # It raises this, which is helpful at runtime, but not here... default_value end res = type.valid_isolated_input?(coerced_default_value) if !res raise InvalidDefaultValueError.new(self) end end class InvalidDefaultValueError < GraphQL::Error def initialize(argument) message = "`#{argument.path}` has an invalid default value: `#{argument.default_value.inspect}` isn't accepted by `#{argument.type.to_type_signature}`; update the default value or the argument type." super(message) end end private def recursively_prepare_input_object(value, type, context) if type.non_null? type = type.of_type end if type.list? && !value.nil? inner_type = type.of_type value.map { |v| recursively_prepare_input_object(v, inner_type, context) } elsif value.is_a?(GraphQL::Schema::InputObject) value.validate_for(context) value.prepare else value end end def validate_input_type(input_type) if input_type.is_a?(String) || input_type.is_a?(GraphQL::Schema::LateBoundType) # Do nothing; assume this will be validated later elsif input_type.kind.non_null? || input_type.kind.list? validate_input_type(input_type.unwrap) elsif !input_type.kind.input? raise ArgumentError, "Invalid input type for #{path}: #{input_type.graphql_name}. Must be scalar, enum, or input object, not #{input_type.kind.name}." else # It's an input type, we're OK end end def validate_deprecated_or_optional(null:, deprecation_reason:) if deprecation_reason && !null raise ArgumentError, "Required arguments cannot be deprecated: #{path}." end end end end end graphql-ruby-2.5.19/lib/graphql/schema/base_64_encoder.rb000066400000000000000000000010271514115062600231410ustar00rootroot00000000000000# frozen_string_literal: true require "base64" module GraphQL class Schema # @api private module Base64Encoder def self.encode(unencoded_text, nonce: false) Base64.urlsafe_encode64(unencoded_text, padding: false) end def self.decode(encoded_text, nonce: false) # urlsafe_decode64 is for forward compatibility Base64.urlsafe_decode64(encoded_text) rescue ArgumentError raise GraphQL::ExecutionError, "Invalid input: #{encoded_text.inspect}" end end end end graphql-ruby-2.5.19/lib/graphql/schema/build_from_definition.rb000066400000000000000000000571441514115062600245640ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/schema/build_from_definition/resolve_map" module GraphQL class Schema module BuildFromDefinition class << self # @see {Schema.from_definition} def from_definition(schema_superclass, definition_string, parser: GraphQL.default_parser, **kwargs) if defined?(parser::SchemaParser) parser = parser::SchemaParser end from_document(schema_superclass, parser.parse(definition_string), **kwargs) end def from_definition_path(schema_superclass, definition_path, parser: GraphQL.default_parser, **kwargs) if defined?(parser::SchemaParser) parser = parser::SchemaParser end from_document(schema_superclass, parser.parse_file(definition_path), **kwargs) end def from_document(schema_superclass, document, default_resolve:, using: {}, base_types: {}, relay: false) Builder.build(schema_superclass, document, default_resolve: default_resolve || {}, relay: relay, using: using, base_types: base_types) end end # @api private module Builder include GraphQL::EmptyObjects extend self def build(schema_superclass, document, default_resolve:, using: {}, base_types: {}, relay:) raise InvalidDocumentError.new('Must provide a document ast.') if !document || !document.is_a?(GraphQL::Language::Nodes::Document) base_types = { object: GraphQL::Schema::Object, interface: GraphQL::Schema::Interface, union: GraphQL::Schema::Union, scalar: GraphQL::Schema::Scalar, enum: GraphQL::Schema::Enum, input_object: GraphQL::Schema::InputObject, }.merge!(base_types) if default_resolve.is_a?(Hash) default_resolve = ResolveMap.new(default_resolve) end schema_defns = document.definitions.select { |d| d.is_a?(GraphQL::Language::Nodes::SchemaDefinition) } if schema_defns.size > 1 raise InvalidDocumentError.new('Must provide only one schema definition.') end schema_definition = schema_defns.first types = {} directives = schema_superclass.directives.dup type_resolver = build_resolve_type(types, directives, ->(type_name) { types[type_name] ||= Schema::LateBoundType.new(type_name)}) # Make a different type resolver because we need to coerce directive arguments # _while_ building the schema. # It will dig for a type if it encounters a custom type. This could be a problem if there are cycles. directive_type_resolver = nil directive_type_resolver = build_resolve_type(types, directives, ->(type_name) { types[type_name] ||= begin defn = document.definitions.find { |d| d.respond_to?(:name) && d.name == type_name } if defn build_definition_from_node(defn, directive_type_resolver, default_resolve, base_types) elsif (built_in_defn = GraphQL::Schema::BUILT_IN_TYPES[type_name]) built_in_defn else raise "No definition for #{type_name.inspect} found in schema document or built-in types. Add a definition for it or remove it." end end }) directives.merge!(GraphQL::Schema.default_directives) document.definitions.each do |definition| if definition.is_a?(GraphQL::Language::Nodes::DirectiveDefinition) directives[definition.name] = build_directive(definition, directive_type_resolver) end end # In case any directives referenced built-in types for their arguments: replace_late_bound_types_with_built_in(types) schema_extensions = nil document.definitions.each do |definition| case definition when GraphQL::Language::Nodes::SchemaDefinition, GraphQL::Language::Nodes::DirectiveDefinition nil # already handled when GraphQL::Language::Nodes::SchemaExtension, GraphQL::Language::Nodes::ScalarTypeExtension, GraphQL::Language::Nodes::ObjectTypeExtension, GraphQL::Language::Nodes::InterfaceTypeExtension, GraphQL::Language::Nodes::UnionTypeExtension, GraphQL::Language::Nodes::EnumTypeExtension, GraphQL::Language::Nodes::InputObjectTypeExtension schema_extensions ||= [] schema_extensions << definition else # It's possible that this was already loaded by the directives prev_type = types[definition.name] if prev_type.nil? || prev_type.is_a?(Schema::LateBoundType) types[definition.name] = build_definition_from_node(definition, type_resolver, default_resolve, base_types) end end end replace_late_bound_types_with_built_in(types) if schema_definition if schema_definition.query raise InvalidDocumentError.new("Specified query type \"#{schema_definition.query}\" not found in document.") unless types[schema_definition.query] query_root_type = types[schema_definition.query] end if schema_definition.mutation raise InvalidDocumentError.new("Specified mutation type \"#{schema_definition.mutation}\" not found in document.") unless types[schema_definition.mutation] mutation_root_type = types[schema_definition.mutation] end if schema_definition.subscription raise InvalidDocumentError.new("Specified subscription type \"#{schema_definition.subscription}\" not found in document.") unless types[schema_definition.subscription] subscription_root_type = types[schema_definition.subscription] end if schema_definition.query.nil? && schema_definition.mutation.nil? && schema_definition.subscription.nil? # This schema may have been given with directives only, # check for defaults: query_root_type = types['Query'] mutation_root_type = types['Mutation'] subscription_root_type = types['Subscription'] end else query_root_type = types['Query'] mutation_root_type = types['Mutation'] subscription_root_type = types['Subscription'] end raise InvalidDocumentError.new('Must provide schema definition with query type or a type named Query.') unless query_root_type schema_extensions&.each do |ext| next if ext.is_a?(GraphQL::Language::Nodes::SchemaExtension) built_type = types[ext.name] case ext when GraphQL::Language::Nodes::ScalarTypeExtension build_directives(built_type, ext, type_resolver) when GraphQL::Language::Nodes::ObjectTypeExtension build_directives(built_type, ext, type_resolver) build_fields(built_type, ext.fields, type_resolver, default_resolve: true) build_interfaces(built_type, ext.interfaces, type_resolver) when GraphQL::Language::Nodes::InterfaceTypeExtension build_directives(built_type, ext, type_resolver) build_fields(built_type, ext.fields, type_resolver, default_resolve: nil) build_interfaces(built_type, ext.interfaces, type_resolver) when GraphQL::Language::Nodes::UnionTypeExtension build_directives(built_type, ext, type_resolver) built_type.possible_types(*ext.types.map { |type_name| type_resolver.call(type_name) }) when GraphQL::Language::Nodes::EnumTypeExtension build_directives(built_type, ext, type_resolver) build_values(built_type, ext.values, type_resolver) when GraphQL::Language::Nodes::InputObjectTypeExtension build_directives(built_type, ext, type_resolver) build_arguments(built_type, ext.fields, type_resolver) end end builder = self found_types = types.values object_types = found_types.select { |t| t.respond_to?(:kind) && t.kind.object? } schema_class = Class.new(schema_superclass) do begin # Add these first so that there's some chance of resolving late-bound types add_type_and_traverse(found_types, root: false) orphan_types(object_types) query query_root_type mutation mutation_root_type subscription subscription_root_type rescue Schema::UnresolvedLateBoundTypeError => err type_name = err.type.name err_backtrace = err.backtrace raise InvalidDocumentError, "Type \"#{type_name}\" not found in document.", err_backtrace end object_types.each do |t| t.interfaces.each do |int_t| if int_t.is_a?(LateBoundType) int_t = types[int_t.graphql_name] t.implements(int_t) end int_t.orphan_types(t) end end if default_resolve.respond_to?(:resolve_type) def self.resolve_type(*args) self.definition_default_resolve.resolve_type(*args) end else def self.resolve_type(*args) NullResolveType.call(*args) end end directives directives.values if schema_definition ast_node(schema_definition) builder.build_directives(self, schema_definition, type_resolver) end using.each do |plugin, options| if options use(plugin, **options) else use(plugin) end end # Empty `orphan_types` -- this will make unreachable types ... unreachable. own_orphan_types.clear class << self attr_accessor :definition_default_resolve end self.definition_default_resolve = default_resolve def definition_default_resolve self.class.definition_default_resolve end def self.inherited(child_class) child_class.definition_default_resolve = self.definition_default_resolve super end end schema_extensions&.each do |ext| if ext.is_a?(GraphQL::Language::Nodes::SchemaExtension) build_directives(schema_class, ext, type_resolver) end end schema_class end NullResolveType = ->(type, obj, ctx) { raise(GraphQL::RequiredImplementationMissingError, "Generated Schema cannot use Interface or Union types for execution. Implement resolve_type on your resolver.") } def build_definition_from_node(definition, type_resolver, default_resolve, base_types) case definition when GraphQL::Language::Nodes::EnumTypeDefinition build_enum_type(definition, type_resolver, base_types[:enum]) when GraphQL::Language::Nodes::ObjectTypeDefinition build_object_type(definition, type_resolver, base_types[:object]) when GraphQL::Language::Nodes::InterfaceTypeDefinition build_interface_type(definition, type_resolver, base_types[:interface]) when GraphQL::Language::Nodes::UnionTypeDefinition build_union_type(definition, type_resolver, base_types[:union]) when GraphQL::Language::Nodes::ScalarTypeDefinition build_scalar_type(definition, type_resolver, base_types[:scalar], default_resolve: default_resolve) when GraphQL::Language::Nodes::InputObjectTypeDefinition build_input_object_type(definition, type_resolver, base_types[:input_object]) when GraphQL::Language::Nodes::DirectiveDefinition build_directive(definition, type_resolver) end end # Modify `types`, replacing any late-bound references to built-in types # with their actual definitions. # # (Schema definitions are allowed to reference those built-ins without redefining them.) # @return void def replace_late_bound_types_with_built_in(types) GraphQL::Schema::BUILT_IN_TYPES.each do |scalar_name, built_in_scalar| existing_type = types[scalar_name] if existing_type.is_a?(GraphQL::Schema::LateBoundType) types[scalar_name] = built_in_scalar end end end def build_directives(definition, ast_node, type_resolver) dirs = prepare_directives(ast_node, type_resolver) dirs.each do |(dir_class, options)| if definition.respond_to?(:schema_directive) # it's a schema definition.schema_directive(dir_class, **options) else definition.directive(dir_class, **options) end end end def prepare_directives(ast_node, type_resolver) dirs = [] ast_node.directives.each do |dir_node| if dir_node.name == "deprecated" # This is handled using `deprecation_reason` next else dir_class = type_resolver.call(dir_node.name) if dir_class.nil? raise ArgumentError, "No definition for @#{dir_node.name} #{ast_node.respond_to?(:name) ? "on #{ast_node.name} " : ""}at #{ast_node.line}:#{ast_node.col}" end options = args_to_kwargs(dir_class, dir_node) dirs << [dir_class, options] end end dirs end def args_to_kwargs(arg_owner, node) if node.respond_to?(:arguments) kwargs = {} node.arguments.each do |arg_node| arg_defn = arg_owner.get_argument(arg_node.name) kwargs[arg_defn.keyword] = args_to_kwargs(arg_defn.type.unwrap, arg_node.value) end kwargs elsif node.is_a?(Array) node.map { |n| args_to_kwargs(arg_owner, n) } elsif node.is_a?(Language::Nodes::Enum) node.name else # scalar node end end def build_enum_type(enum_type_definition, type_resolver, base_type) builder = self Class.new(base_type) do graphql_name(enum_type_definition.name) builder.build_directives(self, enum_type_definition, type_resolver) description(enum_type_definition.description) ast_node(enum_type_definition) builder.build_values(self, enum_type_definition.values, type_resolver) end end def build_values(type_class, enum_value_definitions, type_resolver) enum_value_definitions.each do |enum_value_definition| type_class.value(enum_value_definition.name, value: enum_value_definition.name, deprecation_reason: build_deprecation_reason(enum_value_definition.directives), description: enum_value_definition.description, directives: prepare_directives(enum_value_definition, type_resolver), ast_node: enum_value_definition, ) end end def build_deprecation_reason(directives) deprecated_directive = directives.find{ |d| d.name == 'deprecated' } return unless deprecated_directive reason = deprecated_directive.arguments.find{ |a| a.name == 'reason' } return GraphQL::Schema::Directive::DEFAULT_DEPRECATION_REASON unless reason reason.value end def build_scalar_type(scalar_type_definition, type_resolver, base_type, default_resolve:) builder = self Class.new(base_type) do graphql_name(scalar_type_definition.name) description(scalar_type_definition.description) ast_node(scalar_type_definition) builder.build_directives(self, scalar_type_definition, type_resolver) if default_resolve.respond_to?(:coerce_input) # Put these method definitions in another method to avoid retaining `type_resolve` # from this method's bindiing builder.build_scalar_type_coerce_method(self, :coerce_input, default_resolve) builder.build_scalar_type_coerce_method(self, :coerce_result, default_resolve) end end end def build_scalar_type_coerce_method(scalar_class, method_name, default_definition_resolve) scalar_class.define_singleton_method(method_name) do |val, ctx| default_definition_resolve.public_send(method_name, self, val, ctx) end end def build_union_type(union_type_definition, type_resolver, base_type) builder = self Class.new(base_type) do graphql_name(union_type_definition.name) description(union_type_definition.description) possible_types(*union_type_definition.types.map { |type_name| type_resolver.call(type_name) }) ast_node(union_type_definition) builder.build_directives(self, union_type_definition, type_resolver) end end def build_object_type(object_type_definition, type_resolver, base_type) builder = self Class.new(base_type) do graphql_name(object_type_definition.name) description(object_type_definition.description) ast_node(object_type_definition) builder.build_directives(self, object_type_definition, type_resolver) builder.build_interfaces(self, object_type_definition.interfaces, type_resolver) builder.build_fields(self, object_type_definition.fields, type_resolver, default_resolve: true) end end def build_interfaces(type_class, interface_names, type_resolver) interface_names.each do |interface_name| type_class.implements(type_resolver.call(interface_name)) end end def build_input_object_type(input_object_type_definition, type_resolver, base_type) builder = self Class.new(base_type) do graphql_name(input_object_type_definition.name) description(input_object_type_definition.description) ast_node(input_object_type_definition) builder.build_directives(self, input_object_type_definition, type_resolver) builder.build_arguments(self, input_object_type_definition.fields, type_resolver) end end def build_default_value(default_value) case default_value when GraphQL::Language::Nodes::Enum default_value.name when GraphQL::Language::Nodes::NullValue nil when GraphQL::Language::Nodes::InputObject default_value.to_h when Array default_value.map { |v| build_default_value(v) } else default_value end end def build_arguments(type_class, arguments, type_resolver) builder = self arguments.each do |argument_defn| default_value_kwargs = if !argument_defn.default_value.nil? { default_value: builder.build_default_value(argument_defn.default_value) } else EMPTY_HASH end type_class.argument( argument_defn.name, type: type_resolver.call(argument_defn.type), required: false, description: argument_defn.description, deprecation_reason: builder.build_deprecation_reason(argument_defn.directives), ast_node: argument_defn, camelize: false, directives: prepare_directives(argument_defn, type_resolver), **default_value_kwargs ) end end def build_directive(directive_definition, type_resolver) builder = self Class.new(GraphQL::Schema::Directive) do graphql_name(directive_definition.name) description(directive_definition.description) repeatable(directive_definition.repeatable) locations(*directive_definition.locations.map { |location| location.name.to_sym }) ast_node(directive_definition) builder.build_arguments(self, directive_definition.arguments, type_resolver) end end def build_interface_type(interface_type_definition, type_resolver, base_type) builder = self Module.new do include base_type graphql_name(interface_type_definition.name) description(interface_type_definition.description) builder.build_interfaces(self, interface_type_definition.interfaces, type_resolver) ast_node(interface_type_definition) builder.build_directives(self, interface_type_definition, type_resolver) builder.build_fields(self, interface_type_definition.fields, type_resolver, default_resolve: nil) end end def build_fields(owner, field_definitions, type_resolver, default_resolve:) builder = self field_definitions.each do |field_definition| resolve_method_name = -"resolve_field_#{field_definition.name}" schema_field_defn = owner.field( field_definition.name, description: field_definition.description, type: type_resolver.call(field_definition.type), null: true, connection_extension: nil, deprecation_reason: build_deprecation_reason(field_definition.directives), ast_node: field_definition, method_conflict_warning: false, camelize: false, directives: prepare_directives(field_definition, type_resolver), resolver_method: resolve_method_name, ) builder.build_arguments(schema_field_defn, field_definition.arguments, type_resolver) # Don't do this for interfaces if default_resolve define_field_resolve_method(owner, resolve_method_name, field_definition.name) end end end def define_field_resolve_method(owner, method_name, field_name) owner.define_method(method_name) { |**args| field_instance = context.types.field(owner, field_name) context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context) } end def build_resolve_type(lookup_hash, directives, missing_type_handler) resolve_type_proc = nil resolve_type_proc = ->(ast_node) { case ast_node when GraphQL::Language::Nodes::TypeName type_name = ast_node.name if lookup_hash.key?(type_name) lookup_hash[type_name] else missing_type_handler.call(type_name) end when GraphQL::Language::Nodes::NonNullType resolve_type_proc.call(ast_node.of_type).to_non_null_type when GraphQL::Language::Nodes::ListType resolve_type_proc.call(ast_node.of_type).to_list_type when String directives[ast_node] ||= missing_type_handler.call(ast_node) else raise "Unexpected ast_node: #{ast_node.inspect}" end } resolve_type_proc end end private_constant :Builder end end end graphql-ruby-2.5.19/lib/graphql/schema/build_from_definition/000077500000000000000000000000001514115062600242245ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/schema/build_from_definition/resolve_map.rb000066400000000000000000000054151514115062600270720ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/schema/build_from_definition/resolve_map/default_resolve" module GraphQL class Schema module BuildFromDefinition # Wrap a user-provided hash of resolution behavior for easy access at runtime. # # Coerce scalar values by: # - Checking for a function in the map like `{ Date: { coerce_input: ->(val, ctx) { ... }, coerce_result: ->(val, ctx) { ... } } }` # - Falling back to a passthrough # # Interface/union resolution can be provided as a `resolve_type:` key. # # @api private class ResolveMap module NullScalarCoerce def self.call(val, _ctx) val end end def initialize(user_resolve_hash) @resolve_hash = Hash.new do |h, k| # For each type name, provide a new hash if one wasn't given: h[k] = Hash.new do |h2, k2| if k2 == "coerce_input" || k2 == "coerce_result" # This isn't an object field, it's a scalar coerce function. # Use a passthrough NullScalarCoerce else # For each field, provide a resolver that will # make runtime checks & replace itself h2[k2] = DefaultResolve.new(h2, k2) end end end @user_resolve_hash = user_resolve_hash # User-provided resolve functions take priority over the default: @user_resolve_hash.each do |type_name, fields| type_name_s = type_name.to_s case fields when Hash fields.each do |field_name, resolve_fn| @resolve_hash[type_name_s][field_name.to_s] = resolve_fn end when Proc # for example, "resolve_type" @resolve_hash[type_name_s] = fields else raise ArgumentError, "Unexpected resolve hash value for #{type_name.inspect}: #{fields.inspect} (#{fields.class})" end end # Check the normalized hash, not the user input: if @resolve_hash.key?("resolve_type") define_singleton_method :resolve_type do |type, obj, ctx| @resolve_hash.fetch("resolve_type").call(type, obj, ctx) end end end def call(type, field, obj, args, ctx) resolver = @resolve_hash[type.graphql_name][field.graphql_name] resolver.call(obj, args, ctx) end def coerce_input(type, value, ctx) @resolve_hash[type.graphql_name]["coerce_input"].call(value, ctx) end def coerce_result(type, value, ctx) @resolve_hash[type.graphql_name]["coerce_result"].call(value, ctx) end end end end end graphql-ruby-2.5.19/lib/graphql/schema/build_from_definition/resolve_map/000077500000000000000000000000001514115062600265405ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/schema/build_from_definition/resolve_map/default_resolve.rb000066400000000000000000000032361514115062600322540ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema module BuildFromDefinition class ResolveMap class DefaultResolve def initialize(field_map, field_name) @field_map = field_map @field_name = field_name end # Make some runtime checks about # how `obj` implements the `field_name`. # # Create a new resolve function according to that implementation, then: # - update `field_map` with this implementation # - call the implementation now (to satisfy this field execution) # # If `obj` doesn't implement `field_name`, raise an error. def call(obj, args, ctx) method_name = @field_name if !obj.respond_to?(method_name) raise KeyError, "Can't resolve field #{method_name} on #{obj.inspect}" else method_arity = obj.method(method_name).arity resolver = case method_arity when 0, -1 # -1 Handles method_missing, eg openstruct ->(o, a, c) { o.public_send(method_name) } when 1 ->(o, a, c) { o.public_send(method_name, a) } when 2 ->(o, a, c) { o.public_send(method_name, a, c) } else raise "Unexpected resolve arity: #{method_arity}. Must be 0, 1, 2" end # Call the resolver directly next time @field_map[method_name] = resolver # Call through this time resolver.call(obj, args, ctx) end end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/built_in_types.rb000066400000000000000000000004471514115062600232550ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema BUILT_IN_TYPES = { "Int" => GraphQL::Types::Int, "String" => GraphQL::Types::String, "Float" => GraphQL::Types::Float, "Boolean" => GraphQL::Types::Boolean, "ID" => GraphQL::Types::ID, } end end graphql-ruby-2.5.19/lib/graphql/schema/directive.rb000066400000000000000000000243301514115062600221770ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema # Subclasses of this can influence how {GraphQL::Execution::Interpreter} runs queries. # # - {.include?}: if it returns `false`, the field or fragment will be skipped altogether, as if it were absent # - {.resolve}: Wraps field resolution (so it should call `yield` to continue) class Directive < GraphQL::Schema::Member extend GraphQL::Schema::Member::HasArguments extend GraphQL::Schema::Member::HasArguments::HasDirectiveArguments extend GraphQL::Schema::Member::HasValidators class << self # Directives aren't types, they don't have kinds. undef_method :kind def path "@#{super}" end # Return a name based on the class name, # but downcase the first letter. def default_graphql_name @default_graphql_name ||= begin camelized_name = super.dup camelized_name[0] = camelized_name[0].downcase -camelized_name end end def locations(*new_locations) if !new_locations.empty? new_locations.each do |new_loc| if !LOCATIONS.include?(new_loc.to_sym) raise ArgumentError, "#{self} (#{self.graphql_name}) has an invalid directive location: `locations #{new_loc}` " end end @locations = new_locations else @locations ||= (superclass.respond_to?(:locations) ? superclass.locations : []) end end def default_directive(new_default_directive = nil) if new_default_directive != nil @default_directive = new_default_directive elsif @default_directive.nil? @default_directive = (superclass.respond_to?(:default_directive) ? superclass.default_directive : false) else !!@default_directive end end def default_directive? default_directive end # If false, this part of the query won't be evaluated def include?(_object, arguments, context) static_include?(arguments, context) end # Determines whether {Execution::Lookahead} considers the field to be selected def static_include?(_arguments, _context) true end # Continuing is passed as a block; `yield` to continue def resolve(object, arguments, context) yield end # Continuing is passed as a block, yield to continue. def resolve_each(object, arguments, context) yield end def validate!(arguments, context) Schema::Validator.validate!(validators, self, context, arguments) end def on_field? locations.include?(FIELD) end def on_fragment? locations.include?(FRAGMENT_SPREAD) && locations.include?(INLINE_FRAGMENT) end def on_operation? locations.include?(QUERY) && locations.include?(MUTATION) && locations.include?(SUBSCRIPTION) end def repeatable? !!@repeatable end def repeatable(new_value) @repeatable = new_value end private def inherited(subclass) super subclass.class_exec do @default_graphql_name ||= nil end end end # @return [GraphQL::Schema::Field, GraphQL::Schema::Argument, Class, Module] attr_reader :owner # @return [GraphQL::Interpreter::Arguments] attr_reader :arguments class InvalidArgumentError < GraphQL::Error end def initialize(owner, **arguments) @owner = owner assert_valid_owner # It's be nice if we had the real context here, but we don't. What we _would_ get is: # - error handling # - lazy resolution # Probably, those won't be needed here, since these are configuration arguments, # not runtime arguments. context = Query::NullContext.instance self.class.all_argument_definitions.each do |arg_defn| keyword = arg_defn.keyword arg_type = arg_defn.type if arguments.key?(keyword) value = arguments[keyword] # This is a Ruby-land value; convert it to graphql for validation graphql_value = begin coerce_value = value if arg_type.list? && (!coerce_value.nil?) && (!coerce_value.is_a?(Array)) # When validating inputs, GraphQL accepts a single item # and implicitly converts it to a one-item list. # However, we're using result coercion here to go from Ruby value # to GraphQL value, so it doesn't have that feature. # Keep the GraphQL-type behavior but implement it manually: wrap_type = arg_type while wrap_type.list? if wrap_type.non_null? wrap_type = wrap_type.of_type end wrap_type = wrap_type.of_type coerce_value = [coerce_value] end end arg_type.coerce_isolated_result(coerce_value) rescue GraphQL::Schema::Enum::UnresolvedValueError # Let validation handle this value end else value = graphql_value = nil end result = arg_type.validate_input(graphql_value, context) if !result.valid? raise InvalidArgumentError, "@#{graphql_name}.#{arg_defn.graphql_name} on #{owner.path} is invalid (#{value.inspect}): #{result.problems.first["explanation"]}" end end self.class.validate!(arguments, context) @arguments = self.class.coerce_arguments(nil, arguments, context) if @arguments.is_a?(GraphQL::ExecutionError) raise @arguments end end def graphql_name self.class.graphql_name end LOCATIONS = [ QUERY = :QUERY, MUTATION = :MUTATION, SUBSCRIPTION = :SUBSCRIPTION, FIELD = :FIELD, FRAGMENT_DEFINITION = :FRAGMENT_DEFINITION, FRAGMENT_SPREAD = :FRAGMENT_SPREAD, INLINE_FRAGMENT = :INLINE_FRAGMENT, SCHEMA = :SCHEMA, SCALAR = :SCALAR, OBJECT = :OBJECT, FIELD_DEFINITION = :FIELD_DEFINITION, ARGUMENT_DEFINITION = :ARGUMENT_DEFINITION, INTERFACE = :INTERFACE, UNION = :UNION, ENUM = :ENUM, ENUM_VALUE = :ENUM_VALUE, INPUT_OBJECT = :INPUT_OBJECT, INPUT_FIELD_DEFINITION = :INPUT_FIELD_DEFINITION, VARIABLE_DEFINITION = :VARIABLE_DEFINITION, ] DEFAULT_DEPRECATION_REASON = 'No longer supported' LOCATION_DESCRIPTIONS = { QUERY: 'Location adjacent to a query operation.', MUTATION: 'Location adjacent to a mutation operation.', SUBSCRIPTION: 'Location adjacent to a subscription operation.', FIELD: 'Location adjacent to a field.', FRAGMENT_DEFINITION: 'Location adjacent to a fragment definition.', FRAGMENT_SPREAD: 'Location adjacent to a fragment spread.', INLINE_FRAGMENT: 'Location adjacent to an inline fragment.', SCHEMA: 'Location adjacent to a schema definition.', SCALAR: 'Location adjacent to a scalar definition.', OBJECT: 'Location adjacent to an object type definition.', FIELD_DEFINITION: 'Location adjacent to a field definition.', ARGUMENT_DEFINITION: 'Location adjacent to an argument definition.', INTERFACE: 'Location adjacent to an interface definition.', UNION: 'Location adjacent to a union definition.', ENUM: 'Location adjacent to an enum definition.', ENUM_VALUE: 'Location adjacent to an enum value definition.', INPUT_OBJECT: 'Location adjacent to an input object type definition.', INPUT_FIELD_DEFINITION: 'Location adjacent to an input object field definition.', VARIABLE_DEFINITION: 'Location adjacent to a variable definition.', } private def assert_valid_owner case @owner when Class if @owner < GraphQL::Schema::Object assert_has_location(OBJECT) elsif @owner < GraphQL::Schema::Union assert_has_location(UNION) elsif @owner < GraphQL::Schema::Enum assert_has_location(ENUM) elsif @owner < GraphQL::Schema::InputObject assert_has_location(INPUT_OBJECT) elsif @owner < GraphQL::Schema::Scalar assert_has_location(SCALAR) elsif @owner < GraphQL::Schema assert_has_location(SCHEMA) elsif @owner < GraphQL::Schema::Resolver assert_has_location(FIELD_DEFINITION) else raise "Unexpected directive owner class: #{@owner}" end when Module assert_has_location(INTERFACE) when GraphQL::Schema::Argument if @owner.owner.is_a?(GraphQL::Schema::Field) assert_has_location(ARGUMENT_DEFINITION) else assert_has_location(INPUT_FIELD_DEFINITION) end when GraphQL::Schema::Field assert_has_location(FIELD_DEFINITION) when GraphQL::Schema::EnumValue assert_has_location(ENUM_VALUE) else raise "Unexpected directive owner: #{@owner.inspect}" end end def assert_has_location(location) if !self.class.locations.include?(location) raise ArgumentError, <<-MD Directive `@#{self.class.graphql_name}` can't be attached to #{@owner.graphql_name} because #{location} isn't included in its locations (#{self.class.locations.join(", ")}). Use `locations(#{location})` to update this directive's definition, or remove it from #{@owner.graphql_name}. MD end end end end end graphql-ruby-2.5.19/lib/graphql/schema/directive/000077500000000000000000000000001514115062600216505ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/schema/directive/deprecated.rb000066400000000000000000000015561514115062600243040ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Directive < GraphQL::Schema::Member class Deprecated < GraphQL::Schema::Directive description "Marks an element of a GraphQL schema as no longer supported." locations(GraphQL::Schema::Directive::FIELD_DEFINITION, GraphQL::Schema::Directive::ENUM_VALUE, GraphQL::Schema::Directive::ARGUMENT_DEFINITION, GraphQL::Schema::Directive::INPUT_FIELD_DEFINITION) reason_description = "Explains why this element was deprecated, usually also including a "\ "suggestion for how to access supported similar data. Formatted "\ "in [Markdown](https://daringfireball.net/projects/markdown/)." argument :reason, String, reason_description, default_value: Directive::DEFAULT_DEPRECATION_REASON, required: false default_directive true end end end end graphql-ruby-2.5.19/lib/graphql/schema/directive/feature.rb000066400000000000000000000054061514115062600236350ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Directive < GraphQL::Schema::Member # An example directive to show how you might interact with the runtime. # # This directive might be used along with a server-side feature flag system like Flipper. # # With that system, you could use this directive to exclude parts of a query # if the current viewer doesn't have certain flags enabled. # (So, this flag would be for internal clients, like your iOS app, not third-party API clients.) # # To use it, you have to implement `.enabled?`, for example: # # @example Implementing the Feature directive # # app/graphql/directives/feature.rb # class Directives::Feature < GraphQL::Schema::Directive::Feature # def self.enabled?(flag_name, _obj, context) # # Translate some GraphQL data for Ruby: # flag_key = flag_name.underscore # current_user = context[:viewer] # # Check the feature flag however your app does it: # MyFeatureFlags.enabled?(current_user, flag_key) # end # end # # @example Flagging a part of the query # viewer { # # This field only runs if `.enabled?("recommendationEngine", obj, context)` # # returns true. Otherwise, it's treated as if it didn't exist. # recommendations @feature(flag: "recommendationEngine") { # name # rating # } # } class Feature < Schema::Directive description "Directs the executor to run this only if a certain server-side feature is enabled." locations( GraphQL::Schema::Directive::FIELD, GraphQL::Schema::Directive::FRAGMENT_SPREAD, GraphQL::Schema::Directive::INLINE_FRAGMENT ) argument :flag, String, description: "The name of the feature to check before continuing" # Implement the Directive API def self.include?(object, arguments, context) flag_name = arguments[:flag] self.enabled?(flag_name, object, context) end # Override this method in your app's subclass of this directive. # # @param flag_name [String] The client-provided string of a feature to check # @param object [GraphQL::Schema::Objct] The currently-evaluated GraphQL object instance # @param context [GraphQL::Query::Context] # @return [Boolean] If truthy, execution will continue def self.enabled?(flag_name, object, context) raise GraphQL::RequiredImplementationMissingError, "Implement `.enabled?(flag_name, object, context)` to return true or false for the feature flag (#{flag_name.inspect})" end end end end end graphql-ruby-2.5.19/lib/graphql/schema/directive/flagged.rb000066400000000000000000000045021514115062600235670ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Directive < GraphQL::Schema::Member # This is _similar_ to {Directive::Feature}, except it's prescribed by the server, not the client. # # In this case, the server hides types and fields _entirely_, unless the current context has certain `:flags` present. class Flagged < GraphQL::Schema::Directive def initialize(target, **options) if target.is_a?(Module) # This is type class of some kind, `include` will put this module # in between the type class itself and its super class, so `super` will work fine target.include(VisibleByFlag) elsif !target.is_a?(VisibleByFlag) # This is an instance of a base class. `include` won't put this in front of the # base class implementation, so we need to `.prepend`. # `#visible?` could probably be moved to a module and then this could use `include` instead. target.class.prepend(VisibleByFlag) end super end description "Hides this part of the schema unless the named flag is present in context[:flags]" locations( GraphQL::Schema::Directive::FIELD_DEFINITION, GraphQL::Schema::Directive::OBJECT, GraphQL::Schema::Directive::SCALAR, GraphQL::Schema::Directive::ENUM, GraphQL::Schema::Directive::UNION, GraphQL::Schema::Directive::INTERFACE, GraphQL::Schema::Directive::INPUT_OBJECT, GraphQL::Schema::Directive::ENUM_VALUE, GraphQL::Schema::Directive::ARGUMENT_DEFINITION, GraphQL::Schema::Directive::INPUT_FIELD_DEFINITION, ) argument :by, [String], "Flags to check for this schema member" repeatable(true) module VisibleByFlag def self.included(schema_class) schema_class.extend(self) end def visible?(context) if dir = self.directives.find { |d| d.is_a?(Flagged) } relevant_flags = (f = context[:flags]) && dir.arguments[:by] & f # rubocop:disable Development/ContextIsPassedCop -- definition-related relevant_flags && !relevant_flags.empty? && super else super end end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/directive/include.rb000066400000000000000000000012351514115062600236210ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Directive < GraphQL::Schema::Member class Include < GraphQL::Schema::Directive description "Directs the executor to include this field or fragment only when the `if` argument is true." locations( GraphQL::Schema::Directive::FIELD, GraphQL::Schema::Directive::FRAGMENT_SPREAD, GraphQL::Schema::Directive::INLINE_FRAGMENT ) argument :if, Boolean, description: "Included when true." default_directive true def self.static_include?(args, ctx) !!args[:if] end end end end end graphql-ruby-2.5.19/lib/graphql/schema/directive/one_of.rb000066400000000000000000000010551514115062600234430ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Directive < GraphQL::Schema::Member class OneOf < GraphQL::Schema::Directive description "Requires that exactly one field must be supplied and that field must not be `null`." locations(GraphQL::Schema::Directive::INPUT_OBJECT) default_directive true def initialize(...) super owner.extend(IsOneOf) end module IsOneOf def one_of? true end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/directive/skip.rb000066400000000000000000000012071514115062600231430ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Directive < GraphQL::Schema::Member class Skip < Schema::Directive description "Directs the executor to skip this field or fragment when the `if` argument is true." locations( GraphQL::Schema::Directive::FIELD, GraphQL::Schema::Directive::FRAGMENT_SPREAD, GraphQL::Schema::Directive::INLINE_FRAGMENT ) argument :if, Boolean, description: "Skipped when true." default_directive true def self.static_include?(args, ctx) !args[:if] end end end end end graphql-ruby-2.5.19/lib/graphql/schema/directive/specified_by.rb000066400000000000000000000007031514115062600246220ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Directive < GraphQL::Schema::Member class SpecifiedBy < GraphQL::Schema::Directive description "Exposes a URL that specifies the behavior of this scalar." locations(GraphQL::Schema::Directive::SCALAR) default_directive true argument :url, String, description: "The URL that specifies the behavior of this scalar." end end end end graphql-ruby-2.5.19/lib/graphql/schema/directive/transform.rb000066400000000000000000000036271514115062600242200ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Directive < GraphQL::Schema::Member # An example directive to show how you might interact with the runtime. # # This directive takes the return value of the tagged part of the query, # and if the named transform is whitelisted and applies to the return value, # it's applied by calling a method with that name. # # @example Installing the directive # class MySchema < GraphQL::Schema # directive(GraphQL::Schema::Directive::Transform) # end # # @example Transforming strings # viewer { # username @transform(by: "upcase") # } class Transform < Schema::Directive description "Directs the executor to run named transform on the return value." locations( GraphQL::Schema::Directive::FIELD, ) argument :by, String, description: "The name of the transform to run if applicable" TRANSFORMS = [ "upcase", "downcase", # ?? ] # Implement the Directive API def self.resolve(object, arguments, context) path = context.namespace(:interpreter)[:current_path] return_value = yield transform_name = arguments[:by] if TRANSFORMS.include?(transform_name) && return_value.respond_to?(transform_name) return_value = return_value.public_send(transform_name) response = context.namespace(:interpreter_runtime)[:runtime].final_result *keys, last = path keys.each do |key| if response && (response = response[key]) next else break end end if response response[last] = return_value end nil end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/enum.rb000066400000000000000000000261351514115062600211720ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema # Extend this class to define GraphQL enums in your schema. # # By default, GraphQL enum values are translated into Ruby strings. # You can provide a custom value with the `value:` keyword. # # @example # # equivalent to # # enum PizzaTopping { # # MUSHROOMS # # ONIONS # # PEPPERS # # } # class PizzaTopping < GraphQL::Schema::Enum # value :MUSHROOMS # value :ONIONS # value :PEPPERS # end class Enum < GraphQL::Schema::Member extend GraphQL::Schema::Member::ValidatesInput # This is raised when either: # # - A resolver returns a value which doesn't match any of the enum's configured values; # - Or, the resolver returns a value which matches a value, but that value's `authorized?` check returns false. # # In either case, the field should be modified so that the invalid value isn't returned. # # {GraphQL::Schema::Enum} subclasses get their own subclass of this error, so that bug trackers can better show where they came from. class UnresolvedValueError < GraphQL::Error def initialize(value:, enum:, context:, authorized:) fix_message = if authorized == false ", but this value was unauthorized. Update the field or resolver to return a different value in this case (or return `nil`)." else ", but this isn't a valid value for `#{enum.graphql_name}`. Update the field or resolver to return one of `#{enum.graphql_name}`'s values instead." end message = if (cp = context[:current_path]) && (cf = context[:current_field]) "`#{cf.path}` returned `#{value.inspect}` at `#{cp.join(".")}`#{fix_message}" else "`#{value.inspect}` was returned for `#{enum.graphql_name}`#{fix_message}" end super(message) end end # Raised when a {GraphQL::Schema::Enum} is defined to have no values. # This can also happen when all values return false for `.visible?`. class MissingValuesError < GraphQL::Error def initialize(enum_type) @enum_type = enum_type super("Enum types require at least one value, but #{enum_type.graphql_name} didn't provide any for this query. Make sure at least one value is defined and visible for this query.") end end class << self # Define a value for this enum # @option kwargs [String, Symbol] :graphql_name the GraphQL value for this, usually `SCREAMING_CASE` # @option kwargs [String] :description, the GraphQL description for this value, present in documentation # @option kwargs [String] :comment, the GraphQL comment for this value, present in documentation # @option kwargs [::Object] :value the translated Ruby value for this object (defaults to `graphql_name`) # @option kwargs [::Object] :value_method, the method name to fetch `graphql_name` (defaults to `graphql_name.downcase`) # @option kwargs [String] :deprecation_reason if this object is deprecated, include a message here # @param value_method [Symbol, false] A method to generate for this value, or `false` to skip generation # @return [void] # @see {Schema::EnumValue} which handles these inputs by default def value(*args, value_method: nil, **kwargs, &block) kwargs[:owner] = self value = enum_value_class.new(*args, **kwargs, &block) if value_method || (value_methods && value_method != false) generate_value_method(value, value_method) end key = value.graphql_name prev_value = own_values[key] case prev_value when nil own_values[key] = value when GraphQL::Schema::EnumValue own_values[key] = [prev_value, value] when Array prev_value << value else raise "Invariant: Unexpected enum value for #{key.inspect}: #{prev_value.inspect}" end value end # @return [Array] Possible values of this enum def enum_values(context = GraphQL::Query::NullContext.instance) inherited_values = superclass.respond_to?(:enum_values) ? superclass.enum_values(context) : nil visible_values = [] types = Warden.types_from_context(context) own_values.each do |key, values_entry| visible_value = nil if values_entry.is_a?(Array) values_entry.each do |v| if types.visible_enum_value?(v, context) if visible_value.nil? visible_value = v visible_values << v else raise DuplicateNamesError.new( duplicated_name: v.path, duplicated_definition_1: visible_value.inspect, duplicated_definition_2: v.inspect ) end end end elsif types.visible_enum_value?(values_entry, context) visible_values << values_entry end end if inherited_values # Local values take precedence over inherited ones inherited_values.each do |i_val| if !visible_values.any? { |v| v.graphql_name == i_val.graphql_name } visible_values << i_val end end end visible_values end # @return [Array] An unfiltered list of all definitions def all_enum_value_definitions all_defns = if superclass.respond_to?(:all_enum_value_definitions) superclass.all_enum_value_definitions else [] end @own_values && @own_values.each do |_key, value| if value.is_a?(Array) all_defns.concat(value) else all_defns << value end end all_defns end # @return [Hash GraphQL::Schema::EnumValue>] Possible values of this enum, keyed by name. def values(context = GraphQL::Query::NullContext.instance) enum_values(context).each_with_object({}) { |val, obj| obj[val.graphql_name] = val } end # @return [Class] for handling `value(...)` inputs and building `GraphQL::Enum::EnumValue`s out of them def enum_value_class(new_enum_value_class = nil) if new_enum_value_class @enum_value_class = new_enum_value_class elsif defined?(@enum_value_class) && @enum_value_class @enum_value_class else superclass <= GraphQL::Schema::Enum ? superclass.enum_value_class : nil end end def value_methods(new_value = NOT_CONFIGURED) if NOT_CONFIGURED.equal?(new_value) if @value_methods != nil @value_methods else find_inherited_value(:value_methods, false) end else @value_methods = new_value end end def kind GraphQL::TypeKinds::ENUM end def validate_non_null_input(value_name, ctx, max_errors: nil) allowed_values = ctx.types.enum_values(self) matching_value = allowed_values.find { |v| v.graphql_name == value_name } if matching_value.nil? GraphQL::Query::InputValidationResult.from_problem("Expected #{GraphQL::Language.serialize(value_name)} to be one of: #{allowed_values.map(&:graphql_name).join(', ')}") else nil end # rescue MissingValuesError # nil end # Called by the runtime when a field returns a value to give back to the client. # This method checks that the incoming {value} matches one of the enum's defined values. # @param value [Object] Any value matching the values for this enum. # @param ctx [GraphQL::Query::Context] # @raise [GraphQL::Schema::Enum::UnresolvedValueError] if {value} doesn't match a configured value or if the matching value isn't authorized. # @return [String] The GraphQL-ready string for {value} def coerce_result(value, ctx) types = ctx.types all_values = types ? types.enum_values(self) : values.each_value enum_value = all_values.find { |val| val.value == value } if enum_value && (was_authed = enum_value.authorized?(ctx)) enum_value.graphql_name else raise self::UnresolvedValueError.new(enum: self, value: value, context: ctx, authorized: was_authed) end end # Called by the runtime with incoming string representations from a query. # It will match the string to a configured by name or by Ruby value. # @param value_name [String, Object] A string from a GraphQL query, or a Ruby value matching a `value(..., value: ...)` configuration # @param ctx [GraphQL::Query::Context] # @raise [GraphQL::UnauthorizedEnumValueError] if an {EnumValue} matches but returns false for `.authorized?`. Goes to {Schema.unauthorized_object}. # @return [Object] The Ruby value for the matched {GraphQL::Schema::EnumValue} def coerce_input(value_name, ctx) all_values = ctx.types ? ctx.types.enum_values(self) : values.each_value # This tries matching by incoming GraphQL string, then checks Ruby-defined values if v = (all_values.find { |val| val.graphql_name == value_name } || all_values.find { |val| val.value == value_name }) if v.authorized?(ctx) v.value else raise GraphQL::UnauthorizedEnumValueError.new(type: self, enum_value: v, context: ctx) end else nil end end def inherited(child_class) if child_class.name # Don't assign a custom error class to anonymous classes # because they would end up with names like `#::UnresolvedValueError` which messes up bug trackers child_class.const_set(:UnresolvedValueError, Class.new(Schema::Enum::UnresolvedValueError)) end child_class.class_exec { @value_methods = nil } super end private def own_values @own_values ||= {} end def generate_value_method(value, configured_value_method) return if configured_value_method == false value_method_name = configured_value_method || value.graphql_name.downcase if respond_to?(value_method_name.to_sym) warn "Failed to define value method for :#{value_method_name}, because " \ "#{value.owner.name || value.owner.graphql_name} already responds to that method. Use `value_method:` to override the method name " \ "or `value_method: false` to disable Enum value method generation." return end define_singleton_method(value_method_name) { value.graphql_name } end end enum_value_class(GraphQL::Schema::EnumValue) end end end graphql-ruby-2.5.19/lib/graphql/schema/enum_value.rb000066400000000000000000000046731514115062600223710ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema # A possible value for an {Enum}. # # You can extend this class to customize enum values in your schema. # # @example custom enum value class # # define a custom class: # class CustomEnumValue < GraphQL::Schema::EnumValue # def initialize(*args) # # arguments to `value(...)` in Enum classes are passed here # super # end # end # # class BaseEnum < GraphQL::Schema::Enum # # use it for these enums: # enum_value_class CustomEnumValue # end class EnumValue < GraphQL::Schema::Member include GraphQL::Schema::Member::HasPath include GraphQL::Schema::Member::HasAstNode include GraphQL::Schema::Member::HasDirectives include GraphQL::Schema::Member::HasDeprecationReason attr_reader :graphql_name # @return [Class] The enum type that owns this value attr_reader :owner def initialize(graphql_name, desc = nil, owner:, ast_node: nil, directives: nil, description: nil, comment: nil, value: NOT_CONFIGURED, deprecation_reason: nil, &block) @graphql_name = graphql_name.to_s GraphQL::NameValidator.validate!(@graphql_name) @description = desc || description @comment = comment @value = value == NOT_CONFIGURED ? @graphql_name : value if deprecation_reason self.deprecation_reason = deprecation_reason end @owner = owner @ast_node = ast_node if directives directives.each do |dir_class, dir_options| directive(dir_class, **dir_options) end end if block_given? instance_exec(self, &block) end end def description(new_desc = nil) if new_desc @description = new_desc end @description end def comment(new_comment = nil) if new_comment @comment = new_comment end @comment end def value(new_val = nil) unless new_val.nil? @value = new_val end @value end def inspect "#<#{self.class} #{path} @value=#{@value.inspect}#{description ? " @description=#{description.inspect}" : ""}#{deprecation_reason ? " @deprecation_reason=#{deprecation_reason.inspect}" : ""}>" end def visible?(_ctx); true; end def authorized?(_ctx); true; end end end end graphql-ruby-2.5.19/lib/graphql/schema/field.rb000066400000000000000000001141021514115062600213010ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/schema/field/connection_extension" require "graphql/schema/field/scope_extension" module GraphQL class Schema class Field include GraphQL::Schema::Member::HasArguments include GraphQL::Schema::Member::HasArguments::FieldConfigured include GraphQL::Schema::Member::HasAstNode include GraphQL::Schema::Member::HasPath include GraphQL::Schema::Member::HasValidators extend GraphQL::Schema::FindInheritedValue include GraphQL::EmptyObjects include GraphQL::Schema::Member::HasDirectives include GraphQL::Schema::Member::HasDeprecationReason class FieldImplementationFailed < GraphQL::Error; end # @return [String] the GraphQL name for this field, camelized unless `camelize: false` is provided attr_reader :name alias :graphql_name :name attr_writer :description # @return [Symbol] Method or hash key on the underlying object to look up attr_reader :method_sym # @return [String] Method or hash key on the underlying object to look up attr_reader :method_str attr_reader :hash_key attr_reader :dig_keys # @return [Symbol] The method on the type to look up def resolver_method if @resolver_class @resolver_class.resolver_method else @resolver_method end end # @return [String, nil] def deprecation_reason super || @resolver_class&.deprecation_reason end def directives if @resolver_class && !(r_dirs = @resolver_class.directives).empty? if !(own_dirs = super).empty? new_dirs = own_dirs.dup r_dirs.each do |r_dir| if r_dir.class.repeatable? || ( (r_dir_name = r_dir.graphql_name) && (!new_dirs.any? { |d| d.graphql_name == r_dir_name }) ) new_dirs << r_dir end end new_dirs else r_dirs end else super end end # @return [Class] The thing this field was defined on (type, mutation, resolver) attr_accessor :owner # @return [Class] The GraphQL type this field belongs to. (For fields defined on mutations, it's the payload type) def owner_type @owner_type ||= if owner.nil? raise GraphQL::InvariantError, "Field #{original_name.inspect} (graphql name: #{graphql_name.inspect}) has no owner, but all fields should have an owner. How did this happen?!" elsif owner < GraphQL::Schema::Mutation owner.payload_type else owner end end # @return [Symbol] the original name of the field, passed in by the user attr_reader :original_name # @return [Class, nil] The {Schema::Resolver} this field was derived from, if there is one def resolver @resolver_class end # @return [Boolean] Is this field a predefined introspection field? def introspection? @introspection end def inspect "#<#{self.class} #{path}#{!all_argument_definitions.empty? ? "(...)" : ""}: #{type.to_type_signature}>" end alias :mutation :resolver # @return [Boolean] Apply tracing to this field? (Default: skip scalars, this is the override value) attr_reader :trace # @return [String, nil] def subscription_scope @subscription_scope || (@resolver_class.respond_to?(:subscription_scope) ? @resolver_class.subscription_scope : nil) end attr_writer :subscription_scope # Can be set with `connection: true|false` or inferred from a type name ending in `*Connection` # @return [Boolean] if true, this field will be wrapped with Relay connection behavior def connection? if @connection.nil? # Provide default based on type name return_type_name = if @return_type_expr Member::BuildType.to_type_name(@return_type_expr) elsif @resolver_class && @resolver_class.type Member::BuildType.to_type_name(@resolver_class.type) elsif type # As a last ditch, try to force loading the return type: type.unwrap.name end if return_type_name @connection = return_type_name.end_with?("Connection") && return_type_name != "Connection" else # TODO set this when type is set by method false # not loaded yet? end else @connection end end # @return [Boolean] if true, the return type's `.scope_items` method will be applied to this field's return value def scoped? if !@scope.nil? # The default was overridden @scope elsif @return_type_expr # Detect a list return type, but don't call `type` since that may eager-load an otherwise lazy-loaded type @return_type_expr.is_a?(Array) || (@return_type_expr.is_a?(String) && @return_type_expr.include?("[")) || connection? elsif @resolver_class resolver_type = @resolver_class.type_expr resolver_type.is_a?(Array) || (resolver_type.is_a?(String) && resolver_type.include?("[")) || connection? else false end end # This extension is applied to fields when {#connection?} is true. # # You can override it in your base field definition. # @return [Class] A {FieldExtension} subclass for implementing pagination behavior. # @example Configuring a custom extension # class Types::BaseField < GraphQL::Schema::Field # connection_extension(MyCustomExtension) # end def self.connection_extension(new_extension_class = nil) if new_extension_class @connection_extension = new_extension_class else @connection_extension ||= find_inherited_value(:connection_extension, ConnectionExtension) end end # @return Boolean attr_reader :relay_node_field # @return Boolean attr_reader :relay_nodes_field # @return [Boolean] Should we warn if this field's name conflicts with a built-in method? def method_conflict_warning? @method_conflict_warning end # @param name [Symbol] The underscore-cased version of this field name (will be camelized for the GraphQL API) # @param type [Class, GraphQL::BaseType, Array] The return type of this field # @param owner [Class] The type that this field belongs to # @param null [Boolean] (defaults to `true`) `true` if this field may return `null`, `false` if it is never `null` # @param description [String] Field description # @param comment [String] Field comment # @param deprecation_reason [String] If present, the field is marked "deprecated" with this message # @param method [Symbol] The method to call on the underlying object to resolve this field (defaults to `name`) # @param hash_key [String, Symbol] The hash key to lookup on the underlying object (if its a Hash) to resolve this field (defaults to `name` or `name.to_s`) # @param dig [Array] The nested hash keys to lookup on the underlying hash to resolve this field using dig # @param resolver_method [Symbol] The method on the type to call to resolve this field (defaults to `name`) # @param connection [Boolean] `true` if this field should get automagic connection behavior; default is to infer by `*Connection` in the return type name # @param connection_extension [Class] The extension to add, to implement connections. If `nil`, no extension is added. # @param max_page_size [Integer, nil] For connections, the maximum number of items to return from this field, or `nil` to allow unlimited results. # @param default_page_size [Integer, nil] For connections, the default number of items to return from this field, or `nil` to return unlimited results. # @param introspection [Boolean] If true, this field will be marked as `#introspection?` and the name may begin with `__` # @param resolver_class [Class] (Private) A {Schema::Resolver} which this field was derived from. Use `resolver:` to create a field with a resolver. # @param arguments [{String=>GraphQL::Schema::Argument, Hash}] Arguments for this field (may be added in the block, also) # @param camelize [Boolean] If true, the field name will be camelized when building the schema # @param complexity [Numeric] When provided, set the complexity for this field # @param scope [Boolean] If true, the return type's `.scope_items` method will be called on the return value # @param subscription_scope [Symbol, String] A key in `context` which will be used to scope subscription payloads # @param extensions [Array Object>>] Named extensions to apply to this field (see also {#extension}) # @param directives [Hash{Class => Hash}] Directives to apply to this field # @param trace [Boolean] If true, a {GraphQL::Tracing} tracer will measure this scalar field # @param broadcastable [Boolean] Whether or not this field can be distributed in subscription broadcasts # @param ast_node [Language::Nodes::FieldDefinition, nil] If this schema was parsed from definition, this AST node defined the field # @param method_conflict_warning [Boolean] If false, skip the warning if this field's method conflicts with a built-in method # @param validates [Array] Configurations for validating this field # @param fallback_value [Object] A fallback value if the method is not defined # @param dynamic_introspection [Boolean] (Private, used by GraphQL-Ruby) # @param relay_node_field [Boolean] (Private, used by GraphQL-Ruby) # @param relay_nodes_field [Boolean] (Private, used by GraphQL-Ruby) # @param extras [Array<:ast_node, :parent, :lookahead, :owner, :execution_errors, :graphql_name, :argument_details, Symbol>] Extra arguments to be injected into the resolver for this field # @param definition_block [Proc] an additional block for configuring the field. Receive the field as a block param, or, if no block params are defined, then the block is `instance_eval`'d on the new {Field}. def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CONFIGURED, comment: NOT_CONFIGURED, deprecation_reason: nil, method: nil, hash_key: nil, dig: nil, resolver_method: nil, connection: nil, max_page_size: NOT_CONFIGURED, default_page_size: NOT_CONFIGURED, scope: nil, introspection: false, camelize: true, trace: nil, complexity: nil, ast_node: nil, extras: EMPTY_ARRAY, extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, broadcastable: NOT_CONFIGURED, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, fallback_value: NOT_CONFIGURED, dynamic_introspection: false, &definition_block) if name.nil? raise ArgumentError, "missing first `name` argument or keyword `name:`" end if !(resolver_class) if type.nil? && !block_given? raise ArgumentError, "missing second `type` argument, keyword `type:`, or a block containing `type(...)`" end end @original_name = name name_s = -name.to_s @underscored_name = -Member::BuildType.underscore(name_s) @name = -(camelize ? Member::BuildType.camelize(name_s) : name_s) NameValidator.validate!(@name) @description = description @comment = comment @type = @owner_type = @own_validators = @own_directives = @own_arguments = @arguments_statically_coercible = nil # these will be prepared later if necessary self.deprecation_reason = deprecation_reason if method && hash_key && dig raise ArgumentError, "Provide `method:`, `hash_key:` _or_ `dig:`, not multiple. (called with: `method: #{method.inspect}, hash_key: #{hash_key.inspect}, dig: #{dig.inspect}`)" end if resolver_method if method raise ArgumentError, "Provide `method:` _or_ `resolver_method:`, not both. (called with: `method: #{method.inspect}, resolver_method: #{resolver_method.inspect}`)" end if hash_key || dig raise ArgumentError, "Provide `hash_key:`, `dig:`, _or_ `resolver_method:`, not multiple. (called with: `hash_key: #{hash_key.inspect}, dig: #{dig.inspect}, resolver_method: #{resolver_method.inspect}`)" end end method_name = method || hash_key || name_s @dig_keys = dig if hash_key @hash_key = hash_key @hash_key_str = hash_key.to_s else @hash_key = NOT_CONFIGURED @hash_key_str = NOT_CONFIGURED end @method_str = -method_name.to_s @method_sym = method_name.to_sym @resolver_method = (resolver_method || name_s).to_sym @complexity = complexity @dynamic_introspection = dynamic_introspection @return_type_expr = type @return_type_null = if !null.nil? null elsif resolver_class nil else true end @connection = connection @max_page_size = max_page_size @default_page_size = default_page_size @introspection = introspection @extras = extras @broadcastable = broadcastable @resolver_class = resolver_class @scope = scope @trace = trace @relay_node_field = relay_node_field @relay_nodes_field = relay_nodes_field @ast_node = ast_node @method_conflict_warning = method_conflict_warning @fallback_value = fallback_value @definition_block = definition_block arguments.each do |name, arg| case arg when Hash argument(name: name, **arg) when GraphQL::Schema::Argument add_argument(arg) when Array arg.each { |a| add_argument(a) } else raise ArgumentError, "Unexpected argument config (#{arg.class}): #{arg.inspect}" end end @owner = owner @subscription_scope = subscription_scope @extensions = EMPTY_ARRAY @call_after_define = false set_pagination_extensions(connection_extension: NOT_CONFIGURED.equal?(connection_extension) ? self.class.connection_extension : connection_extension) # Do this last so we have as much context as possible when initializing them: if !extensions.empty? self.extensions(extensions) end if resolver_class && !resolver_class.extensions.empty? self.extensions(resolver_class.extensions) end if !directives.empty? directives.each do |(dir_class, options)| self.directive(dir_class, **options) end end if !validates.empty? self.validates(validates) end if @definition_block.nil? self.extensions.each(&:after_define_apply) @call_after_define = true end end # Calls the definition block, if one was given. # This is deferred so that references to the return type # can be lazily evaluated, reducing Rails boot time. # @return [self] # @api private def ensure_loaded if @definition_block if @definition_block.arity == 1 @definition_block.call(self) else instance_exec(self, &@definition_block) end self.extensions.each(&:after_define_apply) @call_after_define = true @definition_block = nil end self end attr_accessor :dynamic_introspection # If true, subscription updates with this field can be shared between viewers # @return [Boolean, nil] # @see GraphQL::Subscriptions::BroadcastAnalyzer def broadcastable? if !NOT_CONFIGURED.equal?(@broadcastable) @broadcastable elsif @resolver_class @resolver_class.broadcastable? else nil end end # @param text [String] # @return [String] def description(text = nil) if text @description = text elsif !NOT_CONFIGURED.equal?(@description) @description elsif @resolver_class @resolver_class.description else nil end end # @param text [String] # @return [String, nil] def comment(text = nil) if text @comment = text elsif !NOT_CONFIGURED.equal?(@comment) @comment elsif @resolver_class @resolver_class.comment else nil end end # Read extension instances from this field, # or add new classes/options to be initialized on this field. # Extensions are executed in the order they are added. # # @example adding an extension # extensions([MyExtensionClass]) # # @example adding multiple extensions # extensions([MyExtensionClass, AnotherExtensionClass]) # # @example adding an extension with options # extensions([MyExtensionClass, { AnotherExtensionClass => { filter: true } }]) # # @param extensions [Array Hash>>] Add extensions to this field. For hash elements, only the first key/value is used. # @return [Array] extensions to apply to this field def extensions(new_extensions = nil) if new_extensions new_extensions.each do |extension_config| if extension_config.is_a?(Hash) extension_class, options = *extension_config.to_a[0] self.extension(extension_class, **options) else self.extension(extension_config) end end end @extensions end # Add `extension` to this field, initialized with `options` if provided. # # @example adding an extension # extension(MyExtensionClass) # # @example adding an extension with options # extension(MyExtensionClass, filter: true) # # @param extension_class [Class] subclass of {Schema::FieldExtension} # @param options [Hash] if provided, given as `options:` when initializing `extension`. # @return [void] def extension(extension_class, **options) extension_inst = extension_class.new(field: self, options: options) if @extensions.frozen? @extensions = @extensions.dup end if @call_after_define extension_inst.after_define_apply end @extensions << extension_inst nil end # Read extras (as symbols) from this field, # or add new extras to be opted into by this field's resolver. # # @param new_extras [Array] Add extras to this field # @return [Array] def extras(new_extras = nil) if new_extras.nil? # Read the value field_extras = @extras if @resolver_class && !@resolver_class.extras.empty? field_extras + @resolver_class.extras else field_extras end else if @extras.frozen? @extras = @extras.dup end # Append to the set of extras on this field @extras.concat(new_extras) end end def calculate_complexity(query:, nodes:, child_complexity:) if respond_to?(:complexity_for) lookahead = GraphQL::Execution::Lookahead.new(query: query, field: self, ast_nodes: nodes, owner_type: owner) complexity_for(child_complexity: child_complexity, query: query, lookahead: lookahead) elsif connection? arguments = query.arguments_for(nodes.first, self) max_possible_page_size = nil if arguments.respond_to?(:[]) # It might have been an error if arguments[:first] max_possible_page_size = arguments[:first] end if arguments[:last] && (max_possible_page_size.nil? || arguments[:last] > max_possible_page_size) max_possible_page_size = arguments[:last] end elsif arguments.is_a?(GraphQL::ExecutionError) || arguments.is_a?(GraphQL::UnauthorizedError) raise arguments end if max_possible_page_size.nil? max_possible_page_size = default_page_size || query.schema.default_page_size || max_page_size || query.schema.default_max_page_size end if max_possible_page_size.nil? raise GraphQL::Error, "Can't calculate complexity for #{path}, no `first:`, `last:`, `default_page_size`, `max_page_size` or `default_max_page_size`" else metadata_complexity = 0 lookahead = GraphQL::Execution::Lookahead.new(query: query, field: self, ast_nodes: nodes, owner_type: owner) lookahead.selections.each do |next_lookahead| # this includes `pageInfo`, `nodes` and `edges` and any custom fields # TODO this doesn't support procs yet -- unlikely to need it. metadata_complexity += next_lookahead.field.complexity if next_lookahead.name != :nodes && next_lookahead.name != :edges # subfields, eg, for pageInfo -- assumes no subselections metadata_complexity += next_lookahead.selections.size end end # Possible bug: selections on `edges` and `nodes` are _both_ multiplied here. Should they be? items_complexity = child_complexity - metadata_complexity subfields_complexity = (max_possible_page_size * items_complexity) + metadata_complexity # Apply this field's own complexity apply_own_complexity_to(subfields_complexity, query, nodes) end else apply_own_complexity_to(child_complexity, query, nodes) end end def complexity(new_complexity = nil) case new_complexity when Proc if new_complexity.parameters.size != 3 fail( "A complexity proc should always accept 3 parameters: ctx, args, child_complexity. "\ "E.g.: complexity ->(ctx, args, child_complexity) { child_complexity * args[:limit] }" ) else @complexity = new_complexity end when Numeric @complexity = new_complexity when nil if @resolver_class @complexity || @resolver_class.complexity || 1 else @complexity || 1 end else raise("Invalid complexity: #{new_complexity.inspect} on #{@name}") end end # @return [Boolean] True if this field's {#max_page_size} should override the schema default. def has_max_page_size? !NOT_CONFIGURED.equal?(@max_page_size) || (@resolver_class && @resolver_class.has_max_page_size?) end # @return [Integer, nil] Applied to connections if {#has_max_page_size?} def max_page_size if !NOT_CONFIGURED.equal?(@max_page_size) @max_page_size elsif @resolver_class && @resolver_class.has_max_page_size? @resolver_class.max_page_size else nil end end # @return [Boolean] True if this field's {#default_page_size} should override the schema default. def has_default_page_size? !NOT_CONFIGURED.equal?(@default_page_size) || (@resolver_class && @resolver_class.has_default_page_size?) end # @return [Integer, nil] Applied to connections if {#has_default_page_size?} def default_page_size if !NOT_CONFIGURED.equal?(@default_page_size) @default_page_size elsif @resolver_class && @resolver_class.has_default_page_size? @resolver_class.default_page_size else nil end end def freeze type owner_type arguments_statically_coercible? connection? super end class MissingReturnTypeError < GraphQL::Error; end attr_writer :type # Get or set the return type of this field. # # It may return nil if no type was configured or if the given definition block wasn't called yet. # @param new_type [Module, GraphQL::Schema::NonNull, GraphQL::Schema::List] A GraphQL return type # @return [Module, GraphQL::Schema::NonNull, GraphQL::Schema::List, nil] the configured type for this field def type(new_type = NOT_CONFIGURED) if NOT_CONFIGURED.equal?(new_type) if @resolver_class return_type = @return_type_expr || @resolver_class.type_expr if return_type.nil? raise MissingReturnTypeError, "Can't determine the return type for #{self.path} (it has `resolver: #{@resolver_class}`, perhaps that class is missing a `type ...` declaration, or perhaps its type causes a cyclical loading issue)" end nullable = @return_type_null.nil? ? @resolver_class.null : @return_type_null Member::BuildType.parse_type(return_type, null: nullable) elsif !@return_type_expr.nil? @type ||= Member::BuildType.parse_type(@return_type_expr, null: @return_type_null) end else @return_type_expr = new_type # If `type` is set in the definition block, then the `connection_extension: ...` given as a keyword won't be used, hmm... # Also, arguments added by `connection_extension` will clobber anything previously defined, # so `type(...)` should go first. set_pagination_extensions(connection_extension: self.class.connection_extension) end rescue GraphQL::Schema::InvalidDocumentError, MissingReturnTypeError => err # Let this propagate up raise err rescue StandardError => err raise MissingReturnTypeError, "Failed to build return type for #{@owner.graphql_name}.#{name} from #{@return_type_expr.inspect}: (#{err.class}) #{err.message}", err.backtrace end def visible?(context) if @resolver_class @resolver_class.visible?(context) else true end end def authorized?(object, args, context) if @resolver_class # The resolver _instance_ will check itself during `resolve()` @resolver_class.authorized?(object, context) else if args.size > 0 if (arg_values = context[:current_arguments]) # ^^ that's provided by the interpreter at runtime, and includes info about whether the default value was used or not. using_arg_values = true arg_values = arg_values.argument_values else arg_values = args using_arg_values = false end args = context.types.arguments(self) args.each do |arg| arg_key = arg.keyword if arg_values.key?(arg_key) arg_value = arg_values[arg_key] if using_arg_values if arg_value.default_used? # pass -- no auth required for default used next else application_arg_value = arg_value.value if application_arg_value.is_a?(GraphQL::Execution::Interpreter::Arguments) application_arg_value.keyword_arguments end end else application_arg_value = arg_value end if !arg.authorized?(object, application_arg_value, context) return false end end end end true end end # This method is called by the interpreter for each field. # You can extend it in your base field classes. # @param object [GraphQL::Schema::Object] An instance of some type class, wrapping an application object # @param args [Hash] A symbol-keyed hash of Ruby keyword arguments. (Empty if no args) # @param ctx [GraphQL::Query::Context] def resolve(object, args, query_ctx) # Unwrap the GraphQL object to get the application object. application_object = object.object method_receiver = nil method_to_call = nil method_args = nil @own_validators && Schema::Validator.validate!(validators, application_object, query_ctx, args) query_ctx.query.after_lazy(self.authorized?(application_object, args, query_ctx)) do |is_authorized| if is_authorized with_extensions(object, args, query_ctx) do |obj, ruby_kwargs| method_args = ruby_kwargs if @resolver_class if obj.is_a?(GraphQL::Schema::Object) obj = obj.object end obj = @resolver_class.new(object: obj, context: query_ctx, field: self) end inner_object = obj.object if !NOT_CONFIGURED.equal?(@hash_key) hash_value = if inner_object.is_a?(Hash) inner_object.key?(@hash_key) ? inner_object[@hash_key] : inner_object[@hash_key_str] elsif inner_object.respond_to?(:[]) inner_object[@hash_key] else nil end if hash_value == false hash_value else hash_value || (@fallback_value != NOT_CONFIGURED ? @fallback_value : nil) end elsif obj.respond_to?(resolver_method) method_to_call = resolver_method method_receiver = obj # Call the method with kwargs, if there are any if !ruby_kwargs.empty? obj.public_send(resolver_method, **ruby_kwargs) else obj.public_send(resolver_method) end elsif inner_object.is_a?(Hash) if @dig_keys inner_object.dig(*@dig_keys) elsif inner_object.key?(@method_sym) inner_object[@method_sym] elsif inner_object.key?(@method_str) || !inner_object.default_proc.nil? inner_object[@method_str] elsif @fallback_value != NOT_CONFIGURED @fallback_value else nil end elsif inner_object.respond_to?(@method_sym) method_to_call = @method_sym method_receiver = obj.object if !ruby_kwargs.empty? inner_object.public_send(@method_sym, **ruby_kwargs) else inner_object.public_send(@method_sym) end elsif @fallback_value != NOT_CONFIGURED @fallback_value else raise <<-ERR Failed to implement #{@owner.graphql_name}.#{@name}, tried: - `#{obj.class}##{resolver_method}`, which did not exist - `#{inner_object.class}##{@method_sym}`, which did not exist - Looking up hash key `#{@method_sym.inspect}` or `#{@method_str.inspect}` on `#{inner_object}`, but it wasn't a Hash To implement this field, define one of the methods above (and check for typos), or supply a `fallback_value`. ERR end end else raise GraphQL::UnauthorizedFieldError.new(object: application_object, type: object.class, context: query_ctx, field: self) end end rescue GraphQL::UnauthorizedFieldError => err err.field ||= self begin query_ctx.schema.unauthorized_field(err) rescue GraphQL::ExecutionError => err err end rescue GraphQL::UnauthorizedError => err begin query_ctx.schema.unauthorized_object(err) rescue GraphQL::ExecutionError => err err end rescue ArgumentError if method_receiver && method_to_call assert_satisfactory_implementation(method_receiver, method_to_call, method_args) end # if the line above doesn't raise, re-raise raise rescue GraphQL::ExecutionError => err err end # @param ctx [GraphQL::Query::Context] def fetch_extra(extra_name, ctx) if extra_name != :path && extra_name != :ast_node && respond_to?(extra_name) self.public_send(extra_name) elsif ctx.respond_to?(extra_name) ctx.public_send(extra_name) else raise GraphQL::RequiredImplementationMissingError, "Unknown field extra for #{self.path}: #{extra_name.inspect}" end end private def assert_satisfactory_implementation(receiver, method_name, ruby_kwargs) method_defn = receiver.method(method_name) unsatisfied_ruby_kwargs = ruby_kwargs.dup unsatisfied_method_params = [] encountered_keyrest = false method_defn.parameters.each do |(param_type, param_name)| case param_type when :key unsatisfied_ruby_kwargs.delete(param_name) when :keyreq if unsatisfied_ruby_kwargs.key?(param_name) unsatisfied_ruby_kwargs.delete(param_name) else unsatisfied_method_params << "- `#{param_name}:` is required by Ruby, but not by GraphQL. Consider `#{param_name}: nil` instead, or making this argument required in GraphQL." end when :keyrest encountered_keyrest = true when :req unsatisfied_method_params << "- `#{param_name}` is required by Ruby, but GraphQL doesn't pass positional arguments. If it's meant to be a GraphQL argument, use `#{param_name}:` instead. Otherwise, remove it." when :opt, :rest # This is fine, although it will never be present end end if encountered_keyrest unsatisfied_ruby_kwargs.clear end if !unsatisfied_ruby_kwargs.empty? || !unsatisfied_method_params.empty? raise FieldImplementationFailed.new, <<-ERR Failed to call `#{method_name.inspect}` on #{receiver.inspect} because the Ruby method params were incompatible with the GraphQL arguments: #{ unsatisfied_ruby_kwargs .map { |key, value| "- `#{key}: #{value}` was given by GraphQL but not defined in the Ruby method. Add `#{key}:` to the method parameters." } .concat(unsatisfied_method_params) .join("\n") } ERR end end class ExtendedState def initialize(args, object) @arguments = args @object = object @memos = nil @added_extras = nil end attr_accessor :arguments, :object, :memos, :added_extras end # Wrap execution with hooks. # Written iteratively to avoid big stack traces. # @return [Object] Whatever the def with_extensions(obj, args, ctx) if @extensions.empty? yield(obj, args) else # This is a hack to get the _last_ value for extended obj and args, # in case one of the extensions doesn't `yield`. # (There's another implementation that uses multiple-return, but I'm wary of the perf cost of the extra arrays) extended = ExtendedState.new(args, obj) value = run_extensions_before_resolve(obj, args, ctx, extended) do |obj, args| if (added_extras = extended.added_extras) args = args.dup added_extras.each { |e| args.delete(e) } end yield(obj, args) end extended_obj = extended.object extended_args = extended.arguments # rubocop:disable Development/ContextIsPassedCop memos = extended.memos || EMPTY_HASH ctx.query.after_lazy(value) do |resolved_value| idx = 0 @extensions.each do |ext| memo = memos[idx] # TODO after_lazy? resolved_value = ext.after_resolve(object: extended_obj, arguments: extended_args, context: ctx, value: resolved_value, memo: memo) idx += 1 end resolved_value end end end def run_extensions_before_resolve(obj, args, ctx, extended, idx: 0) extension = @extensions[idx] if extension extension.resolve(object: obj, arguments: args, context: ctx) do |extended_obj, extended_args, memo| if memo memos = extended.memos ||= {} memos[idx] = memo end if (extras = extension.added_extras) ae = extended.added_extras ||= [] ae.concat(extras) end extended.object = extended_obj extended.arguments = extended_args run_extensions_before_resolve(extended_obj, extended_args, ctx, extended, idx: idx + 1) { |o, a| yield(o, a) } end else yield(obj, args) end end def apply_own_complexity_to(child_complexity, query, nodes) case (own_complexity = complexity) when Numeric own_complexity + child_complexity when Proc arguments = query.arguments_for(nodes.first, self) if arguments.is_a?(GraphQL::ExecutionError) return child_complexity elsif arguments.respond_to?(:keyword_arguments) arguments = arguments.keyword_arguments end own_complexity.call(query.context, arguments, child_complexity) else raise ArgumentError, "Invalid complexity for #{self.path}: #{own_complexity.inspect}" end end def set_pagination_extensions(connection_extension:) # This should run before connection extension, # but should it run after the definition block? if scoped? self.extension(ScopeExtension, call_after_define: false) end # The problem with putting this after the definition_block # is that it would override arguments if connection? && connection_extension self.extension(connection_extension, call_after_define: false) end end end end end graphql-ruby-2.5.19/lib/graphql/schema/field/000077500000000000000000000000001514115062600207555ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/schema/field/connection_extension.rb000066400000000000000000000057231514115062600255440ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Field class ConnectionExtension < GraphQL::Schema::FieldExtension def apply field.argument :after, "String", "Returns the elements in the list that come after the specified cursor.", required: false field.argument :before, "String", "Returns the elements in the list that come before the specified cursor.", required: false field.argument :first, "Int", "Returns the first _n_ elements from the list.", required: false field.argument :last, "Int", "Returns the last _n_ elements from the list.", required: false end # Remove pagination args before passing it to a user method def resolve(object:, arguments:, context:) next_args = arguments.dup next_args.delete(:first) next_args.delete(:last) next_args.delete(:before) next_args.delete(:after) yield(object, next_args, arguments) end def after_resolve(value:, object:, arguments:, context:, memo:) original_arguments = memo # rename some inputs to avoid conflicts inside the block maybe_lazy = value value = nil context.query.after_lazy(maybe_lazy) do |resolved_value| value = resolved_value if value.is_a? GraphQL::ExecutionError # This isn't even going to work because context doesn't have ast_node anymore context.add_error(value) nil elsif value.nil? nil elsif value.is_a?(GraphQL::Pagination::Connection) # update the connection with some things that may not have been provided value.context ||= context value.parent ||= object.object value.first_value ||= original_arguments[:first] value.after_value ||= original_arguments[:after] value.last_value ||= original_arguments[:last] value.before_value ||= original_arguments[:before] value.arguments ||= original_arguments # rubocop:disable Development/ContextIsPassedCop -- unrelated .arguments method value.field ||= field if field.has_max_page_size? && !value.has_max_page_size_override? value.max_page_size = field.max_page_size end if field.has_default_page_size? && !value.has_default_page_size_override? value.default_page_size = field.default_page_size end if (custom_t = context.schema.connections.edge_class_for_field(@field)) value.edge_class = custom_t end value else context.namespace(:connections)[:all_wrappers] ||= context.schema.connections.all_wrappers context.schema.connections.wrap(field, object.object, value, original_arguments, context) end end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/field/scope_extension.rb000066400000000000000000000016251514115062600245130ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Field class ScopeExtension < GraphQL::Schema::FieldExtension def after_resolve(object:, arguments:, context:, value:, memo:) if value.nil? value else ret_type = @field.type.unwrap if ret_type.respond_to?(:scope_items) scoped_items = ret_type.scope_items(value, context) if !scoped_items.equal?(value) && !ret_type.reauthorize_scoped_objects if (current_runtime_state = Fiber[:__graphql_runtime_info]) && (query_runtime_state = current_runtime_state[context.query]) query_runtime_state.was_authorized_by_scope_items = true end end scoped_items else value end end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/field_extension.rb000066400000000000000000000136511514115062600234040ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema # Extend this class to make field-level customizations to resolve behavior. # # When a extension is added to a field with `extension(MyExtension)`, a `MyExtension` instance # is created, and its hooks are applied whenever that field is called. # # The instance is frozen so that instance variables aren't modified during query execution, # which could cause all kinds of issues due to race conditions. class FieldExtension # @return [GraphQL::Schema::Field] attr_reader :field # @return [Object] attr_reader :options # @return [Array, nil] `default_argument`s added, if any were added (otherwise, `nil`) attr_reader :added_default_arguments # Called when the extension is mounted with `extension(name, options)`. # The instance will be frozen to avoid improper use of state during execution. # @param field [GraphQL::Schema::Field] The field where this extension was mounted # @param options [Object] The second argument to `extension`, or `{}` if nothing was passed. def initialize(field:, options:) @field = field @options = options || {} @added_default_arguments = nil apply end class << self # @return [Array(Array, Hash), nil] A list of default argument configs, or `nil` if there aren't any def default_argument_configurations args = superclass.respond_to?(:default_argument_configurations) ? superclass.default_argument_configurations : nil if @own_default_argument_configurations if args args.concat(@own_default_argument_configurations) else args = @own_default_argument_configurations.dup end end args end # @see Argument#initialize # @see HasArguments#argument def default_argument(*argument_args, **argument_kwargs) configs = @own_default_argument_configurations ||= [] configs << [argument_args, argument_kwargs] end # If configured, these `extras` will be added to the field if they aren't already present, # but removed by from `arguments` before the field's `resolve` is called. # (The extras _will_ be present for other extensions, though.) # # @param new_extras [Array] If provided, assign extras used by this extension # @return [Array] any extras assigned to this extension def extras(new_extras = nil) if new_extras @own_extras = new_extras end inherited_extras = self.superclass.respond_to?(:extras) ? superclass.extras : nil if @own_extras if inherited_extras inherited_extras + @own_extras else @own_extras end elsif inherited_extras inherited_extras else GraphQL::EmptyObjects::EMPTY_ARRAY end end end # Called when this extension is attached to a field. # The field definition may be extended during this method. # @return [void] def apply end # Called after the field's definition block has been executed. # (Any arguments from the block are present on `field`) # @return [void] def after_define end # @api private def after_define_apply after_define if (configs = self.class.default_argument_configurations) existing_keywords = field.all_argument_definitions.map(&:keyword) existing_keywords.uniq! @added_default_arguments = [] configs.each do |config| argument_args, argument_kwargs = config arg_name = argument_args[0] if !existing_keywords.include?(arg_name) @added_default_arguments << arg_name field.argument(*argument_args, **argument_kwargs) end end end if !(extras = self.class.extras).empty? @added_extras = extras - field.extras field.extras(@added_extras) else @added_extras = nil end freeze end # @api private attr_reader :added_extras # Called before resolving {#field}. It should either: # # - `yield` values to continue execution; OR # - return something else to shortcut field execution. # # Whatever this method returns will be used for execution. # # @param object [Object] The object the field is being resolved on # @param arguments [Hash] Ruby keyword arguments for resolving this field # @param context [Query::Context] the context for this query # @yieldparam object [Object] The object to continue resolving the field on # @yieldparam arguments [Hash] The keyword arguments to continue resolving with # @yieldparam memo [Object] Any extension-specific value which will be passed to {#after_resolve} later # @return [Object] The return value for this field. def resolve(object:, arguments:, context:) yield(object, arguments, nil) end # Called after {#field} was resolved, and after any lazy values (like `Promise`s) were synced, # but before the value was added to the GraphQL response. # # Whatever this hook returns will be used as the return value. # # @param object [Object] The object the field is being resolved on # @param arguments [Hash] Ruby keyword arguments for resolving this field # @param context [Query::Context] the context for this query # @param value [Object] Whatever the field previously returned # @param memo [Object] The third value yielded by {#resolve}, or `nil` if there wasn't one # @return [Object] The return value for this field. def after_resolve(object:, arguments:, context:, value:, memo:) value end end end end graphql-ruby-2.5.19/lib/graphql/schema/find_inherited_value.rb000066400000000000000000000015261514115062600243720ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema module FindInheritedValue def self.extended(child_cls) child_cls.singleton_class.include(GraphQL::EmptyObjects) end def self.included(child_cls) child_cls.include(GraphQL::EmptyObjects) end private def find_inherited_value(method_name, default_value = nil) if self.is_a?(Class) superclass.respond_to?(method_name, true) ? superclass.send(method_name) : default_value else ancestors_except_self = ancestors ancestors_except_self.delete(self) ancestors_except_self.each do |ancestor| if ancestor.respond_to?(method_name, true) return ancestor.send(method_name) end end default_value end end end end end graphql-ruby-2.5.19/lib/graphql/schema/finder.rb000066400000000000000000000115131514115062600214670ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema # Find schema members using string paths # # @example Finding object types # MySchema.find("SomeObjectType") # # @example Finding fields # MySchema.find("SomeObjectType.myField") # # @example Finding arguments # MySchema.find("SomeObjectType.myField.anArgument") # # @example Finding directives # MySchema.find("@include") # class Finder class MemberNotFoundError < ArgumentError; end def initialize(schema) @schema = schema end def find(path) path = path.split(".") type_or_directive = path.shift if type_or_directive.start_with?("@") directive = schema.directives[type_or_directive[1..-1]] if directive.nil? raise MemberNotFoundError, "Could not find directive `#{type_or_directive}` in schema." end return directive if path.empty? find_in_directive(directive, path: path) else type = schema.get_type(type_or_directive) # rubocop:disable Development/ContextIsPassedCop -- build-time if type.nil? raise MemberNotFoundError, "Could not find type `#{type_or_directive}` in schema." end return type if path.empty? find_in_type(type, path: path) end end private attr_reader :schema def find_in_directive(directive, path:) argument_name = path.shift argument = directive.get_argument(argument_name) # rubocop:disable Development/ContextIsPassedCop -- build-time if argument.nil? raise MemberNotFoundError, "Could not find argument `#{argument_name}` on directive #{directive}." end argument end def find_in_type(type, path:) case type.kind.name when "OBJECT" find_in_fields_type(type, kind: "object", path: path) when "INTERFACE" find_in_fields_type(type, kind: "interface", path: path) when "INPUT_OBJECT" find_in_input_object(type, path: path) when "UNION" # Error out if path that was provided is too long # i.e UnionType.PossibleType.aField # Use PossibleType.aField instead. if invalid = path.first raise MemberNotFoundError, "Cannot select union possible type `#{invalid}`. Select the type directly instead." end when "ENUM" find_in_enum_type(type, path: path) else raise "Unexpected find_in_type: #{type.inspect} (#{path})" end end def find_in_fields_type(type, kind:, path:) field_name = path.shift field = schema.get_field(type, field_name) if field.nil? raise MemberNotFoundError, "Could not find field `#{field_name}` on #{kind} type `#{type.graphql_name}`." end return field if path.empty? find_in_field(field, path: path) end def find_in_field(field, path:) argument_name = path.shift argument = field.get_argument(argument_name) # rubocop:disable Development/ContextIsPassedCop -- build-time if argument.nil? raise MemberNotFoundError, "Could not find argument `#{argument_name}` on field `#{field.name}`." end # Error out if path that was provided is too long # i.e Type.field.argument.somethingBad if invalid = path.first raise MemberNotFoundError, "Cannot select member `#{invalid}` on a field." end argument end def find_in_input_object(input_object, path:) field_name = path.shift input_field = input_object.get_argument(field_name) # rubocop:disable Development/ContextIsPassedCop -- build-time if input_field.nil? raise MemberNotFoundError, "Could not find input field `#{field_name}` on input object type `#{input_object.graphql_name}`." end # Error out if path that was provided is too long # i.e InputType.inputField.bad if invalid = path.first raise MemberNotFoundError, "Cannot select member `#{invalid}` on an input field." end input_field end def find_in_enum_type(enum_type, path:) value_name = path.shift enum_value = enum_type.enum_values.find { |v| v.graphql_name == value_name } # rubocop:disable Development/ContextIsPassedCop -- build-time, not runtime if enum_value.nil? raise MemberNotFoundError, "Could not find enum value `#{value_name}` on enum type `#{enum_type.graphql_name}`." end # Error out if path that was provided is too long # i.e Enum.VALUE.wat if invalid = path.first raise MemberNotFoundError, "Cannot select member `#{invalid}` on an enum value." end enum_value end end end end graphql-ruby-2.5.19/lib/graphql/schema/has_single_input_argument.rb000066400000000000000000000123511514115062600254560ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema module HasSingleInputArgument def resolve_with_support(**inputs) if inputs[:input].is_a?(InputObject) input = inputs[:input].to_kwargs else input = inputs[:input] end new_extras = field ? field.extras : [] all_extras = self.class.extras + new_extras # Transfer these from the top-level hash to the # shortcutted `input:` object all_extras.each do |ext| # It's possible that the `extra` was not passed along by this point, # don't re-add it if it wasn't given here. if inputs.key?(ext) input[ext] = inputs[ext] end end if input # This is handled by Relay::Mutation::Resolve, a bit hacky, but here we are. input_kwargs = input.to_h else # Relay Classic Mutations with no `argument`s # don't require `input:` input_kwargs = {} end if !input_kwargs.empty? super(**input_kwargs) else super() end end def self.included(base) base.extend(ClassMethods) end module ClassMethods def dummy @dummy ||= begin d = Class.new(GraphQL::Schema::Resolver) d.graphql_name "#{self.graphql_name}DummyResolver" d.argument_class(self.argument_class) # TODO make this lazier? d.argument(:input, input_type, description: "Parameters for #{self.graphql_name}") d end end def field_arguments(context = GraphQL::Query::NullContext.instance) dummy.arguments(context) end def get_field_argument(name, context = GraphQL::Query::NullContext.instance) dummy.get_argument(name, context) end def own_field_arguments dummy.own_arguments end def any_field_arguments? dummy.any_arguments? end def all_field_argument_definitions dummy.all_argument_definitions end # Also apply this argument to the input type: def argument(*args, own_argument: false, **kwargs, &block) it = input_type # make sure any inherited arguments are already added to it arg = super(*args, **kwargs, &block) # This definition might be overriding something inherited; # if it is, remove the inherited definition so it's not confused at runtime as having multiple definitions prev_args = it.own_arguments[arg.graphql_name] case prev_args when GraphQL::Schema::Argument if prev_args.owner != self it.own_arguments.delete(arg.graphql_name) end when Array prev_args.reject! { |a| a.owner != self } if prev_args.empty? it.own_arguments.delete(arg.graphql_name) end end it.add_argument(arg) arg end # The base class for generated input object types # @param new_class [Class] The base class to use for generating input object definitions # @return [Class] The base class for this mutation's generated input object (default is {GraphQL::Schema::InputObject}) def input_object_class(new_class = nil) if new_class @input_object_class = new_class end @input_object_class || (superclass.respond_to?(:input_object_class) ? superclass.input_object_class : GraphQL::Schema::InputObject) end # @param new_input_type [Class, nil] If provided, it configures this mutation to accept `new_input_type` instead of generating an input type # @return [Class] The generated {Schema::InputObject} class for this mutation's `input` def input_type(new_input_type = nil) if new_input_type @input_type = new_input_type end @input_type ||= generate_input_type end private # Generate the input type for the `input:` argument # To customize how input objects are generated, override this method # @return [Class] a subclass of {.input_object_class} def generate_input_type mutation_args = all_argument_definitions mutation_class = self Class.new(input_object_class) do class << self def default_graphql_name "#{self.mutation.graphql_name}Input" end def description(new_desc = nil) super || "Autogenerated input type of #{self.mutation.graphql_name}" end end # For compatibility, in case no arguments are defined: has_no_arguments(true) mutation(mutation_class) # these might be inherited: mutation_args.each do |arg| add_argument(arg) end end end end private def authorize_arguments(args, values) # remove the `input` wrapper to match values input_type = args.find { |a| a.graphql_name == "input" }.type.unwrap input_args = context.types.arguments(input_type) super(input_args, values) end end end end graphql-ruby-2.5.19/lib/graphql/schema/input_object.rb000066400000000000000000000261171514115062600227130ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class InputObject < GraphQL::Schema::Member extend Forwardable extend GraphQL::Schema::Member::HasArguments extend GraphQL::Schema::Member::HasArguments::ArgumentObjectLoader extend GraphQL::Schema::Member::ValidatesInput extend GraphQL::Schema::Member::HasValidators include GraphQL::Dig # Raised when an InputObject doesn't have any arguments defined and hasn't explicitly opted out of this requirement class ArgumentsAreRequiredError < GraphQL::Error def initialize(input_object_type) message = "Input Object types must have arguments, but #{input_object_type.graphql_name} doesn't have any. Define an argument for this type, remove it from your schema, or add `has_no_arguments(true)` to its definition." super(message) end end # @return [GraphQL::Query::Context] The context for this query attr_reader :context # @return [GraphQL::Execution::Interpereter::Arguments] The underlying arguments instance attr_reader :arguments # Ruby-like hash behaviors, read-only def_delegators :@ruby_style_hash, :keys, :values, :each, :map, :any?, :empty? def initialize(arguments, ruby_kwargs:, context:, defaults_used:) @context = context @ruby_style_hash = ruby_kwargs @arguments = arguments # Apply prepares, not great to have it duplicated here. arg_defns = context ? context.types.arguments(self.class) : self.class.arguments(context).each_value arg_defns.each do |arg_defn| ruby_kwargs_key = arg_defn.keyword if @ruby_style_hash.key?(ruby_kwargs_key) # Weirdly, procs are applied during coercion, but not methods. # Probably because these methods require a `self`. if arg_defn.prepare.is_a?(Symbol) || context.nil? prepared_value = arg_defn.prepare_value(self, @ruby_style_hash[ruby_kwargs_key], context: context) overwrite_argument(ruby_kwargs_key, prepared_value) end end end end def to_h unwrap_value(@ruby_style_hash) end def to_hash to_h end def deconstruct_keys(keys = nil) if keys.nil? @ruby_style_hash else new_h = {} keys.each { |k| @ruby_style_hash.key?(k) && new_h[k] = @ruby_style_hash[k] } new_h end end def prepare self end def unwrap_value(value) case value when Array value.map { |item| unwrap_value(item) } when Hash value.reduce({}) do |h, (key, value)| h.merge!(key => unwrap_value(value)) end when InputObject value.to_h else value end end # Lookup a key on this object, it accepts new-style underscored symbols # Or old-style camelized identifiers. # @param key [Symbol, String] def [](key) if @ruby_style_hash.key?(key) @ruby_style_hash[key] elsif @arguments @arguments[key] else nil end end def key?(key) @ruby_style_hash.key?(key) || (@arguments && @arguments.key?(key)) || false end # A copy of the Ruby-style hash def to_kwargs @ruby_style_hash.dup end # @api private def validate_for(context) object = context[:current_object] # Pass this object's class with `as` so that messages are rendered correctly from inherited validators Schema::Validator.validate!(self.class.validators, object, context, @ruby_style_hash, as: self.class) nil end class << self def authorized?(obj, value, ctx) # Authorize each argument (but this doesn't apply if `prepare` is implemented): if value.respond_to?(:key?) ctx.types.arguments(self).each do |input_obj_arg| if value.key?(input_obj_arg.keyword) && !input_obj_arg.authorized?(obj, value[input_obj_arg.keyword], ctx) return false end end end # It didn't early-return false: true end def one_of if !one_of? if all_argument_definitions.any? { |arg| arg.type.non_null? } raise ArgumentError, "`one_of` may not be used with required arguments -- add `required: false` to argument definitions to use `one_of`" end directive(GraphQL::Schema::Directive::OneOf) end end def one_of? false # Re-defined when `OneOf` is added end def argument(*args, **kwargs, &block) argument_defn = super(*args, **kwargs, &block) if one_of? if argument_defn.type.non_null? raise ArgumentError, "Argument '#{argument_defn.path}' must be nullable because it is part of a OneOf type, add `required: false`." end if argument_defn.default_value? raise ArgumentError, "Argument '#{argument_defn.path}' cannot have a default value because it is part of a OneOf type, remove `default_value: ...`." end end # Add a method access suppress_redefinition_warning do define_accessor_method(argument_defn.keyword) end argument_defn end def kind GraphQL::TypeKinds::INPUT_OBJECT end # @api private INVALID_OBJECT_MESSAGE = "Expected %{object} to be a key-value object." def validate_non_null_input(input, ctx, max_errors: nil) types = ctx.types if input.is_a?(Array) return GraphQL::Query::InputValidationResult.from_problem(INVALID_OBJECT_MESSAGE % { object: JSON.generate(input, quirks_mode: true) }) end if !(input.respond_to?(:to_h) || input.respond_to?(:to_unsafe_h)) # We're not sure it'll act like a hash, so reject it: return GraphQL::Query::InputValidationResult.from_problem(INVALID_OBJECT_MESSAGE % { object: JSON.generate(input, quirks_mode: true) }) end result = nil input.each do |argument_name, value| argument = types.argument(self, argument_name) if argument.nil? && ctx.is_a?(Query::NullContext) && argument_name.is_a?(Symbol) # Validating definition directive arguments which come in as Symbols argument = types.arguments(self).find { |arg| arg.keyword == argument_name } end # Items in the input that are unexpected if argument.nil? result ||= Query::InputValidationResult.new result.add_problem("Field is not defined on #{self.graphql_name}", [argument_name]) else # Items in the input that are expected, but have invalid values argument_result = argument.type.validate_input(value, ctx) if !argument_result.valid? result ||= Query::InputValidationResult.new result.merge_result!(argument_name, argument_result) end end end # Check for missing non-null arguments ctx.types.arguments(self).each do |argument| if !input.key?(argument.graphql_name) && argument.type.non_null? && !argument.default_value? result ||= Query::InputValidationResult.new argument_result = argument.type.validate_input(nil, ctx) if !argument_result.valid? result.merge_result!(argument.graphql_name, argument_result) end end end if one_of? if input.size == 1 input.each do |name, value| if value.nil? result ||= Query::InputValidationResult.new result.add_problem("'#{graphql_name}' requires exactly one argument, but '#{name}' was `null`.") end end else result ||= Query::InputValidationResult.new result.add_problem("'#{graphql_name}' requires exactly one argument, but #{input.size} were provided.") end end result end def coerce_input(value, ctx) if value.nil? return nil end arguments = coerce_arguments(nil, value, ctx) ctx.query.after_lazy(arguments) do |resolved_arguments| if resolved_arguments.is_a?(GraphQL::Error) raise resolved_arguments else self.new(resolved_arguments, ruby_kwargs: resolved_arguments.keyword_arguments, context: ctx, defaults_used: nil) end end end # It's funny to think of a _result_ of an input object. # This is used for rendering the default value in introspection responses. def coerce_result(value, ctx) # Allow the application to provide values as :snake_symbols, and convert them to the camelStrings value = value.reduce({}) { |memo, (k, v)| memo[Member::BuildType.camelize(k.to_s)] = v; memo } result = {} arguments(ctx).each do |input_key, input_field_defn| input_value = value[input_key] if value.key?(input_key) result[input_key] = if input_value.nil? nil else input_field_defn.type.coerce_result(input_value, ctx) end end end result end # @param new_has_no_arguments [Boolean] Call with `true` to make this InputObject type ignore the requirement to have any defined arguments. # @return [void] def has_no_arguments(new_has_no_arguments) @has_no_arguments = new_has_no_arguments nil end # @return [Boolean] `true` if `has_no_arguments(true)` was configued def has_no_arguments? @has_no_arguments end def arguments(context = GraphQL::Query::NullContext.instance, require_defined_arguments = true) if require_defined_arguments && !has_no_arguments? && !any_arguments? warn(GraphQL::Schema::InputObject::ArgumentsAreRequiredError.new(self).message + "\n\nThis will raise an error in a future GraphQL-Ruby version.") end super(context, false) end private # Suppress redefinition warning for objectId arguments def suppress_redefinition_warning verbose = $VERBOSE $VERBOSE = nil yield ensure $VERBOSE = verbose end def define_accessor_method(method_name) define_method(method_name) { self[method_name] } alias_method(method_name, method_name) end end private def overwrite_argument(key, value) # Argument keywords come in frozen from the interpreter, dup them before modifying them. if @ruby_style_hash.frozen? @ruby_style_hash = @ruby_style_hash.dup end @ruby_style_hash[key] = value end end end end graphql-ruby-2.5.19/lib/graphql/schema/interface.rb000066400000000000000000000114221514115062600221570ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema module Interface include GraphQL::Schema::Member::GraphQLTypeNames module DefinitionMethods include GraphQL::Schema::Member::BaseDSLMethods # ConfigurationExtension's responsibilities are in `def included` below include GraphQL::Schema::Member::TypeSystemHelpers include GraphQL::Schema::Member::HasFields include GraphQL::Schema::Member::HasPath include GraphQL::Schema::Member::RelayShortcuts include GraphQL::Schema::Member::Scoped include GraphQL::Schema::Member::HasAstNode include GraphQL::Schema::Member::HasUnresolvedTypeError include GraphQL::Schema::Member::HasDataloader include GraphQL::Schema::Member::HasDirectives include GraphQL::Schema::Member::HasInterfaces # Methods defined in this block will be: # - Added as class methods to this interface # - Added as class methods to all child interfaces def definition_methods(&block) # Use an instance variable to tell whether it's been included previously or not; # You can't use constant detection because constants are brought into scope # by `include`, which has already happened at this point. if !defined?(@_definition_methods) defn_methods_module = Module.new @_definition_methods = defn_methods_module const_set(:DefinitionMethods, defn_methods_module) extend(self::DefinitionMethods) end self::DefinitionMethods.module_exec(&block) end # @see {Schema::Warden} hides interfaces without visible implementations def visible?(context) true end def type_membership_class(membership_class = nil) if membership_class @type_membership_class = membership_class else @type_membership_class || find_inherited_value(:type_membership_class, GraphQL::Schema::TypeMembership) end end # Here's the tricky part. Make sure behavior keeps making its way down the inheritance chain. def included(child_class) if !child_class.is_a?(Class) # In this case, it's been included into another interface. # This is how interface inheritance is implemented # We need this before we can call `own_interfaces` child_class.extend(Schema::Interface::DefinitionMethods) child_class.type_membership_class(self.type_membership_class) child_class.ancestors.reverse_each do |ancestor| if ancestor.const_defined?(:DefinitionMethods) && ancestor != child_class child_class.extend(ancestor::DefinitionMethods) end end child_class.introspection(introspection) child_class.description(description) child_class.comment(nil) # If interfaces are mixed into each other, only define this class once if !child_class.const_defined?(:UnresolvedTypeError, false) add_unresolved_type_error(child_class) end elsif child_class < GraphQL::Schema::Object # This is being included into an object type, make sure it's using `implements(...)` backtrace_line = caller_locations(0, 10).find do |location| location.base_label == "implements" && location.path.end_with?("schema/member/has_interfaces.rb") end if !backtrace_line raise "Attach interfaces using `implements(#{self})`, not `include(#{self})`" end end super end # Register other Interface or Object types as implementers of this Interface. # # When those Interfaces or Objects aren't used as the return values of fields, # they may have to be registered using this method so that GraphQL-Ruby can find them. # @param types [Class, Module] # @return [Array] Implementers of this interface, if they're registered def orphan_types(*types) if !types.empty? @orphan_types ||= [] @orphan_types.concat(types) else if defined?(@orphan_types) all_orphan_types = @orphan_types.dup if defined?(super) all_orphan_types += super all_orphan_types.uniq! end all_orphan_types elsif defined?(super) super else EmptyObjects::EMPTY_ARRAY end end end def kind GraphQL::TypeKinds::INTERFACE end end extend DefinitionMethods def unwrap self end end end end graphql-ruby-2.5.19/lib/graphql/schema/introspection_system.rb000066400000000000000000000115301514115062600245230ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class IntrospectionSystem attr_reader :types, :possible_types def initialize(schema) @schema = schema @class_based = !!@schema.is_a?(Class) @built_in_namespace = GraphQL::Introspection @custom_namespace = if @class_based schema.introspection || @built_in_namespace else schema.introspection_namespace || @built_in_namespace end type_defns = [ load_constant(:SchemaType), load_constant(:TypeType), load_constant(:FieldType), load_constant(:DirectiveType), load_constant(:EnumValueType), load_constant(:InputValueType), load_constant(:TypeKindEnum), load_constant(:DirectiveLocationEnum) ] @types = {} @possible_types = {}.compare_by_identity type_defns.each do |t| @types[t.graphql_name] = t @possible_types[t] = [t] end @entry_point_fields = if schema.disable_introspection_entry_points? {} else entry_point_fields = get_fields_from_class(class_sym: :EntryPoints) entry_point_fields.delete('__schema') if schema.disable_schema_introspection_entry_point? entry_point_fields.delete('__type') if schema.disable_type_introspection_entry_point? entry_point_fields end @entry_point_fields.each { |k, v| v.dynamic_introspection = true } @dynamic_fields = get_fields_from_class(class_sym: :DynamicFields) @dynamic_fields.each { |k, v| v.dynamic_introspection = true } end def entry_points @entry_point_fields.values end def entry_point(name:) @entry_point_fields[name] end def dynamic_fields @dynamic_fields.values end def dynamic_field(name:) @dynamic_fields[name] end # The introspection system is prepared with a bunch of LateBoundTypes. # Replace those with the objects that they refer to, since LateBoundTypes # aren't handled at runtime. # # @api private # @return void def resolve_late_bindings @types.each do |name, t| if t.kind.fields? t.all_field_definitions.each do |field_defn| field_defn.type = resolve_late_binding(field_defn.type) end end end @entry_point_fields.each do |name, f| f.type = resolve_late_binding(f.type) end @dynamic_fields.each do |name, f| f.type = resolve_late_binding(f.type) end nil end private def resolve_late_binding(late_bound_type) case late_bound_type when GraphQL::Schema::LateBoundType type_name = late_bound_type.name @types[type_name] || @schema.get_type(type_name) when GraphQL::Schema::List resolve_late_binding(late_bound_type.of_type).to_list_type when GraphQL::Schema::NonNull resolve_late_binding(late_bound_type.of_type).to_non_null_type when Module # It's a normal type -- no change required late_bound_type else raise "Invariant: unexpected type: #{late_bound_type} (#{late_bound_type.class})" end end def load_constant(class_name) const = @custom_namespace.const_get(class_name) dup_type_class(const) rescue NameError # Dup the built-in so that the cached fields aren't shared dup_type_class(@built_in_namespace.const_get(class_name)) end def get_fields_from_class(class_sym:) object_type_defn = load_constant(class_sym) object_type_defn.fields end # This is probably not 100% robust -- but it has to be good enough to avoid modifying the built-in introspection types def dup_type_class(type_class) type_name = type_class.graphql_name Class.new(type_class) do # This won't be inherited like other things will graphql_name(type_name) if type_class.kind.fields? type_class.fields.each do |_name, field_defn| dup_field = field_defn.dup dup_field.owner = self add_field(dup_field) end end end end class PerFieldProxyResolve def initialize(object_class:, inner_resolve:) @object_class = object_class @inner_resolve = inner_resolve end def call(obj, args, ctx) query_ctx = ctx.query.context # Remove the QueryType wrapper if obj.is_a?(GraphQL::Schema::Object) obj = obj.object end wrapped_object = @object_class.wrap(obj, query_ctx) @inner_resolve.call(wrapped_object, args, ctx) end end end end end graphql-ruby-2.5.19/lib/graphql/schema/late_bound_type.rb000066400000000000000000000015701514115062600233770ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema # A stand-in for a type which will be resolved in a given schema, by name. # TODO: support argument types too, make this a public API somehow # @api Private class LateBoundType attr_reader :name alias :graphql_name :name def initialize(local_name) @name = local_name @to_non_null_type = nil @to_list_type = nil end def unwrap self end def to_non_null_type @to_non_null_type ||= GraphQL::Schema::NonNull.new(self) end def to_list_type @to_list_type ||= GraphQL::Schema::List.new(self) end def to_type_signature name end def inspect "#" end def non_null? false end alias :to_s :inspect end end end graphql-ruby-2.5.19/lib/graphql/schema/list.rb000066400000000000000000000044311514115062600211740ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema # Represents a list type in the schema. # Wraps a {Schema::Member} as a list type. # @see Schema::Member::TypeSystemHelpers#to_list_type Create a list type from another GraphQL type class List < GraphQL::Schema::Wrapper include Schema::Member::ValidatesInput # @return [GraphQL::TypeKinds::LIST] def kind GraphQL::TypeKinds::LIST end # @return [true] def list? true end def to_type_signature "[#{@of_type.to_type_signature}]" end # This is for introspection, where it's expected the name will be `null` def graphql_name nil end # Also for implementing introspection def description nil end def coerce_result(value, ctx) value.map { |i| i.nil? ? nil : of_type.coerce_result(i, ctx) } end def coerce_input(value, ctx) if value.nil? nil else coerced = ensure_array(value).map { |item| item.nil? ? item : of_type.coerce_input(item, ctx) } ctx.schema.after_any_lazies(coerced, &:itself) end end def validate_non_null_input(value, ctx, max_errors: nil) result = GraphQL::Query::InputValidationResult.new ensure_array(value).each_with_index do |item, index| item_result = of_type.validate_input(item, ctx) unless item_result.valid? if max_errors if max_errors == 0 add_max_errors_reached_message(result) break end max_errors -= 1 end result.merge_result!(index, item_result) end end result.valid? ? nil : result end private def ensure_array(value) # `Array({ a: 1 })` makes `[[:a, 1]]`, so do it manually if value.is_a?(Array) value else [value] end end def add_max_errors_reached_message(result) message = "Too many errors processing list variable, max validation error limit reached. Execution aborted" item_result = GraphQL::Query::InputValidationResult.from_problem(message) result.merge_result!(nil, item_result) end end end end graphql-ruby-2.5.19/lib/graphql/schema/loader.rb000066400000000000000000000202721514115062600214700ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema # You can use the result of {GraphQL::Introspection::INTROSPECTION_QUERY} # to make a schema. This schema is missing some important details like # `resolve` functions, but it does include the full type system, # so you can use it to validate queries. # # @see GraphQL::Schema.from_introspection for a public API module Loader extend self # Create schema with the result of an introspection query. # @param introspection_result [Hash] A response from {GraphQL::Introspection::INTROSPECTION_QUERY} # @return [Class] the schema described by `input` def load(introspection_result) schema = introspection_result.fetch("data").fetch("__schema") types = {} type_resolver = ->(type) { resolve_type(types, type) } schema.fetch("types").each do |type| next if type.fetch("name").start_with?("__") type_object = define_type(type, type_resolver) types[type["name"]] = type_object end directives = [] schema.fetch("directives", []).each do |directive| next if GraphQL::Schema.default_directives.include?(directive.fetch("name")) directives << define_directive(directive, type_resolver) end Class.new(GraphQL::Schema) do add_type_and_traverse(types.values, root: false) orphan_types(types.values.select { |t| t.kind.object? }) directives(directives) description(schema["description"]) def self.resolve_type(*) raise(GraphQL::RequiredImplementationMissingError, "This schema was loaded from string, so it can't resolve types for objects") end [:query, :mutation, :subscription].each do |root| type = schema["#{root}Type"] if type type_defn = types.fetch(type.fetch("name")) self.public_send(root, type_defn) end end end end NullScalarCoerce = ->(val, _ctx) { val } class << self private def resolve_type(types, type) case kind = type.fetch("kind") when "ENUM", "INTERFACE", "INPUT_OBJECT", "OBJECT", "SCALAR", "UNION" type_name = type.fetch("name") type = types[type_name] || Schema::BUILT_IN_TYPES[type_name] if type.nil? GraphQL::Schema::LateBoundType.new(type_name) else type end when "LIST" Schema::List.new(resolve_type(types, type.fetch("ofType"))) when "NON_NULL" Schema::NonNull.new(resolve_type(types, type.fetch("ofType"))) else fail GraphQL::RequiredImplementationMissingError, "#{kind} not implemented" end end def extract_default_value(default_value_str, input_value_ast) case input_value_ast when String, Integer, Float, TrueClass, FalseClass input_value_ast when GraphQL::Language::Nodes::Enum input_value_ast.name when GraphQL::Language::Nodes::NullValue nil when GraphQL::Language::Nodes::InputObject input_value_ast.to_h when Array input_value_ast.map { |element| extract_default_value(default_value_str, element) } else raise( "Encountered unexpected type when loading default value. "\ "input_value_ast.class is #{input_value_ast.class} "\ "default_value is #{default_value_str}" ) end end def define_type(type, type_resolver) loader = self case type.fetch("kind") when "ENUM" Class.new(GraphQL::Schema::Enum) do graphql_name(type["name"]) description(type["description"]) type["enumValues"].each do |enum_value| value( enum_value["name"], description: enum_value["description"], deprecation_reason: enum_value["deprecationReason"], ) end end when "INTERFACE" Module.new do include GraphQL::Schema::Interface graphql_name(type["name"]) description(type["description"]) loader.build_fields(self, type["fields"] || [], type_resolver) end when "INPUT_OBJECT" Class.new(GraphQL::Schema::InputObject) do graphql_name(type["name"]) description(type["description"]) loader.build_arguments(self, type["inputFields"], type_resolver) end when "OBJECT" Class.new(GraphQL::Schema::Object) do graphql_name(type["name"]) description(type["description"]) if type["interfaces"] type["interfaces"].each do |interface_type| implements(type_resolver.call(interface_type)) end end loader.build_fields(self, type["fields"], type_resolver) end when "SCALAR" type_name = type.fetch("name") if (builtin = GraphQL::Schema::BUILT_IN_TYPES[type_name]) builtin else Class.new(GraphQL::Schema::Scalar) do graphql_name(type["name"]) description(type["description"]) specified_by_url(type["specifiedByURL"]) end end when "UNION" Class.new(GraphQL::Schema::Union) do graphql_name(type["name"]) description(type["description"]) possible_types(*(type["possibleTypes"].map { |pt| type_resolver.call(pt) })) end else fail GraphQL::RequiredImplementationMissingError, "#{type["kind"]} not implemented" end end def define_directive(directive, type_resolver) loader = self Class.new(GraphQL::Schema::Directive) do graphql_name(directive["name"]) description(directive["description"]) locations(*directive["locations"].map(&:to_sym)) repeatable(directive["isRepeatable"]) loader.build_arguments(self, directive["args"], type_resolver) end end public def build_fields(type_defn, fields, type_resolver) loader = self fields.each do |field_hash| unwrapped_field_hash = field_hash while (of_type = unwrapped_field_hash["ofType"]) unwrapped_field_hash = of_type end type_defn.field( field_hash["name"], type: type_resolver.call(field_hash["type"]), description: field_hash["description"], deprecation_reason: field_hash["deprecationReason"], null: true, camelize: false, connection_extension: nil, ) do if !field_hash["args"].empty? loader.build_arguments(self, field_hash["args"], type_resolver) end end end end def build_arguments(arg_owner, args, type_resolver) args.each do |arg| kwargs = { type: type_resolver.call(arg["type"]), description: arg["description"], deprecation_reason: arg["deprecationReason"], required: false, camelize: false, } if arg["defaultValue"] default_value_str = arg["defaultValue"] dummy_query_str = "query getStuff($var: InputObj = #{default_value_str}) { __typename }" # Returns a `GraphQL::Language::Nodes::Document`: dummy_query_ast = GraphQL.parse(dummy_query_str) # Reach into the AST for the default value: input_value_ast = dummy_query_ast.definitions.first.variables.first.default_value kwargs[:default_value] = extract_default_value(default_value_str, input_value_ast) end arg_owner.argument(arg["name"], **kwargs) end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/member.rb000066400000000000000000000024341514115062600214710ustar00rootroot00000000000000# frozen_string_literal: true require 'graphql/schema/member/base_dsl_methods' require 'graphql/schema/member/graphql_type_names' require 'graphql/schema/member/has_ast_node' require 'graphql/schema/member/has_dataloader' require 'graphql/schema/member/has_directives' require 'graphql/schema/member/has_deprecation_reason' require 'graphql/schema/member/has_interfaces' require 'graphql/schema/member/has_path' require 'graphql/schema/member/has_unresolved_type_error' require 'graphql/schema/member/has_validators' require 'graphql/schema/member/relay_shortcuts' require 'graphql/schema/member/scoped' require 'graphql/schema/member/type_system_helpers' require 'graphql/schema/member/validates_input' module GraphQL class Schema # The base class for things that make up the schema, # eg objects, enums, scalars. # # @api private class Member include GraphQLTypeNames extend BaseDSLMethods extend BaseDSLMethods::ConfigurationExtension introspection(false) extend TypeSystemHelpers extend Scoped extend RelayShortcuts extend HasPath extend HasAstNode extend HasDirectives end end end require 'graphql/schema/member/has_arguments' require 'graphql/schema/member/has_fields' require 'graphql/schema/member/build_type' graphql-ruby-2.5.19/lib/graphql/schema/member/000077500000000000000000000000001514115062600211415ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/schema/member/base_dsl_methods.rb000066400000000000000000000104751514115062600247740ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/schema/find_inherited_value" module GraphQL class Schema class Member # DSL methods shared by lots of things in the GraphQL Schema. # @api private # @see Classes that extend this, eg {GraphQL::Schema::Object} module BaseDSLMethods include GraphQL::Schema::FindInheritedValue # Call this with a new name to override the default name for this schema member; OR # call it without an argument to get the name of this schema member # # The default name is implemented in default_graphql_name # @param new_name [String] # @return [String] def graphql_name(new_name = nil) if new_name GraphQL::NameValidator.validate!(new_name) @graphql_name = new_name else @graphql_name ||= default_graphql_name end end # Just a convenience method to point out that people should use graphql_name instead def name(new_name = nil) return super() if new_name.nil? fail( "The new name override method is `graphql_name`, not `name`. Usage: "\ "graphql_name \"#{new_name}\"" ) end # Call this method to provide a new description; OR # call it without an argument to get the description # @param new_description [String] # @return [String] def description(new_description = nil) if new_description @description = new_description elsif defined?(@description) @description else @description = nil end end # Call this method to provide a new comment; OR # call it without an argument to get the comment # @param new_comment [String] # @return [String, nil] def comment(new_comment = NOT_CONFIGURED) if !NOT_CONFIGURED.equal?(new_comment) @comment = new_comment elsif defined?(@comment) @comment else nil end end # This pushes some configurations _down_ the inheritance tree, # in order to prevent repetitive lookups at runtime. module ConfigurationExtension def inherited(child_class) child_class.introspection(introspection) child_class.description(description) child_class.comment(nil) child_class.default_graphql_name = nil if defined?(@graphql_name) && @graphql_name && (self.name.nil? || graphql_name != default_graphql_name) child_class.graphql_name(graphql_name) else child_class.graphql_name = nil end super end end # @return [Boolean] If true, this object is part of the introspection system def introspection(new_introspection = nil) if !new_introspection.nil? @introspection = new_introspection elsif defined?(@introspection) @introspection else false end end def introspection? !!@introspection end # The mutation this type was derived from, if it was derived from a mutation # @return [Class] def mutation(mutation_class = nil) if mutation_class @mutation = mutation_class elsif defined?(@mutation) @mutation else nil end end alias :unwrap :itself # Creates the default name for a schema member. # The default name is the Ruby constant name, # without any namespaces and with any `-Type` suffix removed def default_graphql_name @default_graphql_name ||= begin raise GraphQL::RequiredImplementationMissingError, 'Anonymous class should declare a `graphql_name`' if name.nil? g_name = -name.split("::").last g_name.end_with?("Type") ? g_name.sub(/Type\Z/, "") : g_name end end def visible?(context) true end def authorized?(object, context) true end def default_relay false end protected attr_writer :default_graphql_name, :graphql_name end end end end graphql-ruby-2.5.19/lib/graphql/schema/member/build_type.rb000066400000000000000000000145721514115062600236370ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Member # @api private module BuildType LIST_TYPE_ERROR = "Use an array of [T] or [T, null: true] for list types; other arrays are not supported" module_function # @param type_expr [String, Class, GraphQL::BaseType] # @return [GraphQL::BaseType] def parse_type(type_expr, null:) list_type = false return_type = case type_expr when String case type_expr when "String" GraphQL::Types::String when "Int", "Integer" GraphQL::Types::Int when "Float" GraphQL::Types::Float when "Boolean" GraphQL::Types::Boolean when "ID" GraphQL::Types::ID when /\A\[.*\]\Z/ list_type = true # List members are required by default parse_type(type_expr[1..-2], null: false) when /.*!\Z/ null = false parse_type(type_expr[0..-2], null: true) else maybe_type = constantize(type_expr) case maybe_type when Module # This is a way to check that it's the right kind of module: if maybe_type.respond_to?(:kind) maybe_type else raise ArgumentError, "Unexpected class/module found for GraphQL type: #{type_expr} (must be type definition class/module)" end end end when GraphQL::Schema::LateBoundType type_expr when Array case type_expr.length when 1 list_type = true # List members are required by default parse_type(type_expr.first, null: false) when 2 inner_type, nullable_option = type_expr if nullable_option.keys != [:null] || (nullable_option[:null] != true && nullable_option[:null] != false) raise ArgumentError, LIST_TYPE_ERROR end list_type = true parse_type(inner_type, null: nullable_option[:null]) else raise ArgumentError, LIST_TYPE_ERROR end when GraphQL::Schema::NonNull, GraphQL::Schema::List type_expr when Module # This is a way to check that it's the right kind of module: if type_expr.respond_to?(:kind) type_expr else # Eg `String` => GraphQL::Types::String parse_type(type_expr.name, null: true) end when Proc parse_type(type_expr.call, null: true) when false raise ArgumentError, "Received `false` instead of a type, maybe a `!` should be replaced with `null: true` (for fields) or `required: true` (for arguments)" end if return_type.nil? raise "Unexpected type input: #{type_expr.inspect} (#{type_expr.class})" end # Apply list_type first, that way the # .to_non_null_type applies to the list type, not the inner type if list_type return_type = return_type.to_list_type end if !null return_type = return_type.to_non_null_type end return_type end def to_type_name(something) case something when GraphQL::Schema::LateBoundType something.unwrap.name when Array to_type_name(something.first) when Module if something.respond_to?(:graphql_name) something.graphql_name else to_type_name(something.name) end when String if something.include?("]") || something.include?("[") || something.include?("!") || something.include?("::") something.gsub(/\]\[\!/, "").split("::").last else something end when GraphQL::Schema::NonNull, GraphQL::Schema::List to_type_name(something.unwrap) else raise "Unhandled to_type_name input: #{something} (#{something.class})" end end def camelize(string) return string if string == '_' return string unless string.include?("_") camelized = string.split('_').each(&:capitalize!).join camelized[0] = camelized[0].downcase if string.start_with?("_") match_data = string.match(/\A(_+)/) camelized = "#{match_data[0]}#{camelized}" end camelized end # Resolves constant from string (based on Rails `ActiveSupport::Inflector.constantize`) def constantize(string) names = string.split('::') # Trigger a built-in NameError exception including the ill-formed constant in the message. Object.const_get(string) if names.empty? # Remove the first blank element in case of '::ClassName' notation. names.shift if names.size > 1 && names.first.empty? names.inject(Object) do |constant, name| if constant == Object constant.const_get(name) else candidate = constant.const_get(name) next candidate if constant.const_defined?(name, false) next candidate unless Object.const_defined?(name) # Go down the ancestors to check if it is owned directly. The check # stops when we reach Object or the end of ancestors tree. constant = constant.ancestors.inject do |const, ancestor| break const if ancestor == Object break ancestor if ancestor.const_defined?(name, false) const end # Owner is in Object, so raise. constant.const_get(name, false) end end end def underscore(string) if string.match?(/\A[a-z_]+\Z/) return string end string2 = string.dup string2.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2') # URLDecoder -> URL_Decoder string2.gsub!(/([a-z\d])([A-Z])/,'\1_\2') # someThing -> some_Thing string2.downcase! string2 end end end end end graphql-ruby-2.5.19/lib/graphql/schema/member/graphql_type_names.rb000066400000000000000000000007311514115062600253510ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Member # These constants are interpreted as GraphQL types when defining fields or arguments # # @example # field :is_draft, Boolean, null: false # field :id, ID, null: false # field :score, Int, null: false # # @api private module GraphQLTypeNames Boolean = "Boolean" ID = "ID" Int = "Int" end end end end graphql-ruby-2.5.19/lib/graphql/schema/member/has_arguments.rb000066400000000000000000000455021514115062600243340ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Member module HasArguments def self.included(cls) cls.extend(ArgumentClassAccessor) cls.include(ArgumentObjectLoader) end def self.extended(cls) cls.extend(ArgumentClassAccessor) cls.include(ArgumentObjectLoader) cls.extend(ClassConfigured) end # @param arg_name [Symbol] The underscore-cased name of this argument, `name:` keyword also accepted # @param type_expr The GraphQL type of this argument; `type:` keyword also accepted # @param desc [String] Argument description, `description:` keyword also accepted # @option kwargs [Boolean, :nullable] :required if true, this argument is non-null; if false, this argument is nullable. If `:nullable`, then the argument must be provided, though it may be `null`. # @option kwargs [String] :description Positional argument also accepted # @option kwargs [Class, Array] :type Input type; positional argument also accepted # @option kwargs [Symbol] :name positional argument also accepted # @option kwargs [Object] :default_value # @option kwargs [Class, Array] :loads A GraphQL type to load for the given ID when one is present # @option kwargs [Symbol] :as Override the keyword name when passed to a method # @option kwargs [Symbol] :prepare A method to call to transform this argument's valuebefore sending it to field resolution # @option kwargs [Boolean] :camelize if true, the name will be camelized when building the schema # @option kwargs [Boolean] :from_resolver if true, a Resolver class defined this argument # @option kwargs [Hash{Class => Hash}] :directives # @option kwargs [String] :deprecation_reason # @option kwargs [String] :comment Private, used by GraphQL-Ruby when parsing GraphQL schema files # @option kwargs [GraphQL::Language::Nodes::InputValueDefinition] :ast_node Private, used by GraphQL-Ruby when parsing schema files # @option kwargs [Hash, nil] :validates Options for building validators, if any should be applied # @option kwargs [Boolean] :replace_null_with_default if `true`, incoming values of `null` will be replaced with the configured `default_value` # @param definition_block [Proc] Called with the newly-created {Argument} # @param kwargs [Hash] Keywords for defining an argument. Any keywords not documented here must be handled by your base Argument class. # @return [GraphQL::Schema::Argument] An instance of {argument_class} created from these arguments def argument(arg_name = nil, type_expr = nil, desc = nil, **kwargs, &definition_block) if kwargs[:loads] loads_name = arg_name || kwargs[:name] loads_name_as_string = loads_name.to_s inferred_arg_name = case loads_name_as_string when /_id$/ loads_name_as_string.sub(/_id$/, "").to_sym when /_ids$/ loads_name_as_string.sub(/_ids$/, "") .sub(/([^s])$/, "\\1s") .to_sym else loads_name end kwargs[:as] ||= inferred_arg_name end kwargs[:owner] = self arg_defn = self.argument_class.new( arg_name, type_expr, desc, **kwargs, &definition_block ) add_argument(arg_defn) arg_defn end # Register this argument with the class. # @param arg_defn [GraphQL::Schema::Argument] # @return [GraphQL::Schema::Argument] def add_argument(arg_defn) @own_arguments ||= {} prev_defn = @own_arguments[arg_defn.name] case prev_defn when nil @own_arguments[arg_defn.name] = arg_defn when Array prev_defn << arg_defn when GraphQL::Schema::Argument @own_arguments[arg_defn.name] = [prev_defn, arg_defn] else raise "Invariant: unexpected `@own_arguments[#{arg_defn.name.inspect}]`: #{prev_defn.inspect}" end arg_defn end def remove_argument(arg_defn) prev_defn = @own_arguments[arg_defn.name] case prev_defn when nil # done when Array prev_defn.delete(arg_defn) when GraphQL::Schema::Argument @own_arguments.delete(arg_defn.name) else raise "Invariant: unexpected `@own_arguments[#{arg_defn.name.inspect}]`: #{prev_defn.inspect}" end nil end # @return [Hash GraphQL::Schema::Argument] Arguments defined on this thing, keyed by name. Includes inherited definitions def arguments(context = GraphQL::Query::NullContext.instance, _require_defined_arguments = nil) if !own_arguments.empty? own_arguments_that_apply = {} own_arguments.each do |name, args_entry| if (visible_defn = Warden.visible_entry?(:visible_argument?, args_entry, context)) own_arguments_that_apply[visible_defn.graphql_name] = visible_defn end end end # might be nil if there are actually no arguments own_arguments_that_apply || own_arguments end def any_arguments? !own_arguments.empty? end module ClassConfigured def inherited(child_class) super child_class.extend(InheritedArguments) end module InheritedArguments def arguments(context = GraphQL::Query::NullContext.instance, require_defined_arguments = true) own_arguments = super(context, require_defined_arguments) inherited_arguments = superclass.arguments(context, false) if !own_arguments.empty? if !inherited_arguments.empty? # Local definitions override inherited ones inherited_arguments.merge(own_arguments) else own_arguments end else inherited_arguments end end def any_arguments? super || superclass.any_arguments? end def all_argument_definitions all_defns = {} ancestors.reverse_each do |ancestor| if ancestor.respond_to?(:own_arguments) all_defns.merge!(ancestor.own_arguments) end end all_defns = all_defns.values all_defns.flatten! all_defns end def get_argument(argument_name, context = GraphQL::Query::NullContext.instance) warden = Warden.from_context(context) skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Profile) for ancestor in ancestors if ancestor.respond_to?(:own_arguments) && (a = ancestor.own_arguments[argument_name]) && (skip_visible || (a = Warden.visible_entry?(:visible_argument?, a, context, warden))) return a end end nil end end end module FieldConfigured def arguments(context = GraphQL::Query::NullContext.instance, _require_defined_arguments = nil) own_arguments = super if @resolver_class inherited_arguments = @resolver_class.field_arguments(context) if !own_arguments.empty? if !inherited_arguments.empty? inherited_arguments.merge(own_arguments) else own_arguments end else inherited_arguments end else own_arguments end end def any_arguments? super || (@resolver_class && @resolver_class.any_field_arguments?) end def all_argument_definitions if @resolver_class all_defns = {} @resolver_class.all_field_argument_definitions.each do |arg_defn| key = arg_defn.graphql_name case (current_value = all_defns[key]) when nil all_defns[key] = arg_defn when Array current_value << arg_defn when GraphQL::Schema::Argument all_defns[key] = [current_value, arg_defn] else raise "Invariant: Unexpected argument definition, #{current_value.class}: #{current_value.inspect}" end end all_defns.merge!(own_arguments) all_defns = all_defns.values all_defns.flatten! all_defns else super end end end def all_argument_definitions if !own_arguments.empty? all_defns = own_arguments.values all_defns.flatten! all_defns else EmptyObjects::EMPTY_ARRAY end end # @return [GraphQL::Schema::Argument, nil] Argument defined on this thing, fetched by name. def get_argument(argument_name, context = GraphQL::Query::NullContext.instance) warden = Warden.from_context(context) if (arg_config = own_arguments[argument_name]) && ((context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Profile)) || (visible_arg = Warden.visible_entry?(:visible_argument?, arg_config, context, warden))) visible_arg || arg_config elsif defined?(@resolver_class) && @resolver_class @resolver_class.get_field_argument(argument_name, context) else nil end end # @param new_arg_class [Class] A class to use for building argument definitions def argument_class(new_arg_class = nil) self.class.argument_class(new_arg_class) end # @api private # If given a block, it will eventually yield the loaded args to the block. # # If no block is given, it will immediately dataload (but might return a Lazy). # # @param values [Hash] # @param context [GraphQL::Query::Context] # @yield [Interpreter::Arguments, Execution::Lazy] # @return [Interpreter::Arguments, Execution::Lazy] def coerce_arguments(parent_object, values, context, &block) # Cache this hash to avoid re-merging it arg_defns = context.types.arguments(self) total_args_count = arg_defns.size finished_args = nil prepare_finished_args = -> { if total_args_count == 0 finished_args = GraphQL::Execution::Interpreter::Arguments::EMPTY if block_given? block.call(finished_args) end else argument_values = {} resolved_args_count = 0 raised_error = false arg_defns.each do |arg_defn| context.dataloader.append_job do begin arg_defn.coerce_into_values(parent_object, values, context, argument_values) rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => err raised_error = true finished_args = err if block_given? block.call(finished_args) end end resolved_args_count += 1 if resolved_args_count == total_args_count && !raised_error finished_args = context.schema.after_any_lazies(argument_values.values) { GraphQL::Execution::Interpreter::Arguments.new( argument_values: argument_values, ) } if block_given? block.call(finished_args) end end end end end } if block_given? prepare_finished_args.call nil else # This API returns eagerly, gotta run it now context.dataloader.run_isolated(&prepare_finished_args) finished_args end end # Usually, this is validated statically by RequiredArgumentsArePresent, # but not for directives. # TODO apply static validations on schema definitions? def validate_directive_argument(arg_defn, value) # this is only implemented on directives. nil end module HasDirectiveArguments def validate_directive_argument(arg_defn, value) if value.nil? && arg_defn.type.non_null? raise ArgumentError, "#{arg_defn.path} is required, but no value was given" end end end def arguments_statically_coercible? if defined?(@arguments_statically_coercible) && !@arguments_statically_coercible.nil? @arguments_statically_coercible else @arguments_statically_coercible = all_argument_definitions.all?(&:statically_coercible?) end end module ArgumentClassAccessor def argument_class(new_arg_class = nil) if new_arg_class @argument_class = new_arg_class elsif defined?(@argument_class) && @argument_class @argument_class else superclass.respond_to?(:argument_class) ? superclass.argument_class : GraphQL::Schema::Argument end end end module ArgumentObjectLoader # Look up the corresponding object for a provided ID. # By default, it uses Relay-style {Schema.object_from_id}, # override this to find objects another way. # # @param type [Class, Module] A GraphQL type definition # @param id [String] A client-provided to look up # @param context [GraphQL::Query::Context] the current context def object_from_id(type, id, context) context.schema.object_from_id(id, context) end def load_application_object(argument, id, context) # See if any object can be found for this ID if id.nil? return nil end object_from_id(argument.loads, id, context) end def load_and_authorize_application_object(argument, id, context) loaded_application_object = load_application_object(argument, id, context) authorize_application_object(argument, id, context, loaded_application_object) end def authorize_application_object(argument, id, context, loaded_application_object) context.query.after_lazy(loaded_application_object) do |application_object| if application_object.nil? err = GraphQL::LoadApplicationObjectFailedError.new(context: context, argument: argument, id: id, object: application_object) application_object = load_application_object_failed(err) end # Double-check that the located object is actually of this type # (Don't want to allow arbitrary access to objects this way) if application_object.nil? nil else arg_loads_type = argument.loads maybe_lazy_resolve_type = context.schema.resolve_type(arg_loads_type, application_object, context) context.query.after_lazy(maybe_lazy_resolve_type) do |resolve_type_result| if resolve_type_result.is_a?(Array) && resolve_type_result.size == 2 application_object_type, application_object = resolve_type_result else application_object_type = resolve_type_result # application_object is already assigned end passes_possible_types_check = if context.types.loadable?(arg_loads_type, context) if arg_loads_type.kind.abstract? # This union/interface is used in `loads:` but not otherwise visible to this query context.types.loadable_possible_types(arg_loads_type, context).include?(application_object_type) else true end else context.types.possible_types(arg_loads_type).include?(application_object_type) end if !passes_possible_types_check err = GraphQL::LoadApplicationObjectFailedError.new(context: context, argument: argument, id: id, object: application_object) application_object = load_application_object_failed(err) end if application_object.nil? nil else # This object was loaded successfully # and resolved to the right type, # now apply the `.authorized?` class method if there is one context.query.after_lazy(application_object_type.authorized?(application_object, context)) do |authed| if authed application_object else err = GraphQL::UnauthorizedError.new( object: application_object, type: application_object_type, context: context, ) if self.respond_to?(:unauthorized_object) err.set_backtrace(caller) unauthorized_object(err) else raise err end end end end end end end end # Called when an argument's `loads:` configuration fails to fetch an application object. # By default, this method raises the given error, but you can override it to handle failures differently. # # @param err [GraphQL::LoadApplicationObjectFailedError] The error that occurred # @return [Object, nil] If a value is returned, it will be used instead of the failed load # @api public def load_application_object_failed(err) raise err end end NO_ARGUMENTS = GraphQL::EmptyObjects::EMPTY_HASH def own_arguments @own_arguments || NO_ARGUMENTS end end end end end graphql-ruby-2.5.19/lib/graphql/schema/member/has_ast_node.rb000066400000000000000000000013211514115062600241120ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Member module HasAstNode def self.extended(child_cls) super child_cls.ast_node = nil end def inherited(child_cls) super child_cls.ast_node = nil end # If this schema was parsed from a `.graphql` file (or other SDL), # this is the AST node that defined this part of the schema. def ast_node(new_ast_node = nil) if new_ast_node @ast_node = new_ast_node elsif defined?(@ast_node) @ast_node else nil end end attr_writer :ast_node end end end end graphql-ruby-2.5.19/lib/graphql/schema/member/has_dataloader.rb000066400000000000000000000061641514115062600244300ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Member # @api public # Shared methods for working with {Dataloader} inside GraphQL runtime objects. module HasDataloader # @return [GraphQL::Dataloader] The dataloader for the currently-running query def dataloader context.dataloader end # A shortcut method for loading a key from a source. # Identical to `dataloader.with(source_class, *source_args).load(load_key)` # @param source_class [Class] # @param source_args [Array] Any extra parameters defined in `source_class`'s `initialize` method # @param load_key [Object] The key to look up using `def fetch` def dataload(source_class, *source_args, load_key) dataloader.with(source_class, *source_args).load(load_key) end # Find an object with ActiveRecord via {Dataloader::ActiveRecordSource}. # @param model [Class] # @param find_by_value [Object] Usually an `id`, might be another value if `find_by:` is also provided # @param find_by [Symbol, String] A column name to look the record up by. (Defaults to the model's primary key.) # @return [ActiveRecord::Base, nil] # @example Finding a record by ID # dataload_record(Post, 5) # Like `Post.find(5)`, but dataloaded # @example Finding a record by another attribute # dataload_record(User, "matz", find_by: :handle) # Like `User.find_by(handle: "matz")`, but dataloaded def dataload_record(model, find_by_value, find_by: nil) source = if find_by dataloader.with(Dataloader::ActiveRecordSource, model, find_by: find_by) else dataloader.with(Dataloader::ActiveRecordSource, model) end source.load(find_by_value) end # Look up an associated record using a Rails association (via {Dataloader::ActiveRecordAssociationSource}) # @param association_name [Symbol] A `belongs_to` or `has_one` association. (If a `has_many` association is named here, it will be selected without pagination.) # @param record [ActiveRecord::Base] The object that the association belongs to. # @param scope [ActiveRecord::Relation] A scope to look up the associated record in # @return [ActiveRecord::Base, nil] The associated record, if there is one # @example Looking up a belongs_to on the current object # dataload_association(:parent) # Equivalent to `object.parent`, but dataloaded # @example Looking up an associated record on some other object # dataload_association(comment, :post) # Equivalent to `comment.post`, but dataloaded def dataload_association(record = object, association_name, scope: nil) source = if scope dataloader.with(Dataloader::ActiveRecordAssociationSource, association_name, scope) else dataloader.with(Dataloader::ActiveRecordAssociationSource, association_name) end source.load(record) end end end end end graphql-ruby-2.5.19/lib/graphql/schema/member/has_deprecation_reason.rb000066400000000000000000000022351514115062600261670ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Member module HasDeprecationReason # @return [String, nil] Explains why this member was deprecated (if present, this will be marked deprecated in introspection) attr_reader :deprecation_reason # Set the deprecation reason for this member, or remove it by assigning `nil` # @param text [String, nil] def deprecation_reason=(text) @deprecation_reason = text if text.nil? remove_directive(GraphQL::Schema::Directive::Deprecated) else # This removes a previously-attached directive, if there is one: directive(GraphQL::Schema::Directive::Deprecated, reason: text) end end def self.extended(child_class) super child_class.extend(ClassMethods) end module ClassMethods def deprecation_reason(new_reason = NOT_CONFIGURED) if NOT_CONFIGURED.equal?(new_reason) super() else self.deprecation_reason = new_reason end end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/member/has_directives.rb000066400000000000000000000077751514115062600245020ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Member module HasDirectives def self.extended(child_cls) super child_cls.module_exec { self.own_directives = nil } end def inherited(child_cls) super child_cls.own_directives = nil end # Create an instance of `dir_class` for `self`, using `options`. # # It removes a previously-attached instance of `dir_class`, if there is one. # # @return [void] def directive(dir_class, **options) @own_directives ||= [] HasDirectives.add_directive(self, @own_directives, dir_class, options) nil end # Remove an attached instance of `dir_class`, if there is one # @param dir_class [Class] # @return [viod] def remove_directive(dir_class) HasDirectives.remove_directive(@own_directives, dir_class) nil end def directives HasDirectives.get_directives(self, @own_directives, :directives) end class << self def add_directive(schema_member, directives, directive_class, directive_options) remove_directive(directives, directive_class) unless directive_class.repeatable? directives << directive_class.new(schema_member, **directive_options) end def remove_directive(directives, directive_class) directives && directives.reject! { |d| d.is_a?(directive_class) } end def get_directives(schema_member, directives, directives_method) case schema_member when Class inherited_directives = if schema_member.superclass.respond_to?(directives_method) get_directives(schema_member.superclass, schema_member.superclass.public_send(directives_method), directives_method) else GraphQL::EmptyObjects::EMPTY_ARRAY end if !inherited_directives.empty? && directives dirs = [] merge_directives(dirs, inherited_directives) merge_directives(dirs, directives) dirs elsif directives directives elsif !inherited_directives.empty? inherited_directives else GraphQL::EmptyObjects::EMPTY_ARRAY end when Module dirs = nil schema_member.ancestors.reverse_each do |ancestor| if ancestor.respond_to?(:own_directives) && !(anc_dirs = ancestor.own_directives).empty? dirs ||= [] merge_directives(dirs, anc_dirs) end end if directives dirs ||= [] merge_directives(dirs, directives) end dirs || GraphQL::EmptyObjects::EMPTY_ARRAY when HasDirectives directives || GraphQL::EmptyObjects::EMPTY_ARRAY else raise "Invariant: how could #{schema_member} not be a Class, Module, or instance of HasDirectives?" end end private # Modify `target` by adding items from `dirs` such that: # - Any name conflict is overridden by the incoming member of `dirs` # - Any other member of `dirs` is appended # @param target [Array] # @param dirs [Array] # @return [void] def merge_directives(target, dirs) dirs.each do |dir| if (idx = target.find_index { |d| d.graphql_name == dir.graphql_name }) target.slice!(idx) target.insert(idx, dir) else target << dir end end nil end end protected attr_accessor :own_directives end end end end graphql-ruby-2.5.19/lib/graphql/schema/member/has_fields.rb000066400000000000000000000413751514115062600236010ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Member # Shared code for Objects, Interfaces, Mutations, Subscriptions module HasFields include EmptyObjects # Add a field to this object or interface with the given definition # @param name_positional [Symbol] The underscore-cased version of this field name (will be camelized for the GraphQL API); `name:` keyword is also accepted # @param type_positional [Class, GraphQL::BaseType, Array] The return type of this field; `type:` keyword is also accepted # @param desc_positional [String] Field description; `description:` keyword is also accepted # @option kwargs [Symbol] :name The underscore-cased version of this field name (will be camelized for the GraphQL API); positional argument also accepted # @option kwargs [Class, GraphQL::BaseType, Array] :type The return type of this field; positional argument is also accepted # @option kwargs [Boolean] :null (defaults to `true`) `true` if this field may return `null`, `false` if it is never `null` # @option kwargs [String] :description Field description; positional argument also accepted # @option kwargs [String] :comment Field comment # @option kwargs [String] :deprecation_reason If present, the field is marked "deprecated" with this message # @option kwargs [Symbol] :method The method to call on the underlying object to resolve this field (defaults to `name`) # @option kwargs [String, Symbol] :hash_key The hash key to lookup on the underlying object (if its a Hash) to resolve this field (defaults to `name` or `name.to_s`) # @option kwargs [Array] :dig The nested hash keys to lookup on the underlying hash to resolve this field using dig # @option kwargs [Symbol] :resolver_method The method on the type to call to resolve this field (defaults to `name`) # @option kwargs [Boolean] :connection `true` if this field should get automagic connection behavior; default is to infer by `*Connection` in the return type name # @option kwargs [Class] :connection_extension The extension to add, to implement connections. If `nil`, no extension is added. # @option kwargs [Integer, nil] :max_page_size For connections, the maximum number of items to return from this field, or `nil` to allow unlimited results. # @option kwargs [Integer, nil] :default_page_size For connections, the default number of items to return from this field, or `nil` to return unlimited results. # @option kwargs [Boolean] :introspection If true, this field will be marked as `#introspection?` and the name may begin with `__` # @option kwargs [{String=>GraphQL::Schema::Argument, Hash}] :arguments Arguments for this field (may be added in the block, also) # @option kwargs [Boolean] :camelize If true, the field name will be camelized when building the schema # @option kwargs [Numeric] :complexity When provided, set the complexity for this field # @option kwargs [Boolean] :scope If true, the return type's `.scope_items` method will be called on the return value # @option kwargs [Symbol, String] :subscription_scope A key in `context` which will be used to scope subscription payloads # @option kwargs [Array Object>>] :extensions Named extensions to apply to this field (see also {#extension}) # @option kwargs [Hash{Class => Hash}] :directives Directives to apply to this field # @option kwargs [Boolean] :trace If true, a {GraphQL::Tracing} tracer will measure this scalar field # @option kwargs [Boolean] :broadcastable Whether or not this field can be distributed in subscription broadcasts # @option kwargs [Language::Nodes::FieldDefinition, nil] :ast_node If this schema was parsed from definition, this AST node defined the field # @option kwargs [Boolean] :method_conflict_warning If false, skip the warning if this field's method conflicts with a built-in method # @option kwargs [Array] :validates Configurations for validating this field # @option kwargs [Object] :fallback_value A fallback value if the method is not defined # @option kwargs [Class] :mutation # @option kwargs [Class] :resolver # @option kwargs [Class] :subscription # @option kwargs [Boolean] :dynamic_introspection (Private, used by GraphQL-Ruby) # @option kwargs [Boolean] :relay_node_field (Private, used by GraphQL-Ruby) # @option kwargs [Boolean] :relay_nodes_field (Private, used by GraphQL-Ruby) # @option kwargs [Array<:ast_node, :parent, :lookahead, :owner, :execution_errors, :graphql_name, :argument_details, Symbol>] :extras Extra arguments to be injected into the resolver for this field # @param kwargs [Hash] Keywords for defining the field. Any not documented here will be passed to your base field class where they must be handled. # @param definition_block [Proc] an additional block for configuring the field. Receive the field as a block param, or, if no block params are defined, then the block is `instance_eval`'d on the new {Field}. # @yieldparam field [GraphQL::Schema::Field] The newly-created field instance # @yieldreturn [void] # @return [GraphQL::Schema::Field] def field(name_positional = nil, type_positional = nil, desc_positional = nil, **kwargs, &definition_block) resolver = kwargs.delete(:resolver) mutation = kwargs.delete(:mutation) subscription = kwargs.delete(:subscription) if (resolver_class = resolver || mutation || subscription) # Add a reference to that parent class kwargs[:resolver_class] = resolver_class end kwargs[:name] ||= name_positional if !type_positional.nil? if desc_positional if kwargs[:description] raise ArgumentError, "Provide description as a positional argument or `description:` keyword, but not both (#{desc_positional.inspect}, #{kwargs[:description].inspect})" end kwargs[:description] = desc_positional kwargs[:type] = type_positional elsif (resolver || mutation) && type_positional.is_a?(String) # The return type should be copied from the resolver, and the second positional argument is the description kwargs[:description] = type_positional else kwargs[:type] = type_positional end if type_positional.is_a?(Class) && type_positional < GraphQL::Schema::Mutation raise ArgumentError, "Use `field #{name_positional.inspect}, mutation: Mutation, ...` to provide a mutation to this field instead" end end kwargs[:owner] = self field_defn = field_class.new(**kwargs, &definition_block) add_field(field_defn) field_defn end # A list of Ruby keywords. # # @api private RUBY_KEYWORDS = [:class, :module, :def, :undef, :begin, :rescue, :ensure, :end, :if, :unless, :then, :elsif, :else, :case, :when, :while, :until, :for, :break, :next, :redo, :retry, :in, :do, :return, :yield, :super, :self, :nil, :true, :false, :and, :or, :not, :alias, :defined?, :BEGIN, :END, :__LINE__, :__FILE__] # A list of GraphQL-Ruby keywords. # # @api private GRAPHQL_RUBY_KEYWORDS = [:context, :object, :raw_value] # A list of field names that we should advise users to pick a different # resolve method name. # # @api private CONFLICT_FIELD_NAMES = Set.new(GRAPHQL_RUBY_KEYWORDS + RUBY_KEYWORDS + Object.instance_methods) # Register this field with the class, overriding a previous one if needed. # @param field_defn [GraphQL::Schema::Field] # @return [void] def add_field(field_defn, method_conflict_warning: field_defn.method_conflict_warning?) # Check that `field_defn.original_name` equals `resolver_method` and `method_sym` -- # that shows that no override value was given manually. if method_conflict_warning && CONFLICT_FIELD_NAMES.include?(field_defn.resolver_method) && field_defn.original_name == field_defn.resolver_method && field_defn.original_name == field_defn.method_sym && field_defn.hash_key == NOT_CONFIGURED && field_defn.dig_keys.nil? warn(conflict_field_name_warning(field_defn)) end prev_defn = own_fields[field_defn.name] case prev_defn when nil own_fields[field_defn.name] = field_defn when Array prev_defn << field_defn when GraphQL::Schema::Field own_fields[field_defn.name] = [prev_defn, field_defn] else raise "Invariant: unexpected previous field definition for #{field_defn.name.inspect}: #{prev_defn.inspect}" end nil end # @return [Class] The class to initialize when adding fields to this kind of schema member def field_class(new_field_class = nil) if new_field_class @field_class = new_field_class elsif defined?(@field_class) && @field_class @field_class else find_inherited_value(:field_class, GraphQL::Schema::Field) end end def global_id_field(field_name, **kwargs) type = self field field_name, "ID", **kwargs, null: false define_method(field_name) do context.schema.id_from_object(object, type, context) end end # @param new_has_no_fields [Boolean] Call with `true` to make this Object type ignore the requirement to have any defined fields. # @return [void] def has_no_fields(new_has_no_fields) @has_no_fields = new_has_no_fields nil end # @return [Boolean] `true` if `has_no_fields(true)` was configued def has_no_fields? @has_no_fields end # @return [Hash GraphQL::Schema::Field, Array>] Fields defined on this class _specifically_, not parent classes def own_fields @own_fields ||= {} end def all_field_definitions all_fields = {} ancestors.reverse_each do |ancestor| if ancestor.respond_to?(:own_fields) all_fields.merge!(ancestor.own_fields) end end all_fields = all_fields.values all_fields.flatten! all_fields end module InterfaceMethods def get_field(field_name, context = GraphQL::Query::NullContext.instance) warden = Warden.from_context(context) skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Profile) for ancestor in ancestors if ancestor.respond_to?(:own_fields) && (f_entry = ancestor.own_fields[field_name]) && (skip_visible || (f_entry = Warden.visible_entry?(:visible_field?, f_entry, context, warden))) return f_entry end end nil end # @return [Hash GraphQL::Schema::Field>] Fields on this object, keyed by name, including inherited fields def fields(context = GraphQL::Query::NullContext.instance) warden = Warden.from_context(context) # Local overrides take precedence over inherited fields visible_fields = {} for ancestor in ancestors if ancestor.respond_to?(:own_fields) ancestor.own_fields.each do |field_name, fields_entry| # Choose the most local definition that passes `.visible?` -- # stop checking for fields by name once one has been found. if !visible_fields.key?(field_name) && (f = Warden.visible_entry?(:visible_field?, fields_entry, context, warden)) visible_fields[field_name] = f.ensure_loaded end end end end visible_fields end end module ObjectMethods def get_field(field_name, context = GraphQL::Query::NullContext.instance) # Objects need to check that the interface implementation is visible, too warden = Warden.from_context(context) ancs = ancestors skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Profile) i = 0 while (ancestor = ancs[i]) if ancestor.respond_to?(:own_fields) && visible_interface_implementation?(ancestor, context, warden) && (f_entry = ancestor.own_fields[field_name]) && (skip_visible || (f_entry = Warden.visible_entry?(:visible_field?, f_entry, context, warden))) return (skip_visible ? f_entry : f_entry.ensure_loaded) end i += 1 end nil end # @return [Hash GraphQL::Schema::Field>] Fields on this object, keyed by name, including inherited fields def fields(context = GraphQL::Query::NullContext.instance) # Objects need to check that the interface implementation is visible, too warden = Warden.from_context(context) # Local overrides take precedence over inherited fields visible_fields = {} had_any_fields_at_all = false for ancestor in ancestors if ancestor.respond_to?(:own_fields) && visible_interface_implementation?(ancestor, context, warden) ancestor.own_fields.each do |field_name, fields_entry| had_any_fields_at_all = true # Choose the most local definition that passes `.visible?` -- # stop checking for fields by name once one has been found. if !visible_fields.key?(field_name) && (f = Warden.visible_entry?(:visible_field?, fields_entry, context, warden)) visible_fields[field_name] = f.ensure_loaded end end end end if !had_any_fields_at_all && !has_no_fields? warn(GraphQL::Schema::Object::FieldsAreRequiredError.new(self).message + "\n\nThis will raise an error in a future GraphQL-Ruby version.") end visible_fields end end def self.included(child_class) # Included in an interface definition methods module child_class.include(InterfaceMethods) super end def self.extended(child_class) child_class.extend(ObjectMethods) super end private def inherited(subclass) super subclass.class_exec do @own_fields ||= nil @field_class ||= nil @has_no_fields ||= false end end # If `type` is an interface, and `self` has a type membership for `type`, then make sure it's visible. def visible_interface_implementation?(type, context, warden) if type.respond_to?(:kind) && type.kind.interface? implements_this_interface = false implementation_is_visible = false warden.interface_type_memberships(self, context).each do |tm| if tm.abstract_type == type implements_this_interface ||= true if warden.visible_type_membership?(tm, context) implementation_is_visible = true break end end end # It's possible this interface came by way of `include` in another interface which this # object type _does_ implement, and that's ok implements_this_interface ? implementation_is_visible : true else # If there's no implementation, then we're looking at Ruby-style inheritance instead true end end # @param field_defn [GraphQL::Schema::Field] # @return [String] A warning to give when this field definition might conflict with a built-in method def conflict_field_name_warning(field_defn) "#{self.graphql_name}'s `field :#{field_defn.original_name}` conflicts with a built-in method, use `resolver_method:` to pick a different resolver method for this field (for example, `resolver_method: :resolve_#{field_defn.resolver_method}` and `def resolve_#{field_defn.resolver_method}`). Or use `method_conflict_warning: false` to suppress this warning." end end end end end graphql-ruby-2.5.19/lib/graphql/schema/member/has_interfaces.rb000066400000000000000000000124431514115062600244500ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Member module HasInterfaces def implements(*new_interfaces, **options) new_memberships = [] new_interfaces.each do |int| if int.is_a?(Module) unless int.include?(GraphQL::Schema::Interface) && !int.is_a?(Class) raise "#{int.respond_to?(:graphql_name) ? "#{int.graphql_name} (#{int})" : int.inspect} cannot be implemented since it's not a GraphQL Interface. Use `include` for plain Ruby modules." end new_memberships << int.type_membership_class.new(int, self, **options) # Include the methods here, # `.fields` will use the inheritance chain # to find inherited fields include(int) # If this interface has interfaces of its own, add those, too int.interfaces.each do |next_interface| implements(next_interface) end elsif int.is_a?(String) || int.is_a?(GraphQL::Schema::LateBoundType) if !options.empty? raise ArgumentError, "`implements(...)` doesn't support options with late-loaded types yet. Remove #{options} and open an issue to request this feature." end new_memberships << int else raise ArgumentError, "Unexpected interface definition (expected module): #{int} (#{int.class})" end end # Remove any String or late-bound interfaces which are being replaced own_interface_type_memberships.reject! { |old_i_m| if !(old_i_m.respond_to?(:abstract_type) && old_i_m.abstract_type.is_a?(Module)) old_int_type = old_i_m.respond_to?(:abstract_type) ? old_i_m.abstract_type : old_i_m old_name = Schema::Member::BuildType.to_type_name(old_int_type) new_memberships.any? { |new_i_m| new_int_type = new_i_m.respond_to?(:abstract_type) ? new_i_m.abstract_type : new_i_m new_name = Schema::Member::BuildType.to_type_name(new_int_type) new_name == old_name } end } own_interface_type_memberships.concat(new_memberships) end def own_interface_type_memberships @own_interface_type_memberships ||= [] end def interface_type_memberships own_interface_type_memberships end module ClassConfigured # This combination of extended -> inherited -> extended # means that the base class (`Schema::Object`) *won't* # have the superclass-related code in `InheritedInterfaces`, # but child classes of `Schema::Object` will have it. # That way, we don't need a `superclass.respond_to?(...)` check. def inherited(child_class) super child_class.extend(InheritedInterfaces) end module InheritedInterfaces def interfaces(context = GraphQL::Query::NullContext.instance) visible_interfaces = super inherited_interfaces = superclass.interfaces(context) if !visible_interfaces.empty? if !inherited_interfaces.empty? visible_interfaces.concat(inherited_interfaces) visible_interfaces.uniq! end visible_interfaces elsif !inherited_interfaces.empty? inherited_interfaces else EmptyObjects::EMPTY_ARRAY end end def interface_type_memberships own_tms = super inherited_tms = superclass.interface_type_memberships if inherited_tms.size > 0 own_tms + inherited_tms else own_tms end end end end # param context [Query::Context] If omitted, skip filtering. def interfaces(context = GraphQL::Query::NullContext.instance) warden = Warden.from_context(context) visible_interfaces = nil own_interface_type_memberships.each do |type_membership| case type_membership when Schema::TypeMembership if warden.visible_type_membership?(type_membership, context) visible_interfaces ||= [] visible_interfaces << type_membership.abstract_type end when String, Schema::LateBoundType # During initialization, `type_memberships` can hold late-bound types visible_interfaces ||= [] visible_interfaces << type_membership else raise "Invariant: Unexpected type_membership #{type_membership.class}: #{type_membership.inspect}" end end if visible_interfaces visible_interfaces.uniq! visible_interfaces else EmptyObjects::EMPTY_ARRAY end end private def self.extended(child_class) child_class.extend(ClassConfigured) end def inherited(subclass) super subclass.class_exec do @own_interface_type_memberships ||= nil end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/member/has_path.rb000066400000000000000000000011551514115062600232570ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Member module HasPath # @return [String] A description of this member's place in the GraphQL schema def path path_str = if self.respond_to?(:graphql_name) self.graphql_name elsif self.class.respond_to?(:graphql_name) # Instances of resolvers self.class.graphql_name end if self.respond_to?(:owner) && owner.respond_to?(:path) path_str = "#{owner.path}.#{path_str}" end path_str end end end end end graphql-ruby-2.5.19/lib/graphql/schema/member/has_unresolved_type_error.rb000066400000000000000000000011051514115062600267560ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Member # Set up a type-specific error to make debugging & bug tracker integration better module HasUnresolvedTypeError private def add_unresolved_type_error(child_class) if child_class.name # Don't set this for anonymous classes child_class.const_set(:UnresolvedTypeError, Class.new(GraphQL::UnresolvedTypeError)) else child_class.const_set(:UnresolvedTypeError, UnresolvedTypeError) end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/member/has_validators.rb000066400000000000000000000030431514115062600244710ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Member module HasValidators include GraphQL::EmptyObjects # Build {GraphQL::Schema::Validator}s based on the given configuration # and use them for this schema member # @param validation_config [Hash{Symbol => Hash}] # @return [void] def validates(validation_config) new_validators = GraphQL::Schema::Validator.from_config(self, validation_config) @own_validators ||= [] @own_validators.concat(new_validators) nil end # @return [Array] def validators @own_validators || EMPTY_ARRAY end module ClassConfigured def inherited(child_cls) super child_cls.extend(ClassValidators) end module ClassValidators include GraphQL::EmptyObjects def validators inherited_validators = superclass.validators if !inherited_validators.empty? if @own_validators.nil? inherited_validators else inherited_validators + @own_validators end elsif @own_validators.nil? EMPTY_ARRAY else @own_validators end end end end def self.extended(child_cls) super child_cls.extend(ClassConfigured) end end end end end graphql-ruby-2.5.19/lib/graphql/schema/member/relay_shortcuts.rb000066400000000000000000000055621514115062600247300ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Member module RelayShortcuts def edge_type_class(new_edge_type_class = nil) if new_edge_type_class initialize_relay_metadata @edge_type_class = new_edge_type_class else # Don't call `ancestor.edge_type_class` # because we don't want a fallback from any ancestors -- # only apply the fallback if _no_ ancestor has a configured value! for ancestor in self.ancestors if ancestor.respond_to?(:configured_edge_type_class, true) && (etc = ancestor.configured_edge_type_class) return etc end end Types::Relay::BaseEdge end end def connection_type_class(new_connection_type_class = nil) if new_connection_type_class initialize_relay_metadata @connection_type_class = new_connection_type_class else # Don't call `ancestor.connection_type_class` # because we don't want a fallback from any ancestors -- # only apply the fallback if _no_ ancestor has a configured value! for ancestor in self.ancestors if ancestor.respond_to?(:configured_connection_type_class, true) && (ctc = ancestor.configured_connection_type_class) return ctc end end Types::Relay::BaseConnection end end def edge_type initialize_relay_metadata @edge_type ||= begin edge_name = self.graphql_name + "Edge" node_type_class = self Class.new(edge_type_class) do graphql_name(edge_name) node_type(node_type_class) end end end def connection_type initialize_relay_metadata @connection_type ||= begin conn_name = self.graphql_name + "Connection" edge_type_class = self.edge_type Class.new(connection_type_class) do graphql_name(conn_name) edge_type(edge_type_class) end end end protected def configured_connection_type_class @connection_type_class end def configured_edge_type_class @edge_type_class end attr_writer :edge_type, :connection_type, :connection_type_class, :edge_type_class private # If one of these values is accessed, initialize all the instance variables to retain # a consistent object shape. def initialize_relay_metadata if !defined?(@connection_type) @connection_type = nil @edge_type = nil @connection_type_class = nil @edge_type_class = nil end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/member/scoped.rb000066400000000000000000000022011514115062600227360ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Member module Scoped # This is called when a field has `scope: true`. # The field's return type class receives this call. # # By default, it's a no-op. Override it to scope your objects. # # @param items [Object] Some list-like object (eg, Array, ActiveRecord::Relation) # @param context [GraphQL::Query::Context] # @return [Object] Another list-like object, scoped to the current context def scope_items(items, context) items end def reauthorize_scoped_objects(new_value = nil) if new_value.nil? if @reauthorize_scoped_objects != nil @reauthorize_scoped_objects else find_inherited_value(:reauthorize_scoped_objects, true) end else @reauthorize_scoped_objects = new_value end end def inherited(subclass) super subclass.class_exec do @reauthorize_scoped_objects = nil end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/member/type_system_helpers.rb000066400000000000000000000032041514115062600255740ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Member module TypeSystemHelpers def initialize(...) super @to_non_null_type ||= nil @to_list_type ||= nil end # @return [Schema::NonNull] Make a non-null-type representation of this type def to_non_null_type @to_non_null_type || begin t = GraphQL::Schema::NonNull.new(self) if frozen? t else @to_non_null_type = t end end end # @return [Schema::List] Make a list-type representation of this type def to_list_type @to_list_type || begin t = GraphQL::Schema::List.new(self) if frozen? t else @to_list_type = t end end end # @return [Boolean] true if this is a non-nullable type. A nullable list of non-nullables is considered nullable. def non_null? false end # @return [Boolean] true if this is a list type. A non-nullable list is considered a list. def list? false end def to_type_signature graphql_name end # @return [GraphQL::TypeKinds::TypeKind] def kind raise GraphQL::RequiredImplementationMissingError, "No `.kind` defined for #{self}" end private def inherited(subclass) subclass.class_exec do @to_non_null_type ||= nil @to_list_type ||= nil end super end end end end end graphql-ruby-2.5.19/lib/graphql/schema/member/validates_input.rb000066400000000000000000000015121514115062600246600ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Member module ValidatesInput def valid_input?(val, ctx) validate_input(val, ctx).valid? end def validate_input(val, ctx, max_errors: nil) if val.nil? Query::InputValidationResult::VALID else validate_non_null_input(val, ctx, max_errors: max_errors) || Query::InputValidationResult::VALID end end def valid_isolated_input?(v) valid_input?(v, GraphQL::Query::NullContext.instance) end def coerce_isolated_input(v) coerce_input(v, GraphQL::Query::NullContext.instance) end def coerce_isolated_result(v) coerce_result(v, GraphQL::Query::NullContext.instance) end end end end end graphql-ruby-2.5.19/lib/graphql/schema/mutation.rb000066400000000000000000000055571514115062600220730ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema # This base class accepts configuration for a mutation root field, # then it can be hooked up to your mutation root object type. # # If you want to customize how this class generates types, in your base class, # override the various `generate_*` methods. # # @see {GraphQL::Schema::RelayClassicMutation} for an extension of this class with some conventions built-in. # # @example Creating a comment # # Define the mutation: # class Mutations::CreateComment < GraphQL::Schema::Mutation # argument :body, String, required: true # argument :post_id, ID, required: true # # field :comment, Types::Comment, null: true # field :errors, [String], null: false # # def resolve(body:, post_id:) # post = Post.find(post_id) # comment = post.comments.build(body: body, author: context[:current_user]) # if comment.save # # Successful creation, return the created object with no errors # { # comment: comment, # errors: [], # } # else # # Failed save, return the errors to the client # { # comment: nil, # errors: comment.errors.full_messages # } # end # end # end # # # Hook it up to your mutation: # class Types::Mutation < GraphQL::Schema::Object # field :create_comment, mutation: Mutations::CreateComment # end # # # Call it from GraphQL: # result = MySchema.execute <<-GRAPHQL # mutation { # createComment(postId: "1", body: "Nice Post!") { # errors # comment { # body # author { # login # } # } # } # } # GRAPHQL # class Mutation < GraphQL::Schema::Resolver extend GraphQL::Schema::Member::HasFields extend GraphQL::Schema::Resolver::HasPayloadType # @api private def call_resolve(_args_hash) # Clear any cached values from `loads` or authorization: dataloader.clear_cache super end class << self def visible?(context) true end private def conflict_field_name_warning(field_defn) "#{self.graphql_name}'s `field :#{field_defn.name}` conflicts with a built-in method, use `hash_key:` or `method:` to pick a different resolve behavior for this field (for example, `hash_key: :#{field_defn.resolver_method}_value`, and modify the return hash). Or use `method_conflict_warning: false` to suppress this warning." end # Override this to attach self as `mutation` def generate_payload_type payload_class = super payload_class.mutation(self) payload_class end end end end end graphql-ruby-2.5.19/lib/graphql/schema/non_null.rb000066400000000000000000000033601514115062600220450ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema # Represents a non null type in the schema. # Wraps a {Schema::Member} when it is required. # @see {Schema::Member::TypeSystemHelpers#to_non_null_type} class NonNull < GraphQL::Schema::Wrapper include Schema::Member::ValidatesInput # @return [GraphQL::TypeKinds::NON_NULL] def kind GraphQL::TypeKinds::NON_NULL end # @return [true] def non_null? true end # @return [Boolean] True if this type wraps a list type def list? @of_type.list? end def to_type_signature "#{@of_type.to_type_signature}!" end def inspect "#<#{self.class.name} @of_type=#{@of_type.inspect}>" end def validate_input(value, ctx, max_errors: nil) if value.nil? result = GraphQL::Query::InputValidationResult.new result.add_problem("Expected value to not be null") result else of_type.validate_input(value, ctx, max_errors: max_errors) end end # This is for introspection, where it's expected the name will be `null` def graphql_name nil end def coerce_input(value, ctx) # `.validate_input` above is used for variables, but this method is used for arguments if value.nil? raise GraphQL::ExecutionError, "`null` is not a valid input for `#{to_type_signature}`, please provide a value for this argument." end of_type.coerce_input(value, ctx) end def coerce_result(value, ctx) of_type.coerce_result(value, ctx) end # This is for implementing introspection def description nil end end end end graphql-ruby-2.5.19/lib/graphql/schema/object.rb000066400000000000000000000125661514115062600214770ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/query/null_context" module GraphQL class Schema class Object < GraphQL::Schema::Member extend GraphQL::Schema::Member::HasFields extend GraphQL::Schema::Member::HasInterfaces include Member::HasDataloader # Raised when an Object doesn't have any field defined and hasn't explicitly opted out of this requirement class FieldsAreRequiredError < GraphQL::Error def initialize(object_type) message = "Object types must have fields, but #{object_type.graphql_name} doesn't have any. Define a field for this type, remove it from your schema, or add `has_no_fields(true)` to its definition." super(message) end end # @return [Object] the application object this type is wrapping attr_reader :object # @return [GraphQL::Query::Context] the context instance for this query attr_reader :context # @return [GraphQL::Dataloader] def dataloader context.dataloader end # Call this in a field method to return a value that should be returned to the client # without any further handling by GraphQL. def raw_value(obj) GraphQL::Execution::Interpreter::RawValue.new(obj) end class << self # This is protected so that we can be sure callers use the public method, {.authorized_new} # @see authorized_new to make instances protected :new def wrap_scoped(object, context) scoped_new(object, context) end # This is called by the runtime to return an object to call methods on. def wrap(object, context) authorized_new(object, context) end # Make a new instance of this type _if_ the auth check passes, # otherwise, raise an error. # # Probably only the framework should call this method. # # This might return a {GraphQL::Execution::Lazy} if the user-provided `.authorized?` # hook returns some lazy value (like a Promise). # # The reason that the auth check is in this wrapper method instead of {.new} is because # of how it might return a Promise. It would be weird if `.new` returned a promise; # It would be a headache to try to maintain Promise-y state inside a {Schema::Object} # instance. So, hopefully this wrapper method will do the job. # # @param object [Object] The thing wrapped by this object # @param context [GraphQL::Query::Context] # @return [GraphQL::Schema::Object, GraphQL::Execution::Lazy] # @raise [GraphQL::UnauthorizedError] if the user-provided hook returns `false` def authorized_new(object, context) context.query.current_trace.begin_authorized(self, object, context) begin maybe_lazy_auth_val = context.query.current_trace.authorized(query: context.query, type: self, object: object) do begin authorized?(object, context) rescue GraphQL::UnauthorizedError => err context.schema.unauthorized_object(err) rescue StandardError => err context.query.handle_or_reraise(err) end end ensure context.query.current_trace.end_authorized(self, object, context, maybe_lazy_auth_val) end auth_val = if context.schema.lazy?(maybe_lazy_auth_val) GraphQL::Execution::Lazy.new do context.query.current_trace.begin_authorized(self, object, context) context.query.current_trace.authorized_lazy(query: context.query, type: self, object: object) do res = context.schema.sync_lazy(maybe_lazy_auth_val) context.query.current_trace.end_authorized(self, object, context, res) res end end else maybe_lazy_auth_val end context.query.after_lazy(auth_val) do |is_authorized| if is_authorized self.new(object, context) else # It failed the authorization check, so go to the schema's authorized object hook err = GraphQL::UnauthorizedError.new(object: object, type: self, context: context) # If a new value was returned, wrap that instead of the original value begin new_obj = context.schema.unauthorized_object(err) if new_obj self.new(new_obj, context) else nil end end end end end def scoped_new(object, context) self.new(object, context) end end def initialize(object, context) @object = object @context = context end class << self # Set up a type-specific invalid null error to use when this object's non-null fields wrongly return `nil`. # It should help with debugging and bug tracker integrations. def const_missing(name) if name == :InvalidNullError custom_err_class = GraphQL::InvalidNullError.subclass_for(self) const_set(:InvalidNullError, custom_err_class) custom_err_class else super end end def kind GraphQL::TypeKinds::OBJECT end end end end end graphql-ruby-2.5.19/lib/graphql/schema/printer.rb000066400000000000000000000064071514115062600217110ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema # Used to convert your {GraphQL::Schema} to a GraphQL schema string # # @example print your schema to standard output (via helper) # puts GraphQL::Schema::Printer.print_schema(MySchema) # # @example print your schema to standard output # puts GraphQL::Schema::Printer.new(MySchema).print_schema # # @example print a single type to standard output # class Types::Query < GraphQL::Schema::Object # description "The query root of this schema" # # field :post, Types::Post, null: true # end # # class Types::Post < GraphQL::Schema::Object # description "A blog post" # # field :id, ID, null: false # field :title, String, null: false # field :body, String, null: false # end # # class MySchema < GraphQL::Schema # query(Types::Query) # end # # printer = GraphQL::Schema::Printer.new(MySchema) # puts printer.print_type(Types::Post) # class Printer < GraphQL::Language::Printer attr_reader :schema, :warden # @param schema [GraphQL::Schema] # @param context [Hash] # @param introspection [Boolean] Should include the introspection types in the string? def initialize(schema, context: nil, introspection: false) @document_from_schema = GraphQL::Language::DocumentFromSchemaDefinition.new( schema, context: context, include_introspection_types: introspection, ) @document = @document_from_schema.document @schema = schema end # Return the GraphQL schema string for the introspection type system def self.print_introspection_schema query_root = Class.new(GraphQL::Schema::Object) do graphql_name "Root" field :throwaway_field, String def self.visible?(ctx) false end end schema = Class.new(GraphQL::Schema) { use GraphQL::Schema::Visibility query(query_root) def self.visible?(member, _ctx) member.graphql_name != "Root" end } introspection_schema_ast = GraphQL::Language::DocumentFromSchemaDefinition.new( schema, include_introspection_types: true, include_built_in_directives: true, ).document introspection_schema_ast.to_query_string(printer: IntrospectionPrinter.new) end # Return a GraphQL schema string for the defined types in the schema # @param schema [GraphQL::Schema] # @param context [Hash] # @param only [<#call(member, ctx)>] # @param except [<#call(member, ctx)>] def self.print_schema(schema, **args) printer = new(schema, **args) printer.print_schema end # Return a GraphQL schema string for the defined types in the schema def print_schema print(@document) + "\n" end def print_type(type) node = @document_from_schema.build_type_definition_node(type) print(node) end class IntrospectionPrinter < GraphQL::Language::Printer def print_schema_definition(schema) print_string("schema {\n query: Root\n}") end end end end end graphql-ruby-2.5.19/lib/graphql/schema/ractor_shareable.rb000066400000000000000000000054021514115062600235200ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema module RactorShareable def self.extended(schema_class) schema_class.extend(SchemaExtension) schema_class.freeze_schema end module SchemaExtension def freeze_error_handlers(handlers) handlers[:subclass_handlers].default_proc = nil handlers[:subclass_handlers].each do |_class, subclass_handlers| freeze_error_handlers(subclass_handlers) end Ractor.make_shareable(handlers) end def freeze_schema # warm some ivars: default_analysis_engine default_execution_strategy GraphQL.default_parser default_logger freeze_error_handlers(error_handlers) # TODO: this freezes errors of parent classes which could cause trouble parent_class = superclass while parent_class.respond_to?(:error_handlers) freeze_error_handlers(parent_class.error_handlers) parent_class = parent_class.superclass end own_tracers.freeze @frozen_tracers = tracers.freeze own_trace_modes.each do |m| trace_options_for(m) build_trace_mode(m) end build_trace_mode(:default) Ractor.make_shareable(@trace_options_for_mode) Ractor.make_shareable(own_trace_modes) Ractor.make_shareable(own_multiplex_analyzers) @frozen_multiplex_analyzers = Ractor.make_shareable(multiplex_analyzers) Ractor.make_shareable(own_query_analyzers) @frozen_query_analyzers = Ractor.make_shareable(query_analyzers) Ractor.make_shareable(own_plugins) own_plugins.each do |(plugin, options)| Ractor.make_shareable(plugin) Ractor.make_shareable(options) end @frozen_plugins = Ractor.make_shareable(plugins) Ractor.make_shareable(own_references_to) @frozen_directives = Ractor.make_shareable(directives) Ractor.make_shareable(visibility) Ractor.make_shareable(introspection_system) extend(FrozenMethods) Ractor.make_shareable(self) superclass.respond_to?(:freeze_schema) && superclass.freeze_schema end module FrozenMethods def tracers; @frozen_tracers; end def multiplex_analyzers; @frozen_multiplex_analyzers; end def query_analyzers; @frozen_query_analyzers; end def plugins; @frozen_plugins; end def directives; @frozen_directives; end # This actually accumulates info during execution... # How to support it? def lazy?(_obj); false; end def sync_lazy(obj); obj; end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/relay_classic_mutation.rb000066400000000000000000000044621514115062600247620ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema # Mutations that extend this base class get some conventions added for free: # # - An argument called `clientMutationId` is _always_ added, but it's not passed # to the resolve method. The value is re-inserted to the response. (It's for # client libraries to manage optimistic updates.) # - The returned object type always has a field called `clientMutationId` to support that. # - The mutation accepts one argument called `input`, `argument`s defined in the mutation # class are added to that input object, which is generated by the mutation. # # These conventions were first specified by Relay Classic, but they come in handy: # # - `clientMutationId` supports optimistic updates and cache rollbacks on the client # - using a single `input:` argument makes it easy to post whole JSON objects to the mutation # using one GraphQL variable (`$input`) instead of making a separate variable for each argument. # # @see {GraphQL::Schema::Mutation} for an example, it's basically the same. # class RelayClassicMutation < GraphQL::Schema::Mutation include GraphQL::Schema::HasSingleInputArgument argument :client_mutation_id, String, "A unique identifier for the client performing the mutation.", required: false # The payload should always include this field field(:client_mutation_id, String, "A unique identifier for the client performing the mutation.") # Relay classic default: null(true) # Override {GraphQL::Schema::Resolver#resolve_with_support} to # delete `client_mutation_id` from the kwargs. def resolve_with_support(**inputs) input = inputs[:input].to_kwargs if input # This is handled by Relay::Mutation::Resolve, a bit hacky, but here we are. input_kwargs = input.to_h client_mutation_id = input_kwargs.delete(:client_mutation_id) inputs[:input] = input_kwargs end return_value = super(**inputs) context.query.after_lazy(return_value) do |return_hash| # It might be an error if return_hash.is_a?(Hash) return_hash[:client_mutation_id] = client_mutation_id end return_hash end end end end end graphql-ruby-2.5.19/lib/graphql/schema/resolver.rb000066400000000000000000000400701514115062600220610ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/schema/resolver/has_payload_type" module GraphQL class Schema # A class-based container for field configuration and resolution logic. It supports: # # - Arguments, via `.argument(...)` helper, which will be applied to the field. # - Return type, via `.type(..., null: ...)`, which will be applied to the field. # - Description, via `.description(...)`, which will be applied to the field # - Comment, via `.comment(...)`, which will be applied to the field # - Resolution, via `#resolve(**args)` method, which will be called to resolve the field. # - `#object` and `#context` accessors for use during `#resolve`. # # Resolvers can be attached with the `resolver:` option in a `field(...)` call. # # A resolver's configuration may be overridden with other keywords in the `field(...)` call. # # @see {GraphQL::Schema::Mutation} for a concrete subclass of `Resolver`. # @see {GraphQL::Function} `Resolver` is a replacement for `GraphQL::Function` class Resolver include Schema::Member::GraphQLTypeNames # Really we only need description & comment from here, but: extend Schema::Member::BaseDSLMethods extend GraphQL::Schema::Member::HasArguments extend GraphQL::Schema::Member::HasValidators include Schema::Member::HasPath extend Schema::Member::HasPath extend Schema::Member::HasDirectives include Schema::Member::HasDataloader extend Schema::Member::HasDeprecationReason # @param object [Object] The application object that this field is being resolved on # @param context [GraphQL::Query::Context] # @param field [GraphQL::Schema::Field] def initialize(object:, context:, field:) @object = object @context = context @field = field # Since this hash is constantly rebuilt, cache it for this call @arguments_by_keyword = {} context.types.arguments(self.class).each do |arg| @arguments_by_keyword[arg.keyword] = arg end @prepared_arguments = nil end # @return [Object] The application object this field is being resolved on attr_reader :object # @return [GraphQL::Query::Context] attr_reader :context # @return [GraphQL::Schema::Field] attr_reader :field def arguments @prepared_arguments || raise("Arguments have not been prepared yet, still waiting for #load_arguments to resolve. (Call `.arguments` later in the code.)") end # This method is _actually_ called by the runtime, # it does some preparation and then eventually calls # the user-defined `#resolve` method. # @api private def resolve_with_support(**args) # First call the ready? hook which may raise raw_ready_val = if !args.empty? ready?(**args) else ready? end context.query.after_lazy(raw_ready_val) do |ready_val| if ready_val.is_a?(Array) is_ready, ready_early_return = ready_val if is_ready != false raise "Unexpected result from #ready? (expected `true`, `false` or `[false, {...}]`): [#{is_ready.inspect}, #{ready_early_return.inspect}]" else ready_early_return end elsif ready_val # Then call each prepare hook, which may return a different value # for that argument, or may return a lazy object load_arguments_val = load_arguments(args) context.query.after_lazy(load_arguments_val) do |loaded_args| @prepared_arguments = loaded_args Schema::Validator.validate!(self.class.validators, object, context, loaded_args, as: @field) # Then call `authorized?`, which may raise or may return a lazy object raw_authorized_val = if !loaded_args.empty? authorized?(**loaded_args) else authorized? end context.query.after_lazy(raw_authorized_val) do |authorized_val| # If the `authorized?` returned two values, `false, early_return`, # then use the early return value instead of continuing if authorized_val.is_a?(Array) authorized_result, early_return = authorized_val if authorized_result == false early_return else raise "Unexpected result from #authorized? (expected `true`, `false` or `[false, {...}]`): [#{authorized_result.inspect}, #{early_return.inspect}]" end elsif authorized_val # Finally, all the hooks have passed, so resolve it call_resolve(loaded_args) else raise GraphQL::UnauthorizedFieldError.new(context: context, object: object, type: field.owner, field: field) end end end end end end # @api private {GraphQL::Schema::Mutation} uses this to clear the dataloader cache def call_resolve(args_hash) if !args_hash.empty? public_send(self.class.resolve_method, **args_hash) else public_send(self.class.resolve_method) end end # Do the work. Everything happens here. # @return [Object] An object corresponding to the return type def resolve(**args) raise GraphQL::RequiredImplementationMissingError, "#{self.class.name}#resolve should execute the field's logic" end # Called before arguments are prepared. # Implement this hook to make checks before doing any work. # # If it returns a lazy object (like a promise), it will be synced by GraphQL # (but the resulting value won't be used). # # @param args [Hash] The input arguments, if there are any # @raise [GraphQL::ExecutionError] To add an error to the response # @raise [GraphQL::UnauthorizedError] To signal an authorization failure # @return [Boolean, early_return_data] If `false`, execution will stop (and `early_return_data` will be returned instead, if present.) def ready?(**args) true end # Called after arguments are loaded, but before resolving. # # Override it to check everything before calling the mutation. # @param inputs [Hash] The input arguments # @raise [GraphQL::ExecutionError] To add an error to the response # @raise [GraphQL::UnauthorizedError] To signal an authorization failure # @return [Boolean, early_return_data] If `false`, execution will stop (and `early_return_data` will be returned instead, if present.) def authorized?(**inputs) arg_owner = @field # || self.class args = context.types.arguments(arg_owner) authorize_arguments(args, inputs) end # Called when an object loaded by `loads:` fails the `.authorized?` check for its resolved GraphQL object type. # # By default, the error is re-raised and passed along to {{Schema.unauthorized_object}}. # # Any value returned here will be used _instead of_ of the loaded object. # @param err [GraphQL::UnauthorizedError] def unauthorized_object(err) raise err end private def authorize_arguments(args, inputs) args.each do |argument| arg_keyword = argument.keyword if inputs.key?(arg_keyword) && !(arg_value = inputs[arg_keyword]).nil? && (arg_value != argument.default_value) auth_result = argument.authorized?(self, arg_value, context) if auth_result.is_a?(Array) # only return this second value if the application returned a second value arg_auth, err = auth_result if !arg_auth return arg_auth, err end elsif auth_result == false return auth_result end end end true end def load_arguments(args) prepared_args = {} prepare_lazies = [] args.each do |key, value| arg_defn = @arguments_by_keyword[key] if arg_defn prepped_value = prepared_args[key] = arg_defn.load_and_authorize_value(self, value, context) if context.schema.lazy?(prepped_value) prepare_lazies << context.query.after_lazy(prepped_value) do |finished_prepped_value| prepared_args[key] = finished_prepped_value end end else # these are `extras:` prepared_args[key] = value end end # Avoid returning a lazy if none are needed if !prepare_lazies.empty? GraphQL::Execution::Lazy.all(prepare_lazies).then { prepared_args } else prepared_args end end def get_argument(name, context = GraphQL::Query::NullContext.instance) self.class.get_argument(name, context) end class << self def field_arguments(context = GraphQL::Query::NullContext.instance) arguments(context) end def any_field_arguments? any_arguments? end def get_field_argument(name, context = GraphQL::Query::NullContext.instance) get_argument(name, context) end def all_field_argument_definitions all_argument_definitions end # Default `:resolve` set below. # @return [Symbol] The method to call on instances of this object to resolve the field def resolve_method(new_method = nil) if new_method @resolve_method = new_method end @resolve_method || (superclass.respond_to?(:resolve_method) ? superclass.resolve_method : :resolve) end # Additional info injected into {#resolve} # @see {GraphQL::Schema::Field#extras} def extras(new_extras = nil) if new_extras @own_extras = new_extras end own_extras = @own_extras || [] own_extras + (superclass.respond_to?(:extras) ? superclass.extras : []) end # If `true` (default), then the return type for this resolver will be nullable. # If `false`, then the return type is non-null. # # @see #type which sets the return type of this field and accepts a `null:` option # @param allow_null [Boolean] Whether or not the response can be null def null(allow_null = nil) if !allow_null.nil? @null = allow_null end @null.nil? ? (superclass.respond_to?(:null) ? superclass.null : true) : @null end def resolver_method(new_method_name = nil) if new_method_name @resolver_method = new_method_name else @resolver_method || :resolve_with_support end end # Call this method to get the return type of the field, # or use it as a configuration method to assign a return type # instead of generating one. # TODO unify with {#null} # @param new_type [Class, Array, nil] If a type definition class is provided, it will be used as the return type of the field # @param null [true, false] Whether or not the field may return `nil` # @return [Class] The type which this field returns. def type(new_type = nil, null: nil) if new_type if null.nil? raise ArgumentError, "required argument `null:` is missing" end @type_expr = new_type @null = null else if type_expr GraphQL::Schema::Member::BuildType.parse_type(type_expr, null: self.null) elsif superclass.respond_to?(:type) superclass.type else nil end end end # Specifies the complexity of the field. Defaults to `1` # @return [Integer, Proc] def complexity(new_complexity = nil) if new_complexity @complexity = new_complexity end @complexity || (superclass.respond_to?(:complexity) ? superclass.complexity : 1) end def broadcastable(new_broadcastable) @broadcastable = new_broadcastable end # @return [Boolean, nil] def broadcastable? if defined?(@broadcastable) @broadcastable else (superclass.respond_to?(:broadcastable?) ? superclass.broadcastable? : nil) end end # Get or set the `max_page_size:` which will be configured for fields using this resolver # (`nil` means "unlimited max page size".) # @param max_page_size [Integer, nil] Set a new value # @return [Integer, nil] The `max_page_size` assigned to fields that use this resolver def max_page_size(new_max_page_size = NOT_CONFIGURED) if new_max_page_size != NOT_CONFIGURED @max_page_size = new_max_page_size elsif defined?(@max_page_size) @max_page_size elsif superclass.respond_to?(:max_page_size) superclass.max_page_size else nil end end # @return [Boolean] `true` if this resolver or a superclass has an assigned `max_page_size` def has_max_page_size? (!!defined?(@max_page_size)) || (superclass.respond_to?(:has_max_page_size?) && superclass.has_max_page_size?) end # Get or set the `default_page_size:` which will be configured for fields using this resolver # (`nil` means "unlimited default page size".) # @param default_page_size [Integer, nil] Set a new value # @return [Integer, nil] The `default_page_size` assigned to fields that use this resolver def default_page_size(new_default_page_size = NOT_CONFIGURED) if new_default_page_size != NOT_CONFIGURED @default_page_size = new_default_page_size elsif defined?(@default_page_size) @default_page_size elsif superclass.respond_to?(:default_page_size) superclass.default_page_size else nil end end # @return [Boolean] `true` if this resolver or a superclass has an assigned `default_page_size` def has_default_page_size? (!!defined?(@default_page_size)) || (superclass.respond_to?(:has_default_page_size?) && superclass.has_default_page_size?) end # A non-normalized type configuration, without `null` applied def type_expr @type_expr || (superclass.respond_to?(:type_expr) ? superclass.type_expr : nil) end # Add an argument to this field's signature, but # also add some preparation hook methods which will be used for this argument # @see {GraphQL::Schema::Argument#initialize} for the signature def argument(*args, **kwargs, &block) # Use `from_resolver: true` to short-circuit the InputObject's own `loads:` implementation # so that we can support `#load_{x}` methods below. super(*args, from_resolver: true, **kwargs) end # Registers new extension # @param extension [Class] Extension class # @param options [Hash] Optional extension options def extension(extension, **options) @own_extensions ||= [] @own_extensions << {extension => options} end # @api private def extensions own_exts = @own_extensions # Jump through some hoops to avoid creating arrays when we don't actually need them if superclass.respond_to?(:extensions) s_exts = superclass.extensions if own_exts if !s_exts.empty? own_exts + s_exts else own_exts end else s_exts end else own_exts || EMPTY_ARRAY end end def inherited(child_class) child_class.description(description) super end private attr_reader :own_extensions end end end end graphql-ruby-2.5.19/lib/graphql/schema/resolver/000077500000000000000000000000001514115062600215335ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/schema/resolver/has_payload_type.rb000066400000000000000000000073661514115062600254210ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Resolver # Adds `field(...)` helper to resolvers so that they can # generate payload types. # # Or, an already-defined one can be attached with `payload_type(...)`. module HasPayloadType # Call this method to get the derived return type of the mutation, # or use it as a configuration method to assign a return type # instead of generating one. # @param new_payload_type [Class, nil] If a type definition class is provided, it will be used as the return type of the mutation field # @return [Class] The object type which this mutation returns. def payload_type(new_payload_type = nil) if new_payload_type @payload_type = new_payload_type end @payload_type ||= generate_payload_type end def type(new_type = nil, null: nil) if new_type payload_type(new_type) if !null.nil? self.null(null) end else super() end end alias :type_expr :payload_type def field_class(new_class = nil) if new_class @field_class = new_class elsif defined?(@field_class) && @field_class @field_class else find_inherited_value(:field_class, GraphQL::Schema::Field) end end # An object class to use for deriving return types # @param new_class [Class, nil] Defaults to {GraphQL::Schema::Object} # @return [Class] def object_class(new_class = nil) if new_class if defined?(@payload_type) raise "Can't configure `object_class(...)` after the payload type has already been initialized. Move this configuration higher up the class definition." end @object_class = new_class else @object_class || find_inherited_value(:object_class, GraphQL::Schema::Object) end end NO_INTERFACES = [].freeze def field(*args, **kwargs, &block) pt = payload_type # make sure it's initialized with any inherited fields field_defn = super # Remove any inherited fields to avoid false conflicts at runtime prev_fields = pt.own_fields[field_defn.graphql_name] case prev_fields when GraphQL::Schema::Field if prev_fields.owner != self pt.own_fields.delete(field_defn.graphql_name) end when Array prev_fields.reject! { |f| f.owner != self } if prev_fields.empty? pt.own_fields.delete(field_defn.graphql_name) end end pt.add_field(field_defn, method_conflict_warning: false) field_defn end private # Build a subclass of {.object_class} based on `self`. # This value will be cached as `{.payload_type}`. # Override this hook to customize return type generation. def generate_payload_type resolver_name = graphql_name resolver_fields = all_field_definitions pt = Class.new(object_class) pt.graphql_name("#{resolver_name}Payload") pt.description("Autogenerated return type of #{resolver_name}.") resolver_fields.each do |f| # Reattach the already-defined field here # (The field's `.owner` will still point to the mutation, not the object type, I think) # Don't re-warn about a method conflict. Since this type is generated, it should be fixed in the resolver instead. pt.add_field(f, method_conflict_warning: false) end pt end end end end end graphql-ruby-2.5.19/lib/graphql/schema/scalar.rb000066400000000000000000000034741514115062600214740ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Scalar < GraphQL::Schema::Member extend GraphQL::Schema::Member::ValidatesInput class << self def coerce_input(val, ctx) val end def coerce_result(val, ctx) val end def kind GraphQL::TypeKinds::SCALAR end def specified_by_url(new_url = nil) if new_url directive(GraphQL::Schema::Directive::SpecifiedBy, url: new_url) elsif (directive = directives.find { |dir| dir.graphql_name == "specifiedBy" }) directive.arguments[:url] # rubocop:disable Development/ContextIsPassedCop elsif superclass.respond_to?(:specified_by_url) superclass.specified_by_url else nil end end def default_scalar(is_default = nil) if !is_default.nil? @default_scalar = is_default end @default_scalar end def default_scalar? @default_scalar ||= false end def validate_non_null_input(value, ctx, max_errors: nil) coerced_result = begin coerce_input(value, ctx) rescue GraphQL::CoercionError => err err rescue StandardError => err ctx.query.handle_or_reraise(err) end if coerced_result.nil? Query::InputValidationResult.from_problem("Could not coerce value #{GraphQL::Language.serialize(value)} to #{graphql_name}") elsif coerced_result.is_a?(GraphQL::CoercionError) Query::InputValidationResult.from_problem(coerced_result.message, message: coerced_result.message, extensions: coerced_result.extensions) else nil end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/subscription.rb000066400000000000000000000173701514115062600227530ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema # This class can be extended to create fields on your subscription root. # # It provides hooks for the different parts of the subscription lifecycle: # # - `#authorized?`: called before initial subscription and subsequent updates # - `#subscribe`: called for the initial subscription # - `#update`: called for subsequent update # # Also, `#unsubscribe` terminates the subscription. class Subscription < GraphQL::Schema::Resolver extend GraphQL::Schema::Resolver::HasPayloadType extend GraphQL::Schema::Member::HasFields NO_UPDATE = :no_update # The generated payload type is required; If there's no payload, # propagate null. null false # @api private def initialize(object:, context:, field:) super # Figure out whether this is an update or an initial subscription @mode = context.query.subscription_update? ? :update : :subscribe @subscription_written = false @original_arguments = nil if (subs_ns = context.namespace(:subscriptions)) && (sub_insts = subs_ns[:subscriptions]) sub_insts[context.current_path] = self end end # @api private def resolve_with_support(**args) @original_arguments = args # before `loads:` have been run result = nil unsubscribed = true unsubscribed_result = catch :graphql_subscription_unsubscribed do result = super unsubscribed = false end if unsubscribed if unsubscribed_result context.namespace(:subscriptions)[:final_update] = true unsubscribed_result else context.skip end else result end end # Implement the {Resolve} API. # You can implement this if you want code to run for _both_ the initial subscription # and for later updates. Or, implement {#subscribe} and {#update} def resolve(**args) # Dispatch based on `@mode`, which will raise a `NoMethodError` if we ever # have an unexpected `@mode` public_send("resolve_#{@mode}", **args) end # Wrap the user-defined `#subscribe` hook # @api private def resolve_subscribe(**args) ret_val = !args.empty? ? subscribe(**args) : subscribe if ret_val == :no_response context.skip else ret_val end end # The default implementation returns nothing on subscribe. # Override it to return an object or # `:no_response` to (explicitly) return nothing. def subscribe(args = {}) :no_response end # Wrap the user-provided `#update` hook # @api private def resolve_update(**args) ret_val = !args.empty? ? update(**args) : update if ret_val == NO_UPDATE context.namespace(:subscriptions)[:no_update] = true context.skip else ret_val end end # The default implementation returns the root object. # Override it to return {NO_UPDATE} if you want to # skip updates sometimes. Or override it to return a different object. def update(args = {}) object end # If an argument is flagged with `loads:` and no object is found for it, # remove this subscription (assuming that the object was deleted in the meantime, # or that it became inaccessible). def load_application_object_failed(err) if @mode == :update unsubscribe end super end # Call this to halt execution and remove this subscription from the system # @param update_value [Object] if present, deliver this update before unsubscribing # @return [void] def unsubscribe(update_value = nil) context.namespace(:subscriptions)[:unsubscribed] = true throw :graphql_subscription_unsubscribed, update_value end # Call this method to provide a new subscription_scope; OR # call it without an argument to get the subscription_scope # @param new_scope [Symbol] # @param optional [Boolean] If true, then don't require `scope:` to be provided to updates to this subscription. # @return [Symbol] def self.subscription_scope(new_scope = NOT_CONFIGURED, optional: false) if new_scope != NOT_CONFIGURED @subscription_scope = new_scope @subscription_scope_optional = optional elsif defined?(@subscription_scope) @subscription_scope else find_inherited_value(:subscription_scope) end end def self.subscription_scope_optional? if defined?(@subscription_scope_optional) @subscription_scope_optional else find_inherited_value(:subscription_scope_optional, false) end end # This is called during initial subscription to get a "name" for this subscription. # Later, when `.trigger` is called, this will be called again to build another "name". # Any subscribers with matching topic will begin the update flow. # # The default implementation creates a string using the field name, subscription scope, and argument keys and values. # In that implementation, only `.trigger` calls with _exact matches_ result in updates to subscribers. # # To implement a filtered stream-type subscription flow, override this method to return a string with field name and subscription scope. # Then, implement {#update} to compare its arguments to the current `object` and return {NO_UPDATE} when an # update should be filtered out. # # @see {#update} for how to skip updates when an event comes with a matching topic. # @param arguments [Hash Object>] The arguments for this topic, in GraphQL-style (camelized strings) # @param field [GraphQL::Schema::Field] # @param scope [Object, nil] A value corresponding to `.trigger(... scope:)` (for updates) or the `subscription_scope` found in `context` (for initial subscriptions). # @return [String] An identifier corresponding to a stream of updates def self.topic_for(arguments:, field:, scope:) Subscriptions::Serialize.dump_recursive([scope, field.graphql_name, arguments]) end # Calls through to `schema.subscriptions` to register this subscription with the backend. # This is automatically called by GraphQL-Ruby after a query finishes successfully, # but if you need to commit the subscription during `#subscribe`, you can call it there. # (This method also sets a flag showing that this subscription was already written.) # # If you call this method yourself, you may also need to {#unsubscribe} # or call `subscriptions.delete_subscription` to clean up the database if the query crashes with an error # later in execution. # @return [void] def write_subscription if subscription_written? raise GraphQL::Error, "`write_subscription` was called but `#{self.class}#subscription_written?` is already true. Remove a call to `write subscription`." else @subscription_written = true context.schema.subscriptions.write_subscription(context.query, [event]) end nil end # @return [Boolean] `true` if {#write_subscription} was called already def subscription_written? @subscription_written end # @return [Subscriptions::Event] This object is used as a representation of this subscription for the backend def event @event ||= Subscriptions::Event.new( name: field.name, arguments: @original_arguments, context: context, field: field, ) end end end end graphql-ruby-2.5.19/lib/graphql/schema/timeout.rb000066400000000000000000000120321514115062600217030ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema # This plugin will stop resolving new fields after `max_seconds` have elapsed. # After the time has passed, any remaining fields will be `nil`, with errors added # to the `errors` key. Any already-resolved fields will be in the `data` key, so # you'll get a partial response. # # You can subclass `GraphQL::Schema::Timeout` and override `max_seconds` and/or `handle_timeout` # to provide custom logic when a timeout error occurs. # # Note that this will stop a query _in between_ field resolutions, but # it doesn't interrupt long-running `resolve` functions. Be sure to use # timeout options for external connections. For more info, see # www.mikeperham.com/2015/05/08/timeout-rubys-most-dangerous-api/ # # @example Stop resolving fields after 2 seconds # class MySchema < GraphQL::Schema # use GraphQL::Schema::Timeout, max_seconds: 2 # end # # @example Notifying Bugsnag and logging a timeout # class MyTimeout < GraphQL::Schema::Timeout # def handle_timeout(error, query) # Rails.logger.warn("GraphQL Timeout: #{error.message}: #{query.query_string}") # Bugsnag.notify(error, {query_string: query.query_string}) # end # end # # class MySchema < GraphQL::Schema # use MyTimeout, max_seconds: 2 # end # class Timeout def self.use(schema, max_seconds: nil) timeout = self.new(max_seconds: max_seconds) schema.trace_with(self::Trace, timeout: timeout) end def initialize(max_seconds:) @max_seconds = max_seconds end module Trace # @param max_seconds [Numeric] how many seconds the query should be allowed to resolve new fields def initialize(timeout:, **rest) @timeout = timeout super end def execute_multiplex(multiplex:) multiplex.queries.each do |query| timeout_duration_s = @timeout.max_seconds(query) timeout_state = if timeout_duration_s == false # if the method returns `false`, don't apply a timeout false else now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) timeout_at = now + (timeout_duration_s * 1000) { timeout_at: timeout_at, timed_out: false } end query.context.namespace(@timeout)[:state] = timeout_state end super end def execute_field(query:, field:, **_rest) timeout_state = query.context.namespace(@timeout).fetch(:state) # If the `:state` is `false`, then `max_seconds(query)` opted out of timeout for this query. if timeout_state == false super elsif Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) > timeout_state.fetch(:timeout_at) error = GraphQL::Schema::Timeout::TimeoutError.new(field) # Only invoke the timeout callback for the first timeout if !timeout_state[:timed_out] timeout_state[:timed_out] = true @timeout.handle_timeout(error, query) timeout_state = query.context.namespace(@timeout).fetch(:state) end # `handle_timeout` may have set this to be `false` if timeout_state != false error else super end else super end end end # Called at the start of each query. # The default implementation returns the `max_seconds:` value from installing this plugin. # # @param query [GraphQL::Query] The query that's about to run # @return [Numeric, false] The number of seconds after which to interrupt query execution and call {#handle_error}, or `false` to bypass the timeout. def max_seconds(query) @max_seconds end # Invoked when a query times out. # @param error [GraphQL::Schema::Timeout::TimeoutError] # @param query [GraphQL::Error] def handle_timeout(error, query) # override to do something interesting end # Call this method (eg, from {#handle_timeout}) to disable timeout tracking # for the given query. # @param query [GraphQL::Query] # @return [void] def disable_timeout(query) query.context.namespace(self)[:state] = false nil end # This error is raised when a query exceeds `max_seconds`. # Since it's a child of {GraphQL::ExecutionError}, # its message will be added to the response's `errors` key. # # To raise an error that will stop query resolution, use a custom block # to take this error and raise a new one which _doesn't_ descend from {GraphQL::ExecutionError}, # such as `RuntimeError`. class TimeoutError < GraphQL::ExecutionError def initialize(field) super("Timeout on #{field.path}") end end end end end graphql-ruby-2.5.19/lib/graphql/schema/type_expression.rb000066400000000000000000000031111514115062600234530ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema # @api private module TypeExpression # Fetch a type from a type map by its AST specification. # Return `nil` if not found. # @param type_owner [#type] A thing for looking up types by name # @param ast_node [GraphQL::Language::Nodes::AbstractNode] # @return [Class, GraphQL::Schema::NonNull, GraphQL::Schema:List] def self.build_type(type_owner, ast_node) case ast_node when GraphQL::Language::Nodes::TypeName type_owner.type(ast_node.name) # rubocop:disable Development/ContextIsPassedCop -- this is a `context` or `warden`, it's already query-aware when GraphQL::Language::Nodes::NonNullType ast_inner_type = ast_node.of_type inner_type = build_type(type_owner, ast_inner_type) wrap_type(inner_type, :to_non_null_type) when GraphQL::Language::Nodes::ListType ast_inner_type = ast_node.of_type inner_type = build_type(type_owner, ast_inner_type) wrap_type(inner_type, :to_list_type) else raise "Invariant: unexpected type from ast: #{ast_node.inspect}" end end class << self private def wrap_type(type, wrapper_method) if type.nil? nil elsif wrapper_method == :to_list_type || wrapper_method == :to_non_null_type type.public_send(wrapper_method) else raise ArgumentError, "Unexpected wrapper method: #{wrapper_method.inspect}" end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/type_membership.rb000066400000000000000000000033731514115062600234210ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema # This class joins an object type to an abstract type (interface or union) of which # it is a member. class TypeMembership # @return [Class] attr_accessor :object_type # @return [Class, Module] attr_reader :abstract_type # @return [Hash] attr_reader :options # Called when an object is hooked up to an abstract type, such as {Schema::Union.possible_types} # or {Schema::Object.implements} (for interfaces). # # @param abstract_type [Class, Module] # @param object_type [Class] # @param options [Hash] Any options passed to `.possible_types` or `.implements` def initialize(abstract_type, object_type, **options) @abstract_type = abstract_type @object_type = object_type @options = options end # @return [Boolean] if false, {#object_type} will be treated as _not_ a member of {#abstract_type} def visible?(ctx) warden = Warden.from_context(ctx) (@object_type.respond_to?(:visible?) ? warden.visible_type?(@object_type, ctx) : true) && (@abstract_type.respond_to?(:visible?) ? warden.visible_type?(@abstract_type, ctx) : true) end def graphql_name "#{@object_type.graphql_name}.#{@abstract_type.kind.interface? ? "implements" : "belongsTo" }.#{@abstract_type.graphql_name}" end def path graphql_name end def inspect "#<#{self.class} #{@object_type.inspect} => #{@abstract_type.inspect}>" end alias :type_class :itself end end end graphql-ruby-2.5.19/lib/graphql/schema/union.rb000066400000000000000000000063551514115062600213600ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Union < GraphQL::Schema::Member extend GraphQL::Schema::Member::HasUnresolvedTypeError class << self def inherited(child_class) add_unresolved_type_error(child_class) super end def possible_types(*types, context: GraphQL::Query::NullContext.instance, **options) if !types.empty? types.each do |t| assert_valid_union_member(t) type_memberships << type_membership_class.new(self, t, **options) end else visible_types = [] warden = Warden.from_context(context) type_memberships.each do |type_membership| if warden.visible_type_membership?(type_membership, context) visible_types << type_membership.object_type end end visible_types end end def all_possible_types type_memberships.map(&:object_type) end def type_membership_class(membership_class = nil) if membership_class @type_membership_class = membership_class else @type_membership_class || find_inherited_value(:type_membership_class, GraphQL::Schema::TypeMembership) end end def kind GraphQL::TypeKinds::UNION end def type_memberships @type_memberships ||= [] end # Update a type membership whose `.object_type` is a string or late-bound type # so that the type membership's `.object_type` is the given `object_type`. # (This is used for updating the union after the schema as lazily loaded the union member.) # @api private def assign_type_membership_object_type(object_type) assert_valid_union_member(object_type) type_memberships.each { |tm| possible_type = tm.object_type if possible_type.is_a?(String) && (possible_type == object_type.name) # This is a match of Ruby class names, not graphql names, # since strings are used to refer to constants. tm.object_type = object_type elsif possible_type.is_a?(LateBoundType) && possible_type.graphql_name == object_type.graphql_name tm.object_type = object_type end } nil end private def assert_valid_union_member(type_defn) case type_defn when Class if !type_defn.kind.object? raise ArgumentError, "Union possible_types can only be object types (not #{type_defn.kind.name}, #{type_defn.inspect})" end when Module # it's an interface type, defined as a module raise ArgumentError, "Union possible_types can only be object types (not interface types), remove #{type_defn.graphql_name} (#{type_defn.inspect})" when String, GraphQL::Schema::LateBoundType # Ok - assume it will get checked later else raise ArgumentError, "Union possible_types can only be class-based GraphQL types (not #{type_defn.inspect} (#{type_defn.class.name}))." end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/unique_within_type.rb000066400000000000000000000021211514115062600241440ustar00rootroot00000000000000# frozen_string_literal: true require "base64" module GraphQL class Schema module UniqueWithinType class << self attr_accessor :default_id_separator end self.default_id_separator = "-" module_function # @param type_name [String] # @param object_value [Any] # @return [String] a unique, opaque ID generated as a function of the two inputs def encode(type_name, object_value, separator: self.default_id_separator) object_value_str = object_value.to_s if type_name.include?(separator) raise "encode(#{type_name}, #{object_value_str}) contains reserved characters `#{separator}` in the type name" end Base64.strict_encode64([type_name, object_value_str].join(separator)) end # @param node_id [String] A unique ID generated by {.encode} # @return [Array<(String, String)>] The type name & value passed to {.encode} def decode(node_id, separator: self.default_id_separator) GraphQL::Schema::Base64Encoder.decode(node_id).split(separator, 2) end end end end graphql-ruby-2.5.19/lib/graphql/schema/validator.rb000066400000000000000000000161111514115062600222040ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Validator # The thing being validated # @return [GraphQL::Schema::Argument, GraphQL::Schema::Field, GraphQL::Schema::Resolver, Class] attr_reader :validated # @param validated [GraphQL::Schema::Argument, GraphQL::Schema::Field, GraphQL::Schema::Resolver, Class] The argument or argument owner this validator is attached to # @param allow_blank [Boolean] if `true`, then objects that respond to `.blank?` and return true for `.blank?` will skip this validation # @param allow_null [Boolean] if `true`, then incoming `null`s will skip this validation def initialize(validated:, allow_blank: false, allow_null: false) @validated = validated @allow_blank = allow_blank @allow_null = allow_null end # @param object [Object] The application object that this argument's field is being resolved for # @param context [GraphQL::Query::Context] # @param value [Object] The client-provided value for this argument (after parsing and coercing by the input type) # @return [nil, Array, String] Error message or messages to add def validate(object, context, value) raise GraphQL::RequiredImplementationMissingError, "Validator classes should implement #validate" end # This is like `String#%`, but it supports the case that only some of `string`'s # values are present in `substitutions` def partial_format(string, substitutions) substitutions.each do |key, value| sub_v = value.is_a?(String) ? value : value.to_s string = string.gsub("%{#{key}}", sub_v) end string end # @return [Boolean] `true` if `value` is `nil` and this validator has `allow_null: true` or if value is `.blank?` and this validator has `allow_blank: true` def permitted_empty_value?(value) (value.nil? && @allow_null) || (@allow_blank && value.respond_to?(:blank?) && value.blank?) end # @param schema_member [GraphQL::Schema::Field, GraphQL::Schema::Argument, Class] # @param validates_hash [Hash{Symbol => Hash}, Hash{Class => Hash} nil] A configuration passed as `validates:` # @return [Array] def self.from_config(schema_member, validates_hash) if validates_hash.nil? || validates_hash.empty? EMPTY_ARRAY else validates_hash = validates_hash.dup default_options = {} if validates_hash[:allow_null] default_options[:allow_null] = validates_hash.delete(:allow_null) end if validates_hash[:allow_blank] default_options[:allow_blank] = validates_hash.delete(:allow_blank) end # allow_nil or allow_blank are the _only_ validations: if validates_hash.empty? validates_hash = default_options end validates_hash.map do |validator_name, options| validator_class = case validator_name when Class validator_name else all_validators[validator_name] || raise(ArgumentError, "unknown validation: #{validator_name.inspect}") end if options.is_a?(Hash) validator_class.new(validated: schema_member, **(default_options.merge(options))) else validator_class.new(options, validated: schema_member, **default_options) end end end end # Add `validator_class` to be initialized when `validates:` is given `name`. # (It's initialized with whatever options are given by the key `name`). # @param name [Symbol] # @param validator_class [Class] # @return [void] def self.install(name, validator_class) all_validators[name] = validator_class nil end # Remove whatever validator class is {.install}ed at `name`, if there is one # @param name [Symbol] # @return [void] def self.uninstall(name) all_validators.delete(name) nil end class << self attr_accessor :all_validators end self.all_validators = {} include GraphQL::EmptyObjects class ValidationFailedError < GraphQL::ExecutionError attr_reader :errors def initialize(errors:) @errors = errors super(errors.join(", ")) end end # @param validators [Array] # @param object [Object] # @param context [Query::Context] # @param value [Object] # @return [void] # @raises [ValidationFailedError] def self.validate!(validators, object, context, value, as: nil) # Assuming the default case is no errors, reduce allocations in that case. # This will be replaced with a mutable array if we actually get any errors. all_errors = EMPTY_ARRAY validators.each do |validator| validated = as || validator.validated errors = validator.validate(object, context, value) if errors && (errors.is_a?(Array) && errors != EMPTY_ARRAY) || (errors.is_a?(String)) if all_errors.frozen? # It's empty all_errors = [] end interpolation_vars = { validated: validated.graphql_name, value: value.inspect } if errors.is_a?(String) all_errors << (errors % interpolation_vars) else errors = errors.map { |e| e % interpolation_vars } all_errors.concat(errors) end end end if !all_errors.empty? raise ValidationFailedError.new(errors: all_errors) end nil end end end end require "graphql/schema/validator/length_validator" GraphQL::Schema::Validator.install(:length, GraphQL::Schema::Validator::LengthValidator) require "graphql/schema/validator/numericality_validator" GraphQL::Schema::Validator.install(:numericality, GraphQL::Schema::Validator::NumericalityValidator) require "graphql/schema/validator/format_validator" GraphQL::Schema::Validator.install(:format, GraphQL::Schema::Validator::FormatValidator) require "graphql/schema/validator/inclusion_validator" GraphQL::Schema::Validator.install(:inclusion, GraphQL::Schema::Validator::InclusionValidator) require "graphql/schema/validator/exclusion_validator" GraphQL::Schema::Validator.install(:exclusion, GraphQL::Schema::Validator::ExclusionValidator) require "graphql/schema/validator/required_validator" GraphQL::Schema::Validator.install(:required, GraphQL::Schema::Validator::RequiredValidator) require "graphql/schema/validator/allow_null_validator" GraphQL::Schema::Validator.install(:allow_null, GraphQL::Schema::Validator::AllowNullValidator) require "graphql/schema/validator/allow_blank_validator" GraphQL::Schema::Validator.install(:allow_blank, GraphQL::Schema::Validator::AllowBlankValidator) require "graphql/schema/validator/all_validator" GraphQL::Schema::Validator.install(:all, GraphQL::Schema::Validator::AllValidator) graphql-ruby-2.5.19/lib/graphql/schema/validator/000077500000000000000000000000001514115062600216575ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/schema/validator/all_validator.rb000066400000000000000000000035251514115062600250260ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Validator # Use this to validate each member of an array value. # # @example validate format of all strings in an array # # argument :handles, [String], # validates: { all: { format: { with: /\A[a-z0-9_]+\Z/ } } } # # @example multiple validators can be combined # # argument :handles, [String], # validates: { all: { format: { with: /\A[a-z0-9_]+\Z/ }, length: { maximum: 32 } } } # # @example any type can be used # # argument :choices, [Integer], # validates: { all: { inclusion: { in: 1..12 } } } # class AllValidator < Validator def initialize(validated:, allow_blank: false, allow_null: false, **validators) super(validated: validated, allow_blank: allow_blank, allow_null: allow_null) @validators = Validator.from_config(validated, validators) end def validate(object, context, value) return EMPTY_ARRAY if permitted_empty_value?(value) all_errors = EMPTY_ARRAY value.each do |subvalue| @validators.each do |validator| errors = validator.validate(object, context, subvalue) if errors && (errors.is_a?(Array) && errors != EMPTY_ARRAY) || (errors.is_a?(String)) if all_errors.frozen? # It's empty all_errors = [] end if errors.is_a?(String) all_errors << errors else all_errors.concat(errors) end end end end unless all_errors.frozen? all_errors.uniq! end all_errors end end end end end graphql-ruby-2.5.19/lib/graphql/schema/validator/allow_blank_validator.rb000066400000000000000000000017001514115062600265340ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Validator # Use this to specifically reject values that respond to `.blank?` and respond truthy for that method. # # @example Require a non-empty string for an argument # argument :name, String, required: true, validate: { allow_blank: false } class AllowBlankValidator < Validator def initialize(allow_blank_positional, allow_blank: nil, message: "%{validated} can't be blank", **default_options) @message = message super(**default_options) @allow_blank = allow_blank.nil? ? allow_blank_positional : allow_blank end def validate(_object, _context, value) if value.respond_to?(:blank?) && value.blank? if (value.nil? && @allow_null) || @allow_blank # pass else @message end end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/validator/allow_null_validator.rb000066400000000000000000000015221514115062600264210ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Validator # Use this to specifically reject or permit `nil` values (given as `null` from GraphQL). # # @example require a non-null value for an argument if it is provided # argument :name, String, required: false, validates: { allow_null: false } class AllowNullValidator < Validator MESSAGE = "%{validated} can't be null" def initialize(allow_null_positional, allow_null: nil, message: MESSAGE, **default_options) @message = message super(**default_options) @allow_null = allow_null.nil? ? allow_null_positional : allow_null end def validate(_object, _context, value) if value.nil? && !@allow_null @message end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/validator/exclusion_validator.rb000066400000000000000000000017021514115062600262620ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Validator # Use this to specifically reject values from an argument. # # @example disallow certain values # # argument :favorite_non_prime, Integer, required: true, # validates: { exclusion: { in: [2, 3, 5, 7, ... ]} } # class ExclusionValidator < Validator # @param message [String] # @param in [Array] The values to reject def initialize(message: "%{validated} is reserved", in:, **default_options) # `in` is a reserved word, so work around that @in_list = binding.local_variable_get(:in) @message = message super(**default_options) end def validate(_object, _context, value) if permitted_empty_value?(value) # pass elsif @in_list.include?(value) @message end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/validator/format_validator.rb000066400000000000000000000026601514115062600255450ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Validator # Use this to assert that string values match (or don't match) the given RegExp. # # @example requiring input to match a pattern # # argument :handle, String, required: true, # validates: { format: { with: /\A[a-z0-9_]+\Z/ } } # # @example reject inputs that match a pattern # # argument :word_that_doesnt_begin_with_a_vowel, String, required: true, # validates: { format: { without: /\A[aeiou]/ } } # # # It's pretty hard to come up with a legitimate use case for `without:` # class FormatValidator < Validator # @param with [RegExp, nil] # @param without [Regexp, nil] # @param message [String] def initialize( with: nil, without: nil, message: "%{validated} is invalid", **default_options ) @with_pattern = with @without_pattern = without @message = message super(**default_options) end def validate(_object, _context, value) if permitted_empty_value?(value) # Do nothing elsif value.nil? || (@with_pattern && !value.match?(@with_pattern)) || (@without_pattern && value.match?(@without_pattern)) @message end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/validator/inclusion_validator.rb000066400000000000000000000021141514115062600262520ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Validator # You can use this to allow certain values for an argument. # # Usually, a {GraphQL::Schema::Enum} is better for this, because it's self-documenting. # # @example only allow certain values for an argument # # argument :favorite_prime, Integer, required: true, # validates: { inclusion: { in: [2, 3, 5, 7, 11, ... ] } } # class InclusionValidator < Validator # @param message [String] # @param in [Array] The values to allow def initialize(in:, message: "%{validated} is not included in the list", **default_options) # `in` is a reserved word, so work around that @in_list = binding.local_variable_get(:in) @message = message super(**default_options) end def validate(_object, _context, value) if permitted_empty_value?(value) # pass elsif !@in_list.include?(value) @message end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/validator/length_validator.rb000066400000000000000000000047451514115062600255440ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Validator # Use this to enforce a `.length` restriction on incoming values. It works for both Strings and Lists. # # @example Allow no more than 10 IDs # # argument :ids, [ID], required: true, validates: { length: { maximum: 10 } } # # @example Require three selections # # argument :ice_cream_preferences, [ICE_CREAM_FLAVOR], required: true, validates: { length: { is: 3 } } # class LengthValidator < Validator # @param maximum [Integer] # @param too_long [String] Used when `maximum` is exceeded or value is greater than `within` # @param minimum [Integer] # @param too_short [String] Used with value is less than `minimum` or less than `within` # @param is [Integer] Exact length requirement # @param wrong_length [String] Used when value doesn't match `is` # @param within [Range] An allowed range (becomes `minimum:` and `maximum:` under the hood) # @param message [String] def initialize( maximum: nil, too_long: "%{validated} is too long (maximum is %{count})", minimum: nil, too_short: "%{validated} is too short (minimum is %{count})", is: nil, within: nil, wrong_length: "%{validated} is the wrong length (should be %{count})", message: nil, **default_options ) if within && (minimum || maximum) raise ArgumentError, "`length: { ... }` may include `within:` _or_ `minimum:`/`maximum:`, but not both" end # Under the hood, `within` is decomposed into `minimum` and `maximum` @maximum = maximum || (within && within.max) @too_long = message || too_long @minimum = minimum || (within && within.min) @too_short = message || too_short @is = is @wrong_length = message || wrong_length super(**default_options) end def validate(_object, _context, value) return if permitted_empty_value?(value) # pass in this case length = value.nil? ? 0 : value.length if @maximum && length > @maximum partial_format(@too_long, { count: @maximum }) elsif @minimum && length < @minimum partial_format(@too_short, { count: @minimum }) elsif @is && length != @is partial_format(@wrong_length, { count: @is }) end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/validator/numericality_validator.rb000066400000000000000000000067511514115062600267670ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Validator # Use this to assert numerical comparisons hold true for inputs. # # @example Require a number between 0 and 1 # # argument :batting_average, Float, required: true, validates: { numericality: { within: 0..1 } } # # @example Require the number 42 # # argument :the_answer, Integer, required: true, validates: { numericality: { equal_to: 42 } } # # @example Require a real number # # argument :items_count, Integer, required: true, validates: { numericality: { greater_than_or_equal_to: 0 } } # class NumericalityValidator < Validator # @param greater_than [Integer] # @param greater_than_or_equal_to [Integer] # @param less_than [Integer] # @param less_than_or_equal_to [Integer] # @param equal_to [Integer] # @param other_than [Integer] # @param odd [Boolean] # @param even [Boolean] # @param within [Range] # @param message [String] used for all validation failures def initialize( greater_than: nil, greater_than_or_equal_to: nil, less_than: nil, less_than_or_equal_to: nil, equal_to: nil, other_than: nil, odd: nil, even: nil, within: nil, message: "%{validated} must be %{comparison} %{target}", null_message: Validator::AllowNullValidator::MESSAGE, **default_options ) @greater_than = greater_than @greater_than_or_equal_to = greater_than_or_equal_to @less_than = less_than @less_than_or_equal_to = less_than_or_equal_to @equal_to = equal_to @other_than = other_than @odd = odd @even = even @within = within @message = message @null_message = null_message super(**default_options) end def validate(object, context, value) if permitted_empty_value?(value) # pass in this case elsif value.nil? # @allow_null is handled in the parent class @null_message elsif @greater_than && value <= @greater_than partial_format(@message, { comparison: "greater than", target: @greater_than }) elsif @greater_than_or_equal_to && value < @greater_than_or_equal_to partial_format(@message, { comparison: "greater than or equal to", target: @greater_than_or_equal_to }) elsif @less_than && value >= @less_than partial_format(@message, { comparison: "less than", target: @less_than }) elsif @less_than_or_equal_to && value > @less_than_or_equal_to partial_format(@message, { comparison: "less than or equal to", target: @less_than_or_equal_to }) elsif @equal_to && value != @equal_to partial_format(@message, { comparison: "equal to", target: @equal_to }) elsif @other_than && value == @other_than partial_format(@message, { comparison: "something other than", target: @other_than }) elsif @even && !value.even? (partial_format(@message, { comparison: "even", target: "" })).strip elsif @odd && !value.odd? (partial_format(@message, { comparison: "odd", target: "" })).strip elsif @within && !@within.include?(value) partial_format(@message, { comparison: "within", target: @within }) end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/validator/required_validator.rb000066400000000000000000000145521514115062600261000ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Validator # Use this validator to require _one_ of the named arguments to be present. # Or, use Arrays of symbols to name a valid _set_ of arguments. # # (This is for specifying mutually exclusive sets of arguments.) # # If you use {GraphQL::Schema::Visibility} to hide all the arguments in a `one_of: [..]` set, # then a developer-facing {GraphQL::Error} will be raised during execution. Pass `allow_all_hidden: true` to # skip validation in this case instead. # # This validator also implements `argument ... required: :nullable`. If an argument has `required: :nullable` # but it's hidden with {GraphQL::Schema::Visibility}, then this validator doesn't run. # # @example Require exactly one of these arguments # # field :update_amount, IngredientAmount, null: false do # argument :ingredient_id, ID, required: true # argument :cups, Integer, required: false # argument :tablespoons, Integer, required: false # argument :teaspoons, Integer, required: false # validates required: { one_of: [:cups, :tablespoons, :teaspoons] } # end # # @example Require one of these _sets_ of arguments # # field :find_object, Node, null: true do # argument :node_id, ID, required: false # argument :object_type, String, required: false # argument :object_id, Integer, required: false # # either a global `node_id` or an `object_type`/`object_id` pair is required: # validates required: { one_of: [:node_id, [:object_type, :object_id]] } # end # # @example require _some_ value for an argument, even if it's null # field :update_settings, AccountSettings do # # `required: :nullable` means this argument must be given, but may be `null` # argument :age, Integer, required: :nullable # end # class RequiredValidator < Validator # @param one_of [Array] A list of arguments, exactly one of which is required for this field # @param argument [Symbol] An argument that is required for this field # @param allow_all_hidden [Boolean] If `true`, then this validator won't run if all the `one_of: ...` arguments have been hidden # @param message [String] def initialize(one_of: nil, argument: nil, allow_all_hidden: nil, message: nil, **default_options) @one_of = if one_of one_of elsif argument [ argument ] else raise ArgumentError, "`one_of:` or `argument:` must be given in `validates required: {...}`" end @allow_all_hidden = allow_all_hidden.nil? ? !!argument : allow_all_hidden @message = message super(**default_options) end def validate(_object, context, value) fully_matched_conditions = 0 partially_matched_conditions = 0 visible_keywords = context.types.arguments(@validated).map(&:keyword) no_visible_conditions = true if !value.nil? @one_of.each do |one_of_condition| case one_of_condition when Symbol if no_visible_conditions && visible_keywords.include?(one_of_condition) no_visible_conditions = false end if value.key?(one_of_condition) fully_matched_conditions += 1 end when Array any_match = false full_match = true one_of_condition.each do |k| if no_visible_conditions && visible_keywords.include?(k) no_visible_conditions = false end if value.key?(k) any_match = true else full_match = false end end partial_match = !full_match && any_match if full_match fully_matched_conditions += 1 end if partial_match partially_matched_conditions += 1 end else raise ArgumentError, "Unknown one_of condition: #{one_of_condition.inspect}" end end end if no_visible_conditions if @allow_all_hidden return nil else raise GraphQL::Error, <<~ERR #{@validated.path} validates `required: ...` but all required arguments were hidden. Update your schema definition to allow the client to see some fields or skip validation by adding `required: { ..., allow_all_hidden: true }` ERR end end if fully_matched_conditions == 1 && partially_matched_conditions == 0 nil # OK else @message || build_message(context) end end def build_message(context) argument_definitions = context.types.arguments(@validated) required_names = @one_of.map do |arg_keyword| if arg_keyword.is_a?(Array) names = arg_keyword.map { |arg| arg_keyword_to_graphql_name(argument_definitions, arg) } names.compact! # hidden arguments are `nil` "(" + names.join(" and ") + ")" else arg_keyword_to_graphql_name(argument_definitions, arg_keyword) end end required_names.compact! # remove entries for hidden arguments case required_names.size when 0 # The required definitions were hidden from the client. # Another option here would be to raise an error in the application.... "%{validated} is missing a required argument." when 1 "%{validated} must include the following argument: #{required_names.first}." else "%{validated} must include exactly one of the following arguments: #{required_names.join(", ")}." end end def arg_keyword_to_graphql_name(argument_definitions, arg_keyword) argument_definition = argument_definitions.find { |defn| defn.keyword == arg_keyword } argument_definition&.graphql_name end end end end end graphql-ruby-2.5.19/lib/graphql/schema/visibility.rb000066400000000000000000000253571514115062600224220ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/schema/visibility/profile" require "graphql/schema/visibility/migration" require "graphql/schema/visibility/visit" module GraphQL class Schema # Use this plugin to make some parts of your schema hidden from some viewers. # class Visibility # @param schema [Class] # @param profiles [Hash Hash>] A hash of `name => context` pairs for preloading visibility profiles # @param preload [Boolean] if `true`, load the default schema profile and all named profiles immediately (defaults to `true` for `Rails.env.production?` and `Rails.env.staging?`) # @param migration_errors [Boolean] if `true`, raise an error when `Visibility` and `Warden` return different results def self.use(schema, dynamic: false, profiles: EmptyObjects::EMPTY_HASH, preload: (defined?(Rails.env) ? (Rails.env.production? || Rails.env.staging?) : nil), migration_errors: false) profiles&.each { |name, ctx| ctx[:visibility_profile] = name ctx.freeze } schema.visibility = self.new(schema, dynamic: dynamic, preload: preload, profiles: profiles, migration_errors: migration_errors) end def initialize(schema, dynamic:, preload:, profiles:, migration_errors:) @schema = schema schema.use_visibility_profile = true schema.visibility_profile_class = if migration_errors Visibility::Migration else Visibility::Profile end @preload = preload @profiles = profiles @cached_profiles = {} @dynamic = dynamic @migration_errors = migration_errors # Top-level type caches: @visit = nil @interface_type_memberships = nil @directives = nil @types = nil @all_references = nil @loaded_all = false if preload self.preload end end def freeze load_all @visit = true @interface_type_memberships.default_proc = nil @all_references.default_proc = nil super end def all_directives load_all @directives end def all_interface_type_memberships load_all @interface_type_memberships end def all_references load_all @all_references end def get_type(type_name) load_all @types[type_name] end attr_accessor :types def preload? @preload end def preload # Traverse the schema now (and in the *_configured hooks below) # To make sure things are loaded during boot @preloaded_types = Set.new types_to_visit = [ @schema.query, @schema.mutation, @schema.subscription, *@schema.introspection_system.types.values, *@schema.introspection_system.entry_points.map { |ep| ep.type.unwrap }, *@schema.orphan_types, ] # Root types may have been nil: types_to_visit.compact! ensure_all_loaded(types_to_visit) @profiles.each do |profile_name, example_ctx| prof = profile_for(example_ctx) prof.preload end end # @api private def query_configured(query_type) if @preload ensure_all_loaded([query_type]) end end # @api private def mutation_configured(mutation_type) if @preload ensure_all_loaded([mutation_type]) end end # @api private def subscription_configured(subscription_type) if @preload ensure_all_loaded([subscription_type]) end end # @api private def orphan_types_configured(orphan_types) if @preload ensure_all_loaded(orphan_types) end end # @api private def introspection_system_configured(introspection_system) if @preload introspection_types = [ *@schema.introspection_system.types.values, *@schema.introspection_system.entry_points.map { |ep| ep.type.unwrap }, ] ensure_all_loaded(introspection_types) end end # Make another Visibility for `schema` based on this one # @return [Visibility] # @api private def dup_for(other_schema) self.class.new( other_schema, dynamic: @dynamic, preload: @preload, profiles: @profiles, migration_errors: @migration_errors ) end def migration_errors? @migration_errors end attr_reader :cached_profiles def profile_for(context) if !@profiles.empty? visibility_profile = context[:visibility_profile] if @profiles.include?(visibility_profile) profile_ctx = @profiles[visibility_profile] @cached_profiles[visibility_profile] ||= @schema.visibility_profile_class.new(name: visibility_profile, context: profile_ctx, schema: @schema, visibility: self) elsif @dynamic if context.is_a?(Query::NullContext) top_level_profile else @schema.visibility_profile_class.new(context: context, schema: @schema, visibility: self) end elsif !context.key?(:visibility_profile) raise ArgumentError, "#{@schema} expects a visibility profile, but `visibility_profile:` wasn't passed. Provide a `visibility_profile:` value or add `dynamic: true` to your visibility configuration." else raise ArgumentError, "`#{visibility_profile.inspect}` isn't allowed for `visibility_profile:` (must be one of #{@profiles.keys.map(&:inspect).join(", ")}). Or, add `#{visibility_profile.inspect}` to the list of profiles in the schema definition." end elsif context.is_a?(Query::NullContext) top_level_profile else @schema.visibility_profile_class.new(context: context, schema: @schema, visibility: self) end end attr_reader :top_level # @api private attr_reader :unfiltered_interface_type_memberships def top_level_profile(refresh: false) if refresh @top_level_profile = nil end @top_level_profile ||= @schema.visibility_profile_class.new(context: Query::NullContext.instance, schema: @schema, visibility: self) end private def ensure_all_loaded(types_to_visit) while (type = types_to_visit.shift) if type.kind.fields? && @preloaded_types.add?(type) type.all_field_definitions.each do |field_defn| field_defn.ensure_loaded types_to_visit << field_defn.type.unwrap end end end top_level_profile(refresh: true) nil end def load_all(types: nil) if @visit.nil? # Set up the visit system @interface_type_memberships = Hash.new { |h, interface_type| h[interface_type] = Hash.new { |h2, obj_type| h2[obj_type] = [] }.compare_by_identity }.compare_by_identity @directives = [] @types = {} # String => Module @all_references = Hash.new { |h, member| h[member] = Set.new.compare_by_identity }.compare_by_identity @unions_for_references = Set.new @visit = Visibility::Visit.new(@schema) do |member| if member.is_a?(Module) type_name = member.graphql_name if (prev_t = @types[type_name]) if prev_t.is_a?(Array) prev_t << member else @types[type_name] = [member, prev_t] end else @types[member.graphql_name] = member end member.directives.each { |dir| @all_references[dir.class] << member } if member < GraphQL::Schema::Directive @directives << member elsif member.respond_to?(:interface_type_memberships) member.interface_type_memberships.each do |itm| @all_references[itm.abstract_type] << member # `itm.object_type` may not actually be `member` if this implementation # is inherited from a superclass @interface_type_memberships[itm.abstract_type][member] << itm end elsif member < GraphQL::Schema::Union @unions_for_references << member end elsif member.is_a?(GraphQL::Schema::Argument) member.validate_default_value @all_references[member.type.unwrap] << member if !(dirs = member.directives).empty? dir_owner = member.owner if dir_owner.respond_to?(:owner) dir_owner = dir_owner.owner end dirs.each { |dir| @all_references[dir.class] << dir_owner } end elsif member.is_a?(GraphQL::Schema::Field) @all_references[member.type.unwrap] << member if !(dirs = member.directives).empty? dir_owner = member.owner dirs.each { |dir| @all_references[dir.class] << dir_owner } end elsif member.is_a?(GraphQL::Schema::EnumValue) if !(dirs = member.directives).empty? dir_owner = member.owner dirs.each { |dir| @all_references[dir.class] << dir_owner } end end true end @schema.root_types.each { |t| @all_references[t] << true } @schema.introspection_system.types.each_value { |t| @all_references[t] << true } @schema.directives.each_value { |dir_class| @all_references[dir_class] << true } @visit.visit_each(types: []) # visit default directives end if types @visit.visit_each(types: types, directives: []) elsif @loaded_all == false @loaded_all = true @visit.visit_each else # already loaded all return end # TODO: somehow don't iterate over all these, # only the ones that may have been modified @interface_type_memberships.each do |int_type, obj_type_memberships| referrers = @all_references[int_type].select { |r| r.is_a?(GraphQL::Schema::Field) } if !referrers.empty? obj_type_memberships.each_key do |impl_type| @all_references[impl_type] |= referrers end end end @unions_for_references.each do |union_type| refs = @all_references[union_type] union_type.all_possible_types.each do |object_type| @all_references[object_type] |= refs # Add new items end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/visibility/000077500000000000000000000000001514115062600220615ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/schema/visibility/migration.rb000066400000000000000000000164601514115062600244060ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Visibility # You can use this to see how {GraphQL::Schema::Warden} and {GraphQL::Schema::Visibility::Profile} # handle `.visible?` differently in your schema. # # It runs the same method on both implementations and raises an error when the results diverge. # # To fix the error, modify your schema so that both implementations return the same thing. # Or, open an issue on GitHub to discuss the difference. # # This plugin adds overhead to runtime and may cause unexpected crashes -- **don't** use it in production! # # This plugin adds two keys to `context` when running: # # - `visibility_migration_running: true` # - For the {Schema::Warden} which it instantiates, it adds `visibility_migration_warden_running: true`. # # Use those keys to modify your `visible?` behavior as needed. # # Also, in a pinch, you can set `skip_visibility_migration_error: true` in context to turn off this behavior per-query. # (In that case, it uses {Profile} directly.) # # @example Adding this plugin # # use GraphQL::Schema::Visibility, migration_errors: true # class Migration < GraphQL::Schema::Visibility::Profile class RuntimeTypesMismatchError < GraphQL::Error def initialize(method_called, warden_result, profile_result, method_args) super(<<~ERR) Mismatch in types for `##{method_called}(#{method_args.map(&:inspect).join(", ")})`: #{compare_results(warden_result, profile_result)} Update your `.visible?` implementation to make these implementations return the same value. See: https://graphql-ruby.org/authorization/visibility_migration.html ERR end private def compare_results(warden_result, profile_result) if warden_result.is_a?(Array) && profile_result.is_a?(Array) all_results = warden_result | profile_result all_results.sort_by!(&:graphql_name) entries_text = all_results.map { |entry| "#{entry.graphql_name} (#{entry})"} width = entries_text.map(&:size).max yes = " ✔ " no = " " res = "".dup res << "#{"Result".center(width)} Warden Profile \n" all_results.each_with_index do |entry, idx| res << "#{entries_text[idx].ljust(width)}#{warden_result.include?(entry) ? yes : no}#{profile_result.include?(entry) ? yes : no}\n" end res << "\n" else "- Warden returned: #{humanize(warden_result)}\n\n- Visibility::Profile returned: #{humanize(profile_result)}" end end def humanize(val) case val when Array "#{val.size}: #{val.map { |v| humanize(v) }.sort.inspect}" when Module if val.respond_to?(:graphql_name) "#{val.graphql_name} (#{val.inspect})" else val.inspect end else val.inspect end end end def initialize(context:, schema:, name: nil, visibility:) @name = name @skip_error = context[:skip_visibility_migration_error] || context.is_a?(Query::NullContext) || context.is_a?(Hash) @profile_types = GraphQL::Schema::Visibility::Profile.new(context: context, schema: schema, visibility: visibility) if !@skip_error context[:visibility_migration_running] = true warden_ctx_vals = context.to_h.dup warden_ctx_vals[:visibility_migration_warden_running] = true if schema.const_defined?(:WardenCompatSchema, false) # don't use a defn from a superclass warden_schema = schema.const_get(:WardenCompatSchema, false) else warden_schema = Class.new(schema) warden_schema.use_visibility_profile = false # TODO public API warden_schema.send(:add_type_and_traverse, [warden_schema.query, warden_schema.mutation, warden_schema.subscription].compact, root: true) warden_schema.send(:add_type_and_traverse, warden_schema.directives.values + warden_schema.orphan_types, root: false) schema.const_set(:WardenCompatSchema, warden_schema) end warden_ctx = GraphQL::Query::Context.new(query: context.query, values: warden_ctx_vals) warden_ctx.warden = GraphQL::Schema::Warden.new(schema: warden_schema, context: warden_ctx) warden_ctx.warden.skip_warning = true warden_ctx.types = @warden_types = warden_ctx.warden.visibility_profile end end def loaded_types @profile_types.loaded_types end PUBLIC_PROFILE_METHODS = [ :enum_values, :interfaces, :all_types, :all_types_h, :fields, :loadable?, :loadable_possible_types, :type, :arguments, :argument, :directive_exists?, :directives, :field, :query_root, :mutation_root, :possible_types, :subscription_root, :reachable_type?, :visible_enum_value?, ] PUBLIC_PROFILE_METHODS.each do |profile_method| define_method(profile_method) do |*args| call_method_and_compare(profile_method, args) end end def call_method_and_compare(method, args) res_1 = @profile_types.public_send(method, *args) if @skip_error return res_1 end res_2 = @warden_types.public_send(method, *args) normalized_res_1 = res_1.is_a?(Array) ? Set.new(res_1) : res_1 normalized_res_2 = res_2.is_a?(Array) ? Set.new(res_2) : res_2 if !equivalent_schema_members?(normalized_res_1, normalized_res_2) # Raise the errors with the orignally returned values: err = RuntimeTypesMismatchError.new(method, res_2, res_1, args) raise err else res_1 end end def equivalent_schema_members?(member1, member2) if member1.class != member2.class return false end case member1 when Set member1_array = member1.to_a.sort_by(&:graphql_name) member2_array = member2.to_a.sort_by(&:graphql_name) member1_array.each_with_index do |inner_member1, idx| inner_member2 = member2_array[idx] equivalent_schema_members?(inner_member1, inner_member2) end when GraphQL::Schema::Field member1.ensure_loaded member2.ensure_loaded if member1.introspection? && member2.introspection? member1.inspect == member2.inspect else member1 == member2 end when Module if member1.introspection? && member2.introspection? member1.graphql_name == member2.graphql_name else member1 == member2 end else member1 == member2 end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/visibility/profile.rb000066400000000000000000000352111514115062600240500ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Visibility # This class filters the types, fields, arguments, enum values, and directives in a schema # based on the given `context`. # # It's like {Warden}, but has some differences: # # - It doesn't use {Schema}'s top-level caches (eg {Schema.references_to}, {Schema.possible_types}, {Schema.types}) # - It doesn't hide Interface or Union types when all their possible types are hidden. (Instead, those types should implement `.visible?` to hide in that case.) # - It checks `.visible?` on root introspection types # - It can be used to cache profiles by name for re-use across queries class Profile # @return [Schema::Visibility::Profile] def self.from_context(ctx, schema) if ctx.respond_to?(:types) && (types = ctx.types).is_a?(self) types else schema.visibility.profile_for(ctx) end end def self.null_profile(context:, schema:) profile = self.new(name: "NullProfile", context: context, schema: schema) profile.instance_variable_set(:@cached_visible, Hash.new { |k, v| k[v] = true }.compare_by_identity) profile end # @return [Symbol, nil] attr_reader :name def freeze @cached_visible.default_proc = nil @cached_visible_fields.default_proc = nil @cached_visible_fields.each do |type, fields| fields.default_proc = nil end @cached_visible_arguments.default_proc = nil @cached_visible_arguments.each do |type, fields| fields.default_proc = nil end @cached_parent_fields.default_proc = nil @cached_parent_fields.each do |type, fields| fields.default_proc = nil end @cached_parent_arguments.default_proc = nil @cached_parent_arguments.each do |type, args| args.default_proc = nil end @cached_possible_types.default_proc = nil @cached_enum_values.default_proc = nil @cached_fields.default_proc = nil @cached_arguments.default_proc = nil @loadable_possible_types.default_proc = nil super end def initialize(name: nil, context:, schema:, visibility:) @name = name @context = context @schema = schema @visibility = visibility @all_types = {} @all_types_loaded = false @unvisited_types = [] @all_directives = nil @cached_visible = Hash.new { |h, member| h[member] = @schema.visible?(member, @context) }.compare_by_identity @cached_visible_fields = Hash.new { |h, owner| h[owner] = Hash.new do |h2, field| h2[field] = visible_field_for(owner, field) end.compare_by_identity }.compare_by_identity @cached_visible_arguments = Hash.new do |h, owner| h[owner] = Hash.new do |h2, arg| h2[arg] = if @cached_visible[arg] && (arg_type = arg.type.unwrap) && @cached_visible[arg_type] case owner when GraphQL::Schema::Field @cached_visible_fields[owner.owner][owner] when Class @cached_visible[owner] else raise "Unexpected argument owner for `#{arg.path}`: #{owner.inspect}" end else false end end.compare_by_identity end.compare_by_identity @cached_parent_fields = Hash.new do |h, type| h[type] = Hash.new do |h2, field_name| h2[field_name] = type.get_field(field_name, @context) end end.compare_by_identity @cached_parent_arguments = Hash.new do |h, arg_owner| h[arg_owner] = Hash.new do |h2, arg_name| h2[arg_name] = arg_owner.get_argument(arg_name, @context) end end.compare_by_identity @cached_possible_types = Hash.new { |h, type| h[type] = possible_types_for(type) }.compare_by_identity @cached_enum_values = Hash.new do |h, enum_t| values = non_duplicate_items(enum_t.enum_values(@context), @cached_visible) if values.size == 0 raise GraphQL::Schema::Enum::MissingValuesError.new(enum_t) end h[enum_t] = values end.compare_by_identity @cached_fields = Hash.new do |h, owner| h[owner] = non_duplicate_items(owner.all_field_definitions.each(&:ensure_loaded), @cached_visible_fields[owner]) end.compare_by_identity @cached_arguments = Hash.new do |h, owner| h[owner] = non_duplicate_items(owner.all_argument_definitions, @cached_visible_arguments[owner]) end.compare_by_identity @loadable_possible_types = Hash.new { |h, union_type| h[union_type] = union_type.possible_types }.compare_by_identity end def field_on_visible_interface?(field, owner) ints = owner.interface_type_memberships.map(&:abstract_type) field_name = field.graphql_name filtered_ints = interfaces(owner) any_interface_has_field = false any_interface_has_visible_field = false ints.each do |int_t| if (_int_f_defn = @cached_parent_fields[int_t][field_name]) any_interface_has_field = true if filtered_ints.include?(int_t) # TODO cycles, or maybe not necessary since previously checked? && @cached_visible_fields[owner][field] any_interface_has_visible_field = true break end end end if any_interface_has_field any_interface_has_visible_field else true end end def type(type_name) t = @visibility.get_type(type_name) # rubocop:disable Development/ContextIsPassedCop if t if t.is_a?(Array) vis_t = nil t.each do |t_defn| if @cached_visible[t_defn] && referenced?(t_defn) if vis_t.nil? vis_t = t_defn else raise_duplicate_definition(vis_t, t_defn) end end end vis_t else if t && @cached_visible[t] && referenced?(t) t else nil end end end end def field(owner, field_name) f = if owner.kind.fields? && (field = @cached_parent_fields[owner][field_name]) field elsif owner == query_root && (entry_point_field = @schema.introspection_system.entry_point(name: field_name)) entry_point_field elsif (dynamic_field = @schema.introspection_system.dynamic_field(name: field_name)) dynamic_field else nil end if f.is_a?(Array) visible_f = nil f.each do |f_defn| if @cached_visible_fields[owner][f_defn] if visible_f.nil? visible_f = f_defn else raise_duplicate_definition(visible_f, f_defn) end end end visible_f&.ensure_loaded elsif f && @cached_visible_fields[owner][f.ensure_loaded] f else nil end end def fields(owner) @cached_fields[owner] end def arguments(owner) @cached_arguments[owner] end def argument(owner, arg_name) arg = @cached_parent_arguments[owner][arg_name] if arg.is_a?(Array) visible_arg = nil arg.each do |arg_defn| if @cached_visible_arguments[owner][arg_defn] if visible_arg.nil? visible_arg = arg_defn else raise_duplicate_definition(visible_arg, arg_defn) end end end visible_arg else if arg && @cached_visible_arguments[owner][arg] arg else nil end end end def possible_types(type) @cached_possible_types[type] end def interfaces(obj_or_int_type) ints = obj_or_int_type.interface_type_memberships .select { |itm| @cached_visible[itm] && @cached_visible[itm.abstract_type] } .map!(&:abstract_type) ints.uniq! # Remove any duplicate interfaces implemented via other interfaces ints end def query_root ((t = @schema.query) && @cached_visible[t]) ? t : nil end def mutation_root ((t = @schema.mutation) && @cached_visible[t]) ? t : nil end def subscription_root ((t = @schema.subscription) && @cached_visible[t]) ? t : nil end def all_types load_all_types @all_types.values end def all_types_h load_all_types @all_types end def enum_values(owner) @cached_enum_values[owner] end def directive_exists?(dir_name) directives.any? { |d| d.graphql_name == dir_name } end def directives @all_directives ||= @visibility.all_directives.select { |dir| @cached_visible[dir] && @visibility.all_references[dir].any? { |ref| ref == true || (@cached_visible[ref] && referenced?(ref)) } } end def loadable?(t, _ctx) @cached_visible[t] && !referenced?(t) end def loadable_possible_types(t, _ctx) @loadable_possible_types[t] end def loaded_types @all_types.values end def reachable_type?(type_name) load_all_types !!@all_types[type_name] end def visible_enum_value?(enum_value, _ctx = nil) @cached_visible[enum_value] end def preload load_all_types @all_types.each do |type_name, type_defn| if type_defn.kind.fields? fields(type_defn).each do |f| field(type_defn, f.graphql_name) arguments(f).each do |arg| argument(f, arg.graphql_name) end end @schema.introspection_system.dynamic_fields.each do |f| field(type_defn, f.graphql_name) end elsif type_defn.kind.input_object? arguments(type_defn).each do |arg| argument(type_defn, arg.graphql_name) end elsif type_defn.kind.enum? enum_values(type_defn) end # Lots more to do here end @schema.introspection_system.entry_points.each do |f| arguments(f).each do |arg| argument(f, arg.graphql_name) end field(@schema.query, f.graphql_name) end @schema.introspection_system.dynamic_fields.each do |f| arguments(f).each do |arg| argument(f, arg.graphql_name) end end end private def non_duplicate_items(definitions, visibility_cache) non_dups = [] names = Set.new definitions.each do |defn| if visibility_cache[defn] if !names.add?(defn.graphql_name) dup_defn = non_dups.find { |d| d.graphql_name == defn.graphql_name } raise_duplicate_definition(dup_defn, defn) end non_dups << defn end end non_dups end def raise_duplicate_definition(first_defn, second_defn) raise DuplicateNamesError.new(duplicated_name: first_defn.path, duplicated_definition_1: first_defn.inspect, duplicated_definition_2: second_defn.inspect) end def load_all_types return if @all_types_loaded @all_types_loaded = true visit = Visibility::Visit.new(@schema) do |member| if member.is_a?(Module) && member.respond_to?(:kind) if @cached_visible[member] && referenced?(member) type_name = member.graphql_name if (prev_t = @all_types[type_name]) && !prev_t.equal?(member) raise_duplicate_definition(member, prev_t) end @all_types[type_name] = member true else false end else @cached_visible[member] end end visit.visit_each @all_types.delete_if { |type_name, type_defn| !referenced?(type_defn) } nil end def referenced?(type_defn) @visibility.all_references[type_defn].any? do |ref| case ref when GraphQL::Schema::Argument @cached_visible_arguments[ref.owner][ref] when GraphQL::Schema::Field @cached_visible_fields[ref.owner][ref] when Module @cached_visible[ref] when true true end end end def possible_types_for(type) case type.kind.name when "INTERFACE" pts = [] @visibility.all_interface_type_memberships[type].each do |impl_type, type_memberships| if impl_type.kind.object? && referenced?(impl_type) && @cached_visible[impl_type] if type_memberships.any? { |itm| @cached_visible[itm] } pts << impl_type end end end pts when "UNION" pts = [] type.type_memberships.each { |tm| if @cached_visible[tm] && (ot = tm.object_type) && @cached_visible[ot] && referenced?(ot) pts << ot end } pts when "OBJECT" if @cached_visible[type] [type] else EmptyObjects::EMPTY_ARRAY end else GraphQL::EmptyObjects::EMPTY_ARRAY end end def visible_field_for(owner, field) @cached_visible[field] && (ret_type = field.type.unwrap) && @cached_visible[ret_type] && (owner == field.owner || (!owner.kind.object?) || field_on_visible_interface?(field, owner)) end end end end end graphql-ruby-2.5.19/lib/graphql/schema/visibility/visit.rb000066400000000000000000000165131514115062600235520ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Visibility class Visit def initialize(schema, &visit_block) @schema = schema @late_bound_types = nil @unvisited_types = nil # These accumulate between calls to prevent re-visiting the same types @visited_types = Set.new.compare_by_identity @visited_directives = Set.new.compare_by_identity @visit_block = visit_block end def entry_point_types ept = [ @schema.query, @schema.mutation, @schema.subscription, *@schema.introspection_system.types.values, *@schema.orphan_types, ] ept.compact! ept end def entry_point_directives @schema.directives.values end def visit_each(types: entry_point_types, directives: entry_point_directives) @unvisited_types && raise("Can't re-enter `visit_each` on this Visit (another visit is already in progress)") @unvisited_types = types @late_bound_types = [] directives_to_visit = directives while !@unvisited_types.empty? || !@late_bound_types.empty? while (type = @unvisited_types.pop) if @visited_types.add?(type) && @visit_block.call(type) directives_to_visit.concat(type.directives) case type.kind.name when "OBJECT", "INTERFACE" type.interface_type_memberships.each do |itm| append_unvisited_type(type, itm.abstract_type) end if type.kind.interface? type.orphan_types.each do |orphan_type| append_unvisited_type(type, orphan_type) end end type.all_field_definitions.each do |field| field.ensure_loaded if @visit_block.call(field) directives_to_visit.concat(field.directives) append_unvisited_type(type, field.type.unwrap) field.all_argument_definitions.each do |argument| if @visit_block.call(argument) directives_to_visit.concat(argument.directives) append_unvisited_type(field, argument.type.unwrap) end end end end when "INPUT_OBJECT" type.all_argument_definitions.each do |argument| if @visit_block.call(argument) directives_to_visit.concat(argument.directives) append_unvisited_type(type, argument.type.unwrap) end end when "UNION" type.type_memberships.each do |tm| append_unvisited_type(type, tm.object_type) end when "ENUM" type.all_enum_value_definitions.each do |val| if @visit_block.call(val) directives_to_visit.concat(val.directives) end end when "SCALAR" # pass -- nothing else to visit else raise "Invariant: unhandled type kind: #{type.kind.inspect}" end end end directives_to_visit.each do |dir| dir_class = dir.is_a?(Class) ? dir : dir.class if @visited_directives.add?(dir_class) && @visit_block.call(dir_class) dir_class.all_argument_definitions.each do |arg_defn| if @visit_block.call(arg_defn) directives_to_visit.concat(arg_defn.directives) append_unvisited_type(dir_class, arg_defn.type.unwrap) end end end end missed_late_types_streak = 0 while (owner, late_type = @late_bound_types.shift) if (late_type.is_a?(String) && (type = Member::BuildType.constantize(type))) || (late_type.is_a?(LateBoundType) && (type = @visited_types.find { |t| t.graphql_name == late_type.graphql_name })) missed_late_types_streak = 0 # might succeed next round update_type_owner(owner, type) append_unvisited_type(owner, type) else # Didn't find it -- keep trying missed_late_types_streak += 1 @late_bound_types << [owner, late_type] if missed_late_types_streak == @late_bound_types.size raise UnresolvedLateBoundTypeError.new(type: late_type) end end end end @unvisited_types = nil nil end private def append_unvisited_type(owner, type) if type.is_a?(LateBoundType) || type.is_a?(String) @late_bound_types << [owner, type] else @unvisited_types << type end end def update_type_owner(owner, type) case owner when Module if owner.kind.union? owner.assign_type_membership_object_type(type) elsif type.kind.interface? new_interfaces = [] owner.interfaces.each do |int_t| if int_t.is_a?(String) && int_t == type.graphql_name new_interfaces << type elsif int_t.is_a?(LateBoundType) && int_t.graphql_name == type.graphql_name new_interfaces << type else # Don't re-add proper interface definitions, # they were probably already added, maybe with options. end end owner.implements(*new_interfaces) new_interfaces.each do |int| pt = @possible_types[int] ||= [] if !pt.include?(owner) && owner.is_a?(Class) pt << owner end int.interfaces.each do |indirect_int| if indirect_int.is_a?(LateBoundType) && (indirect_int_type = get_type(indirect_int.graphql_name)) # rubocop:disable Development/ContextIsPassedCop update_type_owner(owner, indirect_int_type) end end end end when GraphQL::Schema::Argument, GraphQL::Schema::Field orig_type = owner.type # Apply list/non-null wrapper as needed if orig_type.respond_to?(:of_type) transforms = [] while (orig_type.respond_to?(:of_type)) if orig_type.kind.non_null? transforms << :to_non_null_type elsif orig_type.kind.list? transforms << :to_list_type else raise "Invariant: :of_type isn't non-null or list" end orig_type = orig_type.of_type end transforms.reverse_each { |t| type = type.public_send(t) } end owner.type = type else raise "Unexpected update: #{owner.inspect} #{type.inspect}" end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/warden.rb000066400000000000000000000566671514115062600215230ustar00rootroot00000000000000# frozen_string_literal: true require 'set' module GraphQL class Schema # Restrict access to a {GraphQL::Schema} with a user-defined `visible?` implementations. # # When validating and executing a query, all access to schema members # should go through a warden. If you access the schema directly, # you may show a client something that it shouldn't be allowed to see. # # @api private class Warden def self.from_context(context) context.warden || PassThruWarden rescue NoMethodError # this might be a hash which won't respond to #warden PassThruWarden end def self.types_from_context(context) context.types || PassThruWarden rescue NoMethodError # this might be a hash which won't respond to #warden PassThruWarden end def self.use(schema) # no-op end # @param visibility_method [Symbol] a Warden method to call for this entry # @param entry [Object, Array] One or more definitions for a given name in a GraphQL Schema # @param context [GraphQL::Query::Context] # @param warden [Warden] # @return [Object] `entry` or one of `entry`'s items if exactly one of them is visible for this context # @return [nil] If neither `entry` nor any of `entry`'s items are visible for this context def self.visible_entry?(visibility_method, entry, context, warden = Warden.from_context(context)) if entry.is_a?(Array) visible_item = nil entry.each do |item| if warden.public_send(visibility_method, item, context) if visible_item.nil? visible_item = item else raise DuplicateNamesError.new( duplicated_name: item.path, duplicated_definition_1: visible_item.inspect, duplicated_definition_2: item.inspect ) end end end visible_item elsif warden.public_send(visibility_method, entry, context) entry else nil end end # This is used when a caller provides a Hash for context. # We want to call the schema's hooks, but we don't have a full-blown warden. # The `context` arguments to these methods exist purely to simplify the code that # calls methods on this object, so it will have everything it needs. class PassThruWarden class << self def visible_field?(field, ctx); field.visible?(ctx); end def visible_argument?(arg, ctx); arg.visible?(ctx); end def visible_type?(type, ctx); type.visible?(ctx); end def visible_enum_value?(ev, ctx); ev.visible?(ctx); end def visible_type_membership?(tm, ctx); tm.visible?(ctx); end def interface_type_memberships(obj_t, ctx); obj_t.interface_type_memberships; end def arguments(owner, ctx); owner.arguments(ctx); end def loadable?(type, ctx); type.visible?(ctx); end def loadable_possible_types(type, ctx); type.possible_types(ctx); end def visibility_profile @visibility_profile ||= Warden::VisibilityProfile.new(self) end end end class NullWarden def initialize(_filter = nil, context:, schema:) @schema = schema @visibility_profile = Warden::VisibilityProfile.new(self) end # No-op, but for compatibility: attr_writer :skip_warning attr_reader :visibility_profile def visible_field?(field_defn, _ctx = nil, owner = nil); true; end def visible_argument?(arg_defn, _ctx = nil); true; end def visible_type?(type_defn, _ctx = nil); true; end def visible_enum_value?(enum_value, _ctx = nil); enum_value.visible?(Query::NullContext.instance); end def visible_type_membership?(type_membership, _ctx = nil); true; end def interface_type_memberships(obj_type, _ctx = nil); obj_type.interface_type_memberships; end def get_type(type_name); @schema.get_type(type_name, Query::NullContext.instance, false); end # rubocop:disable Development/ContextIsPassedCop def arguments(argument_owner, ctx = nil); argument_owner.all_argument_definitions; end def enum_values(enum_defn); enum_defn.enum_values(Query::NullContext.instance); end # rubocop:disable Development/ContextIsPassedCop def get_argument(parent_type, argument_name); parent_type.get_argument(argument_name); end # rubocop:disable Development/ContextIsPassedCop def types; @schema.types; end # rubocop:disable Development/ContextIsPassedCop def root_type_for_operation(op_name); @schema.root_type_for_operation(op_name); end def directives; @schema.directives.values; end def fields(type_defn); type_defn.all_field_definitions; end # rubocop:disable Development/ContextIsPassedCop def get_field(parent_type, field_name); @schema.get_field(parent_type, field_name); end def reachable_type?(type_name); true; end def loadable?(type, _ctx); true; end def loadable_possible_types(abstract_type, _ctx); union_type.possible_types; end def reachable_types; @schema.types.values; end # rubocop:disable Development/ContextIsPassedCop def possible_types(type_defn); @schema.possible_types(type_defn, Query::NullContext.instance, false); end def interfaces(obj_type); obj_type.interfaces; end end def visibility_profile @visibility_profile ||= VisibilityProfile.new(self) end class VisibilityProfile def initialize(warden) @warden = warden end def directives @warden.directives end def directive_exists?(dir_name) @warden.directives.any? { |d| d.graphql_name == dir_name } end def type(name) @warden.get_type(name) end def field(owner, field_name) @warden.get_field(owner, field_name) end def argument(owner, arg_name) @warden.get_argument(owner, arg_name) end def query_root @warden.root_type_for_operation("query") end def mutation_root @warden.root_type_for_operation("mutation") end def subscription_root @warden.root_type_for_operation("subscription") end def arguments(owner) @warden.arguments(owner) end def fields(owner) @warden.fields(owner) end def possible_types(type) @warden.possible_types(type) end def enum_values(enum_type) @warden.enum_values(enum_type) end def all_types @warden.reachable_types end def interfaces(obj_type) @warden.interfaces(obj_type) end def loadable?(t, ctx) # TODO remove ctx here? @warden.loadable?(t, ctx) end def loadable_possible_types(t, ctx) @warden.loadable_possible_types(t, ctx) end def reachable_type?(type_name) !!@warden.reachable_type?(type_name) end def visible_enum_value?(enum_value, ctx = nil) @warden.visible_enum_value?(enum_value, ctx) end end # @param context [GraphQL::Query::Context] # @param schema [GraphQL::Schema] def initialize(context:, schema:) @schema = schema # Cache these to avoid repeated hits to the inheritance chain when one isn't present @query = @schema.query @mutation = @schema.mutation @subscription = @schema.subscription @context = context @visibility_cache = read_through { |m| check_visible(schema, m) } # Initialize all ivars to improve object shape consistency: @types = @visible_types = @reachable_types = @visible_parent_fields = @visible_possible_types = @visible_fields = @visible_arguments = @visible_enum_arrays = @visible_enum_values = @visible_interfaces = @type_visibility = @type_memberships = @visible_and_reachable_type = @unions = @unfiltered_interfaces = @reachable_type_set = @visibility_profile = @loadable_possible_types = nil @skip_warning = schema.plugins.any? { |(plugin, _opts)| plugin == GraphQL::Schema::Warden } end attr_writer :skip_warning # @return [Hash] Visible types in the schema def types @types ||= begin vis_types = {} @schema.types(@context).each do |n, t| if visible_and_reachable_type?(t) vis_types[n] = t end end vis_types end end # @return [Boolean] True if this type is used for `loads:` but not in the schema otherwise and not _explicitly_ hidden. def loadable?(type, _ctx) visible_type?(type) && !referenced?(type) && (type.respond_to?(:interfaces) ? interfaces(type).all? { |i| loadable?(i, _ctx) } : true) end # This abstract type was determined to be used for `loads` only. # All its possible types are valid possibilities here -- no filtering. def loadable_possible_types(abstract_type, _ctx) @loadable_possible_types ||= read_through do |t| if t.is_a?(Class) # union t.possible_types else @schema.possible_types(abstract_type) end end @loadable_possible_types[abstract_type] end # @return [GraphQL::BaseType, nil] The type named `type_name`, if it exists (else `nil`) def get_type(type_name) @visible_types ||= read_through do |name| type_defn = @schema.get_type(name, @context, false) if type_defn && visible_and_reachable_type?(type_defn) type_defn else nil end end @visible_types[type_name] end # @return [Array] Visible and reachable types in the schema def reachable_types @reachable_types ||= reachable_type_set.to_a end # @return Boolean True if the type is visible and reachable in the schema def reachable_type?(type_name) type = get_type(type_name) # rubocop:disable Development/ContextIsPassedCop -- `self` is query-aware type && reachable_type_set.include?(type) end # @return [GraphQL::Field, nil] The field named `field_name` on `parent_type`, if it exists def get_field(parent_type, field_name) @visible_parent_fields ||= read_through do |type| read_through do |f_name| field_defn = @schema.get_field(type, f_name, @context) if field_defn && visible_field?(field_defn, nil, type) field_defn else nil end end end @visible_parent_fields[parent_type][field_name] end # @return [GraphQL::Argument, nil] The argument named `argument_name` on `parent_type`, if it exists and is visible def get_argument(parent_type, argument_name) argument = parent_type.get_argument(argument_name, @context) return argument if argument && visible_argument?(argument, @context) end # @return [Array] The types which may be member of `type_defn` def possible_types(type_defn) @visible_possible_types ||= read_through { |type_defn| pt = @schema.possible_types(type_defn, @context, false) pt.select { |t| visible_and_reachable_type?(t) } } @visible_possible_types[type_defn] end # @param type_defn [GraphQL::ObjectType, GraphQL::InterfaceType] # @return [Array] Fields on `type_defn` def fields(type_defn) @visible_fields ||= read_through { |t| @schema.get_fields(t, @context).values } @visible_fields[type_defn] end # @param argument_owner [GraphQL::Field, GraphQL::InputObjectType] # @return [Array] Visible arguments on `argument_owner` def arguments(argument_owner, ctx = nil) @visible_arguments ||= read_through { |o| args = o.arguments(@context) if !args.empty? args = args.values args.select! { |a| visible_argument?(a, @context) } args else EmptyObjects::EMPTY_ARRAY end } @visible_arguments[argument_owner] end # @return [Array] Visible members of `enum_defn` def enum_values(enum_defn) @visible_enum_arrays ||= read_through { |e| values = e.enum_values(@context) if values.size == 0 raise GraphQL::Schema::Enum::MissingValuesError.new(e) end values } @visible_enum_arrays[enum_defn] end def visible_enum_value?(enum_value, _ctx = nil) @visible_enum_values ||= read_through { |ev| visible?(ev) } @visible_enum_values[enum_value] end # @return [Array] Visible interfaces implemented by `obj_type` def interfaces(obj_type) @visible_interfaces ||= read_through { |t| ints = t.interfaces(@context) if !ints.empty? ints.select! { |i| visible_type?(i) } end ints } @visible_interfaces[obj_type] end def directives @schema.directives.each_value.select { |d| visible?(d) } end def root_type_for_operation(op_name) root_type = @schema.root_type_for_operation(op_name) if root_type && visible?(root_type) root_type else nil end end # @param owner [Class, Module] If provided, confirm that field has the given owner. def visible_field?(field_defn, _ctx = nil, owner = field_defn.owner) # This field is visible in its own right visible?(field_defn) && # This field's return type is visible visible_and_reachable_type?(field_defn.type.unwrap) && # This field is either defined on this object type, # or the interface it's inherited from is also visible ((field_defn.respond_to?(:owner) && field_defn.owner == owner) || field_on_visible_interface?(field_defn, owner)) end def visible_argument?(arg_defn, _ctx = nil) visible?(arg_defn) && visible_and_reachable_type?(arg_defn.type.unwrap) end def visible_type?(type_defn, _ctx = nil) @type_visibility ||= read_through { |type_defn| visible?(type_defn) } @type_visibility[type_defn] end def visible_type_membership?(type_membership, _ctx = nil) visible?(type_membership) end def interface_type_memberships(obj_type, _ctx = nil) @type_memberships ||= read_through do |obj_t| obj_t.interface_type_memberships end @type_memberships[obj_type] end private def visible_and_reachable_type?(type_defn) @visible_and_reachable_type ||= read_through do |type_defn| next false unless visible_type?(type_defn) next true if root_type?(type_defn) || type_defn.introspection? if type_defn.kind.union? !possible_types(type_defn).empty? && (referenced?(type_defn) || orphan_type?(type_defn)) elsif type_defn.kind.interface? if !possible_types(type_defn).empty? true else if @context.respond_to?(:logger) && (logger = @context.logger) logger.debug { "Interface `#{type_defn.graphql_name}` hidden because it has no visible implementers" } end false end else if referenced?(type_defn) true elsif type_defn.kind.object? # Show this object if it belongs to ... interfaces(type_defn).any? { |t| referenced?(t) } || # an interface which is referenced in the schema union_memberships(type_defn).any? { |t| referenced?(t) || orphan_type?(t) } # or a union which is referenced or added via orphan_types else false end end end @visible_and_reachable_type[type_defn] end def union_memberships(obj_type) @unions ||= read_through { |obj_type| @schema.union_memberships(obj_type).select { |u| visible?(u) } } @unions[obj_type] end # We need this to tell whether a field was inherited by an interface # even when that interface is hidden from `#interfaces` def unfiltered_interfaces(type_defn) @unfiltered_interfaces ||= read_through(&:interfaces) @unfiltered_interfaces[type_defn] end # If this field was inherited from an interface, and the field on that interface is _hidden_, # then treat this inherited field as hidden. # (If it _wasn't_ inherited, then don't hide it for this reason.) def field_on_visible_interface?(field_defn, type_defn) if type_defn.kind.object? any_interface_has_field = false any_interface_has_visible_field = false ints = unfiltered_interfaces(type_defn) ints.each do |interface_type| if (iface_field_defn = interface_type.get_field(field_defn.graphql_name, @context)) any_interface_has_field = true if interfaces(type_defn).include?(interface_type) && visible_field?(iface_field_defn, nil, interface_type) any_interface_has_visible_field = true end end end if any_interface_has_field any_interface_has_visible_field else # it's the object's own field true end else true end end def root_type?(type_defn) @query == type_defn || @mutation == type_defn || @subscription == type_defn end def referenced?(type_defn) members = @schema.references_to(type_defn) members.any? { |m| visible?(m) } end def orphan_type?(type_defn) @schema.orphan_types.include?(type_defn) end def visible?(member) @visibility_cache[member] end def read_through Hash.new { |h, k| h[k] = yield(k) }.compare_by_identity end def check_visible(schema, member) if schema.visible?(member, @context) true elsif @skip_warning false else member_s = member.respond_to?(:path) ? member.path : member.inspect member_type = case member when Module if member.respond_to?(:kind) member.kind.name.downcase else "" end when GraphQL::Schema::Field "field" when GraphQL::Schema::EnumValue "enum value" when GraphQL::Schema::Argument "argument" else "" end schema_s = schema.name ? "#{schema.name}'s" : "" schema_name = schema.name ? "#{schema.name}" : "your schema" warn(ADD_WARDEN_WARNING % { schema_s: schema_s, schema_name: schema_name, member: member_s, member_type: member_type }) @skip_warning = true # only warn once per query # If there's no schema name, add the backtrace for additional context: if schema_s == "" puts caller.map { |l| " #{l}"} end false end end ADD_WARDEN_WARNING = <<~WARNING DEPRECATION: %{schema_s} "%{member}" %{member_type} returned `false` for `.visible?` but `GraphQL::Schema::Visibility` isn't configured yet. Address this warning by adding: use GraphQL::Schema::Visibility to the definition for %{schema_name}. (Future GraphQL-Ruby versions won't check `.visible?` methods by default.) Alternatively, for legacy behavior, add: use GraphQL::Schema::Warden # legacy visibility behavior For more information see: https://graphql-ruby.org/authorization/visibility.html WARNING def reachable_type_set return @reachable_type_set if @reachable_type_set @reachable_type_set = Set.new rt_hash = {} unvisited_types = [] ['query', 'mutation', 'subscription'].each do |op_name| root_type = root_type_for_operation(op_name) unvisited_types << root_type if root_type end unvisited_types.concat(@schema.introspection_system.types.values) directives.each do |dir_class| arguments(dir_class).each do |arg_defn| arg_t = arg_defn.type.unwrap if get_type(arg_t.graphql_name) # rubocop:disable Development/ContextIsPassedCop -- `self` is query-aware unvisited_types << arg_t end end end @schema.orphan_types.each do |orphan_type| if get_type(orphan_type.graphql_name) == orphan_type # rubocop:disable Development/ContextIsPassedCop -- `self` is query-aware unvisited_types << orphan_type end end included_interface_possible_types_set = Set.new until unvisited_types.empty? type = unvisited_types.pop visit_type(type, unvisited_types, @reachable_type_set, rt_hash, included_interface_possible_types_set, include_interface_possible_types: false) end @reachable_type_set end def visit_type(type, unvisited_types, visited_type_set, type_by_name_hash, included_interface_possible_types_set, include_interface_possible_types:) if visited_type_set.add?(type) || (include_interface_possible_types && type.kind.interface? && included_interface_possible_types_set.add?(type)) type_by_name = type_by_name_hash[type.graphql_name] ||= type if type_by_name != type name_1, name_2 = [type.inspect, type_by_name.inspect].sort raise DuplicateNamesError.new( duplicated_name: type.graphql_name, duplicated_definition_1: name_1, duplicated_definition_2: name_2 ) end if type.kind.input_object? # recurse into visible arguments arguments(type).each do |argument| argument_type = argument.type.unwrap unvisited_types << argument_type end elsif type.kind.union? # recurse into visible possible types possible_types(type).each do |possible_type| unvisited_types << possible_type end elsif type.kind.fields? if type.kind.object? # recurse into visible implemented interfaces interfaces(type).each do |interface| unvisited_types << interface end elsif include_interface_possible_types possible_types(type).each do |pt| unvisited_types << pt end end # Don't visit interface possible types -- it's not enough to justify visibility # recurse into visible fields fields(type).each do |field| field_type = field.type.unwrap # In this case, if it's an interface, we want to include visit_type(field_type, unvisited_types, visited_type_set, type_by_name_hash, included_interface_possible_types_set, include_interface_possible_types: true) # recurse into visible arguments arguments(field).each do |argument| argument_type = argument.type.unwrap unvisited_types << argument_type end end end end end end end end graphql-ruby-2.5.19/lib/graphql/schema/wrapper.rb000066400000000000000000000010071514115062600216750ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Schema class Wrapper include GraphQL::Schema::Member::TypeSystemHelpers # @return [Class, Module] The inner type of this wrapping type, the type of which one or more objects may be present. attr_reader :of_type def initialize(of_type) @of_type = of_type end def unwrap @of_type.unwrap end def ==(other) self.class == other.class && of_type == other.of_type end end end end graphql-ruby-2.5.19/lib/graphql/static_validation.rb000066400000000000000000000011701514115062600224570ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/static_validation/error" require "graphql/static_validation/definition_dependencies" require "graphql/static_validation/validator" require "graphql/static_validation/validation_context" require "graphql/static_validation/validation_timeout_error" require "graphql/static_validation/literal_validator" require "graphql/static_validation/base_visitor" rules_glob = File.expand_path("../static_validation/rules/*.rb", __FILE__) Dir.glob(rules_glob).each do |file| require(file) end require "graphql/static_validation/all_rules" require "graphql/static_validation/interpreter_visitor" graphql-ruby-2.5.19/lib/graphql/static_validation/000077500000000000000000000000001514115062600221335ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/static_validation/all_rules.rb000066400000000000000000000041501514115062600244420ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation # Default rules for {GraphQL::StaticValidation::Validator} # # Order is important here. Some validators skip later hooks. # which stops the visit on that node. That way it doesn't try to find fields on types that # don't exist, etc. ALL_RULES = [ GraphQL::StaticValidation::NoDefinitionsArePresent, GraphQL::StaticValidation::DirectivesAreDefined, GraphQL::StaticValidation::DirectivesAreInValidLocations, GraphQL::StaticValidation::UniqueDirectivesPerLocation, GraphQL::StaticValidation::OperationNamesAreValid, GraphQL::StaticValidation::FragmentNamesAreUnique, GraphQL::StaticValidation::FragmentsAreFinite, GraphQL::StaticValidation::FragmentsAreNamed, GraphQL::StaticValidation::FragmentsAreUsed, GraphQL::StaticValidation::FragmentTypesExist, GraphQL::StaticValidation::FragmentsAreOnCompositeTypes, GraphQL::StaticValidation::FragmentSpreadsArePossible, GraphQL::StaticValidation::FieldsAreDefinedOnType, GraphQL::StaticValidation::FieldsWillMerge, GraphQL::StaticValidation::FieldsHaveAppropriateSelections, GraphQL::StaticValidation::ArgumentsAreDefined, GraphQL::StaticValidation::ArgumentLiteralsAreCompatible, GraphQL::StaticValidation::RequiredArgumentsArePresent, GraphQL::StaticValidation::RequiredInputObjectAttributesArePresent, GraphQL::StaticValidation::ArgumentNamesAreUnique, GraphQL::StaticValidation::VariableNamesAreUnique, GraphQL::StaticValidation::VariablesAreInputTypes, GraphQL::StaticValidation::VariableDefaultValuesAreCorrectlyTyped, GraphQL::StaticValidation::VariablesAreUsedAndDefined, GraphQL::StaticValidation::VariableUsagesAreAllowed, GraphQL::StaticValidation::MutationRootExists, GraphQL::StaticValidation::QueryRootExists, GraphQL::StaticValidation::SubscriptionRootExistsAndSingleSubscriptionSelection, GraphQL::StaticValidation::InputObjectNamesAreUnique, GraphQL::StaticValidation::OneOfInputObjectsAreValid, ].freeze end end graphql-ruby-2.5.19/lib/graphql/static_validation/base_visitor.rb000066400000000000000000000134261514115062600251570ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class BaseVisitor < GraphQL::Language::StaticVisitor def initialize(document, context) @path = [] @object_types = [] @directives = [] @field_definitions = [] @argument_definitions = [] @directive_definitions = [] @context = context @types = context.query.types @schema = context.schema super(document) end attr_reader :context # @return [Array] Types whose scope we've entered attr_reader :object_types # @return [Array] The nesting of the current position in the AST def path @path.dup end # Build a class to visit the AST and perform validation, # or use a pre-built class if rules is `ALL_RULES` or empty. # @param rules [Array] # @return [Class] A class for validating `rules` during visitation def self.including_rules(rules) if rules.empty? # It's not doing _anything?!?_ BaseVisitor elsif rules == ALL_RULES InterpreterVisitor else visitor_class = Class.new(self) do include(GraphQL::StaticValidation::DefinitionDependencies) end rules.reverse_each do |r| # If it's a class, it gets attached later. if !r.is_a?(Class) visitor_class.include(r) end end visitor_class.include(ContextMethods) visitor_class end end module ContextMethods def on_operation_definition(node, parent) object_type = @schema.root_type_for_operation(node.operation_type) push_type(object_type) @path.push("#{node.operation_type}#{node.name ? " #{node.name}" : ""}") super @object_types.pop @path.pop end def on_fragment_definition(node, parent) on_fragment_with_type(node) do @path.push("fragment #{node.name}") super end end def on_inline_fragment(node, parent) on_fragment_with_type(node) do @path.push("...#{node.type ? " on #{node.type.to_query_string}" : ""}") super end end def on_field(node, parent) parent_type = @object_types.last field_definition = @types.field(parent_type, node.name) @field_definitions.push(field_definition) if !field_definition.nil? next_object_type = field_definition.type.unwrap push_type(next_object_type) else push_type(nil) end @path.push(node.alias || node.name) super @field_definitions.pop @object_types.pop @path.pop end def on_directive(node, parent) directive_defn = @context.schema_directives[node.name] @directive_definitions.push(directive_defn) super @directive_definitions.pop end def on_argument(node, parent) argument_defn = if (arg = @argument_definitions.last) arg_type = arg.type.unwrap if arg_type.kind.input_object? @types.argument(arg_type, node.name) else nil end elsif (directive_defn = @directive_definitions.last) @types.argument(directive_defn, node.name) elsif (field_defn = @field_definitions.last) @types.argument(field_defn, node.name) else nil end @argument_definitions.push(argument_defn) @path.push(node.name) super @argument_definitions.pop @path.pop end def on_fragment_spread(node, parent) @path.push("... #{node.name}") super @path.pop end def on_input_object(node, parent) arg_defn = @argument_definitions.last if arg_defn && arg_defn.type.list? @path.push(parent.children.index(node)) super @path.pop else super end end # @return [GraphQL::BaseType] The current object type def type_definition @object_types.last end # @return [GraphQL::BaseType] The type which the current type came from def parent_type_definition @object_types[-2] end # @return [GraphQL::Field, nil] The most-recently-entered GraphQL::Field, if currently inside one def field_definition @field_definitions.last end # @return [GraphQL::Directive, nil] The most-recently-entered GraphQL::Directive, if currently inside one def directive_definition @directive_definitions.last end # @return [GraphQL::Argument, nil] The most-recently-entered GraphQL::Argument, if currently inside one def argument_definition # Don't get the _last_ one because that's the current one. # Get the second-to-last one, which is the parent of the current one. @argument_definitions[-2] end private def on_fragment_with_type(node) object_type = if node.type @types.type(node.type.name) else @object_types.last end push_type(object_type) yield(node) @object_types.pop @path.pop end def push_type(t) @object_types.push(t) end end private def add_error(error, path: nil) if @context.too_many_errors? throw :too_many_validation_errors end error.path ||= (path || @path.dup) context.errors << error end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/definition_dependencies.rb000066400000000000000000000177171514115062600273330ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation # Track fragment dependencies for operations # and expose the fragment definitions which # are used by a given operation module DefinitionDependencies attr_reader :dependencies def initialize(*) super @defdep_node_paths = {} # { name => [node, ...] } pairs for fragments (although duplicate-named fragments are _invalid_, they are _possible_) @defdep_fragment_definitions = Hash.new{ |h, k| h[k] = [] } # This tracks dependencies from fragment to Node where it was used # { fragment_definition_name => [dependent_node, dependent_node]} @defdep_dependent_definitions = Hash.new { |h, k| h[k] = Set.new } # First-level usages of spreads within definitions # (When a key has an empty list as its value, # we can resolve that key's dependents) # { definition_node => [node, node ...] } @defdep_immediate_dependencies = Hash.new { |h, k| h[k] = Set.new } # When we encounter a spread, # this node is the one who depends on it @defdep_current_parent = nil end def on_document(node, parent) node.definitions.each do |definition| if definition.is_a? GraphQL::Language::Nodes::FragmentDefinition @defdep_fragment_definitions[definition.name] << definition end end super @dependencies = dependency_map { |defn, spreads, frag| context.on_dependency_resolve_handlers.each { |h| h.call(defn, spreads, frag) } } end def on_operation_definition(node, prev_node) @defdep_node_paths[node.name] = NodeWithPath.new(node, context.path) @defdep_current_parent = node super @defdep_current_parent = nil end def on_fragment_definition(node, parent) @defdep_node_paths[node] = NodeWithPath.new(node, context.path) @defdep_current_parent = node super @defdep_current_parent = nil end def on_fragment_spread(node, parent) @defdep_node_paths[node] = NodeWithPath.new(node, context.path) # Track both sides of the dependency @defdep_dependent_definitions[node.name] << @defdep_current_parent @defdep_immediate_dependencies[@defdep_current_parent] << node super end # A map of operation definitions to an array of that operation's dependencies # @return [DependencyMap] def dependency_map(&block) @dependency_map ||= resolve_dependencies(&block) end # Map definition AST nodes to the definition AST nodes they depend on. # Expose circular dependencies. class DependencyMap # @return [Array] attr_reader :cyclical_definitions # @return [Hash>] attr_reader :unmet_dependencies # @return [Array] attr_reader :unused_dependencies def initialize @dependencies = Hash.new { |h, k| h[k] = [] } @cyclical_definitions = [] @unmet_dependencies = Hash.new { |h, k| h[k] = [] } @unused_dependencies = [] end # @return [Array] dependencies for `definition_node` def [](definition_node) @dependencies[definition_node] end end class NodeWithPath extend Forwardable attr_reader :node, :path def initialize(node, path) @node = node @path = path end def_delegators :@node, :name, :eql?, :hash end private # Return a hash of { node => [node, node ... ]} pairs # Keys are top-level definitions # Values are arrays of flattened dependencies def resolve_dependencies dependency_map = DependencyMap.new # Don't allow the loop to run more times # than the number of fragments in the document max_loops = 0 @defdep_fragment_definitions.each_value do |v| max_loops += v.size end loops = 0 # Instead of tracking independent fragments _as you visit_, # determine them at the end. This way, we can treat fragments with the # same name as if they were the same name. If _any_ of the fragments # with that name has a dependency, we record it. independent_fragment_nodes = @defdep_fragment_definitions.values.flatten - @defdep_immediate_dependencies.keys visited_fragment_names = Set.new while fragment_node = independent_fragment_nodes.pop if visited_fragment_names.add?(fragment_node.name) # this is a new fragment name else # this is a duplicate fragment name next end loops += 1 if loops > max_loops raise("Resolution loops exceeded the number of definitions; infinite loop detected. (Max: #{max_loops}, Current: #{loops})") end # Since it's independent, let's remove it from here. # That way, we can use the remainder to identify cycles @defdep_immediate_dependencies.delete(fragment_node) fragment_usages = @defdep_dependent_definitions[fragment_node.name] if fragment_usages.empty? # If we didn't record any usages during the visit, # then this fragment is unused. dependency_map.unused_dependencies << @defdep_node_paths[fragment_node] else fragment_usages.each do |definition_node| # Register the dependency AND second-order dependencies dependency_map[definition_node] << fragment_node dependency_map[definition_node].concat(dependency_map[fragment_node]) # Since we've registered it, remove it from our to-do list deps = @defdep_immediate_dependencies[definition_node] # Can't find a way to _just_ delete from `deps` and return the deleted entries removed, remaining = deps.partition { |spread| spread.name == fragment_node.name } @defdep_immediate_dependencies[definition_node] = remaining if block_given? yield(definition_node, removed, fragment_node) end if remaining.empty? && definition_node.is_a?(GraphQL::Language::Nodes::FragmentDefinition) && definition_node.name != fragment_node.name # If all of this definition's dependencies have # been resolved, we can now resolve its # own dependents. # # But, it's possible to have a duplicate-named fragment here. # Skip it in that case independent_fragment_nodes << definition_node end end end end # If any dependencies were _unmet_ # (eg, spreads with no corresponding definition) # then they're still in there @defdep_immediate_dependencies.each do |defn_node, deps| deps.each do |spread| if !@defdep_fragment_definitions.key?(spread.name) dependency_map.unmet_dependencies[@defdep_node_paths[defn_node]] << @defdep_node_paths[spread] deps.delete(spread) end end if deps.empty? @defdep_immediate_dependencies.delete(defn_node) end end # Anything left in @immediate_dependencies is cyclical cyclical_nodes = @defdep_immediate_dependencies.keys.map { |n| @defdep_node_paths[n] } # @immediate_dependencies also includes operation names, but we don't care about # those. They became nil when we looked them up on `@fragment_definitions`, so remove them. cyclical_nodes.compact! dependency_map.cyclical_definitions.concat(cyclical_nodes) dependency_map end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/error.rb000066400000000000000000000022171514115062600236130ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation # Generates GraphQL-compliant validation message. class Error # Convenience for validators module ErrorHelper # Error `error_message` is located at `node` def error(error_message, nodes, context: nil, path: nil, extensions: {}) path ||= context.path nodes = Array(nodes) GraphQL::StaticValidation::Error.new(error_message, nodes: nodes, path: path) end end attr_reader :message attr_accessor :path def initialize(message, path: nil, nodes: []) @message = message @nodes = Array(nodes) @path = path end # A hash representation of this Message def to_h { "message" => message, "locations" => locations }.tap { |h| h["path"] = path unless path.nil? } end attr_reader :nodes private def locations nodes.map do |node| h = {"line" => node.line, "column" => node.col} h["filename"] = node.filename if node.filename h end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/interpreter_visitor.rb000066400000000000000000000004701514115062600266030ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class InterpreterVisitor < BaseVisitor include(GraphQL::StaticValidation::DefinitionDependencies) StaticValidation::ALL_RULES.reverse_each do |r| include(r) end include(ContextMethods) end end end graphql-ruby-2.5.19/lib/graphql/static_validation/literal_validator.rb000066400000000000000000000134251514115062600261660ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation # Test whether `ast_value` is a valid input for `type` class LiteralValidator def initialize(context:) @context = context @types = context.types @invalid_response = GraphQL::Query::InputValidationResult.new(valid: false, problems: []) @valid_response = GraphQL::Query::InputValidationResult.new(valid: true, problems: []) end def validate(ast_value, type) catch(:invalid) do recursively_validate(ast_value, type) end end private def replace_nulls_in(ast_value) case ast_value when Array ast_value.map { |v| replace_nulls_in(v) } when GraphQL::Language::Nodes::InputObject ast_value.to_h when GraphQL::Language::Nodes::NullValue nil else ast_value end end def recursively_validate(ast_value, type) if type.nil? # this means we're an undefined argument, see #present_input_field_values_are_valid maybe_raise_if_invalid(ast_value) do @invalid_response end elsif ast_value.is_a?(GraphQL::Language::Nodes::NullValue) maybe_raise_if_invalid(ast_value) do type.kind.non_null? ? @invalid_response : @valid_response end elsif type.kind.non_null? maybe_raise_if_invalid(ast_value) do ast_value.nil? ? @invalid_response : recursively_validate(ast_value, type.of_type) end elsif type.kind.list? item_type = type.of_type results = ensure_array(ast_value).map { |val| recursively_validate(val, item_type) } merge_results(results) elsif ast_value.is_a?(GraphQL::Language::Nodes::VariableIdentifier) @valid_response elsif type.kind.scalar? && constant_scalar?(ast_value) maybe_raise_if_invalid(ast_value) do ruby_value = replace_nulls_in(ast_value) type.validate_input(ruby_value, @context) end elsif type.kind.enum? maybe_raise_if_invalid(ast_value) do if ast_value.is_a?(GraphQL::Language::Nodes::Enum) type.validate_input(ast_value.name, @context) else # if our ast_value isn't an Enum it's going to be invalid so return false @invalid_response end end elsif type.kind.input_object? && ast_value.is_a?(GraphQL::Language::Nodes::InputObject) maybe_raise_if_invalid(ast_value) do merge_results([ required_input_fields_are_present(type, ast_value), present_input_field_values_are_valid(type, ast_value) ]) end else maybe_raise_if_invalid(ast_value) do @invalid_response end end end # When `error_bubbling` is false, we want to bail on the first failure that we find. # Use `throw` to escape the current call stack, returning the invalid response. def maybe_raise_if_invalid(ast_value) ret = yield if !@context.schema.error_bubbling && !ret.valid? throw(:invalid, ret) else ret end end # The GraphQL grammar supports variables embedded within scalars but graphql.js # doesn't support it so we won't either for simplicity def constant_scalar?(ast_value) if ast_value.is_a?(GraphQL::Language::Nodes::VariableIdentifier) false elsif ast_value.is_a?(Array) ast_value.all? { |element| constant_scalar?(element) } elsif ast_value.is_a?(GraphQL::Language::Nodes::InputObject) ast_value.arguments.all? { |arg| constant_scalar?(arg.value) } else true end end def required_input_fields_are_present(type, ast_node) # TODO - would be nice to use these to create an error message so the caller knows # that required fields are missing required_field_names = @types.arguments(type) .select { |argument| argument.type.kind.non_null? && !argument.default_value? } .map!(&:name) present_field_names = ast_node.arguments.map(&:name) missing_required_field_names = required_field_names - present_field_names if @context.schema.error_bubbling missing_required_field_names.empty? ? @valid_response : @invalid_response else results = missing_required_field_names.map do |name| arg_type = @types.argument(type, name).type recursively_validate(GraphQL::Language::Nodes::NullValue.new(name: name), arg_type) end if type.one_of? && ast_node.arguments.size != 1 results << Query::InputValidationResult.from_problem("`#{type.graphql_name}` is a OneOf type, so only one argument may be given (instead of #{ast_node.arguments.size})") end merge_results(results) end end def present_input_field_values_are_valid(type, ast_node) results = ast_node.arguments.map do |value| field = @types.argument(type, value.name) # we want to call validate on an argument even if it's an invalid one # so that our raise exception is on it instead of the entire InputObject field_type = field && field.type recursively_validate(value.value, field_type) end merge_results(results) end def ensure_array(value) value.is_a?(Array) ? value : [value] end def merge_results(results_list) merged_result = Query::InputValidationResult.new results_list.each do |inner_result| merged_result.merge_result!([], inner_result) end merged_result end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/000077500000000000000000000000001514115062600232655ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb000066400000000000000000000045221514115062600323640ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module ArgumentLiteralsAreCompatible def on_argument(node, parent) # Check the child arguments first; # don't add a new error if one of them reports an error super # Don't validate variables here if node.value.is_a?(GraphQL::Language::Nodes::VariableIdentifier) return end if @context.schema.error_bubbling || context.errors.none? { |err| err.path.take(@path.size) == @path } parent_defn = parent_definition(parent) if parent_defn && (arg_defn = @types.argument(parent_defn, node.name)) validation_result = context.validate_literal(node.value, arg_defn.type) if !validation_result.valid? kind_of_node = node_type(parent) error_arg_name = parent_name(parent, parent_defn) string_value = if node.value == Float::INFINITY "" else " (#{GraphQL::Language::Printer.new.print(node.value)})" end problems = validation_result.problems first_problem = problems && problems.first if first_problem message = first_problem["message"] # This is some legacy stuff from when `CoercionError` was raised thru the stack if message coerce_extensions = first_problem["extensions"] || { "code" => "argumentLiteralsIncompatible" } end end error_options = { nodes: parent, type: kind_of_node, argument_name: node.name, argument: arg_defn, value: node.value } if coerce_extensions error_options[:coerce_extensions] = coerce_extensions end message ||= "Argument '#{node.name}' on #{kind_of_node} '#{error_arg_name}' has an invalid value#{string_value}. Expected type '#{arg_defn.type.to_type_signature}'." error = GraphQL::StaticValidation::ArgumentLiteralsAreCompatibleError.new( message, **error_options ) add_error(error) end end end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/argument_literals_are_compatible_error.rb000066400000000000000000000025711514115062600335770ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class ArgumentLiteralsAreCompatibleError < StaticValidation::Error attr_reader :type_name attr_reader :argument_name attr_reader :argument attr_reader :value def initialize(message, path: nil, nodes: [], type:, argument_name: nil, extensions: nil, coerce_extensions: nil, argument: nil, value: nil) super(message, path: path, nodes: nodes) @type_name = type @argument_name = argument_name @extensions = extensions @coerce_extensions = coerce_extensions @argument = argument @value = value end # A hash representation of this Message def to_h if @coerce_extensions extensions = @coerce_extensions # This is for legacy compat -- but this key is supposed to be a GraphQL type name :confounded: extensions["typeName"] = "CoercionError" else extensions = { "code" => code, "typeName" => type_name } if argument_name extensions["argumentName"] = argument_name end end extensions.merge!(@extensions) unless @extensions.nil? super.merge({ "extensions" => extensions }) end def code "argumentLiteralsIncompatible" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/argument_names_are_unique.rb000066400000000000000000000015671514115062600310450ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module ArgumentNamesAreUnique include GraphQL::StaticValidation::Error::ErrorHelper def on_field(node, parent) validate_arguments(node) super end def on_directive(node, parent) validate_arguments(node) super end def validate_arguments(node) argument_defns = node.arguments if !argument_defns.empty? args_by_name = Hash.new { |h, k| h[k] = [] } argument_defns.each { |a| args_by_name[a.name] << a } args_by_name.each do |name, defns| if defns.size > 1 add_error(GraphQL::StaticValidation::ArgumentNamesAreUniqueError.new("There can be only one argument named \"#{name}\"", nodes: defns, name: name)) end end end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/argument_names_are_unique_error.rb000066400000000000000000000011271514115062600322460ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class ArgumentNamesAreUniqueError < StaticValidation::Error attr_reader :name def initialize(message, path: nil, nodes: [], name:) super(message, path: path, nodes: nodes) @name = name end # A hash representation of this Message def to_h extensions = { "code" => code, "name" => name } super.merge({ "extensions" => extensions }) end def code "argumentNotUnique" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/arguments_are_defined.rb000066400000000000000000000044141514115062600301270ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module ArgumentsAreDefined def on_argument(node, parent) parent_defn = parent_definition(parent) if parent_defn && @types.argument(parent_defn, node.name) super elsif parent_defn kind_of_node = node_type(parent) error_arg_name = parent_name(parent, parent_defn) arg_names = context.types.arguments(parent_defn).map(&:graphql_name) add_error(GraphQL::StaticValidation::ArgumentsAreDefinedError.new( "#{kind_of_node} '#{error_arg_name}' doesn't accept argument '#{node.name}'#{context.did_you_mean_suggestion(node.name, arg_names)}", nodes: node, name: error_arg_name, type: kind_of_node, argument_name: node.name, parent: parent_defn )) else # Some other weird error super end end private # TODO smell: these methods are added to all visitors, since they're included in a module. def parent_name(parent, type_defn) case parent when GraphQL::Language::Nodes::Field parent.alias || parent.name when GraphQL::Language::Nodes::InputObject type_defn.graphql_name when GraphQL::Language::Nodes::Argument, GraphQL::Language::Nodes::Directive parent.name else raise "Invariant: Unexpected parent #{parent.inspect} (#{parent.class})" end end def node_type(parent) parent.class.name.split("::").last end def parent_definition(parent) case parent when GraphQL::Language::Nodes::InputObject arg_defn = context.argument_definition if arg_defn.nil? nil else arg_ret_type = arg_defn.type.unwrap if arg_ret_type.kind.input_object? arg_ret_type else nil end end when GraphQL::Language::Nodes::Directive context.schema_directives[parent.name] when GraphQL::Language::Nodes::Field context.field_definition else raise "Unexpected argument parent: #{parent.class} (##{parent})" end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/arguments_are_defined_error.rb000066400000000000000000000015651514115062600313440ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class ArgumentsAreDefinedError < StaticValidation::Error attr_reader :name attr_reader :type_name attr_reader :argument_name attr_reader :parent def initialize(message, path: nil, nodes: [], name:, type:, argument_name:, parent:) super(message, path: path, nodes: nodes) @name = name @type_name = type @argument_name = argument_name @parent = parent end # A hash representation of this Message def to_h extensions = { "code" => code, "name" => name, "typeName" => type_name, "argumentName" => argument_name } super.merge({ "extensions" => extensions }) end def code "argumentNotAccepted" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/directives_are_defined.rb000066400000000000000000000015431514115062600302630ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module DirectivesAreDefined def initialize(*) super end def on_directive(node, parent) if !@types.directive_exists?(node.name) @directives_are_defined_errors_by_name ||= {} error = @directives_are_defined_errors_by_name[node.name] ||= begin @directive_names ||= @types.directives.map(&:graphql_name) err = GraphQL::StaticValidation::DirectivesAreDefinedError.new( "Directive @#{node.name} is not defined#{context.did_you_mean_suggestion(node.name, @directive_names)}", nodes: [], directive: node.name ) add_error(err) err end error.nodes << node else super end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/directives_are_defined_error.rb000066400000000000000000000012061514115062600314700ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class DirectivesAreDefinedError < StaticValidation::Error attr_reader :directive_name def initialize(message, path: nil, nodes: [], directive:) super(message, path: path, nodes: nodes) @directive_name = directive end # A hash representation of this Message def to_h extensions = { "code" => code, "directiveName" => directive_name } super.merge({ "extensions" => extensions }) end def code "undefinedDirective" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb000066400000000000000000000056641514115062600325350ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module DirectivesAreInValidLocations include GraphQL::Language def on_directive(node, parent) validate_location(node, parent, context.schema_directives) super end private LOCATION_MESSAGE_NAMES = { GraphQL::Schema::Directive::QUERY => "queries", GraphQL::Schema::Directive::MUTATION => "mutations", GraphQL::Schema::Directive::SUBSCRIPTION => "subscriptions", GraphQL::Schema::Directive::FIELD => "fields", GraphQL::Schema::Directive::FRAGMENT_DEFINITION => "fragment definitions", GraphQL::Schema::Directive::FRAGMENT_SPREAD => "fragment spreads", GraphQL::Schema::Directive::INLINE_FRAGMENT => "inline fragments", GraphQL::Schema::Directive::VARIABLE_DEFINITION => "variable definitions", } SIMPLE_LOCATIONS = { Nodes::Field => GraphQL::Schema::Directive::FIELD, Nodes::InlineFragment => GraphQL::Schema::Directive::INLINE_FRAGMENT, Nodes::FragmentSpread => GraphQL::Schema::Directive::FRAGMENT_SPREAD, Nodes::FragmentDefinition => GraphQL::Schema::Directive::FRAGMENT_DEFINITION, Nodes::VariableDefinition => GraphQL::Schema::Directive::VARIABLE_DEFINITION, } SIMPLE_LOCATION_NODES = SIMPLE_LOCATIONS.keys def validate_location(ast_directive, ast_parent, directives) directive_defn = directives[ast_directive.name] case ast_parent when Nodes::OperationDefinition required_location = GraphQL::Schema::Directive.const_get(ast_parent.operation_type.upcase) assert_includes_location(directive_defn, ast_directive, required_location) when *SIMPLE_LOCATION_NODES required_location = SIMPLE_LOCATIONS[ast_parent.class] assert_includes_location(directive_defn, ast_directive, required_location) else add_error(GraphQL::StaticValidation::DirectivesAreInValidLocationsError.new( "Directives can't be applied to #{ast_parent.class.name}s", nodes: ast_directive, target: ast_parent.class.name )) end end def assert_includes_location(directive_defn, directive_ast, required_location) if !directive_defn.locations.include?(required_location) location_name = LOCATION_MESSAGE_NAMES[required_location] allowed_location_names = directive_defn.locations.map { |loc| LOCATION_MESSAGE_NAMES[loc] } add_error(GraphQL::StaticValidation::DirectivesAreInValidLocationsError.new( "'@#{directive_defn.graphql_name}' can't be applied to #{location_name} (allowed: #{allowed_location_names.join(", ")})", nodes: directive_ast, target: location_name, name: directive_defn.graphql_name )) end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/directives_are_in_valid_locations_error.rb000066400000000000000000000013511514115062600337330ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class DirectivesAreInValidLocationsError < StaticValidation::Error attr_reader :target_name attr_reader :name def initialize(message, path: nil, nodes: [], target:, name: nil) super(message, path: path, nodes: nodes) @target_name = target @name = name end # A hash representation of this Message def to_h extensions = { "code" => code, "targetName" => target_name }.tap { |h| h["name"] = name unless name.nil? } super.merge({ "extensions" => extensions }) end def code "directiveCannotBeApplied" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb000066400000000000000000000025521514115062600311260ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module FieldsAreDefinedOnType def on_field(node, parent) parent_type = @object_types[-2] field = context.query.types.field(parent_type, node.name) if field.nil? if parent_type.kind.union? add_error(GraphQL::StaticValidation::FieldsHaveAppropriateSelectionsError.new( "Selections can't be made directly on unions (see selections on #{parent_type.graphql_name})", nodes: parent, node_name: parent_type.graphql_name )) else possible_fields = possible_fields(context, parent_type) suggestion = context.did_you_mean_suggestion(node.name, possible_fields) message = "Field '#{node.name}' doesn't exist on type '#{parent_type.graphql_name}'#{suggestion}" add_error(GraphQL::StaticValidation::FieldsAreDefinedOnTypeError.new( message, nodes: node, field: node.name, type: parent_type.graphql_name )) end else super end end private def possible_fields(context, parent_type) return EmptyObjects::EMPTY_ARRAY if parent_type.kind.leaf? context.types.fields(parent_type).map(&:graphql_name) end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/fields_are_defined_on_type_error.rb000066400000000000000000000013151514115062600323330ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class FieldsAreDefinedOnTypeError < StaticValidation::Error attr_reader :type_name attr_reader :field_name def initialize(message, path: nil, nodes: [], type:, field:) super(message, path: path, nodes: nodes) @type_name = type @field_name = field end # A hash representation of this Message def to_h extensions = { "code" => code, "typeName" => type_name, "fieldName" => field_name } super.merge({ "extensions" => extensions }) end def code "undefinedField" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb000066400000000000000000000107031514115062600327220ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation # Scalars _can't_ have selections # Objects _must_ have selections module FieldsHaveAppropriateSelections include GraphQL::StaticValidation::Error::ErrorHelper def on_field(node, parent) field_defn = field_definition if validate_field_selections(node, field_defn.type.unwrap) super end end def on_operation_definition(node, _parent) if validate_field_selections(node, type_definition) super end end private def validate_field_selections(ast_node, resolved_type) msg = if resolved_type.nil? nil elsif resolved_type.kind.leaf? if !ast_node.selections.empty? selection_strs = ast_node.selections.map do |n| case n when GraphQL::Language::Nodes::InlineFragment "\"... on #{n.type.name} { ... }\"" when GraphQL::Language::Nodes::Field "\"#{n.name}\"" when GraphQL::Language::Nodes::FragmentSpread "\"#{n.name}\"" else raise "Invariant: unexpected selection node: #{n}" end end "Selections can't be made on #{resolved_type.kind.name.sub("_", " ").downcase}s (%{node_name} returns #{resolved_type.graphql_name} but has selections [#{selection_strs.join(", ")}])" else nil end elsif ast_node.selections.empty? return_validation_error = true legacy_invalid_empty_selection_result = nil if !resolved_type.kind.fields? case @schema.allow_legacy_invalid_empty_selections_on_union when true legacy_invalid_empty_selection_result = @schema.legacy_invalid_empty_selections_on_union_with_type(@context.query, resolved_type) case legacy_invalid_empty_selection_result when :return_validation_error # keep `return_validation_error = true` when String return_validation_error = false # the string is returned below when nil # No error: return_validation_error = false legacy_invalid_empty_selection_result = nil else raise GraphQL::InvariantError, "Unexpected return value from legacy_invalid_empty_selections_on_union_with_type, must be `:return_validation_error`, String, or nil (got: #{legacy_invalid_empty_selection_result.inspect})" end when false # pass -- error below else return_validation_error = false @context.query.logger.warn("Unions require selections but #{ast_node.alias || ast_node.name} (#{resolved_type.graphql_name}) doesn't have any. This will fail with a validation error on a future GraphQL-Ruby version. More info: https://graphql-ruby.org/api-doc/#{GraphQL::VERSION}/GraphQL/Schema.html#allow_legacy_invalid_empty_selections_on_union-class_method") end end if return_validation_error "Field must have selections (%{node_name} returns #{resolved_type.graphql_name} but has no selections. Did you mean '#{ast_node.name} { ... }'?)" else legacy_invalid_empty_selection_result end else nil end if msg node_name = case ast_node when GraphQL::Language::Nodes::Field "field '#{ast_node.name}'" when GraphQL::Language::Nodes::OperationDefinition if ast_node.name.nil? "anonymous query" else "#{ast_node.operation_type} '#{ast_node.name}'" end else raise("Unexpected node #{ast_node}") end extensions = { "rule": "StaticValidation::FieldsHaveAppropriateSelections", "name": node_name.to_s } unless resolved_type.nil? extensions["type"] = resolved_type.to_type_signature end add_error(GraphQL::StaticValidation::FieldsHaveAppropriateSelectionsError.new( msg % { node_name: node_name }, nodes: ast_node, node_name: node_name.to_s, type: resolved_type.nil? ? nil : resolved_type.graphql_name, )) false else true end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/fields_have_appropriate_selections_error.rb000066400000000000000000000013721514115062600341350ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class FieldsHaveAppropriateSelectionsError < StaticValidation::Error attr_reader :type_name attr_reader :node_name def initialize(message, path: nil, nodes: [], node_name:, type: nil) super(message, path: path, nodes: nodes) @node_name = node_name @type_name = type end # A hash representation of this Message def to_h extensions = { "code" => code, "nodeName" => node_name }.tap { |h| h["typeName"] = type_name unless type_name.nil? } super.merge({ "extensions" => extensions }) end def code "selectionMismatch" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/fields_will_merge.rb000066400000000000000000000425631514115062600273000ustar00rootroot00000000000000 # frozen_string_literal: true # frozen_string_literal: true module GraphQL module StaticValidation module FieldsWillMerge # Validates that a selection set is valid if all fields (including spreading any # fragments) either correspond to distinct response names or can be merged # without ambiguity. # # Original Algorithm: https://github.com/graphql/graphql-js/blob/master/src/validation/rules/OverlappingFieldsCanBeMerged.js NO_ARGS = GraphQL::EmptyObjects::EMPTY_HASH Field = Struct.new(:node, :definition, :owner_type, :parents) FragmentSpread = Struct.new(:name, :parents) def initialize(*) super @visited_fragments = {} @compared_fragments = {} @conflict_count = 0 end def on_operation_definition(node, _parent) setting_errors { conflicts_within_selection_set(node, type_definition) } super end def on_field(node, _parent) setting_errors { conflicts_within_selection_set(node, type_definition) } super end private def conflicts @conflicts ||= Hash.new do |h, error_type| h[error_type] = Hash.new do |h2, field_name| h2[field_name] = GraphQL::StaticValidation::FieldsWillMergeError.new(kind: error_type, field_name: field_name) end end end def setting_errors @conflicts = nil yield # don't initialize these if they weren't initialized in the block: @conflicts&.each_value { |error_type| error_type.each_value { |error| add_error(error) } } end def conflicts_within_selection_set(node, parent_type) return if parent_type.nil? fields, fragment_spreads = fields_and_fragments_from_selection(node, owner_type: parent_type, parents: nil) # (A) Find find all conflicts "within" the fields of this selection set. find_conflicts_within(fields) fragment_spreads.each_with_index do |fragment_spread, i| are_mutually_exclusive = mutually_exclusive?( fragment_spread.parents, [parent_type] ) # (B) Then find conflicts between these fields and those represented by # each spread fragment name found. find_conflicts_between_fields_and_fragment( fragment_spread, fields, mutually_exclusive: are_mutually_exclusive, ) # (C) Then compare this fragment with all other fragments found in this # selection set to collect conflicts between fragments spread together. # This compares each item in the list of fragment names to every other # item in that same list (except for itself). fragment_spreads[i + 1..-1].each do |fragment_spread2| are_mutually_exclusive = mutually_exclusive?( fragment_spread.parents, fragment_spread2.parents ) find_conflicts_between_fragments( fragment_spread, fragment_spread2, mutually_exclusive: are_mutually_exclusive, ) end end end def find_conflicts_between_fragments(fragment_spread1, fragment_spread2, mutually_exclusive:) fragment_name1 = fragment_spread1.name fragment_name2 = fragment_spread2.name return if fragment_name1 == fragment_name2 cache_key = compared_fragments_key( fragment_name1, fragment_name2, mutually_exclusive, ) if @compared_fragments.key?(cache_key) return else @compared_fragments[cache_key] = true end fragment1 = context.fragments[fragment_name1] fragment2 = context.fragments[fragment_name2] return if fragment1.nil? || fragment2.nil? fragment_type1 = context.query.types.type(fragment1.type.name) fragment_type2 = context.query.types.type(fragment2.type.name) return if fragment_type1.nil? || fragment_type2.nil? fragment_fields1, fragment_spreads1 = fields_and_fragments_from_selection( fragment1, owner_type: fragment_type1, parents: [*fragment_spread1.parents, fragment_type1] ) fragment_fields2, fragment_spreads2 = fields_and_fragments_from_selection( fragment2, owner_type: fragment_type1, parents: [*fragment_spread2.parents, fragment_type2] ) # (F) First, find all conflicts between these two collections of fields # (not including any nested fragments). find_conflicts_between( fragment_fields1, fragment_fields2, mutually_exclusive: mutually_exclusive, ) # (G) Then collect conflicts between the first fragment and any nested # fragments spread in the second fragment. fragment_spreads2.each do |fragment_spread| find_conflicts_between_fragments( fragment_spread1, fragment_spread, mutually_exclusive: mutually_exclusive, ) end # (G) Then collect conflicts between the first fragment and any nested # fragments spread in the second fragment. fragment_spreads1.each do |fragment_spread| find_conflicts_between_fragments( fragment_spread2, fragment_spread, mutually_exclusive: mutually_exclusive, ) end end def find_conflicts_between_fields_and_fragment(fragment_spread, fields, mutually_exclusive:) fragment_name = fragment_spread.name return if @visited_fragments.key?(fragment_name) @visited_fragments[fragment_name] = true fragment = context.fragments[fragment_name] return if fragment.nil? fragment_type = @types.type(fragment.type.name) return if fragment_type.nil? fragment_fields, fragment_spreads = fields_and_fragments_from_selection(fragment, owner_type: fragment_type, parents: [*fragment_spread.parents, fragment_type]) # (D) First find any conflicts between the provided collection of fields # and the collection of fields represented by the given fragment. find_conflicts_between( fields, fragment_fields, mutually_exclusive: mutually_exclusive, ) # (E) Then collect any conflicts between the provided collection of fields # and any fragment names found in the given fragment. fragment_spreads.each do |fragment_spread| find_conflicts_between_fields_and_fragment( fragment_spread, fields, mutually_exclusive: mutually_exclusive, ) end end def find_conflicts_within(response_keys) response_keys.each do |key, fields| next if fields.size < 2 # find conflicts within nodes i = 0 while i < fields.size j = i + 1 while j < fields.size find_conflict(key, fields[i], fields[j]) j += 1 end i += 1 end end end def find_conflict(response_key, field1, field2, mutually_exclusive: false) return if @conflict_count >= context.max_errors return if field1.definition.nil? || field2.definition.nil? node1 = field1.node node2 = field2.node are_mutually_exclusive = mutually_exclusive || mutually_exclusive?(field1.parents, field2.parents) if !are_mutually_exclusive if node1.name != node2.name conflict = conflicts[:field][response_key] conflict.add_conflict(node1, node1.name) conflict.add_conflict(node2, node2.name) @conflict_count += 1 end if !same_arguments?(node1, node2) conflict = conflicts[:argument][response_key] conflict.add_conflict(node1, GraphQL::Language.serialize(serialize_field_args(node1))) conflict.add_conflict(node2, GraphQL::Language.serialize(serialize_field_args(node2))) @conflict_count += 1 end end if !conflicts[:field].key?(response_key) && (t1 = field1.definition&.type) && (t2 = field2.definition&.type) && return_types_conflict?(t1, t2) return_error = nil message_override = nil case @schema.allow_legacy_invalid_return_type_conflicts when false return_error = true when true legacy_handling = @schema.legacy_invalid_return_type_conflicts(@context.query, t1, t2, node1, node2) case legacy_handling when nil return_error = false when :return_validation_error return_error = true when String return_error = true message_override = legacy_handling else raise GraphQL::Error, "#{@schema}.legacy_invalid_scalar_conflicts returned unexpected value: #{legacy_handling.inspect}. Expected `nil`, String, or `:return_validation_error`." end else return_error = false @context.query.logger.warn <<~WARN GraphQL-Ruby encountered mismatched types in this query: `#{t1.to_type_signature}` (at #{node1.line}:#{node1.col}) vs. `#{t2.to_type_signature}` (at #{node2.line}:#{node2.col}). This will return an error in future GraphQL-Ruby versions, as per the GraphQL specification Learn about migrating here: https://graphql-ruby.org/api-doc/#{GraphQL::VERSION}/GraphQL/Schema.html#allow_legacy_invalid_return_type_conflicts-class_method WARN end if return_error conflict = conflicts[:return_type][response_key] if message_override conflict.message = message_override end conflict.add_conflict(node1, "`#{t1.to_type_signature}`") conflict.add_conflict(node2, "`#{t2.to_type_signature}`") @conflict_count += 1 end end find_conflicts_between_sub_selection_sets( field1, field2, mutually_exclusive: are_mutually_exclusive, ) end def return_types_conflict?(type1, type2) if type1.list? if type2.list? return_types_conflict?(type1.of_type, type2.of_type) else true end elsif type2.list? true elsif type1.non_null? if type2.non_null? return_types_conflict?(type1.of_type, type2.of_type) else true end elsif type2.non_null? true elsif type1.kind.leaf? && type2.kind.leaf? type1 != type2 else # One or more of these are composite types, # their selections will be validated later on. false end end def find_conflicts_between_sub_selection_sets(field1, field2, mutually_exclusive:) return if field1.definition.nil? || field2.definition.nil? || (field1.node.selections.empty? && field2.node.selections.empty?) return_type1 = field1.definition.type.unwrap return_type2 = field2.definition.type.unwrap parents1 = [return_type1] parents2 = [return_type2] fields, fragment_spreads = fields_and_fragments_from_selection( field1.node, owner_type: return_type1, parents: parents1 ) fields2, fragment_spreads2 = fields_and_fragments_from_selection( field2.node, owner_type: return_type2, parents: parents2 ) # (H) First, collect all conflicts between these two collections of field. find_conflicts_between(fields, fields2, mutually_exclusive: mutually_exclusive) # (I) Then collect conflicts between the first collection of fields and # those referenced by each fragment name associated with the second. fragment_spreads2.each do |fragment_spread| find_conflicts_between_fields_and_fragment( fragment_spread, fields, mutually_exclusive: mutually_exclusive, ) end # (I) Then collect conflicts between the second collection of fields and # those referenced by each fragment name associated with the first. fragment_spreads.each do |fragment_spread| find_conflicts_between_fields_and_fragment( fragment_spread, fields2, mutually_exclusive: mutually_exclusive, ) end # (J) Also collect conflicts between any fragment names by the first and # fragment names by the second. This compares each item in the first set of # names to each item in the second set of names. fragment_spreads.each do |frag1| fragment_spreads2.each do |frag2| find_conflicts_between_fragments( frag1, frag2, mutually_exclusive: mutually_exclusive ) end end end def find_conflicts_between(response_keys, response_keys2, mutually_exclusive:) response_keys.each do |key, fields| fields2 = response_keys2[key] if fields2 fields.each do |field| fields2.each do |field2| find_conflict( key, field, field2, mutually_exclusive: mutually_exclusive, ) end end end end end NO_SELECTIONS = [GraphQL::EmptyObjects::EMPTY_HASH, GraphQL::EmptyObjects::EMPTY_ARRAY].freeze def fields_and_fragments_from_selection(node, owner_type:, parents:) if node.selections.empty? NO_SELECTIONS else parents ||= [] fields, fragment_spreads = find_fields_and_fragments(node.selections, owner_type: owner_type, parents: parents, fields: [], fragment_spreads: []) response_keys = fields.group_by { |f| f.node.alias || f.node.name } [response_keys, fragment_spreads] end end def find_fields_and_fragments(selections, owner_type:, parents:, fields:, fragment_spreads:) selections.each do |node| case node when GraphQL::Language::Nodes::Field definition = @types.field(owner_type, node.name) fields << Field.new(node, definition, owner_type, parents) when GraphQL::Language::Nodes::InlineFragment fragment_type = node.type ? @types.type(node.type.name) : owner_type find_fields_and_fragments(node.selections, parents: [*parents, fragment_type], owner_type: fragment_type, fields: fields, fragment_spreads: fragment_spreads) if fragment_type when GraphQL::Language::Nodes::FragmentSpread fragment_spreads << FragmentSpread.new(node.name, parents) end end [fields, fragment_spreads] end def same_arguments?(field1, field2) # Check for incompatible / non-identical arguments on this node: arguments1 = field1.arguments arguments2 = field2.arguments return false if arguments1.length != arguments2.length arguments1.all? do |argument1| argument2 = arguments2.find { |argument| argument.name == argument1.name } return false if argument2.nil? serialize_arg(argument1.value) == serialize_arg(argument2.value) end end def serialize_arg(arg_value) case arg_value when GraphQL::Language::Nodes::AbstractNode arg_value.to_query_string when Array "[#{arg_value.map { |a| serialize_arg(a) }.join(", ")}]" else GraphQL::Language.serialize(arg_value) end end def serialize_field_args(field) serialized_args = {} field.arguments.each do |argument| serialized_args[argument.name] = serialize_arg(argument.value) end serialized_args end def compared_fragments_key(frag1, frag2, exclusive) # Cache key to not compare two fragments more than once. # The key includes both fragment names sorted (this way we # avoid computing "A vs B" and "B vs A"). It also includes # "exclusive" since the result may change depending on the parent_type "#{[frag1, frag2].sort.join('-')}-#{exclusive}" end # Given two list of parents, find out if they are mutually exclusive # In this context, `parents` represents the "self scope" of the field, # what types may be found at this point in the query. def mutually_exclusive?(parents1, parents2) if parents1.empty? || parents2.empty? false elsif parents1.length == parents2.length parents1.length.times.any? do |i| type1 = parents1[i - 1] type2 = parents2[i - 1] if type1 == type2 # If the types we're comparing are the same type, # then they aren't mutually exclusive false else # Check if these two scopes have _any_ types in common. possible_right_types = context.types.possible_types(type1) possible_left_types = context.types.possible_types(type2) (possible_right_types & possible_left_types).empty? end end else true end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/fields_will_merge_error.rb000066400000000000000000000026361514115062600305060ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class FieldsWillMergeError < StaticValidation::Error attr_reader :field_name attr_reader :kind def initialize(kind:, field_name:) super(nil) @field_name = field_name @kind = kind @conflicts = [] end def message @message || "Field '#{field_name}' has #{kind == :argument ? 'an' : 'a'} #{kind} conflict: #{conflicts}?" end attr_writer :message def path [] end def conflicts @conflicts.join(' or ') end def add_conflict(node, conflict_str) # Can't use `.include?` here because AST nodes implement `#==` # based on string value, not including location. But sometimes, # identical nodes conflict because of their differing return types. if nodes.any? { |n| n == node && n.line == node.line && n.col == node.col } # already have an error for this node return end @nodes << node @conflicts << conflict_str end # A hash representation of this Message def to_h extensions = { "code" => code, "fieldName" => field_name, "conflicts" => conflicts } super.merge({ "extensions" => extensions }) end def code "fieldConflict" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/fragment_names_are_unique.rb000066400000000000000000000013301514115062600310120ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module FragmentNamesAreUnique def initialize(*) super @fragments_by_name = Hash.new { |h, k| h[k] = [] } end def on_fragment_definition(node, parent) @fragments_by_name[node.name] << node super end def on_document(_n, _p) super @fragments_by_name.each do |name, fragments| if fragments.length > 1 add_error(GraphQL::StaticValidation::FragmentNamesAreUniqueError.new( %|Fragment name "#{name}" must be unique|, nodes: fragments, name: name )) end end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/fragment_names_are_unique_error.rb000066400000000000000000000011711514115062600322260ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class FragmentNamesAreUniqueError < StaticValidation::Error attr_reader :fragment_name def initialize(message, path: nil, nodes: [], name:) super(message, path: path, nodes: nodes) @fragment_name = name end # A hash representation of this Message def to_h extensions = { "code" => code, "fragmentName" => fragment_name } super.merge({ "extensions" => extensions }) end def code "fragmentNotUnique" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb000066400000000000000000000046431514115062600316740ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module FragmentSpreadsArePossible def initialize(*) super @spreads_to_validate = [] end def on_inline_fragment(node, parent) fragment_parent = context.object_types[-2] fragment_child = context.object_types.last if fragment_child validate_fragment_in_scope(fragment_parent, fragment_child, node, context, context.path) end super end def on_fragment_spread(node, parent) fragment_parent = context.object_types.last @spreads_to_validate << FragmentSpread.new(node: node, parent_type: fragment_parent, path: context.path) super end def on_document(node, parent) super @spreads_to_validate.each do |frag_spread| frag_node = context.fragments[frag_spread.node.name] if frag_node fragment_child_name = frag_node.type.name fragment_child = @types.type(fragment_child_name) # Might be non-existent type name if fragment_child validate_fragment_in_scope(frag_spread.parent_type, fragment_child, frag_spread.node, context, frag_spread.path) end end end end private def validate_fragment_in_scope(parent_type, child_type, node, context, path) if !child_type.kind.fields? # It's not a valid fragment type, this error was handled someplace else return end parent_types = @types.possible_types(parent_type.unwrap) child_types = @types.possible_types(child_type.unwrap) if child_types.none? { |c| parent_types.include?(c) } name = node.respond_to?(:name) ? " #{node.name}" : "" add_error(GraphQL::StaticValidation::FragmentSpreadsArePossibleError.new( "Fragment#{name} on #{child_type.graphql_name} can't be spread inside #{parent_type.graphql_name}", nodes: node, path: path, fragment_name: name.empty? ? "unknown" : name, type: child_type.graphql_name, parent: parent_type.graphql_name )) end end class FragmentSpread attr_reader :node, :parent_type, :path def initialize(node:, parent_type:, path:) @node = node @parent_type = parent_type @path = path end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/fragment_spreads_are_possible_error.rb000066400000000000000000000015401514115062600330760ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class FragmentSpreadsArePossibleError < StaticValidation::Error attr_reader :type_name attr_reader :fragment_name attr_reader :parent_name def initialize(message, path: nil, nodes: [], type:, fragment_name:, parent:) super(message, path: path, nodes: nodes) @type_name = type @fragment_name = fragment_name @parent_name = parent end # A hash representation of this Message def to_h extensions = { "code" => code, "typeName" => type_name, "fragmentName" => fragment_name, "parentName" => parent_name } super.merge({ "extensions" => extensions }) end def code "cannotSpreadFragment" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/fragment_types_exist.rb000066400000000000000000000024121514115062600300540ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module FragmentTypesExist def on_fragment_definition(node, _parent) if validate_type_exists(node) super end end def on_inline_fragment(node, _parent) if validate_type_exists(node) super end end private def validate_type_exists(fragment_node) if !fragment_node.type true else type_name = fragment_node.type.name type = @types.type(type_name) if type.nil? @all_possible_fragment_type_names ||= begin names = [] context.types.all_types.each do |type| if type.kind.fields? names << type.graphql_name end end names end add_error(GraphQL::StaticValidation::FragmentTypesExistError.new( "No such type #{type_name}, so it can't be a fragment condition#{context.did_you_mean_suggestion(type_name, @all_possible_fragment_type_names)}", nodes: fragment_node, type: type_name )) false else true end end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/fragment_types_exist_error.rb000066400000000000000000000011411514115062600312630ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class FragmentTypesExistError < StaticValidation::Error attr_reader :type_name def initialize(message, path: nil, nodes: [], type:) super(message, path: path, nodes: nodes) @type_name = type end # A hash representation of this Message def to_h extensions = { "code" => code, "typeName" => type_name } super.merge({ "extensions" => extensions }) end def code "undefinedType" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/fragments_are_finite.rb000066400000000000000000000011661514115062600277710ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module FragmentsAreFinite def on_document(_n, _p) super dependency_map = context.dependencies dependency_map.cyclical_definitions.each do |defn| if defn.node.is_a?(GraphQL::Language::Nodes::FragmentDefinition) add_error(GraphQL::StaticValidation::FragmentsAreFiniteError.new( "Fragment #{defn.name} contains an infinite loop", nodes: defn.node, path: defn.path, name: defn.name )) end end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/fragments_are_finite_error.rb000066400000000000000000000011601514115062600311740ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class FragmentsAreFiniteError < StaticValidation::Error attr_reader :fragment_name def initialize(message, path: nil, nodes: [], name:) super(message, path: path, nodes: nodes) @fragment_name = name end # A hash representation of this Message def to_h extensions = { "code" => code, "fragmentName" => fragment_name } super.merge({ "extensions" => extensions }) end def code "infiniteLoop" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/fragments_are_named.rb000066400000000000000000000006031514115062600275720ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module FragmentsAreNamed def on_fragment_definition(node, _parent) if node.name.nil? add_error(GraphQL::StaticValidation::FragmentsAreNamedError.new( "Fragment definition has no name", nodes: node )) end super end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/fragments_are_named_error.rb000066400000000000000000000010041514115062600307770ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class FragmentsAreNamedError < StaticValidation::Error def initialize(message, path: nil, nodes: []) super(message, path: path, nodes: nodes) end # A hash representation of this Message def to_h extensions = { "code" => code, } super.merge({ "extensions" => extensions }) end def code "anonymousFragment" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb000066400000000000000000000017661514115062600324430ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module FragmentsAreOnCompositeTypes def on_fragment_definition(node, parent) validate_type_is_composite(node) && super end def on_inline_fragment(node, parent) validate_type_is_composite(node) && super end private def validate_type_is_composite(node) node_type = node.type if node_type.nil? # Inline fragment on the same type true else type_name = node_type.to_query_string type_def = @types.type(type_name) if type_def.nil? || !type_def.kind.composite? add_error(GraphQL::StaticValidation::FragmentsAreOnCompositeTypesError.new( "Invalid fragment on type #{type_name} (must be Union, Interface or Object)", nodes: node, type: type_name )) false else true end end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/fragments_are_on_composite_types_error.rb000066400000000000000000000012311514115062600336370ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class FragmentsAreOnCompositeTypesError < StaticValidation::Error attr_reader :type_name attr_reader :argument_name def initialize(message, path: nil, nodes: [], type:) super(message, path: path, nodes: nodes) @type_name = type end # A hash representation of this Message def to_h extensions = { "code" => code, "typeName" => type_name } super.merge({ "extensions" => extensions }) end def code "fragmentOnNonCompositeType" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/fragments_are_used.rb000066400000000000000000000020461514115062600274510ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module FragmentsAreUsed def on_document(node, parent) super dependency_map = context.dependencies dependency_map.unmet_dependencies.each do |op_defn, spreads| spreads.each do |fragment_spread| add_error(GraphQL::StaticValidation::FragmentsAreUsedError.new( "Fragment #{fragment_spread.name} was used, but not defined", nodes: fragment_spread.node, path: fragment_spread.path, fragment: fragment_spread.name )) end end dependency_map.unused_dependencies.each do |fragment| if fragment && !fragment.name.nil? add_error(GraphQL::StaticValidation::FragmentsAreUsedError.new( "Fragment #{fragment.name} was defined, but not used", nodes: fragment.node, path: fragment.path, fragment: fragment.name )) end end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/fragments_are_used_error.rb000066400000000000000000000011761514115062600306650ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class FragmentsAreUsedError < StaticValidation::Error attr_reader :fragment_name def initialize(message, path: nil, nodes: [], fragment:) super(message, path: path, nodes: nodes) @fragment_name = fragment end # A hash representation of this Message def to_h extensions = { "code" => code, "fragmentName" => fragment_name } super.merge({ "extensions" => extensions }) end def code "useAndDefineFragment" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/input_object_names_are_unique.rb000066400000000000000000000014651514115062600317050ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module InputObjectNamesAreUnique def on_input_object(node, parent) validate_input_fields(node) super end private def validate_input_fields(node) input_field_defns = node.arguments input_fields_by_name = Hash.new { |h, k| h[k] = [] } input_field_defns.each { |a| input_fields_by_name[a.name] << a } input_fields_by_name.each do |name, defns| if defns.size > 1 error = GraphQL::StaticValidation::InputObjectNamesAreUniqueError.new( "There can be only one input field named \"#{name}\"", nodes: defns, name: name ) add_error(error) end end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/input_object_names_are_unique_error.rb000066400000000000000000000011341514115062600331070ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class InputObjectNamesAreUniqueError < StaticValidation::Error attr_reader :name def initialize(message, path: nil, nodes: [], name:) super(message, path: path, nodes: nodes) @name = name end # A hash representation of this Message def to_h extensions = { "code" => code, "name" => name } super.merge({ "extensions" => extensions }) end def code "inputFieldNotUnique" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/mutation_root_exists.rb000066400000000000000000000007311514115062600301150ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module MutationRootExists def on_operation_definition(node, _parent) if node.operation_type == 'mutation' && context.query.types.mutation_root.nil? add_error(GraphQL::StaticValidation::MutationRootExistsError.new( 'Schema is not configured for mutations', nodes: node )) else super end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/mutation_root_exists_error.rb000066400000000000000000000010201514115062600313160ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class MutationRootExistsError < StaticValidation::Error def initialize(message, path: nil, nodes: []) super(message, path: path, nodes: nodes) end # A hash representation of this Message def to_h extensions = { "code" => code, } super.merge({ "extensions" => extensions }) end def code "missingMutationConfiguration" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/no_definitions_are_present.rb000066400000000000000000000027141514115062600312140ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module NoDefinitionsArePresent include GraphQL::StaticValidation::Error::ErrorHelper def initialize(*) super @schema_definition_nodes = [] end def on_invalid_node(node, parent) @schema_definition_nodes << node nil end alias :on_directive_definition :on_invalid_node alias :on_schema_definition :on_invalid_node alias :on_scalar_type_definition :on_invalid_node alias :on_object_type_definition :on_invalid_node alias :on_input_object_type_definition :on_invalid_node alias :on_interface_type_definition :on_invalid_node alias :on_union_type_definition :on_invalid_node alias :on_enum_type_definition :on_invalid_node alias :on_schema_extension :on_invalid_node alias :on_scalar_type_extension :on_invalid_node alias :on_object_type_extension :on_invalid_node alias :on_input_object_type_extension :on_invalid_node alias :on_interface_type_extension :on_invalid_node alias :on_union_type_extension :on_invalid_node alias :on_enum_type_extension :on_invalid_node def on_document(node, parent) super if !@schema_definition_nodes.empty? add_error(GraphQL::StaticValidation::NoDefinitionsArePresentError.new(%|Query cannot contain schema definitions|, nodes: @schema_definition_nodes)) end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/no_definitions_are_present_error.rb000066400000000000000000000010261514115062600324200ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class NoDefinitionsArePresentError < StaticValidation::Error def initialize(message, path: nil, nodes: []) super(message, path: path, nodes: nodes) end # A hash representation of this Message def to_h extensions = { "code" => code, } super.merge({ "extensions" => extensions }) end def code "queryContainsSchemaDefinitions" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/not_single_subscription_error.rb000066400000000000000000000010131514115062600317630ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class NotSingleSubscriptionError < StaticValidation::Error def initialize(message, path: nil, nodes: []) super(message, path: path, nodes: nodes) end # A hash representation of this Message def to_h extensions = { "code" => code, } super.merge({ "extensions" => extensions }) end def code "notSingleSubscription" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/one_of_input_objects_are_valid.rb000066400000000000000000000042151514115062600320170ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module OneOfInputObjectsAreValid def on_input_object(node, parent) return super unless parent.is_a?(GraphQL::Language::Nodes::Argument) parent_type = get_parent_type(context, parent) return super unless parent_type && parent_type.kind.input_object? && parent_type.one_of? validate_one_of_input_object(node, context, parent_type) super end private def validate_one_of_input_object(ast_node, context, parent_type) present_fields = ast_node.arguments.map(&:name) input_object_type = parent_type.to_type_signature if present_fields.count != 1 add_error( OneOfInputObjectsAreValidError.new( "OneOf Input Object '#{input_object_type}' must specify exactly one key.", path: context.path, nodes: ast_node, input_object_type: input_object_type ) ) return end field = present_fields.first value = ast_node.arguments.first.value if value.is_a?(GraphQL::Language::Nodes::NullValue) add_error( OneOfInputObjectsAreValidError.new( "Argument '#{input_object_type}.#{field}' must be non-null.", path: [*context.path, field], nodes: ast_node.arguments.first, input_object_type: input_object_type ) ) return end if value.is_a?(GraphQL::Language::Nodes::VariableIdentifier) variable_name = value.name variable_type = @declared_variables[variable_name].type unless variable_type.is_a?(GraphQL::Language::Nodes::NonNullType) add_error( OneOfInputObjectsAreValidError.new( "Variable '#{variable_name}' must be non-nullable to be used for OneOf Input Object '#{input_object_type}'.", path: [*context.path, field], nodes: ast_node, input_object_type: input_object_type ) ) end end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/one_of_input_objects_are_valid_error.rb000066400000000000000000000012441514115062600332270ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class OneOfInputObjectsAreValidError < StaticValidation::Error attr_reader :input_object_type def initialize(message, path:, nodes:, input_object_type:) super(message, path: path, nodes: nodes) @input_object_type = input_object_type end # A hash representation of this Message def to_h extensions = { "code" => code, "inputObjectType" => input_object_type } super.merge({ "extensions" => extensions }) end def code "invalidOneOfInputObject" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/operation_names_are_valid.rb000066400000000000000000000020211514115062600307760ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module OperationNamesAreValid def initialize(*) super @operation_names = Hash.new { |h, k| h[k] = [] } end def on_operation_definition(node, parent) @operation_names[node.name] << node super end def on_document(node, parent) super op_count = @operation_names.values.inject(0) { |m, v| m + v.size } @operation_names.each do |name, nodes| if name.nil? && op_count > 1 add_error(GraphQL::StaticValidation::OperationNamesAreValidError.new( %|Operation name is required when multiple operations are present|, nodes: nodes )) elsif nodes.length > 1 add_error(GraphQL::StaticValidation::OperationNamesAreValidError.new( %|Operation name "#{name}" must be unique|, nodes: nodes, name: name )) end end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/operation_names_are_valid_error.rb000066400000000000000000000012451514115062600322160ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class OperationNamesAreValidError < StaticValidation::Error attr_reader :operation_name def initialize(message, path: nil, nodes: [], name: nil) super(message, path: path, nodes: nodes) @operation_name = name end # A hash representation of this Message def to_h extensions = { "code" => code }.tap { |h| h["operationName"] = operation_name unless operation_name.nil? } super.merge({ "extensions" => extensions }) end def code "uniquelyNamedOperations" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/query_root_exists.rb000066400000000000000000000007511514115062600274240ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module QueryRootExists def on_operation_definition(node, _parent) if (node.operation_type == 'query' || node.operation_type.nil?) && context.query.types.query_root.nil? add_error(GraphQL::StaticValidation::QueryRootExistsError.new( 'Schema is not configured for queries', nodes: node )) else super end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/query_root_exists_error.rb000066400000000000000000000010121514115062600306240ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class QueryRootExistsError < StaticValidation::Error def initialize(message, path: nil, nodes: []) super(message, path: path, nodes: nodes) end # A hash representation of this Message def to_h extensions = { "code" => code, } super.merge({ "extensions" => extensions }) end def code "missingQueryConfiguration" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/required_arguments_are_present.rb000066400000000000000000000025321514115062600321100ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module RequiredArgumentsArePresent def on_field(node, _parent) assert_required_args(node, field_definition) super end def on_directive(node, _parent) directive_defn = context.schema_directives[node.name] assert_required_args(node, directive_defn) super end private def assert_required_args(ast_node, defn) args = @context.query.types.arguments(defn) return if args.empty? present_argument_names = ast_node.arguments.map(&:name) required_argument_names = context.query.types.arguments(defn) .select { |a| a.type.kind.non_null? && !a.default_value? && context.query.types.argument(defn, a.name) } .map!(&:name) missing_names = required_argument_names - present_argument_names if !missing_names.empty? add_error(GraphQL::StaticValidation::RequiredArgumentsArePresentError.new( "#{ast_node.class.name.split("::").last} '#{ast_node.name}' is missing required arguments: #{missing_names.join(", ")}", nodes: ast_node, class_name: ast_node.class.name.split("::").last, name: ast_node.name, arguments: "#{missing_names.join(", ")}" )) end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/required_arguments_are_present_error.rb000066400000000000000000000014771514115062600333300ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class RequiredArgumentsArePresentError < StaticValidation::Error attr_reader :class_name attr_reader :name attr_reader :arguments def initialize(message, path: nil, nodes: [], class_name:, name:, arguments:) super(message, path: path, nodes: nodes) @class_name = class_name @name = name @arguments = arguments end # A hash representation of this Message def to_h extensions = { "code" => code, "className" => class_name, "name" => name, "arguments" => arguments } super.merge({ "extensions" => extensions }) end def code "missingRequiredArguments" end end end end required_input_object_attributes_are_present.rb000066400000000000000000000042741514115062600347640ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/static_validation/rules# frozen_string_literal: true module GraphQL module StaticValidation module RequiredInputObjectAttributesArePresent def on_input_object(node, parent) if parent.is_a? GraphQL::Language::Nodes::Argument validate_input_object(node, context, parent) end super end private def get_parent_type(context, parent) # If argument_definition is defined we're at nested object # and need to refer to the containing input object type rather # than the field_definition. # h/t @rmosolgo arg_defn = context.argument_definition # Double checking that arg_defn is an input object as nested # scalars, namely JSON, can make it to this branch defn = if arg_defn && arg_defn.type.unwrap.kind.input_object? arg_defn.type.unwrap else context.directive_definition || context.field_definition end parent_type = context.types.argument(defn, parent_name(parent, defn)) parent_type ? parent_type.type.unwrap : nil end def validate_input_object(ast_node, context, parent) parent_type = get_parent_type(context, parent) return unless parent_type && parent_type.kind.input_object? required_fields = context.types.arguments(parent_type) .select{ |arg| arg.type.kind.non_null? && !arg.default_value? } .map!(&:graphql_name) present_fields = ast_node.arguments.map(&:name) missing_fields = required_fields - present_fields missing_fields.each do |missing_field| path = [*context.path, missing_field] missing_field_type = context.types.argument(parent_type, missing_field).type add_error(RequiredInputObjectAttributesArePresentError.new( "Argument '#{missing_field}' on InputObject '#{parent_type.to_type_signature}' is required. Expected type #{missing_field_type.to_type_signature}", argument_name: missing_field, argument_type: missing_field_type.to_type_signature, input_object_type: parent_type.to_type_signature, path: path, nodes: ast_node, )) end end end end end required_input_object_attributes_are_present_error.rb000066400000000000000000000017051514115062600361710ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/static_validation/rules# frozen_string_literal: true module GraphQL module StaticValidation class RequiredInputObjectAttributesArePresentError < StaticValidation::Error attr_reader :argument_type attr_reader :argument_name attr_reader :input_object_type def initialize(message, path:, nodes:, argument_type:, argument_name:, input_object_type:) super(message, path: path, nodes: nodes) @argument_type = argument_type @argument_name = argument_name @input_object_type = input_object_type end # A hash representation of this Message def to_h extensions = { "code" => code, "argumentName" => argument_name, "argumentType" => argument_type, "inputObjectType" => input_object_type, } super.merge({ "extensions" => extensions }) end def code "missingRequiredInputObjectAttribute" end end end end subscription_root_exists_and_single_subscription_selection.rb000066400000000000000000000014601514115062600377560ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/static_validation/rules# frozen_string_literal: true module GraphQL module StaticValidation module SubscriptionRootExistsAndSingleSubscriptionSelection def on_operation_definition(node, parent) if node.operation_type == "subscription" if context.types.subscription_root.nil? add_error(GraphQL::StaticValidation::SubscriptionRootExistsError.new( 'Schema is not configured for subscriptions', nodes: node )) elsif node.selections.size != 1 add_error(GraphQL::StaticValidation::NotSingleSubscriptionError.new( 'A subscription operation may only have one selection', nodes: node, )) else super end else super end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/subscription_root_exists_error.rb000066400000000000000000000010301514115062600322030ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class SubscriptionRootExistsError < StaticValidation::Error def initialize(message, path: nil, nodes: []) super(message, path: path, nodes: nodes) end # A hash representation of this Message def to_h extensions = { "code" => code, } super.merge({ "extensions" => extensions }) end def code "missingSubscriptionConfiguration" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/unique_directives_per_location.rb000066400000000000000000000041651514115062600321050ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module UniqueDirectivesPerLocation DIRECTIVE_NODE_HOOKS = [ :on_fragment_definition, :on_fragment_spread, :on_inline_fragment, :on_operation_definition, :on_scalar_type_definition, :on_object_type_definition, :on_input_value_definition, :on_field_definition, :on_interface_type_definition, :on_union_type_definition, :on_enum_type_definition, :on_enum_value_definition, :on_input_object_type_definition, :on_field, ] VALIDATE_DIRECTIVE_LOCATION_ON_NODE = <<~RUBY def %{method_name}(node, parent) if !node.directives.empty? validate_directive_location(node) end super(node, parent) end RUBY DIRECTIVE_NODE_HOOKS.each do |method_name| # Can't use `define_method {...}` here because the proc can't be isolated for use in non-main Ractors module_eval(VALIDATE_DIRECTIVE_LOCATION_ON_NODE % { method_name: method_name }) # rubocop:disable Development/NoEvalCop end private def validate_directive_location(node) used_directives = {} node.directives.each do |ast_directive| directive_name = ast_directive.name if (first_node = used_directives[directive_name]) @directives_are_unique_errors_by_first_node ||= {} err = @directives_are_unique_errors_by_first_node[first_node] ||= begin error = GraphQL::StaticValidation::UniqueDirectivesPerLocationError.new( "The directive \"#{directive_name}\" can only be used once at this location.", nodes: [used_directives[directive_name]], directive: directive_name, ) add_error(error) error end err.nodes << ast_directive elsif !((dir_defn = context.schema_directives[directive_name]) && dir_defn.repeatable?) used_directives[directive_name] = ast_directive end end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/unique_directives_per_location_error.rb000066400000000000000000000012301514115062600333040ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class UniqueDirectivesPerLocationError < StaticValidation::Error attr_reader :directive_name def initialize(message, path: nil, nodes: [], directive:) super(message, path: path, nodes: nodes) @directive_name = directive end # A hash representation of this Message def to_h extensions = { "code" => code, "directiveName" => directive_name } super.merge({ "extensions" => extensions }) end def code "directiveNotUniqueForLocation" end end end end variable_default_values_are_correctly_typed.rb000066400000000000000000000024111514115062600345230ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/static_validation/rules# frozen_string_literal: true module GraphQL module StaticValidation module VariableDefaultValuesAreCorrectlyTyped def on_variable_definition(node, parent) if !node.default_value.nil? value = node.default_value type = context.schema.type_from_ast(node.type, context: context) if type.nil? # This is handled by another validator else validation_result = context.validate_literal(value, type) if !validation_result.valid? problems = validation_result.problems first_problem = problems && problems.first if first_problem error_message = first_problem["explanation"] end error_message ||= "Default value for $#{node.name} doesn't match type #{type.to_type_signature}" add_error(GraphQL::StaticValidation::VariableDefaultValuesAreCorrectlyTypedError.new( error_message, nodes: node, name: node.name, type: type.to_type_signature, error_type: VariableDefaultValuesAreCorrectlyTypedError::VIOLATIONS[:INVALID_TYPE], )) end end end super end end end end variable_default_values_are_correctly_typed_error.rb000066400000000000000000000021141514115062600357340ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/static_validation/rules# frozen_string_literal: true module GraphQL module StaticValidation class VariableDefaultValuesAreCorrectlyTypedError < StaticValidation::Error attr_reader :variable_name attr_reader :type_name attr_reader :violation VIOLATIONS = { :INVALID_TYPE => "defaultValueInvalidType", :INVALID_ON_NON_NULL => "defaultValueInvalidOnNonNullVariable", } def initialize(message, path: nil, nodes: [], name:, type: nil, error_type:) super(message, path: path, nodes: nodes) @variable_name = name @type_name = type raise("Unexpected error type: #{error_type}") if !VIOLATIONS.values.include?(error_type) @violation = error_type end # A hash representation of this Message def to_h extensions = { "code" => code, "variableName" => variable_name }.tap { |h| h["typeName"] = type_name unless type_name.nil? } super.merge({ "extensions" => extensions }) end def code @violation end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/variable_names_are_unique.rb000066400000000000000000000013141514115062600307760ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module VariableNamesAreUnique def on_operation_definition(node, parent) var_defns = node.variables if !var_defns.empty? vars_by_name = Hash.new { |h, k| h[k] = [] } var_defns.each { |v| vars_by_name[v.name] << v } vars_by_name.each do |name, defns| if defns.size > 1 add_error(GraphQL::StaticValidation::VariableNamesAreUniqueError.new( "There can only be one variable named \"#{name}\"", nodes: defns, name: name )) end end end super end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/variable_names_are_unique_error.rb000066400000000000000000000011711514115062600322100ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class VariableNamesAreUniqueError < StaticValidation::Error attr_reader :variable_name def initialize(message, path: nil, nodes: [], name:) super(message, path: path, nodes: nodes) @variable_name = name end # A hash representation of this Message def to_h extensions = { "code" => code, "variableName" => variable_name } super.merge({ "extensions" => extensions }) end def code "variableNotUnique" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb000066400000000000000000000127241514115062600313120ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module VariableUsagesAreAllowed def initialize(*) super # holds { name => ast_node } pairs @declared_variables = {} end def on_operation_definition(node, parent) @declared_variables = node.variables.each_with_object({}) { |var, memo| memo[var.name] = var } super end def on_argument(node, parent) node_values = if node.value.is_a?(Array) node.value else [node.value] end node_values = node_values.select { |value| value.is_a? GraphQL::Language::Nodes::VariableIdentifier } if !node_values.empty? argument_owner = case parent when GraphQL::Language::Nodes::Field context.field_definition when GraphQL::Language::Nodes::Directive context.directive_definition when GraphQL::Language::Nodes::InputObject arg_type = context.argument_definition.type.unwrap if arg_type.kind.input_object? arg_type else # This is some kind of error nil end else raise("Unexpected argument parent: #{parent}") end node_values.each do |node_value| var_defn_ast = @declared_variables[node_value.name] # Might be undefined :( # VariablesAreUsedAndDefined can't finalize its search until the end of the document. var_defn_ast && argument_owner && validate_usage(argument_owner, node, var_defn_ast) end end super end private def validate_usage(argument_owner, arg_node, ast_var) var_type = context.schema.type_from_ast(ast_var.type, context: context) if var_type.nil? return end if !ast_var.default_value.nil? unless var_type.kind.non_null? # If the value is required, but the argument is not, # and yet there's a non-nil default, then we impliclty # make the argument also a required type. var_type = var_type.to_non_null_type end end arg_defn = @types.argument(argument_owner, arg_node.name) arg_defn_type = arg_defn.type # If the argument is non-null, but it was given a default value, # then treat it as nullable in practice, see https://github.com/rmosolgo/graphql-ruby/issues/3793 if arg_defn_type.non_null? && arg_defn.default_value? arg_defn_type = arg_defn_type.of_type end var_inner_type = var_type.unwrap arg_inner_type = arg_defn_type.unwrap var_type = wrap_var_type_with_depth_of_arg(var_type, arg_node) if var_inner_type != arg_inner_type create_error("Type mismatch", var_type, ast_var, arg_defn, arg_node) elsif list_dimension(var_type) != list_dimension(arg_defn_type) create_error("List dimension mismatch", var_type, ast_var, arg_defn, arg_node) elsif !non_null_levels_match(arg_defn_type, var_type) create_error("Nullability mismatch", var_type, ast_var, arg_defn, arg_node) end end def create_error(error_message, var_type, ast_var, arg_defn, arg_node) add_error(GraphQL::StaticValidation::VariableUsagesAreAllowedError.new( "#{error_message} on variable $#{ast_var.name} and argument #{arg_node.name} (#{var_type.to_type_signature} / #{arg_defn.type.to_type_signature})", nodes: arg_node, name: ast_var.name, type: var_type.to_type_signature, argument: arg_node.name, error: error_message )) end def wrap_var_type_with_depth_of_arg(var_type, arg_node) arg_node_value = arg_node.value return var_type unless arg_node_value.is_a?(Array) new_var_type = var_type depth_of_array(arg_node_value).times do # Since the array _is_ present, treat it like a non-null type # (It satisfies a non-null requirement AND a nullable requirement) new_var_type = new_var_type.to_list_type.to_non_null_type end new_var_type end # @return [Integer] Returns the max depth of `array`, or `0` if it isn't an array at all def depth_of_array(array) case array when Array max_child_depth = 0 array.each do |item| item_depth = depth_of_array(item) if item_depth > max_child_depth max_child_depth = item_depth end end 1 + max_child_depth else 0 end end def list_dimension(type) if type.kind.list? 1 + list_dimension(type.of_type) elsif type.kind.non_null? list_dimension(type.of_type) else 0 end end def non_null_levels_match(arg_type, var_type) if arg_type.kind.non_null? && !var_type.kind.non_null? false elsif arg_type.kind.wraps? && var_type.kind.wraps? # If var_type is a non-null wrapper for a type, and arg_type is nullable, peel off the wrapper # That way, a var_type of `[DairyAnimal]!` works with an arg_type of `[DairyAnimal]` if var_type.kind.non_null? && !arg_type.kind.non_null? var_type = var_type.of_type end non_null_levels_match(arg_type.of_type, var_type.of_type) else true end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/variable_usages_are_allowed_error.rb000066400000000000000000000017071514115062600325220ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class VariableUsagesAreAllowedError < StaticValidation::Error attr_reader :type_name attr_reader :variable_name attr_reader :argument_name attr_reader :error_message def initialize(message, path: nil, nodes: [], type:, name:, argument:, error:) super(message, path: path, nodes: nodes) @type_name = type @variable_name = name @argument_name = argument @error_message = error end # A hash representation of this Message def to_h extensions = { "code" => code, "variableName" => variable_name, "typeName" => type_name, "argumentName" => argument_name, "errorMessage" => error_message } super.merge({ "extensions" => extensions }) end def code "variableMismatch" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/variables_are_input_types.rb000066400000000000000000000025551514115062600310630ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation module VariablesAreInputTypes def on_variable_definition(node, parent) type_name = get_type_name(node.type) type = context.query.types.type(type_name) if type.nil? @all_possible_input_type_names ||= begin names = [] context.types.all_types.each { |(t)| if t.kind.input? names << t.graphql_name end } names end add_error(GraphQL::StaticValidation::VariablesAreInputTypesError.new( "#{type_name} isn't a defined input type (on $#{node.name})#{context.did_you_mean_suggestion(type_name, @all_possible_input_type_names)}", nodes: node, name: node.name, type: type_name )) elsif !type.kind.input? add_error(GraphQL::StaticValidation::VariablesAreInputTypesError.new( "#{type.graphql_name} isn't a valid input type (on $#{node.name})", nodes: node, name: node.name, type: type_name )) end super end private def get_type_name(ast_type) if ast_type.respond_to?(:of_type) get_type_name(ast_type.of_type) else ast_type.name end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/variables_are_input_types_error.rb000066400000000000000000000013421514115062600322650ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class VariablesAreInputTypesError < StaticValidation::Error attr_reader :type_name attr_reader :variable_name def initialize(message, path: nil, nodes: [], type:, name:) super(message, path: path, nodes: nodes) @type_name = type @variable_name = name end # A hash representation of this Message def to_h extensions = { "code" => code, "typeName" => type_name, "variableName" => variable_name } super.merge({ "extensions" => extensions }) end def code "variableRequiresValidType" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/variables_are_used_and_defined.rb000066400000000000000000000137031514115062600317350ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation # The problem is # - Variable $usage must be determined at the OperationDefinition level # - You can't tell how fragments use variables until you visit FragmentDefinitions (which may be at the end of the document) # # So, this validator includes some crazy logic to follow fragment spreads recursively, while avoiding infinite loops. # # `graphql-js` solves this problem by: # - re-visiting the AST for each validator # - allowing validators to say `followSpreads: true` # module VariablesAreUsedAndDefined class VariableUsage attr_accessor :ast_node, :used_by, :declared_by, :path def used? !!@used_by end def declared? !!@declared_by end end def initialize(*) super @variable_usages_for_context = Hash.new {|hash, key| hash[key] = Hash.new {|h, k| h[k] = VariableUsage.new } } @spreads_for_context = Hash.new {|hash, key| hash[key] = [] } @variable_context_stack = [] end def on_operation_definition(node, parent) # initialize the hash of vars for this context: @variable_usages_for_context[node] @variable_context_stack.push(node) # mark variables as defined: var_hash = @variable_usages_for_context[node] node.variables.each { |var| var_usage = var_hash[var.name] var_usage.declared_by = node var_usage.path = context.path } super @variable_context_stack.pop end def on_fragment_definition(node, parent) # initialize the hash of vars for this context: @variable_usages_for_context[node] @variable_context_stack.push(node) super @variable_context_stack.pop end # For FragmentSpreads: # - find the context on the stack # - mark the context as containing this spread def on_fragment_spread(node, parent) variable_context = @variable_context_stack.last @spreads_for_context[variable_context] << node.name super end # For VariableIdentifiers: # - mark the variable as used # - assign its AST node def on_variable_identifier(node, parent) usage_context = @variable_context_stack.last declared_variables = @variable_usages_for_context[usage_context] usage = declared_variables[node.name] usage.used_by = usage_context usage.ast_node = node usage.path = context.path super end def on_document(node, parent) super fragment_definitions = @variable_usages_for_context.select { |key, value| key.is_a?(GraphQL::Language::Nodes::FragmentDefinition) } operation_definitions = @variable_usages_for_context.select { |key, value| key.is_a?(GraphQL::Language::Nodes::OperationDefinition) } operation_definitions.each do |node, node_variables| follow_spreads(node, node_variables, @spreads_for_context, fragment_definitions, []) create_errors(node_variables) end end private # Follow spreads in `node`, looking them up from `spreads_for_context` and finding their match in `fragment_definitions`. # Use those fragments to update {VariableUsage}s in `parent_variables`. # Avoid infinite loops by skipping anything in `visited_fragments`. def follow_spreads(node, parent_variables, spreads_for_context, fragment_definitions, visited_fragments) spreads = spreads_for_context[node] - visited_fragments spreads.each do |spread_name| def_node = nil variables = nil # Implement `.find` by hand to avoid Ruby's internal allocations fragment_definitions.each do |frag_def_node, vars| if frag_def_node.name == spread_name def_node = frag_def_node variables = vars break end end next if !def_node visited_fragments << spread_name variables.each do |name, child_usage| parent_usage = parent_variables[name] if child_usage.used? parent_usage.ast_node = child_usage.ast_node parent_usage.used_by = child_usage.used_by parent_usage.path = child_usage.path end end follow_spreads(def_node, parent_variables, spreads_for_context, fragment_definitions, visited_fragments) end end # Determine all the error messages, # Then push messages into the validation context def create_errors(node_variables) # Declared but not used: node_variables .select { |name, usage| usage.declared? && !usage.used? } .each { |var_name, usage| declared_by_error_name = usage.declared_by.name || "anonymous #{usage.declared_by.operation_type}" add_error(GraphQL::StaticValidation::VariablesAreUsedAndDefinedError.new( "Variable $#{var_name} is declared by #{declared_by_error_name} but not used", nodes: usage.declared_by, path: usage.path, name: var_name, error_type: VariablesAreUsedAndDefinedError::VIOLATIONS[:VARIABLE_NOT_USED] )) } # Used but not declared: node_variables .select { |name, usage| usage.used? && !usage.declared? } .each { |var_name, usage| used_by_error_name = usage.used_by.name || "anonymous #{usage.used_by.operation_type}" add_error(GraphQL::StaticValidation::VariablesAreUsedAndDefinedError.new( "Variable $#{var_name} is used by #{used_by_error_name} but not declared", nodes: usage.ast_node, path: usage.path, name: var_name, error_type: VariablesAreUsedAndDefinedError::VIOLATIONS[:VARIABLE_NOT_DEFINED] )) } end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/rules/variables_are_used_and_defined_error.rb000066400000000000000000000016521514115062600331460ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class VariablesAreUsedAndDefinedError < StaticValidation::Error attr_reader :variable_name attr_reader :violation VIOLATIONS = { :VARIABLE_NOT_USED => "variableNotUsed", :VARIABLE_NOT_DEFINED => "variableNotDefined", } def initialize(message, path: nil, nodes: [], name:, error_type:) super(message, path: path, nodes: nodes) @variable_name = name raise("Unexpected error type: #{error_type}") if !VIOLATIONS.values.include?(error_type) @violation = error_type end # A hash representation of this Message def to_h extensions = { "code" => code, "variableName" => variable_name } super.merge({ "extensions" => extensions }) end def code @violation end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/validation_context.rb000066400000000000000000000043611514115062600263620ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation # The validation context gets passed to each validator. # # It exposes a {GraphQL::Language::Visitor} where validators may add hooks. ({Language::Visitor#visit} is called in {Validator#validate}) # # It provides access to the schema & fragments which validators may read from. # # It holds a list of errors which each validator may add to. class ValidationContext extend Forwardable attr_reader :query, :errors, :visitor, :on_dependency_resolve_handlers, :max_errors, :types, :schema def_delegators :@query, :document, :fragments, :operations def initialize(query, visitor_class, max_errors) @query = query @types = query.types # TODO update migrated callers to use this accessor @schema = query.schema @literal_validator = LiteralValidator.new(context: query.context) @errors = [] @max_errors = max_errors || Float::INFINITY @on_dependency_resolve_handlers = [] @visitor = visitor_class.new(document, self) end # TODO stop using def_delegators because of Array allocations def_delegators :@visitor, :path, :type_definition, :field_definition, :argument_definition, :parent_type_definition, :directive_definition, :object_types, :dependencies def on_dependency_resolve(&handler) @on_dependency_resolve_handlers << handler end def validate_literal(ast_value, type) @literal_validator.validate(ast_value, type) end def too_many_errors? @errors.length >= @max_errors end def schema_directives @schema_directives ||= schema.directives end def did_you_mean_suggestion(name, options) if did_you_mean = schema.did_you_mean suggestions = did_you_mean::SpellChecker.new(dictionary: options).correct(name) case suggestions.size when 0 "" when 1 " (Did you mean `#{suggestions.first}`?)" else last_sugg = suggestions.pop " (Did you mean #{suggestions.map {|s| "`#{s}`"}.join(", ")} or `#{last_sugg}`?)" end end end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/validation_timeout_error.rb000066400000000000000000000010021514115062600275620ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module StaticValidation class ValidationTimeoutError < StaticValidation::Error def initialize(message, path: nil, nodes: []) super(message, path: path, nodes: nodes) end # A hash representation of this Message def to_h extensions = { "code" => code } super.merge({ "extensions" => extensions }) end def code "validationTimeout" end end end end graphql-ruby-2.5.19/lib/graphql/static_validation/validator.rb000066400000000000000000000063271514115062600244550ustar00rootroot00000000000000# frozen_string_literal: true require "timeout" module GraphQL module StaticValidation # Initialized with a {GraphQL::Schema}, then it can validate {GraphQL::Language::Nodes::Documents}s based on that schema. # # By default, it's used by {GraphQL::Query} # # @example Validate a query # validator = GraphQL::StaticValidation::Validator.new(schema: MySchema) # query = GraphQL::Query.new(MySchema, query_string) # errors = validator.validate(query)[:errors] # class Validator # @param schema [GraphQL::Schema] # @param rules [Array<#validate(context)>] a list of rules to use when validating def initialize(schema:, rules: GraphQL::StaticValidation::ALL_RULES) @schema = schema @rules = rules end # Validate `query` against the schema. Returns an array of message hashes. # @param query [GraphQL::Query] # @param validate [Boolean] # @param timeout [Float] Number of seconds to wait before aborting validation. Any positive number may be used, including Floats to specify fractional seconds. # @param max_errors [Integer] Maximum number of errors before aborting validation. Any positive number will limit the number of errors. Defaults to nil for no limit. # @return [Array] def validate(query, validate: true, timeout: nil, max_errors: nil) errors = nil query.current_trace.begin_validate(query, validate) query.current_trace.validate(validate: validate, query: query) do begin_t = Time.now errors = if validate == false [] else rules_to_use = validate ? @rules : [] visitor_class = BaseVisitor.including_rules(rules_to_use) context = GraphQL::StaticValidation::ValidationContext.new(query, visitor_class, max_errors) begin # CAUTION: Usage of the timeout module makes the assumption that validation rules are stateless Ruby code that requires no cleanup if process was interrupted. This means no blocking IO calls, native gems, locks, or `rescue` clauses that must be reached. # A timeout value of 0 or nil will execute the block without any timeout. Timeout::timeout(timeout) do catch(:too_many_validation_errors) do context.visitor.visit end end rescue Timeout::Error handle_timeout(query, context) end context.errors end { remaining_timeout: timeout ? (timeout - (Time.now - begin_t)) : nil, errors: errors, } end rescue GraphQL::ExecutionError => e errors = [e] { remaining_timeout: nil, errors: errors, } ensure query.current_trace.end_validate(query, validate, errors) end # Invoked when static validation times out. # @param query [GraphQL::Query] # @param context [GraphQL::StaticValidation::ValidationContext] def handle_timeout(query, context) context.errors << GraphQL::StaticValidation::ValidationTimeoutError.new( "Timeout on validation of query" ) end end end end graphql-ruby-2.5.19/lib/graphql/string_encoding_error.rb000066400000000000000000000011161514115062600233430ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class StringEncodingError < GraphQL::RuntimeTypeError attr_reader :string, :field, :path def initialize(str, context:) @string = str @field = context[:current_field] @path = context[:current_path] message = "String #{str.inspect} was encoded as #{str.encoding}".dup if @path message << " @ #{@path.join(".")}" end if @field message << " (#{@field.path})" end message << ". GraphQL requires an encoding compatible with UTF-8." super(message) end end end graphql-ruby-2.5.19/lib/graphql/subscriptions.rb000066400000000000000000000272631514115062600217000ustar00rootroot00000000000000# frozen_string_literal: true require "securerandom" require "graphql/subscriptions/broadcast_analyzer" require "graphql/subscriptions/event" require "graphql/subscriptions/serialize" require "graphql/subscriptions/action_cable_subscriptions" require "graphql/subscriptions/default_subscription_resolve_extension" module GraphQL class Subscriptions # Raised when either: # - the triggered `event_name` doesn't match a field in the schema; or # - one or more arguments don't match the field arguments class InvalidTriggerError < GraphQL::Error end # Raised when either: # - An initial subscription didn't have a value for `context[subscription_scope]` # - Or, an update didn't pass `.trigger(..., scope:)` # When raised, the initial subscription or update fails completely. class SubscriptionScopeMissingError < GraphQL::Error end # @see {Subscriptions#initialize} for options, concrete implementations may add options. def self.use(defn, options = {}) schema = defn.is_a?(Class) ? defn : defn.target if schema.subscriptions(inherited: false) raise ArgumentError, "Can't reinstall subscriptions. #{schema} is using #{schema.subscriptions}, can't also add #{self}" end options[:schema] = schema schema.subscriptions = self.new(**options) schema.add_subscription_extension_if_necessary nil end # @param schema [Class] the GraphQL schema this manager belongs to # @param validate_update [Boolean] If false, then validation is skipped when executing updates def initialize(schema:, validate_update: true, broadcast: false, default_broadcastable: false, **rest) if broadcast schema.query_analyzer(Subscriptions::BroadcastAnalyzer) end @default_broadcastable = default_broadcastable @schema = schema @validate_update = validate_update end # @return [Boolean] Used when fields don't have `broadcastable:` explicitly set attr_reader :default_broadcastable # Fetch subscriptions matching this field + arguments pair # And pass them off to the queue. # @param event_name [String] # @param args [Hash Object] # @param object [Object] # @param scope [Symbol, String] # @param context [Hash] # @return [void] def trigger(event_name, args, object, scope: nil, context: {}) # Make something as context-like as possible, even though there isn't a current query: dummy_query = @schema.query_class.new(@schema, "{ __typename }", validate: false, context: context) context = dummy_query.context event_name = event_name.to_s # Try with the verbatim input first: field = dummy_query.types.field(@schema.subscription, event_name) # rubocop:disable Development/ContextIsPassedCop if field.nil? # And if it wasn't found, normalize it: normalized_event_name = normalize_name(event_name) field = dummy_query.types.field(@schema.subscription, normalized_event_name) # rubocop:disable Development/ContextIsPassedCop if field.nil? raise InvalidTriggerError, "No subscription matching trigger: #{event_name} (looked for #{@schema.subscription.graphql_name}.#{normalized_event_name})" end else # Since we found a field, the original input was already normalized normalized_event_name = event_name end # Normalize symbol-keyed args to strings, try camelizing them # Should this accept a real context somehow? normalized_args = normalize_arguments(normalized_event_name, field, args, GraphQL::Query::NullContext.instance) event = Subscriptions::Event.new( name: normalized_event_name, arguments: normalized_args, field: field, scope: scope, context: context, ) execute_all(event, object) end # `event` was triggered on `object`, and `subscription_id` was subscribed, # so it should be updated. # # Load `subscription_id`'s GraphQL data, re-evaluate the query and return the result. # # @param subscription_id [String] # @param event [GraphQL::Subscriptions::Event] The event which was triggered # @param object [Object] The value for the subscription field # @return [GraphQL::Query::Result] def execute_update(subscription_id, event, object) # Lookup the saved data for this subscription query_data = read_subscription(subscription_id) if query_data.nil? delete_subscription(subscription_id) return nil end # Fetch the required keys from the saved data query_string = query_data.fetch(:query_string) variables = query_data.fetch(:variables) context = query_data.fetch(:context) operation_name = query_data.fetch(:operation_name) execute_options = { query: query_string, context: context, subscription_topic: event.topic, operation_name: operation_name, variables: variables, root_value: object, } # merge event's and query's context together context.merge!(event.context) unless event.context.nil? || context.nil? execute_options[:validate] = validate_update?(**execute_options) result = @schema.execute(**execute_options) subscriptions_context = result.context.namespace(:subscriptions) if subscriptions_context[:no_update] result = nil end if subscriptions_context[:unsubscribed] && !subscriptions_context[:final_update] # `unsubscribe` was called, clean up on our side # The transport should also send `{more: false}` to client delete_subscription(subscription_id) result = nil end result end # Define this method to customize whether to validate # this subscription when executing an update. # # @return [Boolean] defaults to `true`, or false if `validate: false` is provided. def validate_update?(query:, context:, root_value:, subscription_topic:, operation_name:, variables:) @validate_update end # Run the update query for this subscription and deliver it # @see {#execute_update} # @see {#deliver} # @return [void] def execute(subscription_id, event, object) res = execute_update(subscription_id, event, object) if !res.nil? deliver(subscription_id, res) if res.context.namespace(:subscriptions)[:unsubscribed] # `unsubscribe` was called, clean up on our side # The transport should also send `{more: false}` to client delete_subscription(subscription_id) end end end # Event `event` occurred on `object`, # Update all subscribers. # @param event [Subscriptions::Event] # @param object [Object] # @return [void] def execute_all(event, object) raise GraphQL::RequiredImplementationMissingError end # The system wants to send an update to this subscription. # Read its data and return it. # @param subscription_id [String] # @return [Hash] Containing required keys def read_subscription(subscription_id) raise GraphQL::RequiredImplementationMissingError end # A subscription query was re-evaluated, returning `result`. # The result should be send to `subscription_id`. # @param subscription_id [String] # @param result [Hash] # @return [void] def deliver(subscription_id, result) raise GraphQL::RequiredImplementationMissingError end # `query` was executed and found subscriptions to `events`. # Update the database to reflect this new state. # @param query [GraphQL::Query] # @param events [Array] # @return [void] def write_subscription(query, events) raise GraphQL::RequiredImplementationMissingError end # A subscription was terminated server-side. # Clean up the database. # @param subscription_id [String] # @return void. def delete_subscription(subscription_id) raise GraphQL::RequiredImplementationMissingError end # @return [String] A new unique identifier for a subscription def build_id SecureRandom.uuid end # Convert a user-provided event name or argument # to the equivalent in GraphQL. # # By default, it converts the identifier to camelcase. # Override this in a subclass to change the transformation. # # @param event_or_arg_name [String, Symbol] # @return [String] def normalize_name(event_or_arg_name) Schema::Member::BuildType.camelize(event_or_arg_name.to_s) end # @return [Boolean] if true, then a query like this one would be broadcasted def broadcastable?(query_str, **query_options) query = @schema.query_class.new(@schema, query_str, **query_options) if !query.valid? raise "Invalid query: #{query.validation_errors.map(&:to_h).inspect}" end GraphQL::Analysis.analyze_query(query, @schema.query_analyzers) query.context.namespace(:subscriptions)[:subscription_broadcastable] end private # Recursively normalize `args` as belonging to `arg_owner`: # - convert symbols to strings, # - if needed, camelize the string (using {#normalize_name}) # @param arg_owner [GraphQL::Field, GraphQL::BaseType] # @param args [Hash, Array, Any] some GraphQL input value to coerce as `arg_owner` # @return [Any] normalized arguments value def normalize_arguments(event_name, arg_owner, args, context) case arg_owner when GraphQL::Schema::Field, Class return args if args.nil? if arg_owner.is_a?(Class) && !arg_owner.kind.input_object? # it's a type, but not an input object return args end normalized_args = {} missing_arg_names = [] args.each do |k, v| arg_name = k.to_s arg_defn = arg_owner.get_argument(arg_name, context) if arg_defn normalized_arg_name = arg_name else normalized_arg_name = normalize_name(arg_name) arg_defn = arg_owner.get_argument(normalized_arg_name, context) end if arg_defn if arg_defn.loads normalized_arg_name = arg_defn.keyword.to_s end normalized = normalize_arguments(event_name, arg_defn.type, v, context) normalized_args[normalized_arg_name] = normalized else # Couldn't find a matching argument definition missing_arg_names << arg_name end end # Backfill default values so that trigger arguments # match query arguments. arg_owner.arguments(context).each do |_name, arg_defn| if arg_defn.default_value? && !normalized_args.key?(arg_defn.name) default_value = arg_defn.default_value # We don't have an underlying "object" here, so it can't call methods. # This is broken. normalized_args[arg_defn.name] = arg_defn.prepare_value(nil, default_value, context: context) end end if !missing_arg_names.empty? arg_owner_name = if arg_owner.is_a?(GraphQL::Schema::Field) arg_owner.path elsif arg_owner.is_a?(Class) arg_owner.graphql_name else arg_owner.to_s end raise InvalidTriggerError, "Can't trigger Subscription.#{event_name}, received undefined arguments: #{missing_arg_names.join(", ")}. (Should match arguments of #{arg_owner_name}.)" end normalized_args when GraphQL::Schema::List args&.map { |a| normalize_arguments(event_name, arg_owner.of_type, a, context) } when GraphQL::Schema::NonNull normalize_arguments(event_name, arg_owner.of_type, args, context) else args end end end end graphql-ruby-2.5.19/lib/graphql/subscriptions/000077500000000000000000000000001514115062600213415ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/subscriptions/action_cable_subscriptions.rb000066400000000000000000000243031514115062600272620ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Subscriptions # A subscriptions implementation that sends data # as ActionCable broadcastings. # # Some things to keep in mind: # # - No queueing system; ActiveJob should be added # - Take care to reload context when re-delivering the subscription. (see {Query#subscription_update?}) # - Avoid the async ActionCable adapter and use the redis or PostgreSQL adapters instead. Otherwise calling #trigger won't work from background jobs or the Rails console. # # @example Adding ActionCableSubscriptions to your schema # class MySchema < GraphQL::Schema # # ... # use GraphQL::Subscriptions::ActionCableSubscriptions # end # # @example Implementing a channel for GraphQL Subscriptions # class GraphqlChannel < ApplicationCable::Channel # def subscribed # @subscription_ids = [] # end # # def execute(data) # query = data["query"] # variables = ensure_hash(data["variables"]) # operation_name = data["operationName"] # context = { # # Re-implement whatever context methods you need # # in this channel or ApplicationCable::Channel # # current_user: current_user, # # Make sure the channel is in the context # channel: self, # } # # result = MySchema.execute( # query, # context: context, # variables: variables, # operation_name: operation_name # ) # # payload = { # result: result.to_h, # more: result.subscription?, # } # # # Track the subscription here so we can remove it # # on unsubscribe. # if result.context[:subscription_id] # @subscription_ids << result.context[:subscription_id] # end # # transmit(payload) # end # # def unsubscribed # @subscription_ids.each { |sid| # MySchema.subscriptions.delete_subscription(sid) # } # end # # private # # def ensure_hash(ambiguous_param) # case ambiguous_param # when String # if ambiguous_param.present? # ensure_hash(JSON.parse(ambiguous_param)) # else # {} # end # when Hash, ActionController::Parameters # ambiguous_param # when nil # {} # else # raise ArgumentError, "Unexpected parameter: #{ambiguous_param}" # end # end # end # # @see GraphQL::Testing::MockActionCable for test helpers class ActionCableSubscriptions < GraphQL::Subscriptions SUBSCRIPTION_PREFIX = "graphql-subscription:" EVENT_PREFIX = "graphql-event:" # @param serializer [<#dump(obj), #load(string)] Used for serializing messages before handing them to `.broadcast(msg)` # @param namespace [string] Used to namespace events and subscriptions (default: '') def initialize(serializer: Serialize, namespace: '', action_cable: ActionCable, action_cable_coder: ActiveSupport::JSON, **rest) # A per-process map of subscriptions to deliver. # This is provided by Rails, so let's use it @subscriptions = Concurrent::Map.new @events = Concurrent::Map.new do |h, k| h.compute_if_absent(k) do Concurrent::Map.new do |h2, k2| h2.compute_if_absent(k2) { Concurrent::Array.new } end end end @action_cable = action_cable @action_cable_coder = action_cable_coder @serializer = serializer @serialize_with_context = case @serializer.method(:load).arity when 1 false when 2 true else raise ArgumentError, "#{@serializer} must respond to `.load` accepting one or two arguments" end @transmit_ns = namespace super end # An event was triggered; Push the data over ActionCable. # Subscribers will re-evaluate locally. def execute_all(event, object) stream = stream_event_name(event) message = @serializer.dump(object) @action_cable.server.broadcast(stream, message) end # This subscription was re-evaluated. # Send it to the specific stream where this client was waiting. def deliver(subscription_id, result) has_more = !result.context.namespace(:subscriptions)[:final_update] payload = { result: result.to_h, more: has_more } @action_cable.server.broadcast(stream_subscription_name(subscription_id), payload) end # A query was run where these events were subscribed to. # Store them in memory in _this_ ActionCable frontend. # It will receive notifications when events come in # and re-evaluate the query locally. def write_subscription(query, events) unless (channel = query.context[:channel]) raise GraphQL::Error, "This GraphQL Subscription client does not support the transport protocol expected"\ "by the backend Subscription Server implementation (graphql-ruby ActionCableSubscriptions in this case)."\ "Some official client implementation including Apollo (https://graphql-ruby.org/javascript_client/apollo_subscriptions.html), "\ "Relay Modern (https://graphql-ruby.org/javascript_client/relay_subscriptions.html#actioncable)."\ "GraphiQL via `graphiql-rails` may not work out of box (#1051)." end subscription_id = query.context[:subscription_id] ||= build_id stream = stream_subscription_name(subscription_id) channel.stream_from(stream) @subscriptions[subscription_id] = query events.each do |event| # Setup a new listener to run all events with this topic in this process setup_stream(channel, event) # Add this event to the list of events to be updated @events[event.topic][event.fingerprint] << event end end # Every subscribing channel is listening here, but only one of them takes any action. # This is so we can reuse payloads when possible, and make one payload to send to # all subscribers. # # But the problem is, any channel could close at any time, so each channel has to # be ready to take over the primary position. # # To make sure there's always one-and-only-one channel building payloads, # let the listener belonging to the first event on the list be # the one to build and publish payloads. # def setup_stream(channel, initial_event) topic = initial_event.topic event_stream = stream_event_name(initial_event) channel.stream_from(event_stream, coder: @action_cable_coder) do |message| events_by_fingerprint = @events[topic] object = nil events_by_fingerprint.each do |_fingerprint, events| if !events.empty? && events.first == initial_event # The fingerprint has told us that this response should be shared by all subscribers, # so just run it once, then deliver the result to every subscriber first_event = events.first first_subscription_id = first_event.context.fetch(:subscription_id) object ||= load_action_cable_message(message, first_event.context) result = execute_update(first_subscription_id, first_event, object) if !result.nil? # Having calculated the result _once_, send the same payload to all subscribers events.each do |event| subscription_id = event.context.fetch(:subscription_id) deliver(subscription_id, result) end end end end nil end end # This is called to turn an ActionCable-broadcasted string (JSON) # into a query-ready application object. # @param message [String] n ActionCable-broadcasted string (JSON) # @param context [GraphQL::Query::Context] the context of the first event for a given subscription fingerprint def load_action_cable_message(message, context) if @serialize_with_context @serializer.load(message, context) else @serializer.load(message) end end # Return the query from "storage" (in memory) def read_subscription(subscription_id) query = @subscriptions[subscription_id] if query.nil? # This can happen when a subscription is triggered from an unsubscribed channel, # see https://github.com/rmosolgo/graphql-ruby/issues/2478. # (This `nil` is handled by `#execute_update`) nil else { query_string: query.query_string, variables: query.provided_variables, context: query.context.to_h, operation_name: query.operation_name, } end end # The channel was closed, forget about it. def delete_subscription(subscription_id) query = @subscriptions.delete(subscription_id) # In case this came from the server, tell the client to unsubscribe: @action_cable.server.broadcast(stream_subscription_name(subscription_id), { more: false }) # This can be `nil` when `.trigger` happens inside an unsubscribed ActionCable channel, # see https://github.com/rmosolgo/graphql-ruby/issues/2478 if query events = query.context.namespace(:subscriptions)[:events] events.each do |event| ev_by_fingerprint = @events[event.topic] ev_for_fingerprint = ev_by_fingerprint[event.fingerprint] ev_for_fingerprint.delete(event) if ev_for_fingerprint.empty? ev_by_fingerprint.delete(event.fingerprint) end end end end private def stream_subscription_name(subscription_id) [SUBSCRIPTION_PREFIX, @transmit_ns, subscription_id].join end def stream_event_name(event) [EVENT_PREFIX, @transmit_ns, event.topic].join end end end end graphql-ruby-2.5.19/lib/graphql/subscriptions/broadcast_analyzer.rb000066400000000000000000000067731514115062600255520ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Subscriptions # Detect whether the current operation: # - Is a subscription operation # - Is completely broadcastable # # Assign the result to `context.namespace(:subscriptions)[:subscription_broadcastable]` # @api private # @see Subscriptions#broadcastable? for a public API class BroadcastAnalyzer < GraphQL::Analysis::Analyzer def initialize(subject) super @default_broadcastable = subject.schema.subscriptions.default_broadcastable # Maybe this will get set to false while analyzing @subscription_broadcastable = true end # Only analyze subscription operations def analyze? @query.subscription? end def on_enter_field(node, parent, visitor) if (@subscription_broadcastable == false) || visitor.skipping? return end current_field = visitor.field_definition current_type = visitor.parent_type_definition apply_broadcastable(current_type, current_field) if current_type.kind.interface? pt = @query.possible_types(current_type) pt.each do |object_type| ot_field = @query.get_field(object_type, current_field.graphql_name) # Inherited fields would be exactly the same object; # only check fields that are overrides of the inherited one if ot_field && ot_field != current_field apply_broadcastable(object_type, ot_field) end end end end # Assign the result to context. # (This method is allowed to return an error, but we don't need to) # @return [void] def result query.context.namespace(:subscriptions)[:subscription_broadcastable] = @subscription_broadcastable nil end private # Modify `@subscription_broadcastable` based on `field_defn`'s configuration (and/or the default value) def apply_broadcastable(owner_type, field_defn) current_field_broadcastable = field_defn.introspection? || field_defn.broadcastable? if current_field_broadcastable.nil? && owner_type.respond_to?(:default_broadcastable?) current_field_broadcastable = owner_type.default_broadcastable? end case current_field_broadcastable when nil query.logger.debug { "`broadcastable: nil` for field: #{field_defn.path}" } # If the value wasn't set, mix in the default value: # - If the default is false and the current value is true, make it false # - If the default is true and the current value is true, it stays true # - If the default is false and the current value is false, keep it false # - If the default is true and the current value is false, keep it false @subscription_broadcastable = @subscription_broadcastable && @default_broadcastable when false query.logger.debug { "`broadcastable: false` for field: #{field_defn.path}" } # One non-broadcastable field is enough to make the whole subscription non-broadcastable @subscription_broadcastable = false when true # Leave `@broadcastable_query` true if it's already true, # but don't _set_ it to true if it was set to false by something else. # Actually, just leave it! else raise ArgumentError, "Unexpected `.broadcastable?` value for #{field_defn.path}: #{current_field_broadcastable}" end end end end end graphql-ruby-2.5.19/lib/graphql/subscriptions/default_subscription_resolve_extension.rb000066400000000000000000000042731514115062600317570ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Subscriptions class DefaultSubscriptionResolveExtension < GraphQL::Schema::FieldExtension def resolve(context:, object:, arguments:) has_override_implementation = @field.resolver || object.respond_to?(@field.resolver_method) if !has_override_implementation if context.query.subscription_update? object.object else context.skip end else yield(object, arguments) end end def after_resolve(value:, context:, object:, arguments:, **rest) if value.is_a?(GraphQL::ExecutionError) value elsif @field.resolver&.method_defined?(:subscription_written?) && (subscription_namespace = context.namespace(:subscriptions)) && (subscriptions_by_path = subscription_namespace[:subscriptions]) (subscription_instance = subscriptions_by_path[context.current_path]) # If it was already written, don't append this event to be written later if !subscription_instance.subscription_written? events = context.namespace(:subscriptions)[:events] events << subscription_instance.event end value elsif (events = context.namespace(:subscriptions)[:events]) # This is the first execution, so gather an Event # for the backend to register: event = Subscriptions::Event.new( name: field.name, arguments: arguments, context: context, field: field, ) events << event value elsif context.query.subscription_topic == Subscriptions::Event.serialize( field.name, arguments, field, scope: (field.subscription_scope ? context[field.subscription_scope] : nil), ) # This is a subscription update. The resolver returned `skip` if it should be skipped, # or else it returned an object to resolve the update. value else # This is a subscription update, but this event wasn't triggered. context.skip end end end end end graphql-ruby-2.5.19/lib/graphql/subscriptions/event.rb000066400000000000000000000146301514115062600230130ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class Subscriptions # This thing can be: # - Subscribed to by `subscription { ... }` # - Triggered by `MySchema.subscriber.trigger(name, arguments, obj)` # class Event # @return [String] Corresponds to the Subscription root field name attr_reader :name # @return [GraphQL::Execution::Interpreter::Arguments] attr_reader :arguments # @return [GraphQL::Query::Context] attr_reader :context # @return [String] An opaque string which identifies this event, derived from `name` and `arguments` attr_reader :topic def initialize(name:, arguments:, field: nil, context: nil, scope: nil) @name = name @arguments = self.class.arguments_without_field_extras(arguments: arguments, field: field) @context = context field ||= context.field scope_key = field.subscription_scope scope_val = scope || (context && scope_key && context[scope_key]) if scope_key && (subscription = field.resolver) && (subscription.respond_to?(:subscription_scope_optional?)) && !subscription.subscription_scope_optional? && scope_val.nil? raise Subscriptions::SubscriptionScopeMissingError, "#{field.path} (#{subscription}) requires a `scope:` value to trigger updates (Set `subscription_scope ..., optional: true` to disable this requirement)" end @topic = self.class.serialize(name, arguments, field, scope: scope_val, context: context) end # @return [String] an identifier for this unit of subscription def self.serialize(_name, arguments, field, scope:, context: GraphQL::Query::NullContext.instance) subscription = field.resolver || GraphQL::Schema::Subscription arguments = arguments_without_field_extras(field: field, arguments: arguments) normalized_args = stringify_args(field, arguments.to_h, context) subscription.topic_for(arguments: normalized_args, field: field, scope: scope) end # @return [String] a logical identifier for this event. (Stable when the query is broadcastable.) def fingerprint @fingerprint ||= begin # When this query has been flagged as broadcastable, # use a generalized, stable fingerprint so that # duplicate subscriptions can be evaluated and distributed in bulk. # (`@topic` includes field, args, and subscription scope already.) if @context.namespace(:subscriptions)[:subscription_broadcastable] "#{@topic}/#{@context.query.fingerprint}" else # not broadcastable, build a unique ID for this event @context.schema.subscriptions.build_id end end end class << self def arguments_without_field_extras(arguments:, field:) if !field.extras.empty? arguments = arguments.dup field.extras.each do |extra_key| arguments.delete(extra_key) end end arguments end private # This method does not support cyclic references in the Hash, # nor does it support Hashes whose keys are not sortable # with respect to their peers ( cases where a <=> b might throw an error ) def deep_sort_hash_keys(hash_to_sort) raise ArgumentError.new("Argument must be a Hash") unless hash_to_sort.is_a?(Hash) hash_to_sort.keys.sort.map do |k| if hash_to_sort[k].is_a?(Hash) [k, deep_sort_hash_keys(hash_to_sort[k])] elsif hash_to_sort[k].is_a?(Array) [k, deep_sort_array_hashes(hash_to_sort[k])] else [k, hash_to_sort[k]] end end.to_h end def deep_sort_array_hashes(array_to_inspect) raise ArgumentError.new("Argument must be an Array") unless array_to_inspect.is_a?(Array) array_to_inspect.map do |v| if v.is_a?(Hash) deep_sort_hash_keys(v) elsif v.is_a?(Array) deep_sort_array_hashes(v) else v end end end def stringify_args(arg_owner, args, context) arg_owner = arg_owner.respond_to?(:unwrap) ? arg_owner.unwrap : arg_owner # remove list and non-null wrappers case args when Hash next_args = {} args.each do |k, v| arg_name = k.to_s camelized_arg_name = GraphQL::Schema::Member::BuildType.camelize(arg_name) arg_defn = get_arg_definition(arg_owner, camelized_arg_name, context) arg_defn ||= get_arg_definition(arg_owner, arg_name, context) normalized_arg_name = arg_defn.graphql_name arg_base_type = arg_defn.type.unwrap # In the case where the value being emitted is seen as a "JSON" # type, treat the value as one atomic unit of serialization is_json_definition = arg_base_type && arg_base_type <= GraphQL::Types::JSON if is_json_definition sorted_value = if v.is_a?(Hash) deep_sort_hash_keys(v) elsif v.is_a?(Array) deep_sort_array_hashes(v) else v end next_args[normalized_arg_name] = sorted_value.respond_to?(:to_json) ? sorted_value.to_json : sorted_value else next_args[normalized_arg_name] = stringify_args(arg_base_type, v, context) end end # Make sure they're deeply sorted next_args.sort.to_h when Array args.map { |a| stringify_args(arg_owner, a, context) } when GraphQL::Schema::InputObject stringify_args(arg_owner, args.to_h, context) else if arg_owner.is_a?(Class) && arg_owner < GraphQL::Schema::Enum # `prepare:` may have made the value something other than # a defined value of this enum -- use _that_ in this case. arg_owner.coerce_isolated_input(args) || args else args end end end def get_arg_definition(arg_owner, arg_name, context) context.types.argument(arg_owner, arg_name) || context.types.arguments(arg_owner).find { |v| v.keyword.to_s == arg_name } end end end end end graphql-ruby-2.5.19/lib/graphql/subscriptions/serialize.rb000066400000000000000000000140641514115062600236620ustar00rootroot00000000000000# frozen_string_literal: true require "set" module GraphQL class Subscriptions # Serialization helpers for passing subscription data around. # @api private module Serialize GLOBALID_KEY = "__gid__" SYMBOL_KEY = "__sym__" SYMBOL_KEYS_KEY = "__sym_keys__" TIMESTAMP_KEY = "__timestamp__" TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S.%N%z" # eg '2020-01-01 23:59:59.123456789+05:00' OPEN_STRUCT_KEY = "__ostruct__" module_function # @param str [String] A serialized object from {.dump} # @return [Object] An object equivalent to the one passed to {.dump} def load(str) parsed_obj = JSON.parse(str) load_value(parsed_obj) end # @param obj [Object] Some subscription-related data to dump # @return [String] The stringified object def dump(obj) JSON.generate(dump_value(obj), quirks_mode: true) end # This is for turning objects into subscription scopes. # It's a one-way transformation, can't reload this :'( # @param obj [Object] # @return [String] def dump_recursive(obj) case when obj.is_a?(Array) obj.map { |i| dump_recursive(i) }.join(':') when obj.is_a?(Hash) obj.map { |k, v| "#{dump_recursive(k)}:#{dump_recursive(v)}" }.join(":") when obj.is_a?(GraphQL::Schema::InputObject) dump_recursive(obj.to_h) when obj.respond_to?(:to_gid_param) obj.to_gid_param when obj.respond_to?(:to_param) obj.to_param else obj.to_s end end class << self private # @param value [Object] A parsed JSON object # @return [Object] An object that load Global::Identification recursive def load_value(value) if value.is_a?(Array) is_gids = (v1 = value[0]).is_a?(Hash) && v1.size == 1 && v1[GLOBALID_KEY] if is_gids # Assume it's an array of global IDs ids = value.map { |v| v[GLOBALID_KEY] } GlobalID::Locator.locate_many(ids) else value.map { |item| load_value(item) } end elsif value.is_a?(Hash) if value.size == 1 case value.keys.first # there's only 1 key when GLOBALID_KEY GlobalID::Locator.locate(value[GLOBALID_KEY]) when SYMBOL_KEY value[SYMBOL_KEY].to_sym when TIMESTAMP_KEY timestamp_class_name, *timestamp_args = value[TIMESTAMP_KEY] timestamp_class = Object.const_get(timestamp_class_name) if defined?(ActiveSupport::TimeWithZone) && timestamp_class <= ActiveSupport::TimeWithZone zone_name, timestamp_s = timestamp_args zone = ActiveSupport::TimeZone[zone_name] raise "Zone #{zone_name} not found, unable to deserialize" unless zone zone.strptime(timestamp_s, TIMESTAMP_FORMAT) else timestamp_s = timestamp_args.first timestamp_class.strptime(timestamp_s, TIMESTAMP_FORMAT) end when OPEN_STRUCT_KEY ostruct_values = load_value(value[OPEN_STRUCT_KEY]) OpenStruct.new(ostruct_values) else key = value.keys.first { key => load_value(value[key]) } end else loaded_h = {} sym_keys = value.fetch(SYMBOL_KEYS_KEY, []) value.each do |k, v| if k == SYMBOL_KEYS_KEY next end if sym_keys.include?(k) k = k.to_sym end loaded_h[k] = load_value(v) end loaded_h end else value end end # @param obj [Object] Some subscription-related data to dump # @return [Object] The object that converted Global::Identification def dump_value(obj) if obj.is_a?(Array) obj.map{|item| dump_value(item)} elsif obj.is_a?(Hash) symbol_keys = nil dumped_h = {} obj.each do |k, v| dumped_h[k.to_s] = dump_value(v) if k.is_a?(Symbol) symbol_keys ||= Set.new symbol_keys << k.to_s end end if symbol_keys dumped_h[SYMBOL_KEYS_KEY] = symbol_keys.to_a end dumped_h elsif obj.is_a?(Symbol) { SYMBOL_KEY => obj.to_s } elsif obj.respond_to?(:to_gid_param) {GLOBALID_KEY => obj.to_gid_param} elsif defined?(ActiveSupport::TimeWithZone) && obj.is_a?(ActiveSupport::TimeWithZone) && obj.class.name != Time.name # This handles a case where Rails prior to 7 would # make the class ActiveSupport::TimeWithZone return "Time" for # its name. In Rails 7, it will now return "ActiveSupport::TimeWithZone", # which happens to be incompatible with expectations we have # with what a Time class supports ( notably, strptime in `load_value` ). # # This now passes along the name of the zone, such that a future deserialization # of this string will use the correct time zone from the ActiveSupport TimeZone # list to produce the time. # { TIMESTAMP_KEY => [obj.class.name, obj.time_zone.name, obj.strftime(TIMESTAMP_FORMAT)] } elsif obj.is_a?(Date) || obj.is_a?(Time) # DateTime extends Date; for TimeWithZone, call `.utc` first. { TIMESTAMP_KEY => [obj.class.name, obj.strftime(TIMESTAMP_FORMAT)] } elsif defined?(OpenStruct) && obj.is_a?(OpenStruct) { OPEN_STRUCT_KEY => dump_value(obj.to_h) } elsif defined?(ActiveRecord::Relation) && obj.is_a?(ActiveRecord::Relation) dump_value(obj.to_a) else obj end end end end end end graphql-ruby-2.5.19/lib/graphql/testing.rb000066400000000000000000000001541514115062600204340ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/testing/helpers" require "graphql/testing/mock_action_cable" graphql-ruby-2.5.19/lib/graphql/testing/000077500000000000000000000000001514115062600201075ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/testing/helpers.rb000066400000000000000000000147001514115062600221000ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Testing module Helpers # @param schema_class [Class] # @return [Module] A helpers module which always uses the given schema def self.for(schema_class) SchemaHelpers.for(schema_class) end class Error < GraphQL::Error end class TypeNotVisibleError < Error def initialize(type_name:) message = "`#{type_name}` should be `visible?` this field resolution and `context`, but it was not" super(message) end end class FieldNotVisibleError < Error def initialize(type_name:, field_name:) message = "`#{type_name}.#{field_name}` should be `visible?` for this resolution, but it was not" super(message) end end class TypeNotDefinedError < Error def initialize(type_name:) message = "No type named `#{type_name}` is defined; choose another type name or define this type." super(message) end end class FieldNotDefinedError < Error def initialize(type_name:, field_name:) message = "`#{type_name}` has no field named `#{field_name}`; pick another name or define this field." super(message) end end def run_graphql_field(schema, field_path, object, arguments: {}, context: {}, ast_node: nil, lookahead: nil, visibility_profile: nil) type_name, *field_names = field_path.split(".") dummy_query = GraphQL::Query.new(schema, "{ __typename }", context: context, visibility_profile: visibility_profile) query_context = dummy_query.context dataloader = query_context.dataloader object_type = dummy_query.types.type(type_name) # rubocop:disable Development/ContextIsPassedCop if object_type graphql_result = object field_names.each do |field_name| inner_object = graphql_result dataloader.run_isolated { graphql_result = object_type.wrap(inner_object, query_context) } if graphql_result.nil? return nil end visible_field = dummy_query.types.field(object_type, field_name) # rubocop:disable Development/ContextIsPassedCop if visible_field dataloader.run_isolated { query_context[:current_field] = visible_field field_args = visible_field.coerce_arguments(graphql_result, arguments, query_context) field_args = schema.sync_lazy(field_args) if !visible_field.extras.empty? extra_args = {} visible_field.extras.each do |extra| extra_args[extra] = case extra when :ast_node ast_node ||= GraphQL::Language::Nodes::Field.new(name: visible_field.graphql_name) when :lookahead lookahead ||= begin ast_node ||= GraphQL::Language::Nodes::Field.new(name: visible_field.graphql_name) Execution::Lookahead.new( query: dummy_query, ast_nodes: [ast_node], field: visible_field, ) end else raise ArgumentError, "This extra isn't supported in `run_graphql_field` yet: `#{extra.inspect}`. Open an issue on GitHub to request it: https://github.com/rmosolgo/graphql-ruby/issues/new" end end field_args = field_args.merge_extras(extra_args) end graphql_result = visible_field.resolve(graphql_result, field_args.keyword_arguments, query_context) graphql_result = schema.sync_lazy(graphql_result) } object_type = visible_field.type.unwrap elsif object_type.all_field_definitions.any? { |f| f.graphql_name == field_name } raise FieldNotVisibleError.new(field_name: field_name, type_name: type_name) else raise FieldNotDefinedError.new(type_name: type_name, field_name: field_name) end end graphql_result else unfiltered_type = schema.use_visibility_profile? ? schema.visibility.get_type(type_name) : schema.get_type(type_name) # rubocop:disable Development/ContextIsPassedCop if unfiltered_type raise TypeNotVisibleError.new(type_name: type_name) else raise TypeNotDefinedError.new(type_name: type_name) end end end def with_resolution_context(schema, type:, object:, context:{}, visibility_profile: nil) resolution_context = ResolutionAssertionContext.new( self, schema: schema, type_name: type, object: object, context: context, visibility_profile: visibility_profile, ) yield(resolution_context) end class ResolutionAssertionContext def initialize(test, type_name:, object:, schema:, context:, visibility_profile:) @test = test @type_name = type_name @object = object @schema = schema @context = context @visibility_profile = visibility_profile end attr_reader :visibility_profile def run_graphql_field(field_name, arguments: {}) if @schema @test.run_graphql_field(@schema, "#{@type_name}.#{field_name}", @object, arguments: arguments, context: @context, visibility_profile: @visibility_profile) else @test.run_graphql_field("#{@type_name}.#{field_name}", @object, arguments: arguments, context: @context, visibility_profile: @visibility_profile) end end end module SchemaHelpers include Helpers def run_graphql_field(field_path, object, arguments: {}, context: {}, visibility_profile: nil) super(@@schema_class_for_helpers, field_path, object, arguments: arguments, context: context, visibility_profile: visibility_profile) end def with_resolution_context(*args, **kwargs, &block) # schema will be added later super(nil, *args, **kwargs, &block) end def self.for(schema_class) Module.new do include SchemaHelpers @@schema_class_for_helpers = schema_class end end end end end end graphql-ruby-2.5.19/lib/graphql/testing/mock_action_cable.rb000066400000000000000000000070351514115062600240550ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Testing # A stub implementation of ActionCable. # Any methods to support the mock backend have `mock` in the name. # # @example Configuring your schema to use MockActionCable in the test environment # class MySchema < GraphQL::Schema # # Use MockActionCable in test: # use GraphQL::Subscriptions::ActionCableSubscriptions, # action_cable: Rails.env.test? ? GraphQL::Testing::MockActionCable : ActionCable # end # # @example Clearing old data before each test # setup do # GraphQL::Testing::MockActionCable.clear_mocks # end # # @example Using MockActionCable in a test case # # Create a channel to use in the test, pass it to GraphQL # mock_channel = GraphQL::Testing::MockActionCable.get_mock_channel # ActionCableTestSchema.execute("subscription { newsFlash { text } }", context: { channel: mock_channel }) # # # Trigger a subscription update # ActionCableTestSchema.subscriptions.trigger(:news_flash, {}, {text: "After yesterday's rain, someone stopped on Rio Road to help a box turtle across five lanes of traffic"}) # # # Check messages on the channel # expected_msg = { # result: { # "data" => { # "newsFlash" => { # "text" => "After yesterday's rain, someone stopped on Rio Road to help a box turtle across five lanes of traffic" # } # } # }, # more: true, # } # assert_equal [expected_msg], mock_channel.mock_broadcasted_messages # class MockActionCable class MockChannel def initialize @mock_broadcasted_messages = [] end # @return [Array] Payloads "sent" to this channel by GraphQL-Ruby attr_reader :mock_broadcasted_messages # Called by ActionCableSubscriptions. Implements a Rails API. def stream_from(stream_name, coder: nil, &block) # Rails uses `coder`, we don't block ||= ->(msg) { @mock_broadcasted_messages << msg } MockActionCable.mock_stream_for(stream_name).add_mock_channel(self, block) end end # Used by mock code # @api private class MockStream def initialize @mock_channels = {} end def add_mock_channel(channel, handler) @mock_channels[channel] = handler end def mock_broadcast(message) @mock_channels.each do |channel, handler| handler && handler.call(message) end end end class << self # Call this before each test run to make sure that MockActionCable's data is empty def clear_mocks @mock_streams = {} end # Implements Rails API def server self end # Implements Rails API def broadcast(stream_name, message) stream = @mock_streams[stream_name] stream && stream.mock_broadcast(message) end # Used by mock code def mock_stream_for(stream_name) @mock_streams[stream_name] ||= MockStream.new end # Use this as `context[:channel]` to simulate an ActionCable channel # # @return [GraphQL::Testing::MockActionCable::MockChannel] def get_mock_channel MockChannel.new end # @return [Array] Streams that currently have subscribers def mock_stream_names @mock_streams.keys end end end end end graphql-ruby-2.5.19/lib/graphql/tracing.rb000066400000000000000000000061521514115062600204120ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Tracing autoload :Trace, "graphql/tracing/trace" autoload :CallLegacyTracers, "graphql/tracing/call_legacy_tracers" autoload :LegacyTrace, "graphql/tracing/legacy_trace" autoload :LegacyHooksTrace, "graphql/tracing/legacy_hooks_trace" autoload :NullTrace, "graphql/tracing/null_trace" autoload :ActiveSupportNotificationsTracing, "graphql/tracing/active_support_notifications_tracing" autoload :PlatformTracing, "graphql/tracing/platform_tracing" autoload :AppOpticsTracing, "graphql/tracing/appoptics_tracing" autoload :AppsignalTracing, "graphql/tracing/appsignal_tracing" autoload :DataDogTracing, "graphql/tracing/data_dog_tracing" autoload :NewRelicTracing, "graphql/tracing/new_relic_tracing" autoload :NotificationsTracing, "graphql/tracing/notifications_tracing" autoload :ScoutTracing, "graphql/tracing/scout_tracing" autoload :StatsdTracing, "graphql/tracing/statsd_tracing" autoload :PrometheusTracing, "graphql/tracing/prometheus_tracing" autoload :ActiveSupportNotificationsTrace, "graphql/tracing/active_support_notifications_trace" autoload :PlatformTrace, "graphql/tracing/platform_trace" autoload :AppOpticsTrace, "graphql/tracing/appoptics_trace" autoload :AppsignalTrace, "graphql/tracing/appsignal_trace" autoload :DataDogTrace, "graphql/tracing/data_dog_trace" autoload :MonitorTrace, "graphql/tracing/monitor_trace" autoload :NewRelicTrace, "graphql/tracing/new_relic_trace" autoload :NotificationsTrace, "graphql/tracing/notifications_trace" autoload :SentryTrace, "graphql/tracing/sentry_trace" autoload :ScoutTrace, "graphql/tracing/scout_trace" autoload :StatsdTrace, "graphql/tracing/statsd_trace" autoload :PrometheusTrace, "graphql/tracing/prometheus_trace" autoload :PerfettoTrace, "graphql/tracing/perfetto_trace" autoload :DetailedTrace, "graphql/tracing/detailed_trace" # Objects may include traceable to gain a `.trace(...)` method. # The object must have a `@tracers` ivar of type `Array<<#trace(k, d, &b)>>`. # @api private module Traceable # @param key [String] The name of the event in GraphQL internals # @param metadata [Hash] Event-related metadata (can be anything) # @return [Object] Must return the value of the block def trace(key, metadata, &block) return yield if @tracers.empty? call_tracers(0, key, metadata, &block) end private # If there's a tracer at `idx`, call it and then increment `idx`. # Otherwise, yield. # # @param idx [Integer] Which tracer to call # @param key [String] The current event name # @param metadata [Object] The current event object # @return Whatever the block returns def call_tracers(idx, key, metadata, &block) if idx == @tracers.length yield else @tracers[idx].trace(key, metadata) { call_tracers(idx + 1, key, metadata, &block) } end end end module NullTracer module_function def trace(k, v) yield end end end end graphql-ruby-2.5.19/lib/graphql/tracing/000077500000000000000000000000001514115062600200615ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/tracing/active_support_notifications_trace.rb000066400000000000000000000014641514115062600275710ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing/notifications_trace" module GraphQL module Tracing # This implementation forwards events to ActiveSupport::Notifications with a `graphql` suffix. # # @example Sending execution events to ActiveSupport::Notifications # class MySchema < GraphQL::Schema # trace_with(GraphQL::Tracing::ActiveSupportNotificationsTrace) # end # # @example Subscribing to GraphQL events with ActiveSupport::Notifications # ActiveSupport::Notifications.subscribe(/graphql/) do |event| # pp event.name # pp event.payload # end # module ActiveSupportNotificationsTrace include NotificationsTrace def initialize(engine: ActiveSupport::Notifications, **rest) super end end end end graphql-ruby-2.5.19/lib/graphql/tracing/active_support_notifications_tracing.rb000066400000000000000000000012361514115062600301170ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing/notifications_tracing" module GraphQL module Tracing # This implementation forwards events to ActiveSupport::Notifications # with a `graphql` suffix. # # @see KEYS for event names module ActiveSupportNotificationsTracing # A cache of frequently-used keys to avoid needless string allocations KEYS = NotificationsTracing::KEYS NOTIFICATIONS_ENGINE = NotificationsTracing.new(ActiveSupport::Notifications) if defined?(ActiveSupport::Notifications) def self.trace(key, metadata, &blk) NOTIFICATIONS_ENGINE.trace(key, metadata, &blk) end end end end graphql-ruby-2.5.19/lib/graphql/tracing/appoptics_trace.rb000066400000000000000000000206301514115062600235670ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing/platform_trace" module GraphQL module Tracing # This class uses the AppopticsAPM SDK from the appoptics_apm gem to create # traces for GraphQL. # # There are 4 configurations available. They can be set in the # appoptics_apm config file or in code. Please see: # {https://docs.appoptics.com/kb/apm_tracing/ruby/configure} # # AppOpticsAPM::Config[:graphql][:enabled] = true|false # AppOpticsAPM::Config[:graphql][:transaction_name] = true|false # AppOpticsAPM::Config[:graphql][:sanitize_query] = true|false # AppOpticsAPM::Config[:graphql][:remove_comments] = true|false module AppOpticsTrace # These GraphQL events will show up as 'graphql.prep' spans PREP_KEYS = ['lex', 'parse', 'validate', 'analyze_query', 'analyze_multiplex'].freeze # These GraphQL events will show up as 'graphql.execute' spans EXEC_KEYS = ['execute_multiplex', 'execute_query', 'execute_query_lazy'].freeze # During auto-instrumentation this version of AppOpticsTracing is compared # with the version provided in the appoptics_apm gem, so that the newer # version of the class can be used def self.version Gem::Version.new('1.0.0') end # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time [ 'lex', 'parse', 'validate', 'analyze_query', 'analyze_multiplex', 'execute_multiplex', 'execute_query', 'execute_query_lazy', ].each do |trace_method| module_eval <<-RUBY, __FILE__, __LINE__ def #{trace_method}(**data) return super if !defined?(AppOpticsAPM) || gql_config[:enabled] == false layer = span_name("#{trace_method}") kvs = metadata(data, layer) kvs[:Key] = "#{trace_method}" if (PREP_KEYS + EXEC_KEYS).include?("#{trace_method}") transaction_name(kvs[:InboundQuery]) if kvs[:InboundQuery] && layer == 'graphql.execute' ::AppOpticsAPM::SDK.trace(layer, kvs) do kvs.clear # we don't have to send them twice super end end RUBY end # rubocop:enable Development/NoEvalCop def execute_field(query:, field:, ast_node:, arguments:, object:) return_type = field.type.unwrap trace_field = if return_type.kind.scalar? || return_type.kind.enum? (field.trace.nil? && @trace_scalars) || field.trace else true end platform_key = if trace_field @platform_key_cache[AppOpticsTrace].platform_field_key_cache[field] else nil end if platform_key && trace_field return super if !defined?(AppOpticsAPM) || gql_config[:enabled] == false layer = platform_key kvs = metadata({query: query, field: field, ast_node: ast_node, arguments: arguments, object: object}, layer) ::AppOpticsAPM::SDK.trace(layer, kvs) do kvs.clear # we don't have to send them twice super end else super end end def execute_field_lazy(query:, field:, ast_node:, arguments:, object:) # rubocop:disable Development/TraceCallsSuperCop execute_field(query: query, field: field, ast_node: ast_node, arguments: arguments, object: object) end def authorized(**data) return super if !defined?(AppOpticsAPM) || gql_config[:enabled] == false layer = @platform_key_cache[AppOpticsTrace].platform_authorized_key_cache[data[:type]] kvs = metadata(data, layer) ::AppOpticsAPM::SDK.trace(layer, kvs) do kvs.clear # we don't have to send them twice super end end def authorized_lazy(**data) return super if !defined?(AppOpticsAPM) || gql_config[:enabled] == false layer = @platform_key_cache[AppOpticsTrace].platform_authorized_key_cache[data[:type]] kvs = metadata(data, layer) ::AppOpticsAPM::SDK.trace(layer, kvs) do kvs.clear # we don't have to send them twice super end end def resolve_type(**data) return super if !defined?(AppOpticsAPM) || gql_config[:enabled] == false layer = @platform_key_cache[AppOpticsTrace].platform_resolve_type_key_cache[data[:type]] kvs = metadata(data, layer) ::AppOpticsAPM::SDK.trace(layer, kvs) do kvs.clear # we don't have to send them twice super end end def resolve_type_lazy(**data) return super if !defined?(AppOpticsAPM) || gql_config[:enabled] == false layer = @platform_key_cache[AppOpticsTrace].platform_resolve_type_key_cache[data[:type]] kvs = metadata(data, layer) ::AppOpticsAPM::SDK.trace(layer, kvs) do kvs.clear # we don't have to send them twice super end end include PlatformTrace def platform_field_key(field) "graphql.#{field.owner.graphql_name}.#{field.graphql_name}" end def platform_authorized_key(type) "graphql.authorized.#{type.graphql_name}" end def platform_resolve_type_key(type) "graphql.resolve_type.#{type.graphql_name}" end private def gql_config ::AppOpticsAPM::Config[:graphql] ||= {} end def transaction_name(query) return if gql_config[:transaction_name] == false || ::AppOpticsAPM::SDK.get_transaction_name split_query = query.strip.split(/\W+/, 3) split_query[0] = 'query' if split_query[0].empty? name = "graphql.#{split_query[0..1].join('.')}" ::AppOpticsAPM::SDK.set_transaction_name(name) end def multiplex_transaction_name(names) return if gql_config[:transaction_name] == false || ::AppOpticsAPM::SDK.get_transaction_name name = "graphql.multiplex.#{names.join('.')}" name = "#{name[0..251]}..." if name.length > 254 ::AppOpticsAPM::SDK.set_transaction_name(name) end def span_name(key) return 'graphql.prep' if PREP_KEYS.include?(key) return 'graphql.execute' if EXEC_KEYS.include?(key) key[/^graphql\./] ? key : "graphql.#{key}" end # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def metadata(data, layer) data.keys.map do |key| case key when :context graphql_context(data[key], layer) when :query graphql_query(data[key]) when :query_string graphql_query_string(data[key]) when :multiplex graphql_multiplex(data[key]) when :path [key, data[key].join(".")] else [key, data[key]] end end.tap { _1.flatten!(2) }.each_slice(2).to_h.merge(Spec: 'graphql') end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength def graphql_context(context, layer) context.errors && context.errors.each do |err| AppOpticsAPM::API.log_exception(layer, err) end [[:Path, context.path.join('.')]] end def graphql_query(query) return [] unless query query_string = query.query_string query_string = remove_comments(query_string) if gql_config[:remove_comments] != false query_string = sanitize(query_string) if gql_config[:sanitize_query] != false [[:InboundQuery, query_string], [:Operation, query.selected_operation_name]] end def graphql_query_string(query_string) query_string = remove_comments(query_string) if gql_config[:remove_comments] != false query_string = sanitize(query_string) if gql_config[:sanitize_query] != false [:InboundQuery, query_string] end def graphql_multiplex(data) names = data.queries.map(&:operations).map!(&:keys).tap(&:flatten!).tap(&:compact!) multiplex_transaction_name(names) if names.size > 1 [:Operations, names.join(', ')] end def sanitize(query) return unless query # remove arguments query.gsub(/"[^"]*"/, '"?"') # strings .gsub(/-?[0-9]*\.?[0-9]+e?[0-9]*/, '?') # ints + floats .gsub(/\[[^\]]*\]/, '[?]') # arrays end def remove_comments(query) return unless query query.gsub(/#[^\n\r]*/, '') end end end end graphql-ruby-2.5.19/lib/graphql/tracing/appoptics_tracing.rb000066400000000000000000000135071514115062600241250ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing/platform_tracing" module GraphQL module Tracing # This class uses the AppopticsAPM SDK from the appoptics_apm gem to create # traces for GraphQL. # # There are 4 configurations available. They can be set in the # appoptics_apm config file or in code. Please see: # {https://docs.appoptics.com/kb/apm_tracing/ruby/configure} # # AppOpticsAPM::Config[:graphql][:enabled] = true|false # AppOpticsAPM::Config[:graphql][:transaction_name] = true|false # AppOpticsAPM::Config[:graphql][:sanitize_query] = true|false # AppOpticsAPM::Config[:graphql][:remove_comments] = true|false class AppOpticsTracing < GraphQL::Tracing::PlatformTracing # These GraphQL events will show up as 'graphql.prep' spans PREP_KEYS = ['lex', 'parse', 'validate', 'analyze_query', 'analyze_multiplex'].freeze # These GraphQL events will show up as 'graphql.execute' spans EXEC_KEYS = ['execute_multiplex', 'execute_query', 'execute_query_lazy'].freeze def initialize(...) warn "GraphQL::Tracing::AppOptics tracing is deprecated; update to SolarWindsAPM instead, which uses OpenTelemetry." super end # During auto-instrumentation this version of AppOpticsTracing is compared # with the version provided in the appoptics_apm gem, so that the newer # version of the class can be used def self.version Gem::Version.new('1.0.0') end self.platform_keys = { 'lex' => 'lex', 'parse' => 'parse', 'validate' => 'validate', 'analyze_query' => 'analyze_query', 'analyze_multiplex' => 'analyze_multiplex', 'execute_multiplex' => 'execute_multiplex', 'execute_query' => 'execute_query', 'execute_query_lazy' => 'execute_query_lazy' } def platform_trace(platform_key, _key, data) return yield if !defined?(AppOpticsAPM) || gql_config[:enabled] == false layer = span_name(platform_key) kvs = metadata(data, layer) kvs[:Key] = platform_key if (PREP_KEYS + EXEC_KEYS).include?(platform_key) transaction_name(kvs[:InboundQuery]) if kvs[:InboundQuery] && layer == 'graphql.execute' ::AppOpticsAPM::SDK.trace(layer, kvs) do kvs.clear # we don't have to send them twice yield end end def platform_field_key(type, field) "graphql.#{type.graphql_name}.#{field.graphql_name}" end def platform_authorized_key(type) "graphql.authorized.#{type.graphql_name}" end def platform_resolve_type_key(type) "graphql.resolve_type.#{type.graphql_name}" end private def gql_config ::AppOpticsAPM::Config[:graphql] ||= {} end def transaction_name(query) return if gql_config[:transaction_name] == false || ::AppOpticsAPM::SDK.get_transaction_name split_query = query.strip.split(/\W+/, 3) split_query[0] = 'query' if split_query[0].empty? name = "graphql.#{split_query[0..1].join('.')}" ::AppOpticsAPM::SDK.set_transaction_name(name) end def multiplex_transaction_name(names) return if gql_config[:transaction_name] == false || ::AppOpticsAPM::SDK.get_transaction_name name = "graphql.multiplex.#{names.join('.')}" name = "#{name[0..251]}..." if name.length > 254 ::AppOpticsAPM::SDK.set_transaction_name(name) end def span_name(key) return 'graphql.prep' if PREP_KEYS.include?(key) return 'graphql.execute' if EXEC_KEYS.include?(key) key[/^graphql\./] ? key : "graphql.#{key}" end # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def metadata(data, layer) data.keys.map do |key| case key when :context graphql_context(data[key], layer) when :query graphql_query(data[key]) when :query_string graphql_query_string(data[key]) when :multiplex graphql_multiplex(data[key]) when :path [key, data[key].join(".")] else [key, data[key]] end end.tap { _1.flatten!(2) }.each_slice(2).to_h.merge(Spec: 'graphql') end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength def graphql_context(context, layer) context.errors && context.errors.each do |err| AppOpticsAPM::API.log_exception(layer, err) end [[:Path, context.path.join('.')]] end def graphql_query(query) return [] unless query query_string = query.query_string query_string = remove_comments(query_string) if gql_config[:remove_comments] != false query_string = sanitize(query_string) if gql_config[:sanitize_query] != false [[:InboundQuery, query_string], [:Operation, query.selected_operation_name]] end def graphql_query_string(query_string) query_string = remove_comments(query_string) if gql_config[:remove_comments] != false query_string = sanitize(query_string) if gql_config[:sanitize_query] != false [:InboundQuery, query_string] end def graphql_multiplex(data) names = data.queries.map(&:operations).map!(&:keys).tap(&:flatten!).tap(&:compact!) multiplex_transaction_name(names) if names.size > 1 [:Operations, names.join(', ')] end def sanitize(query) return unless query # remove arguments query.gsub(/"[^"]*"/, '"?"') # strings .gsub(/-?[0-9]*\.?[0-9]+e?[0-9]*/, '?') # ints + floats .gsub(/\[[^\]]*\]/, '[?]') # arrays end def remove_comments(query) return unless query query.gsub(/#[^\n\r]*/, '') end end end end graphql-ruby-2.5.19/lib/graphql/tracing/appsignal_trace.rb000066400000000000000000000036011514115062600235420ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing/monitor_trace" module GraphQL module Tracing # Instrumentation for reporting GraphQL-Ruby times to Appsignal. # # @example Installing the tracer # class MySchema < GraphQL::Schema # trace_with GraphQL::Tracing::AppsignalTrace # end AppsignalTrace = MonitorTrace.create_module("appsignal") module AppsignalTrace # @param set_action_name [Boolean] If true, the GraphQL operation name will be used as the transaction name. # This is not advised if you run more than one query per HTTP request, for example, with `graphql-client` or multiplexing. # It can also be specified per-query with `context[:set_appsignal_action_name]`. def initialize(set_action_name: false, **rest) rest[:set_transaction_name] ||= set_action_name setup_appsignal_monitor(**rest) super end class AppsignalMonitor < MonitorTrace::Monitor def instrument(keyword, object) if keyword == :execute query = object.queries.first set_this_txn_name = query.context[:set_appsignal_action_name] if set_this_txn_name == true || (set_this_txn_name.nil? && @set_transaction_name) Appsignal::Transaction.current.set_action(transaction_name(query)) end end Appsignal.instrument(name_for(keyword, object)) do yield end end include MonitorTrace::Monitor::GraphQLSuffixNames class Event < GraphQL::Tracing::MonitorTrace::Monitor::Event def start Appsignal::Transaction.current.start_event end def finish Appsignal::Transaction.current.finish_event( @monitor.name_for(@keyword, @object), "", "" ) end end end end end end graphql-ruby-2.5.19/lib/graphql/tracing/appsignal_tracing.rb000066400000000000000000000033761514115062600241040ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing/platform_tracing" module GraphQL module Tracing class AppsignalTracing < PlatformTracing self.platform_keys = { "lex" => "lex.graphql", "parse" => "parse.graphql", "validate" => "validate.graphql", "analyze_query" => "analyze.graphql", "analyze_multiplex" => "analyze.graphql", "execute_multiplex" => "execute.graphql", "execute_query" => "execute.graphql", "execute_query_lazy" => "execute.graphql", } # @param set_action_name [Boolean] If true, the GraphQL operation name will be used as the transaction name. # This is not advised if you run more than one query per HTTP request, for example, with `graphql-client` or multiplexing. # It can also be specified per-query with `context[:set_appsignal_action_name]`. def initialize(options = {}) @set_action_name = options.fetch(:set_action_name, false) super end def platform_trace(platform_key, key, data) if key == "execute_query" set_this_txn_name = data[:query].context[:set_appsignal_action_name] if set_this_txn_name == true || (set_this_txn_name.nil? && @set_action_name) Appsignal::Transaction.current.set_action(transaction_name(data[:query])) end end Appsignal.instrument(platform_key) do yield end end def platform_field_key(type, field) "#{type.graphql_name}.#{field.graphql_name}.graphql" end def platform_authorized_key(type) "#{type.graphql_name}.authorized.graphql" end def platform_resolve_type_key(type) "#{type.graphql_name}.resolve_type.graphql" end end end end graphql-ruby-2.5.19/lib/graphql/tracing/call_legacy_tracers.rb000066400000000000000000000053141514115062600243730ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Tracing # This trace class calls legacy-style tracer with payload hashes. # New-style `trace_with` modules significantly reduce the overhead of tracing, # but that advantage is lost when legacy-style tracers are also used (since the payload hashes are still constructed). module CallLegacyTracers def lex(query_string:) (@multiplex || @query).trace("lex", { query_string: query_string }) { super } end def parse(query_string:) (@multiplex || @query).trace("parse", { query_string: query_string }) { super } end def validate(query:, validate:) query.trace("validate", { validate: validate, query: query }) { super } end def analyze_multiplex(multiplex:) multiplex.trace("analyze_multiplex", { multiplex: multiplex }) { super } end def analyze_query(query:) query.trace("analyze_query", { query: query }) { super } end def execute_multiplex(multiplex:) multiplex.trace("execute_multiplex", { multiplex: multiplex }) { super } end def execute_query(query:) query.trace("execute_query", { query: query }) { super } end def execute_query_lazy(query:, multiplex:) multiplex.trace("execute_query_lazy", { multiplex: multiplex, query: query }) { super } end def execute_field(field:, query:, ast_node:, arguments:, object:) query.trace("execute_field", { field: field, query: query, ast_node: ast_node, arguments: arguments, object: object, owner: field.owner, path: query.context[:current_path] }) { super } end def execute_field_lazy(field:, query:, ast_node:, arguments:, object:) query.trace("execute_field_lazy", { field: field, query: query, ast_node: ast_node, arguments: arguments, object: object, owner: field.owner, path: query.context[:current_path] }) { super } end def authorized(query:, type:, object:) query.trace("authorized", { context: query.context, type: type, object: object, path: query.context[:current_path] }) { super } end def authorized_lazy(query:, type:, object:) query.trace("authorized_lazy", { context: query.context, type: type, object: object, path: query.context[:current_path] }) { super } end def resolve_type(query:, type:, object:) query.trace("resolve_type", { context: query.context, type: type, object: object, path: query.context[:current_path] }) { super } end def resolve_type_lazy(query:, type:, object:) query.trace("resolve_type_lazy", { context: query.context, type: type, object: object, path: query.context[:current_path] }) { super } end end end end graphql-ruby-2.5.19/lib/graphql/tracing/data_dog_trace.rb000066400000000000000000000050301514115062600233240ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing/monitor_trace" module GraphQL module Tracing # A tracer for reporting to DataDog # @example Adding this tracer to your schema # class MySchema < GraphQL::Schema # trace_with GraphQL::Tracing::DataDogTrace # end # @example Skipping `resolve_type` and `authorized` events # trace_with GraphQL::Tracing::DataDogTrace, trace_authorized: false, trace_resolve_type: false DataDogTrace = MonitorTrace.create_module("datadog") module DataDogTrace class DatadogMonitor < MonitorTrace::Monitor def initialize(set_transaction_name:, service: nil, tracer: nil, **_rest) super if tracer.nil? tracer = defined?(Datadog::Tracing) ? Datadog::Tracing : Datadog.tracer end @tracer = tracer @service_name = service @has_prepare_span = @trace.respond_to?(:prepare_span) end attr_reader :tracer, :service_name def instrument(keyword, object) trace_key = name_for(keyword, object) @tracer.trace(trace_key, service: @service_name, type: 'custom') do |span| span.set_tag('component', 'graphql') op_name = keyword.respond_to?(:name) ? keyword.name : keyword.to_s span.set_tag('operation', op_name) if keyword == :execute operations = object.queries.map(&:selected_operation_name).join(', ') first_query = object.queries.first resource = if operations.empty? fallback_transaction_name(first_query && first_query.context) else operations end span.resource = resource if resource span.set_tag("selected_operation_name", first_query.selected_operation_name) span.set_tag("selected_operation_type", first_query.selected_operation&.operation_type) span.set_tag("query_string", first_query.query_string) end if @has_prepare_span @trace.prepare_span(keyword, object, span) end yield end end include MonitorTrace::Monitor::GraphQLSuffixNames class Event < MonitorTrace::Monitor::Event def start name = @monitor.name_for(keyword, object) @dd_span = @monitor.tracer.trace(name, service: @monitor.service_name, type: 'custom') end def finish @dd_span.finish end end end end end end graphql-ruby-2.5.19/lib/graphql/tracing/data_dog_tracing.rb000066400000000000000000000057021514115062600236630ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing/platform_tracing" module GraphQL module Tracing class DataDogTracing < PlatformTracing self.platform_keys = { 'lex' => 'lex.graphql', 'parse' => 'parse.graphql', 'validate' => 'validate.graphql', 'analyze_query' => 'analyze.graphql', 'analyze_multiplex' => 'analyze.graphql', 'execute_multiplex' => 'execute.graphql', 'execute_query' => 'execute.graphql', 'execute_query_lazy' => 'execute.graphql', } def platform_trace(platform_key, key, data) tracer.trace(platform_key, service: options[:service], type: 'custom') do |span| span.set_tag('component', 'graphql') span.set_tag('operation', key) if key == 'execute_multiplex' operations = data[:multiplex].queries.map(&:selected_operation_name).join(', ') resource = if operations.empty? first_query = data[:multiplex].queries.first fallback_transaction_name(first_query && first_query.context) else operations end span.resource = resource if resource # [Deprecated] will be removed in the future span.set_metric('_dd1.sr.eausr', analytics_sample_rate) if analytics_enabled? end if key == 'execute_query' span.set_tag(:selected_operation_name, data[:query].selected_operation_name) span.set_tag(:selected_operation_type, data[:query].selected_operation.operation_type) span.set_tag(:query_string, data[:query].query_string) end prepare_span(key, data, span) yield end end # Implement this method in a subclass to apply custom tags to datadog spans # @param key [String] The event being traced # @param data [Hash] The runtime data for this event (@see GraphQL::Tracing for keys for each event) # @param span [Datadog::Tracing::SpanOperation] The datadog span for this event def prepare_span(key, data, span) end def tracer default_tracer = defined?(Datadog::Tracing) ? Datadog::Tracing : Datadog.tracer # [Deprecated] options[:tracer] will be removed in the future options.fetch(:tracer, default_tracer) end def analytics_enabled? # [Deprecated] options[:analytics_enabled] will be removed in the future options.fetch(:analytics_enabled, false) end def analytics_sample_rate # [Deprecated] options[:analytics_sample_rate] will be removed in the future options.fetch(:analytics_sample_rate, 1.0) end def platform_field_key(type, field) "#{type.graphql_name}.#{field.graphql_name}" end def platform_authorized_key(type) "#{type.graphql_name}.authorized" end def platform_resolve_type_key(type) "#{type.graphql_name}.resolve_type" end end end end graphql-ruby-2.5.19/lib/graphql/tracing/detailed_trace.rb000066400000000000000000000134051514115062600233420ustar00rootroot00000000000000# frozen_string_literal: true if defined?(ActiveRecord) require "graphql/tracing/detailed_trace/active_record_backend" end require "graphql/tracing/detailed_trace/memory_backend" require "graphql/tracing/detailed_trace/redis_backend" module GraphQL module Tracing # `DetailedTrace` can make detailed profiles for a subset of production traffic. Install it in Rails with `rails generate graphql:detailed_trace`. # # When `MySchema.detailed_trace?(query)` returns `true`, a profiler-specific `trace_mode: ...` will be used for the query, # overriding the one in `context[:trace_mode]`. # # By default, the detailed tracer calls `.inspect` on application objects returned from fields. You can customize # this behavior by extending {DetailedTrace} and overriding {#inspect_object}. You can opt out of debug annotations # entirely with `use ..., debug: false` or for a single query with `context: { detailed_trace_debug: false }`. # # You can store saved traces in two ways: # # - __ActiveRecord__: With `rails generate graphql:detailed_trace`, a new migration will be added to your app. # That table will be used to store trace data. # # - __Redis__: Pass `redis: ...` to save trace data to a Redis database. Depending on your needs, # you can configure this database to retain all data (persistent) or to expire data according to your rules. # # If you need to save traces indefinitely, you can download them from Perfetto after opening them there. # # @example Installing with Rails # rails generate graphql:detailed_trace # optional: --redis # # @example Adding the sampler to your schema # class MySchema < GraphQL::Schema # # Add the sampler: # use GraphQL::Tracing::DetailedTrace, redis: Redis.new(...), limit: 100 # # # And implement this hook to tell it when to take a sample: # def self.detailed_trace?(query) # # Could use `query.context`, `query.selected_operation_name`, `query.query_string` here # # Could call out to Flipper, etc # rand <= 0.000_1 # one in ten thousand # end # end # # @see Graphql::Dashboard GraphQL::Dashboard for viewing stored results # # @example Customizing debug output in traces # class CustomDetailedTrace < GraphQL::Tracing::DetailedTrace # def inspect_object(object) # if object.is_a?(SomeThing) # # handle it specially ... # else # super # end # end # end # # @example disabling debug annotations completely # use DetailedTrace, debug: false, ... # # @example disabling debug annotations for one query # MySchema.execute(query_str, context: { detailed_trace_debug: false }) # class DetailedTrace # @param redis [Redis] If provided, profiles will be stored in Redis for later review # @param limit [Integer] A maximum number of profiles to store # @param debug [Boolean] if `false`, it won't create `debug` annotations in Perfetto traces (reduces overhead) # @param model_class [Class] Overrides {ActiveRecordBackend::GraphqlDetailedTrace} if present def self.use(schema, trace_mode: :profile_sample, memory: false, debug: debug?, redis: nil, limit: nil, model_class: nil) storage = if redis RedisBackend.new(redis: redis, limit: limit) elsif memory MemoryBackend.new(limit: limit) elsif defined?(ActiveRecord) ActiveRecordBackend.new(limit: limit, model_class: model_class) else raise ArgumentError, "To store traces, install ActiveRecord or provide `redis: ...`" end detailed_trace = self.new(storage: storage, trace_mode: trace_mode, debug: debug) schema.detailed_trace = detailed_trace schema.trace_with(PerfettoTrace, mode: trace_mode, save_profile: true) end def initialize(storage:, trace_mode:, debug:) @storage = storage @trace_mode = trace_mode @debug = debug end # @return [Symbol] The trace mode to use when {Schema.detailed_trace?} returns `true` attr_reader :trace_mode # @return [String] ID of saved trace def save_trace(operation_name, duration_ms, begin_ms, trace_data) @storage.save_trace(operation_name, duration_ms, begin_ms, trace_data) end # @return [Boolean] def debug? @debug end # @param last [Integer] # @param before [Integer] Timestamp in milliseconds since epoch # @return [Enumerable] def traces(last: nil, before: nil) @storage.traces(last: last, before: before) end # @return [StoredTrace, nil] def find_trace(id) @storage.find_trace(id) end # @return [void] def delete_trace(id) @storage.delete_trace(id) end # @return [void] def delete_all_traces @storage.delete_all_traces end def inspect_object(object) self.class.inspect_object(object) end def self.inspect_object(object) if defined?(ActiveRecord::Relation) && object.is_a?(ActiveRecord::Relation) "#{object.class}, .to_sql=#{object.to_sql.inspect}" else object.inspect end end # Default debug setting # @return [true] def self.debug? true end class StoredTrace def initialize(id:, operation_name:, duration_ms:, begin_ms:, trace_data:) @id = id @operation_name = operation_name @duration_ms = duration_ms @begin_ms = begin_ms @trace_data = trace_data end attr_reader :id, :operation_name, :duration_ms, :begin_ms, :trace_data end end end end graphql-ruby-2.5.19/lib/graphql/tracing/detailed_trace/000077500000000000000000000000001514115062600230125ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/tracing/detailed_trace/active_record_backend.rb000066400000000000000000000035041514115062600276210ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Tracing class DetailedTrace class ActiveRecordBackend class GraphqlDetailedTrace < ActiveRecord::Base end def initialize(limit: nil, model_class: nil) @limit = limit @model_class = model_class || GraphqlDetailedTrace end def traces(last:, before:) gdts = @model_class.all.order("begin_ms DESC") if before gdts = gdts.where("begin_ms < ?", before) end if last gdts = gdts.limit(last) end gdts.map { |gdt| record_to_stored_trace(gdt) } end def delete_trace(id) @model_class.where(id: id).destroy_all nil end def delete_all_traces @model_class.all.destroy_all end def find_trace(id) gdt = @model_class.find_by(id: id) if gdt record_to_stored_trace(gdt) else nil end end def save_trace(operation_name, duration_ms, begin_ms, trace_data) gdt = @model_class.create!( begin_ms: begin_ms, operation_name: operation_name, duration_ms: duration_ms, trace_data: trace_data, ) if @limit @model_class .where("id NOT IN(SELECT id FROM graphql_detailed_traces ORDER BY begin_ms DESC LIMIT ?)", @limit) .delete_all end gdt.id end private def record_to_stored_trace(gdt) StoredTrace.new( id: gdt.id, begin_ms: gdt.begin_ms, operation_name: gdt.operation_name, duration_ms: gdt.duration_ms, trace_data: gdt.trace_data ) end end end end end graphql-ruby-2.5.19/lib/graphql/tracing/detailed_trace/memory_backend.rb000066400000000000000000000027621514115062600263250ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Tracing class DetailedTrace # An in-memory trace storage backend. Suitable for testing and development only. # It won't work for multi-process deployments and everything is erased when the app is restarted. class MemoryBackend def initialize(limit: nil) @limit = limit @traces = {} @next_id = 0 end def traces(last:, before:) page = [] @traces.values.reverse_each do |trace| if page.size == last break elsif before.nil? || trace.begin_ms < before page << trace end end page end def find_trace(id) @traces[id] end def delete_trace(id) @traces.delete(id.to_i) nil end def delete_all_traces @traces.clear nil end def save_trace(operation_name, duration, begin_ms, trace_data) id = @next_id @next_id += 1 @traces[id] = DetailedTrace::StoredTrace.new( id: id, operation_name: operation_name, duration_ms: duration, begin_ms: begin_ms, trace_data: trace_data ) if @limit && @traces.size > @limit del_keys = @traces.keys[0...-@limit] del_keys.each { |k| @traces.delete(k) } end id end end end end end graphql-ruby-2.5.19/lib/graphql/tracing/detailed_trace/redis_backend.rb000066400000000000000000000036551514115062600261250ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Tracing class DetailedTrace class RedisBackend KEY_PREFIX = "gql:trace:" def initialize(redis:, limit: nil) @redis = redis @key = KEY_PREFIX + "traces" @remrangebyrank_limit = limit ? -limit - 1 : nil end def traces(last:, before:) before = case before when Numeric "(#{before}" when nil "+inf" end str_pairs = @redis.zrange(@key, before, 0, byscore: true, rev: true, limit: [0, last || 100], withscores: true) str_pairs.map do |(str_data, score)| entry_to_trace(score, str_data) end end def delete_trace(id) @redis.zremrangebyscore(@key, id, id) nil end def delete_all_traces @redis.del(@key) end def find_trace(id) str_data = @redis.zrange(@key, id, id, byscore: true).first if str_data.nil? nil else entry_to_trace(id, str_data) end end def save_trace(operation_name, duration_ms, begin_ms, trace_data) id = begin_ms data = JSON.dump({ "o" => operation_name, "d" => duration_ms, "b" => begin_ms, "t" => Base64.encode64(trace_data) }) @redis.pipelined do |pipeline| pipeline.zadd(@key, id, data) if @remrangebyrank_limit pipeline.zremrangebyrank(@key, 0, @remrangebyrank_limit) end end id end private def entry_to_trace(id, json_str) data = JSON.parse(json_str) StoredTrace.new( id: id, operation_name: data["o"], duration_ms: data["d"].to_f, begin_ms: data["b"].to_i, trace_data: Base64.decode64(data["t"]), ) end end end end end graphql-ruby-2.5.19/lib/graphql/tracing/legacy_hooks_trace.rb000066400000000000000000000050321514115062600242330ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Tracing module LegacyHooksTrace def execute_multiplex(multiplex:) multiplex_instrumenters = multiplex.schema.instrumenters[:multiplex] query_instrumenters = multiplex.schema.instrumenters[:query] # First, run multiplex instrumentation, then query instrumentation for each query RunHooks.call_hooks(multiplex_instrumenters, multiplex, :before_multiplex, :after_multiplex) do RunHooks.each_query_call_hooks(query_instrumenters, multiplex.queries) do super end end end module RunHooks module_function # Call the before_ hooks of each query, # Then yield if no errors. # `call_hooks` takes care of appropriate cleanup. def each_query_call_hooks(instrumenters, queries, i = 0) if i >= queries.length yield else query = queries[i] call_hooks(instrumenters, query, :before_query, :after_query) { each_query_call_hooks(instrumenters, queries, i + 1) { yield } } end end # Call each before hook, and if they all succeed, yield. # If they don't all succeed, call after_ for each one that succeeded. def call_hooks(instrumenters, object, before_hook_name, after_hook_name) begin successful = [] instrumenters.each do |instrumenter| instrumenter.public_send(before_hook_name, object) successful << instrumenter end # if any before hooks raise an exception, quit calling before hooks, # but call the after hooks on anything that succeeded but also # raise the exception that came from the before hook. rescue GraphQL::ExecutionError => err object.context.errors << err rescue => e raise call_after_hooks(successful, object, after_hook_name, e) end begin yield # Call the user code ensure ex = call_after_hooks(successful, object, after_hook_name, nil) raise ex if ex end end def call_after_hooks(instrumenters, object, after_hook_name, ex) instrumenters.reverse_each do |instrumenter| begin instrumenter.public_send(after_hook_name, object) rescue => e ex = e end end ex end end end end end graphql-ruby-2.5.19/lib/graphql/tracing/legacy_trace.rb000066400000000000000000000003361514115062600230320ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing/trace" require "graphql/tracing/call_legacy_tracers" module GraphQL module Tracing class LegacyTrace < Trace include CallLegacyTracers end end end graphql-ruby-2.5.19/lib/graphql/tracing/monitor_trace.rb000066400000000000000000000221171514115062600232560ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Tracing # This module is the basis for Ruby-level integration with third-party monitoring platforms. # Platform-specific traces include this module and implement an adapter. # # @see ActiveSupportNotificationsTrace Integration via ActiveSupport::Notifications, an alternative approach. module MonitorTrace class Monitor def initialize(trace:, set_transaction_name:, **_rest) @trace = trace @set_transaction_name = set_transaction_name @platform_field_key_cache = Hash.new { |h, k| h[k] = platform_field_key(k) }.compare_by_identity @platform_authorized_key_cache = Hash.new { |h, k| h[k] = platform_authorized_key(k) }.compare_by_identity @platform_resolve_type_key_cache = Hash.new { |h, k| h[k] = platform_resolve_type_key(k) }.compare_by_identity @platform_source_class_key_cache = Hash.new { |h, source_cls| h[source_cls] = platform_source_class_key(source_cls) }.compare_by_identity end def instrument(keyword, object, &block) raise "Implement #{self.class}#instrument to measure the block" end def start_event(keyword, object) ev = self.class::Event.new(self, keyword, object) ev.start ev end # Get the transaction name based on the operation type and name if possible, or fall back to a user provided # one. Useful for anonymous queries. def transaction_name(query) selected_op = query.selected_operation txn_name = if selected_op op_type = selected_op.operation_type op_name = selected_op.name || fallback_transaction_name(query.context) || "anonymous" "#{op_type}.#{op_name}" else "query.anonymous" end "GraphQL/#{txn_name}" end def fallback_transaction_name(context) context[:tracing_fallback_transaction_name] end def name_for(keyword, object) case keyword when :execute_field @platform_field_key_cache[object] when :authorized @platform_authorized_key_cache[object] when :resolve_type @platform_resolve_type_key_cache[object] when :dataloader_source @platform_source_class_key_cache[object.class] when :parse then self.class::PARSE_NAME when :lex then self.class::LEX_NAME when :execute then self.class::EXECUTE_NAME when :analyze then self.class::ANALYZE_NAME when :validate then self.class::VALIDATE_NAME else raise "No name for #{keyword.inspect}" end end class Event def initialize(monitor, keyword, object) @monitor = monitor @keyword = keyword @object = object end attr_reader :keyword, :object def start raise "Implement #{self.class}#start to begin a new event (#{inspect})" end def finish raise "Implement #{self.class}#finish to end this event (#{inspect})" end end module GraphQLSuffixNames PARSE_NAME = "parse.graphql" LEX_NAME = "lex.graphql" VALIDATE_NAME = "validate.graphql" EXECUTE_NAME = "execute.graphql" ANALYZE_NAME = "analyze.graphql" def platform_field_key(field) "#{field.path}.graphql" end def platform_authorized_key(type) "#{type.graphql_name}.authorized.graphql" end def platform_resolve_type_key(type) "#{type.graphql_name}.resolve_type.graphql" end def platform_source_class_key(source_class) "#{source_class.name.gsub("::", "_")}.fetch.graphql" end end module GraphQLPrefixNames PARSE_NAME = "graphql.parse" LEX_NAME = "graphql.lex" VALIDATE_NAME = "graphql.validate" EXECUTE_NAME = "graphql.execute" ANALYZE_NAME = "graphql.analyze" def platform_field_key(field) "graphql.#{field.path}" end def platform_authorized_key(type) "graphql.authorized.#{type.graphql_name}" end def platform_resolve_type_key(type) "graphql.resolve_type.#{type.graphql_name}" end def platform_source_class_key(source_class) "graphql.fetch.#{source_class.name.gsub("::", "_")}" end end end def self.create_module(monitor_name) if !monitor_name.match?(/[a-z]+/) raise ArgumentError, "monitor name must be [a-z]+, not: #{monitor_name.inspect}" end trace_module = Module.new code = MODULE_TEMPLATE % { monitor: monitor_name, monitor_class: monitor_name.capitalize + "Monitor", } trace_module.module_eval(code, __FILE__, __LINE__ + 5) # rubocop:disable Development/NoEvalCop This is build-time with a validated string trace_module end MODULE_TEMPLATE = <<~RUBY # @param set_transaction_name [Boolean] If `true`, use the GraphQL operation name as the request name on the monitoring platform # @param trace_scalars [Boolean] If `true`, leaf fields will be traced too (Scalars _and_ Enums) # @param trace_authorized [Boolean] If `false`, skip tracing `authorized?` calls # @param trace_resolve_type [Boolean] If `false`, skip tracing `resolve_type?` calls def initialize(...) setup_%{monitor}_monitor(...) super end def setup_%{monitor}_monitor(trace_scalars: false, trace_authorized: true, trace_resolve_type: true, set_transaction_name: false, **kwargs) @trace_scalars = trace_scalars @trace_authorized = trace_authorized @trace_resolve_type = trace_resolve_type @set_transaction_name = set_transaction_name @%{monitor} = %{monitor_class}.new(trace: self, set_transaction_name: @set_transaction_name, **kwargs) end def parse(query_string:) @%{monitor}.instrument(:parse, query_string) do super end end def lex(query_string:) @%{monitor}.instrument(:lex, query_string) do super end end def validate(query:, validate:) @%{monitor}.instrument(:validate, query) do super end end def begin_analyze_multiplex(multiplex, analyzers) begin_%{monitor}_event(:analyze, nil) super end def end_analyze_multiplex(multiplex, analyzers) finish_%{monitor}_event super end def execute_multiplex(multiplex:) @%{monitor}.instrument(:execute, multiplex) do super end end def begin_execute_field(field, object, arguments, query) return_type = field.type.unwrap trace_field = if return_type.kind.scalar? || return_type.kind.enum? (field.trace.nil? && @trace_scalars) || field.trace else true end if trace_field begin_%{monitor}_event(:execute_field, field) end super end def end_execute_field(field, object, arguments, query, result) finish_%{monitor}_event super end def dataloader_fiber_yield(source) Fiber[PREVIOUS_EV_KEY] = finish_%{monitor}_event super end def dataloader_fiber_resume(source) prev_ev = Fiber[PREVIOUS_EV_KEY] if prev_ev begin_%{monitor}_event(prev_ev.keyword, prev_ev.object) end super end def begin_authorized(type, object, context) @trace_authorized && begin_%{monitor}_event(:authorized, type) super end def end_authorized(type, object, context, result) finish_%{monitor}_event super end def begin_resolve_type(type, value, context) @trace_resolve_type && begin_%{monitor}_event(:resolve_type, type) super end def end_resolve_type(type, value, context, resolved_type) finish_%{monitor}_event super end def begin_dataloader_source(source) begin_%{monitor}_event(:dataloader_source, source) super end def end_dataloader_source(source) finish_%{monitor}_event super end CURRENT_EV_KEY = :__graphql_%{monitor}_trace_event PREVIOUS_EV_KEY = :__graphql_%{monitor}_trace_previous_event private def begin_%{monitor}_event(keyword, object) Fiber[CURRENT_EV_KEY] = @%{monitor}.start_event(keyword, object) end def finish_%{monitor}_event if ev = Fiber[CURRENT_EV_KEY] ev.finish # Use `false` to prevent grabbing an event from a parent fiber Fiber[CURRENT_EV_KEY] = false ev end end RUBY end end end graphql-ruby-2.5.19/lib/graphql/tracing/new_relic_trace.rb000066400000000000000000000044361514115062600235420ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing/monitor_trace" module GraphQL module Tracing # A tracer for reporting GraphQL-Ruby time to New Relic # # @example Installing the tracer # class MySchema < GraphQL::Schema # trace_with GraphQL::Tracing::NewRelicTrace # # # Optional, use the operation name to set the new relic transaction name: # # trace_with GraphQL::Tracing::NewRelicTrace, set_transaction_name: true # end # # @example Installing without trace events for `authorized?` or `resolve_type` calls # trace_with GraphQL::Tracing::NewRelicTrace, trace_authorized: false, trace_resolve_type: false NewRelicTrace = MonitorTrace.create_module("newrelic") module NewRelicTrace class NewrelicMonitor < MonitorTrace::Monitor PARSE_NAME = "GraphQL/parse" LEX_NAME = "GraphQL/lex" VALIDATE_NAME = "GraphQL/validate" EXECUTE_NAME = "GraphQL/execute" ANALYZE_NAME = "GraphQL/analyze" def instrument(keyword, payload, &block) if keyword == :execute query = payload.queries.first set_this_txn_name = query.context[:set_new_relic_transaction_name] if set_this_txn_name || (set_this_txn_name.nil? && @set_transaction_name) NewRelic::Agent.set_transaction_name(transaction_name(query)) end end ::NewRelic::Agent::MethodTracerHelpers.trace_execution_scoped(name_for(keyword, payload), &block) end def platform_source_class_key(source_class) "GraphQL/Source/#{source_class.name}" end def platform_field_key(field) "GraphQL/#{field.owner.graphql_name}/#{field.graphql_name}" end def platform_authorized_key(type) "GraphQL/Authorized/#{type.graphql_name}" end def platform_resolve_type_key(type) "GraphQL/ResolveType/#{type.graphql_name}" end class Event < MonitorTrace::Monitor::Event def start name = @monitor.name_for(keyword, object) @nr_ev = NewRelic::Agent::Tracer.start_transaction_or_segment(partial_name: name, category: :web) end def finish @nr_ev.finish end end end end end end graphql-ruby-2.5.19/lib/graphql/tracing/new_relic_tracing.rb000066400000000000000000000034731514115062600240730ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing/platform_tracing" module GraphQL module Tracing class NewRelicTracing < PlatformTracing self.platform_keys = { "lex" => "GraphQL/lex", "parse" => "GraphQL/parse", "validate" => "GraphQL/validate", "analyze_query" => "GraphQL/analyze", "analyze_multiplex" => "GraphQL/analyze", "execute_multiplex" => "GraphQL/execute", "execute_query" => "GraphQL/execute", "execute_query_lazy" => "GraphQL/execute", } # @param set_transaction_name [Boolean] If true, the GraphQL operation name will be used as the transaction name. # This is not advised if you run more than one query per HTTP request, for example, with `graphql-client` or multiplexing. # It can also be specified per-query with `context[:set_new_relic_transaction_name]`. def initialize(options = {}) @set_transaction_name = options.fetch(:set_transaction_name, false) super end def platform_trace(platform_key, key, data) if key == "execute_query" set_this_txn_name = data[:query].context[:set_new_relic_transaction_name] if set_this_txn_name == true || (set_this_txn_name.nil? && @set_transaction_name) NewRelic::Agent.set_transaction_name(transaction_name(data[:query])) end end NewRelic::Agent::MethodTracerHelpers.trace_execution_scoped(platform_key) do yield end end def platform_field_key(type, field) "GraphQL/#{type.graphql_name}/#{field.graphql_name}" end def platform_authorized_key(type) "GraphQL/Authorize/#{type.graphql_name}" end def platform_resolve_type_key(type) "GraphQL/ResolveType/#{type.graphql_name}" end end end end graphql-ruby-2.5.19/lib/graphql/tracing/notifications_trace.rb000066400000000000000000000124161514115062600244410ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Tracing # This implementation forwards events to a notification handler # (i.e. ActiveSupport::Notifications or Dry::Monitor::Notifications) with a `graphql` suffix. # # @see ActiveSupportNotificationsTrace ActiveSupport::Notifications integration module NotificationsTrace # @api private class Adapter def instrument(keyword, payload, &block) raise "Implement #{self.class}#instrument to measure the block" end def start_event(keyword, payload) ev = self.class::Event.new(keyword, payload) ev.start ev end class Event def initialize(name, payload) @name = name @payload = payload end attr_reader :name, :payload def start raise "Implement #{self.class}#start to begin a new event (#{inspect})" end def finish raise "Implement #{self.class}#finish to end this event (#{inspect})" end end end # @api private class DryMonitorAdapter < Adapter def instrument(...) Dry::Monitor.instrument(...) end class Event < Adapter::Event def start Dry::Monitor.start(@name, @payload) end def finish Dry::Monitor.stop(@name, @payload) end end end # @api private class ActiveSupportNotificationsAdapter < Adapter def instrument(...) ActiveSupport::Notifications.instrument(...) end class Event < Adapter::Event def start @asn_event = ActiveSupport::Notifications.instrumenter.new_event(@name, @payload) @asn_event.start! end def finish @asn_event.finish! ActiveSupport::Notifications.publish_event(@asn_event) end end end # @param engine [Class] The notifications engine to use, eg `Dry::Monitor` or `ActiveSupport::Notifications` def initialize(engine:, **rest) adapter = if defined?(Dry::Monitor) && engine == Dry::Monitor DryMonitoringAdapter elsif defined?(ActiveSupport::Notifications) && engine == ActiveSupport::Notifications ActiveSupportNotificationsAdapter else engine end @notifications = adapter.new super end def parse(**payload) @notifications.instrument("parse.graphql", payload) do super end end def lex(**payload) @notifications.instrument("lex.graphql", payload) do super end end def validate(**payload) @notifications.instrument("validate.graphql", payload) do super end end def begin_analyze_multiplex(multiplex, analyzers) begin_notifications_event("analyze.graphql", {multiplex: multiplex, analyzers: analyzers}) super end def end_analyze_multiplex(_multiplex, _analyzers) finish_notifications_event super end def execute_multiplex(**payload) @notifications.instrument("execute.graphql", payload) do super end end def begin_execute_field(field, object, arguments, query) begin_notifications_event("execute_field.graphql", {field: field, object: object, arguments: arguments, query: query}) super end def end_execute_field(_field, _object, _arguments, _query, _result) finish_notifications_event super end def dataloader_fiber_yield(source) Fiber[PREVIOUS_EV_KEY] = finish_notifications_event super end def dataloader_fiber_resume(source) prev_ev = Fiber[PREVIOUS_EV_KEY] if prev_ev begin_notifications_event(prev_ev.name, prev_ev.payload) end super end def begin_authorized(type, object, context) begin_notifications_event("authorized.graphql", {type: type, object: object, context: context}) super end def end_authorized(type, object, context, result) finish_notifications_event super end def begin_resolve_type(type, object, context) begin_notifications_event("resolve_type.graphql", {type: type, object: object, context: context}) super end def end_resolve_type(type, object, context, resolved_type) finish_notifications_event super end def begin_dataloader_source(source) begin_notifications_event("dataloader_source.graphql", { source: source }) super end def end_dataloader_source(source) finish_notifications_event super end CURRENT_EV_KEY = :__notifications_graphql_trace_event PREVIOUS_EV_KEY = :__notifications_graphql_trace_previous_event private def begin_notifications_event(name, payload) Fiber[CURRENT_EV_KEY] = @notifications.start_event(name, payload) end def finish_notifications_event if ev = Fiber[CURRENT_EV_KEY] ev.finish # Use `false` to prevent grabbing an event from a parent fiber Fiber[CURRENT_EV_KEY] = false ev end end end end end graphql-ruby-2.5.19/lib/graphql/tracing/notifications_tracing.rb000066400000000000000000000042121514115062600247650ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing/platform_tracing" module GraphQL module Tracing # This implementation forwards events to a notification handler (i.e. # ActiveSupport::Notifications or Dry::Monitor::Notifications) # with a `graphql` suffix. # # @see KEYS for event names class NotificationsTracing # A cache of frequently-used keys to avoid needless string allocations KEYS = { "lex" => "lex.graphql", "parse" => "parse.graphql", "validate" => "validate.graphql", "analyze_multiplex" => "analyze_multiplex.graphql", "analyze_query" => "analyze_query.graphql", "execute_query" => "execute_query.graphql", "execute_query_lazy" => "execute_query_lazy.graphql", "execute_field" => "execute_field.graphql", "execute_field_lazy" => "execute_field_lazy.graphql", "authorized" => "authorized.graphql", "authorized_lazy" => "authorized_lazy.graphql", "resolve_type" => "resolve_type.graphql", "resolve_type_lazy" => "resolve_type.graphql", } MAX_KEYS_SIZE = 100 # Initialize a new NotificationsTracing instance # # @param [Object] notifications_engine The notifications engine to use def initialize(notifications_engine) @notifications_engine = notifications_engine end # Sends a GraphQL tracing event to the notification handler # # @example # . notifications_engine = Dry::Monitor::Notifications.new(:graphql) # . tracer = GraphQL::Tracing::NotificationsTracing.new(notifications_engine) # . tracer.trace("lex") { ... } # # @param [string] key The key for the event # @param [Hash] metadata The metadata for the event # @yield The block to execute for the event def trace(key, metadata, &blk) prefixed_key = KEYS[key] || "#{key}.graphql" # Cache the new keys while making sure not to induce a memory leak if KEYS.size < MAX_KEYS_SIZE KEYS[key] ||= prefixed_key end @notifications_engine.instrument(prefixed_key, metadata, &blk) end end end end graphql-ruby-2.5.19/lib/graphql/tracing/null_trace.rb000066400000000000000000000002131514115062600225320ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing/trace" module GraphQL module Tracing NullTrace = Trace.new.freeze end end graphql-ruby-2.5.19/lib/graphql/tracing/perfetto_trace.rb000066400000000000000000000734531514115062600234300ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Tracing # This produces a trace file for inspecting in the [Perfetto Trace Viewer](https://ui.perfetto.dev). # # To get the file, call {#write} on the trace. # # Use "trace modes" to configure this to run on command or on a sample of traffic. # # @example Writing trace output # # result = MySchema.execute(...) # result.query.trace.write(file: "tmp/trace.dump") # # @example Running this instrumenter when `trace: true` is present in the request # # class MySchema < GraphQL::Schema # # Only run this tracer when `context[:trace_mode]` is `:trace` # trace_with GraphQL::Tracing::Perfetto, mode: :trace # end # # # In graphql_controller.rb: # # context[:trace_mode] = params[:trace] ? :trace : nil # result = MySchema.execute(query_str, context: context, variables: variables, ...) # if context[:trace_mode] == :trace # result.trace.write(file: ...) # end # module PerfettoTrace # TODOs: # - Make debug annotations visible on both parts when dataloader is involved PROTOBUF_AVAILABLE = begin require "google/protobuf" true rescue LoadError false end if PROTOBUF_AVAILABLE require "graphql/tracing/perfetto_trace/trace_pb" end def self.included(_trace_class) if !PROTOBUF_AVAILABLE raise "#{self} can't be used because the `google-protobuf` gem wasn't available. Add it to your project, then try again." end end DATALOADER_CATEGORY_IIDS = [5] FIELD_EXECUTE_CATEGORY_IIDS = [6] ACTIVE_SUPPORT_NOTIFICATIONS_CATEGORY_IIDS = [7] AUTHORIZED_CATEGORY_IIDS = [8] RESOLVE_TYPE_CATEGORY_IIDS = [9] DA_OBJECT_IID = 10 DA_RESULT_IID = 11 DA_ARGUMENTS_IID = 12 DA_FETCH_KEYS_IID = 13 DA_STR_VAL_NIL_IID = 14 REVERSE_DEBUG_NAME_LOOKUP = { DA_OBJECT_IID => "object", DA_RESULT_IID => "result", DA_ARGUMENTS_IID => "arguments", DA_FETCH_KEYS_IID => "fetch keys", } DEBUG_INSPECT_CATEGORY_IIDS = [15] DA_DEBUG_INSPECT_CLASS_IID = 16 DEBUG_INSPECT_EVENT_NAME_IID = 17 DA_DEBUG_INSPECT_FOR_IID = 18 # @param active_support_notifications_pattern [String, RegExp, false] A filter for `ActiveSupport::Notifications`, if it's present. Or `false` to skip subscribing. def initialize(active_support_notifications_pattern: nil, save_profile: false, **_rest) super @active_support_notifications_pattern = active_support_notifications_pattern @save_profile = save_profile query = if @multiplex @multiplex.queries.first else @query # could still be nil in some initializations end @detailed_trace = query&.schema&.detailed_trace || DetailedTrace @create_debug_annotations = if (ctx = query&.context).nil? || (ctx_debug = ctx[:detailed_trace_debug]).nil? @detailed_trace.debug? else ctx_debug end @arguments_filter = if (ctx = query&.context) && (dtf = ctx[:detailed_trace_filter]) dtf elsif defined?(ActiveSupport::ParameterFilter) fp = if defined?(Rails) && Rails.application && (app_config = Rails.application.config.filter_parameters).present? && !app_config.empty? app_config elsif ActiveSupport.respond_to?(:filter_parameters) ActiveSupport.filter_parameters else EmptyObjects::EMPTY_ARRAY end ActiveSupport::ParameterFilter.new(fp, mask: ArgumentsFilter::FILTERED) else ArgumentsFilter.new end Fiber[:graphql_flow_stack] = nil @sequence_id = object_id @pid = Process.pid @flow_ids = Hash.new { |h, source_inst| h[source_inst] = [] }.compare_by_identity @new_interned_event_names = {} @interned_event_name_iids = Hash.new { |h, k| new_id = 100 + h.size @new_interned_event_names[k] = new_id h[k] = new_id } @source_name_iids = Hash.new do |h, source_class| h[source_class] = @interned_event_name_iids[source_class.name] end.compare_by_identity @auth_name_iids = Hash.new do |h, graphql_type| h[graphql_type] = @interned_event_name_iids["Authorize: #{graphql_type.graphql_name}"] end.compare_by_identity @resolve_type_name_iids = Hash.new do |h, graphql_type| h[graphql_type] = @interned_event_name_iids["Resolve Type: #{graphql_type.graphql_name}"] end.compare_by_identity @new_interned_da_names = {} @interned_da_name_ids = Hash.new { |h, k| next_id = 100 + h.size @new_interned_da_names[k] = next_id h[k] = next_id } @new_interned_da_string_values = {} @interned_da_string_values = Hash.new do |h, k| new_id = 100 + h.size @new_interned_da_string_values[k] = new_id h[k] = new_id end @class_name_iids = Hash.new do |h, k| h[k] = @interned_da_string_values[k.name] end.compare_by_identity @starting_objects = GC.stat(:total_allocated_objects) @objects_counter_id = :objects_counter.object_id @fibers_counter_id = :fibers_counter.object_id @fields_counter_id = :fields_counter.object_id @counts_objects = [@objects_counter_id] @counts_objects_and_fields = [@objects_counter_id, @fields_counter_id] @counts_fibers = [@fibers_counter_id] @counts_fibers_and_objects = [@fibers_counter_id, @objects_counter_id] @begin_validate = nil @begin_time = nil @packets = [] @packets << TracePacket.new( track_descriptor: TrackDescriptor.new( uuid: tid, name: "Main Thread", child_ordering: TrackDescriptor::ChildTracksOrdering::CHRONOLOGICAL, ), first_packet_on_sequence: true, previous_packet_dropped: true, trusted_packet_sequence_id: @sequence_id, sequence_flags: 3, ) @packets << TracePacket.new( interned_data: InternedData.new( event_categories: [ EventCategory.new(name: "Dataloader", iid: DATALOADER_CATEGORY_IIDS.first), EventCategory.new(name: "Field Execution", iid: FIELD_EXECUTE_CATEGORY_IIDS.first), EventCategory.new(name: "ActiveSupport::Notifications", iid: ACTIVE_SUPPORT_NOTIFICATIONS_CATEGORY_IIDS.first), EventCategory.new(name: "Authorized", iid: AUTHORIZED_CATEGORY_IIDS.first), EventCategory.new(name: "Resolve Type", iid: RESOLVE_TYPE_CATEGORY_IIDS.first), EventCategory.new(name: "Debug Inspect", iid: DEBUG_INSPECT_CATEGORY_IIDS.first), ], debug_annotation_names: [ *REVERSE_DEBUG_NAME_LOOKUP.map { |(iid, name)| DebugAnnotationName.new(name: name, iid: iid) }, DebugAnnotationName.new(name: "inspect instance of", iid: DA_DEBUG_INSPECT_CLASS_IID), DebugAnnotationName.new(name: "inspecting for", iid: DA_DEBUG_INSPECT_FOR_IID) ], debug_annotation_string_values: [ InternedString.new(str: "(nil)", iid: DA_STR_VAL_NIL_IID), ], event_names: [ EventName.new(name: "#{(@detailed_trace.is_a?(Class) ? @detailed_trace : @detailed_trace.class).name}#inspect_object", iid: DEBUG_INSPECT_EVENT_NAME_IID) ], ), trusted_packet_sequence_id: @sequence_id, sequence_flags: 2, ) @main_fiber_id = fid @packets << track_descriptor_packet(tid, fid, "Main Fiber") @packets << track_descriptor_packet(tid, @objects_counter_id, "Allocated Objects", counter: {}) @packets << trace_packet( type: TrackEvent::Type::TYPE_COUNTER, track_uuid: @objects_counter_id, counter_value: count_allocations, ) @packets << track_descriptor_packet(tid, @fibers_counter_id, "Active Fibers", counter: {}) @fibers_count = 0 @packets << trace_packet( type: TrackEvent::Type::TYPE_COUNTER, track_uuid: @fibers_counter_id, counter_value: count_fibers(0), ) @packets << track_descriptor_packet(tid, @fields_counter_id, "Resolved Fields", counter: {}) @fields_count = -1 @packets << trace_packet( type: TrackEvent::Type::TYPE_COUNTER, track_uuid: @fields_counter_id, counter_value: count_fields, ) end def execute_multiplex(multiplex:) if defined?(ActiveSupport::Notifications) && @active_support_notifications_pattern != false subscribe_to_active_support_notifications(@active_support_notifications_pattern) end @operation_name = multiplex.queries.map { |q| q.selected_operation_name || "anonymous" }.join(",") @begin_time = Time.now @packets << trace_packet( type: TrackEvent::Type::TYPE_SLICE_BEGIN, track_uuid: fid, name: "Multiplex" ) { [ payload_to_debug("query_string", multiplex.queries.map(&:sanitized_query_string).join("\n\n")) ] } result = super @packets << trace_packet( type: TrackEvent::Type::TYPE_SLICE_END, track_uuid: fid, ) result ensure unsubscribe_from_active_support_notifications if @save_profile begin_ts = (@begin_time.to_f * 1000).round end_ts = (Time.now.to_f * 1000).round duration_ms = end_ts - begin_ts multiplex.schema.detailed_trace.save_trace(@operation_name, duration_ms, begin_ts, Trace.encode(Trace.new(packet: @packets))) end end def begin_execute_field(field, object, arguments, query) packet = trace_packet( type: TrackEvent::Type::TYPE_SLICE_BEGIN, track_uuid: fid, name: query.context.current_path.join("."), category_iids: FIELD_EXECUTE_CATEGORY_IIDS, extra_counter_track_uuids: @counts_objects, extra_counter_values: [count_allocations], ) @packets << packet fiber_flow_stack << packet super end def end_execute_field(field, object, arguments, query, app_result) end_ts = ts start_field = fiber_flow_stack.pop if @create_debug_annotations start_field.track_event = dup_with(start_field.track_event,{ debug_annotations: [ payload_to_debug(nil, object.object, iid: DA_OBJECT_IID, intern_value: true), payload_to_debug(nil, arguments, iid: DA_ARGUMENTS_IID), payload_to_debug(nil, app_result, iid: DA_RESULT_IID, intern_value: true) ] }) end @packets << trace_packet( timestamp: end_ts, type: TrackEvent::Type::TYPE_SLICE_END, track_uuid: fid, extra_counter_track_uuids: @counts_objects_and_fields, extra_counter_values: [count_allocations, count_fields], ) super end def begin_analyze_multiplex(m, analyzers) @packets << trace_packet( type: TrackEvent::Type::TYPE_SLICE_BEGIN, track_uuid: fid, extra_counter_track_uuids: @counts_objects, extra_counter_values: [count_allocations], name: "Analysis") { [ payload_to_debug("analyzers_count", analyzers.size), payload_to_debug("analyzers", analyzers), ] } super end def end_analyze_multiplex(m, analyzers) end_ts = ts @packets << trace_packet( timestamp: end_ts, type: TrackEvent::Type::TYPE_SLICE_END, track_uuid: fid, extra_counter_track_uuids: @counts_objects, extra_counter_values: [count_allocations], ) super end def parse(query_string:) @packets << trace_packet( type: TrackEvent::Type::TYPE_SLICE_BEGIN, track_uuid: fid, extra_counter_track_uuids: @counts_objects, extra_counter_values: [count_allocations], name: "Parse" ) result = super end_ts = ts @packets << trace_packet( timestamp: end_ts, type: TrackEvent::Type::TYPE_SLICE_END, track_uuid: fid, extra_counter_track_uuids: @counts_objects, extra_counter_values: [count_allocations], ) result end def begin_validate(query, validate) @begin_validate = trace_packet( type: TrackEvent::Type::TYPE_SLICE_BEGIN, track_uuid: fid, extra_counter_track_uuids: @counts_objects, extra_counter_values: [count_allocations], name: "Validate") { [payload_to_debug("validate?", validate)] } @packets << @begin_validate super end def end_validate(query, validate, validation_errors) end_ts = ts @packets << trace_packet( timestamp: end_ts, type: TrackEvent::Type::TYPE_SLICE_END, track_uuid: fid, extra_counter_track_uuids: @counts_objects, extra_counter_values: [count_allocations], ) if @create_debug_annotations new_bv_track_event = dup_with( @begin_validate.track_event, { debug_annotations: [ @begin_validate.track_event.debug_annotations.first, payload_to_debug("valid?", validation_errors.empty?) ] } ) @begin_validate.track_event = new_bv_track_event end super end def dataloader_spawn_execution_fiber(jobs) @packets << trace_packet( type: TrackEvent::Type::TYPE_INSTANT, track_uuid: fid, name: "Create Execution Fiber", category_iids: DATALOADER_CATEGORY_IIDS, extra_counter_track_uuids: @counts_fibers_and_objects, extra_counter_values: [count_fibers(1), count_allocations] ) @packets << track_descriptor_packet(@did, fid, "Exec Fiber ##{fid}") super end def dataloader_spawn_source_fiber(pending_sources) @packets << trace_packet( type: TrackEvent::Type::TYPE_INSTANT, track_uuid: fid, name: "Create Source Fiber", category_iids: DATALOADER_CATEGORY_IIDS, extra_counter_track_uuids: @counts_fibers_and_objects, extra_counter_values: [count_fibers(1), count_allocations] ) @packets << track_descriptor_packet(@did, fid, "Source Fiber ##{fid}") super end def dataloader_fiber_yield(source) ls = fiber_flow_stack.last if (flow_id = ls.track_event.flow_ids.first) # got it else flow_id = ls.track_event.name.object_id ls.track_event = dup_with(ls.track_event, {flow_ids: [flow_id] }, delete_counters: true) end @flow_ids[source] << flow_id @packets << trace_packet( type: TrackEvent::Type::TYPE_SLICE_END, track_uuid: fid, ) @packets << trace_packet( type: TrackEvent::Type::TYPE_INSTANT, track_uuid: fid, name: "Fiber Yield", category_iids: DATALOADER_CATEGORY_IIDS, ) super end def dataloader_fiber_resume(source) @packets << trace_packet( type: TrackEvent::Type::TYPE_INSTANT, track_uuid: fid, name: "Fiber Resume", category_iids: DATALOADER_CATEGORY_IIDS, ) ls = fiber_flow_stack.pop @packets << packet = TracePacket.new( timestamp: ts, track_event: dup_with(ls.track_event, { type: TrackEvent::Type::TYPE_SLICE_BEGIN }), trusted_packet_sequence_id: @sequence_id, ) fiber_flow_stack << packet super end def dataloader_fiber_exit @packets << trace_packet( type: TrackEvent::Type::TYPE_INSTANT, track_uuid: fid, name: "Fiber Exit", category_iids: DATALOADER_CATEGORY_IIDS, extra_counter_track_uuids: @counts_fibers, extra_counter_values: [count_fibers(-1)], ) super end def begin_dataloader(dl) @packets << trace_packet( type: TrackEvent::Type::TYPE_COUNTER, track_uuid: @fibers_counter_id, counter_value: count_fibers(1), ) @did = fid @packets << track_descriptor_packet(@main_fiber_id, @did, "Dataloader Fiber ##{@did}") super end def end_dataloader(dl) @packets << trace_packet( type: TrackEvent::Type::TYPE_COUNTER, track_uuid: @fibers_counter_id, counter_value: count_fibers(-1), ) super end def begin_dataloader_source(source) fds = @flow_ids[source] fds_copy = fds.dup fds.clear packet = trace_packet( type: TrackEvent::Type::TYPE_SLICE_BEGIN, track_uuid: fid, name_iid: @source_name_iids[source.class], category_iids: DATALOADER_CATEGORY_IIDS, flow_ids: fds_copy, extra_counter_track_uuids: @counts_objects, extra_counter_values: [count_allocations]) { [ payload_to_debug(nil, source.pending.values, iid: DA_FETCH_KEYS_IID, intern_value: true), *(source.instance_variables - [:@pending, :@fetching, :@results, :@dataloader]).map { |iv| payload_to_debug(iv.to_s, source.instance_variable_get(iv), intern_value: true) } ] } @packets << packet fiber_flow_stack << packet super end def end_dataloader_source(source) end_ts = ts @packets << trace_packet( timestamp: end_ts, type: TrackEvent::Type::TYPE_SLICE_END, track_uuid: fid, extra_counter_track_uuids: @counts_objects, extra_counter_values: [count_allocations], ) fiber_flow_stack.pop super end def begin_authorized(type, obj, ctx) packet = trace_packet( type: TrackEvent::Type::TYPE_SLICE_BEGIN, track_uuid: fid, category_iids: AUTHORIZED_CATEGORY_IIDS, extra_counter_track_uuids: @counts_objects, extra_counter_values: [count_allocations], name_iid: @auth_name_iids[type], ) @packets << packet fiber_flow_stack << packet super end def end_authorized(type, obj, ctx, is_authorized) end_ts = ts @packets << trace_packet( timestamp: end_ts, type: TrackEvent::Type::TYPE_SLICE_END, track_uuid: fid, extra_counter_track_uuids: @counts_objects, extra_counter_values: [count_allocations], ) beg_auth = fiber_flow_stack.pop if @create_debug_annotations beg_auth.track_event = dup_with(beg_auth.track_event, { debug_annotations: [payload_to_debug("authorized?", is_authorized)] }) end super end def begin_resolve_type(type, value, context) packet = trace_packet( type: TrackEvent::Type::TYPE_SLICE_BEGIN, track_uuid: fid, category_iids: RESOLVE_TYPE_CATEGORY_IIDS, extra_counter_track_uuids: @counts_objects, extra_counter_values: [count_allocations], name_iid: @resolve_type_name_iids[type], ) @packets << packet fiber_flow_stack << packet super end def end_resolve_type(type, value, context, resolved_type) end_ts = ts @packets << trace_packet( timestamp: end_ts, type: TrackEvent::Type::TYPE_SLICE_END, track_uuid: fid, extra_counter_track_uuids: @counts_objects, extra_counter_values: [count_allocations], ) rt_begin = fiber_flow_stack.pop if @create_debug_annotations rt_begin.track_event = dup_with(rt_begin.track_event, { debug_annotations: [payload_to_debug("resolved_type", resolved_type, intern_value: true)] }) end super end # Dump protobuf output in the specified file. # @param file [String] path to a file in a directory that already exists # @param debug_json [Boolean] True to print JSON instead of binary # @return [nil, String, Hash] If `file` was given, `nil`. If `file` was `nil`, a Hash if `debug_json: true`, else binary data. def write(file:, debug_json: false) trace = Trace.new( packet: @packets, ) data = if debug_json small_json = Trace.encode_json(trace) JSON.pretty_generate(JSON.parse(small_json)) else Trace.encode(trace) end if file File.write(file, data, mode: 'wb') nil else data end end private def ts Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) end def tid Thread.current.object_id end def fid Fiber.current.object_id end class ArgumentsFilter # From Rails defaults # https://github.com/rails/rails/blob/main/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt#L6-L8 SENSITIVE_KEY = /passw|token|crypt|email|_key|salt|certificate|secret|ssn|cvv|cvc|otp/i FILTERED = "[FILTERED]" def filter_param(key, value) if (key.is_a?(String) && SENSITIVE_KEY.match?(key)) || (key.is_a?(Symbol) && SENSITIVE_KEY.match?(key.name)) FILTERED else value end end end def debug_annotation(iid, value_key, value) if iid DebugAnnotation.new(name_iid: iid, value_key => value) else DebugAnnotation.new(value_key => value) end end def payload_to_debug(k, v, iid: nil, intern_value: false) if iid.nil? iid = @interned_da_name_ids[k] end case v when String if intern_value v = @interned_da_string_values[v] debug_annotation(iid, :string_value_iid, v) else debug_annotation(iid, :string_value, v) end when Float debug_annotation(iid, :double_value, v) when Integer debug_annotation(iid, :int_value, v) when true, false debug_annotation(iid, :bool_value, v) when nil if iid DebugAnnotation.new(name_iid: iid, string_value_iid: DA_STR_VAL_NIL_IID) else DebugAnnotation.new(name: k, string_value_iid: DA_STR_VAL_NIL_IID) end when Module if intern_value val_iid = @class_name_iids[v] debug_annotation(iid, :string_value_iid, val_iid) else debug_annotation(iid, :string_value, v.name) end when Symbol debug_annotation(iid, :string_value, v.inspect) when Array debug_annotation(iid, :array_values, v.each_with_index.map { |v2, idx| payload_to_debug((k ? "#{k}.#{idx}" : String(idx)), v2, intern_value: intern_value) }.compact) when Hash debug_v = v.map { |k2, v2| debug_k = case k2 when String k2 when Symbol k2.name else String(k2) end filtered_v2 = @arguments_filter.filter_param(debug_k, v2) payload_to_debug(debug_k, filtered_v2, intern_value: intern_value) } debug_v.compact! debug_annotation(iid, :dict_entries, debug_v) when GraphQL::Schema::InputObject payload_to_debug(k, v.to_h, iid: iid, intern_value: intern_value) else class_name_iid = @interned_da_string_values[v.class.name] da = [ debug_annotation(DA_DEBUG_INSPECT_CLASS_IID, :string_value_iid, class_name_iid), ] if k k_str_value_iid = @interned_da_string_values[k] da << debug_annotation(DA_DEBUG_INSPECT_FOR_IID, :string_value_iid, k_str_value_iid) elsif iid k = REVERSE_DEBUG_NAME_LOOKUP[iid] || @interned_da_name_ids.key(iid) if k.nil? da << debug_annotation(DA_DEBUG_INSPECT_FOR_IID, :string_value_iid, DA_STR_VAL_NIL_IID) else k_str_value_iid = @interned_da_string_values[k] da << debug_annotation(DA_DEBUG_INSPECT_FOR_IID, :string_value_iid, k_str_value_iid) end end @packets << trace_packet( type: TrackEvent::Type::TYPE_SLICE_BEGIN, track_uuid: fid, name_iid: DEBUG_INSPECT_EVENT_NAME_IID, category_iids: DEBUG_INSPECT_CATEGORY_IIDS, extra_counter_track_uuids: @counts_objects, extra_counter_values: [count_allocations], debug_annotations: da, ) debug_str = @detailed_trace.inspect_object(v) @packets << trace_packet( type: TrackEvent::Type::TYPE_SLICE_END, track_uuid: fid, ) if intern_value str_iid = @interned_da_string_values[debug_str] debug_annotation(iid, :string_value_iid, str_iid) else debug_annotation(iid, :string_value, debug_str) end end end def count_allocations GC.stat(:total_allocated_objects) - @starting_objects end def count_fibers(diff) @fibers_count += diff end def count_fields @fields_count += 1 end def dup_with(message, attrs, delete_counters: false) new_attrs = message.to_h if delete_counters new_attrs.delete(:extra_counter_track_uuids) new_attrs.delete(:extra_counter_values) end new_attrs.merge!(attrs) message.class.new(**new_attrs) end def fiber_flow_stack Fiber[:graphql_flow_stack] ||= [] end def trace_packet(timestamp: ts, **event_attrs) if @create_debug_annotations && block_given? event_attrs[:debug_annotations] = yield end track_event = TrackEvent.new(event_attrs) TracePacket.new( timestamp: timestamp, track_event: track_event, trusted_packet_sequence_id: @sequence_id, sequence_flags: 2, interned_data: new_interned_data ) end def new_interned_data if !@new_interned_da_names.empty? da_names = @new_interned_da_names.map { |(name, iid)| DebugAnnotationName.new(iid: iid, name: name) } @new_interned_da_names.clear end if !@new_interned_event_names.empty? ev_names = @new_interned_event_names.map { |(name, iid)| EventName.new(iid: iid, name: name) } @new_interned_event_names.clear end if !@new_interned_da_string_values.empty? str_vals = @new_interned_da_string_values.map { |name, iid| InternedString.new(iid: iid, str: name.b) } @new_interned_da_string_values.clear end if ev_names || da_names || str_vals InternedData.new( event_names: ev_names, debug_annotation_names: da_names, debug_annotation_string_values: str_vals, ) else nil end end def track_descriptor_packet(parent_uuid, uuid, name, counter: nil) td = if counter TrackDescriptor.new( parent_uuid: parent_uuid, uuid: uuid, name: name, counter: counter ) else TrackDescriptor.new( parent_uuid: parent_uuid, uuid: uuid, name: name, child_ordering: TrackDescriptor::ChildTracksOrdering::CHRONOLOGICAL, ) end TracePacket.new( track_descriptor: td, trusted_packet_sequence_id: @sequence_id, sequence_flags: 2, ) end def unsubscribe_from_active_support_notifications if defined?(@as_subscriber) ActiveSupport::Notifications.unsubscribe(@as_subscriber) end end def subscribe_to_active_support_notifications(pattern) @as_subscriber = ActiveSupport::Notifications.monotonic_subscribe(pattern) do |name, start, finish, id, payload| metadata = @create_debug_annotations ? payload.map { |k, v| payload_to_debug(String(k), v, intern_value: true) } : nil metadata&.compact! te = if metadata.nil? || metadata.empty? TrackEvent.new( type: TrackEvent::Type::TYPE_SLICE_BEGIN, track_uuid: fid, category_iids: ACTIVE_SUPPORT_NOTIFICATIONS_CATEGORY_IIDS, name: name, ) else TrackEvent.new( type: TrackEvent::Type::TYPE_SLICE_BEGIN, track_uuid: fid, name: name, category_iids: ACTIVE_SUPPORT_NOTIFICATIONS_CATEGORY_IIDS, debug_annotations: metadata, ) end @packets << TracePacket.new( timestamp: (start * 1_000_000_000).to_i, track_event: te, trusted_packet_sequence_id: @sequence_id, sequence_flags: 2, interned_data: new_interned_data ) @packets << TracePacket.new( timestamp: (finish * 1_000_000_000).to_i, track_event: TrackEvent.new( type: TrackEvent::Type::TYPE_SLICE_END, track_uuid: fid, name: name, extra_counter_track_uuids: @counts_objects, extra_counter_values: [count_allocations] ), trusted_packet_sequence_id: @sequence_id, sequence_flags: 2, ) end end end end end graphql-ruby-2.5.19/lib/graphql/tracing/perfetto_trace/000077500000000000000000000000001514115062600230675ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/tracing/perfetto_trace/trace.proto000066400000000000000000000070101514115062600252500ustar00rootroot00000000000000// This is an abbreviated version of the full Perfetto schema. // Most of them are for OS or Chrome traces and we'll never use them. // Full doc: https://github.com/google/perfetto/tree/main/protos/perfetto // // Build it with // protoc --ruby_out=lib/graphql/tracing/perfetto_trace --proto_path=lib/graphql/tracing/perfetto_trace trace.proto syntax = "proto2"; package perfetto_trace.protos; option ruby_package = "GraphQL::Tracing::PerfettoTrace"; message Trace { repeated TracePacket packet = 1; } message TracePacket { optional uint64 timestamp = 8; oneof data { TrackEvent track_event = 11; TrackDescriptor track_descriptor = 60; } oneof optional_trusted_packet_sequence_id { uint32 trusted_packet_sequence_id = 10; } optional InternedData interned_data = 12; optional bool first_packet_on_sequence = 87; optional bool previous_packet_dropped = 42; optional uint32 sequence_flags = 13; } message TrackEvent { repeated uint64 category_iids = 3; repeated string categories = 22; oneof name_field { uint64 name_iid = 10; string name = 23; } enum Type { TYPE_UNSPECIFIED = 0; TYPE_SLICE_BEGIN = 1; TYPE_SLICE_END = 2; TYPE_INSTANT = 3; TYPE_COUNTER = 4; } optional Type type = 9; optional uint64 track_uuid = 11; oneof counter_value_field { int64 counter_value = 30; double double_counter_value = 44; } repeated uint64 extra_counter_track_uuids = 31; repeated int64 extra_counter_values = 12; repeated uint64 extra_double_counter_track_uuids = 45; repeated double extra_double_counter_values = 46; repeated fixed64 flow_ids = 47; repeated fixed64 terminating_flow_ids = 48; repeated DebugAnnotation debug_annotations = 4; } message DebugAnnotation { oneof name_field { uint64 name_iid = 1; string name = 10; } oneof value { bool bool_value = 2; uint64 uint_value = 3; int64 int_value = 4; double double_value = 5; string string_value = 6; uint64 string_value_iid = 17; } repeated DebugAnnotation dict_entries = 11; repeated DebugAnnotation array_values = 12; uint64 string_value_iid = 17; } message TrackDescriptor { optional uint64 uuid = 1; optional uint64 parent_uuid = 5; oneof static_or_dynamic_name { string name = 2; } optional CounterDescriptor counter = 8; enum ChildTracksOrdering { UNKNOWN = 0; LEXICOGRAPHIC = 1; CHRONOLOGICAL = 2; EXPLICIT = 3; } optional ChildTracksOrdering child_ordering = 11; optional int32 sibling_order_rank = 12; } message CounterDescriptor { enum BuiltinCounterType { COUNTER_UNSPECIFIED = 0; COUNTER_THREAD_TIME_NS = 1; COUNTER_THREAD_INSTRUCTION_COUNT = 2; } enum Unit { UNIT_UNSPECIFIED = 0; UNIT_TIME_NS = 1; UNIT_COUNT = 2; UNIT_SIZE_BYTES = 3; } optional BuiltinCounterType type = 1; repeated string categories = 2; optional Unit unit = 3; optional string unit_name = 6; optional int64 unit_multiplier = 4; optional bool is_incremental = 5; } message InternedData { repeated EventCategory event_categories = 1; repeated EventName event_names = 2; repeated DebugAnnotationName debug_annotation_names = 3; repeated InternedString debug_annotation_string_values = 29; } message InternedString { optional uint64 iid = 1; optional bytes str = 2; } message EventCategory { optional uint64 iid = 1; optional string name = 2; } message EventName { optional uint64 iid = 1; optional string name = 2; } message DebugAnnotationName { optional uint64 iid = 1; optional string name = 2; } graphql-ruby-2.5.19/lib/graphql/tracing/perfetto_trace/trace_pb.rb000066400000000000000000000153721514115062600252030ustar00rootroot00000000000000# frozen_string_literal: true # Generated by the protocol buffer compiler. DO NOT EDIT! # source: trace.proto require 'google/protobuf' descriptor_data = "\n\x0btrace.proto\x12\x15perfetto_trace.protos\";\n\x05Trace\x12\x32\n\x06packet\x18\x01 \x03(\x0b\x32\".perfetto_trace.protos.TracePacket\"\x8a\x03\n\x0bTracePacket\x12\x11\n\ttimestamp\x18\x08 \x01(\x04\x12\x38\n\x0btrack_event\x18\x0b \x01(\x0b\x32!.perfetto_trace.protos.TrackEventH\x00\x12\x42\n\x10track_descriptor\x18< \x01(\x0b\x32&.perfetto_trace.protos.TrackDescriptorH\x00\x12$\n\x1atrusted_packet_sequence_id\x18\n \x01(\rH\x01\x12:\n\rinterned_data\x18\x0c \x01(\x0b\x32#.perfetto_trace.protos.InternedData\x12 \n\x18\x66irst_packet_on_sequence\x18W \x01(\x08\x12\x1f\n\x17previous_packet_dropped\x18* \x01(\x08\x12\x16\n\x0esequence_flags\x18\r \x01(\rB\x06\n\x04\x64\x61taB%\n#optional_trusted_packet_sequence_id\"\xf2\x04\n\nTrackEvent\x12\x15\n\rcategory_iids\x18\x03 \x03(\x04\x12\x12\n\ncategories\x18\x16 \x03(\t\x12\x12\n\x08name_iid\x18\n \x01(\x04H\x00\x12\x0e\n\x04name\x18\x17 \x01(\tH\x00\x12\x34\n\x04type\x18\t \x01(\x0e\x32&.perfetto_trace.protos.TrackEvent.Type\x12\x12\n\ntrack_uuid\x18\x0b \x01(\x04\x12\x17\n\rcounter_value\x18\x1e \x01(\x03H\x01\x12\x1e\n\x14\x64ouble_counter_value\x18, \x01(\x01H\x01\x12!\n\x19\x65xtra_counter_track_uuids\x18\x1f \x03(\x04\x12\x1c\n\x14\x65xtra_counter_values\x18\x0c \x03(\x03\x12(\n extra_double_counter_track_uuids\x18- \x03(\x04\x12#\n\x1b\x65xtra_double_counter_values\x18. \x03(\x01\x12\x10\n\x08\x66low_ids\x18/ \x03(\x06\x12\x1c\n\x14terminating_flow_ids\x18\x30 \x03(\x06\x12\x41\n\x11\x64\x65\x62ug_annotations\x18\x04 \x03(\x0b\x32&.perfetto_trace.protos.DebugAnnotation\"j\n\x04Type\x12\x14\n\x10TYPE_UNSPECIFIED\x10\x00\x12\x14\n\x10TYPE_SLICE_BEGIN\x10\x01\x12\x12\n\x0eTYPE_SLICE_END\x10\x02\x12\x10\n\x0cTYPE_INSTANT\x10\x03\x12\x10\n\x0cTYPE_COUNTER\x10\x04\x42\x0c\n\nname_fieldB\x15\n\x13\x63ounter_value_field\"\xd5\x02\n\x0f\x44\x65\x62ugAnnotation\x12\x12\n\x08name_iid\x18\x01 \x01(\x04H\x00\x12\x0e\n\x04name\x18\n \x01(\tH\x00\x12\x14\n\nbool_value\x18\x02 \x01(\x08H\x01\x12\x14\n\nuint_value\x18\x03 \x01(\x04H\x01\x12\x13\n\tint_value\x18\x04 \x01(\x03H\x01\x12\x16\n\x0c\x64ouble_value\x18\x05 \x01(\x01H\x01\x12\x16\n\x0cstring_value\x18\x06 \x01(\tH\x01\x12\x1a\n\x10string_value_iid\x18\x11 \x01(\x04H\x01\x12<\n\x0c\x64ict_entries\x18\x0b \x03(\x0b\x32&.perfetto_trace.protos.DebugAnnotation\x12<\n\x0c\x61rray_values\x18\x0c \x03(\x0b\x32&.perfetto_trace.protos.DebugAnnotationB\x0c\n\nname_fieldB\x07\n\x05value\"\xe1\x02\n\x0fTrackDescriptor\x12\x0c\n\x04uuid\x18\x01 \x01(\x04\x12\x13\n\x0bparent_uuid\x18\x05 \x01(\x04\x12\x0e\n\x04name\x18\x02 \x01(\tH\x00\x12\x39\n\x07\x63ounter\x18\x08 \x01(\x0b\x32(.perfetto_trace.protos.CounterDescriptor\x12R\n\x0e\x63hild_ordering\x18\x0b \x01(\x0e\x32:.perfetto_trace.protos.TrackDescriptor.ChildTracksOrdering\x12\x1a\n\x12sibling_order_rank\x18\x0c \x01(\x05\"V\n\x13\x43hildTracksOrdering\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x11\n\rLEXICOGRAPHIC\x10\x01\x12\x11\n\rCHRONOLOGICAL\x10\x02\x12\x0c\n\x08\x45XPLICIT\x10\x03\x42\x18\n\x16static_or_dynamic_name\"\xb9\x03\n\x11\x43ounterDescriptor\x12I\n\x04type\x18\x01 \x01(\x0e\x32;.perfetto_trace.protos.CounterDescriptor.BuiltinCounterType\x12\x12\n\ncategories\x18\x02 \x03(\t\x12;\n\x04unit\x18\x03 \x01(\x0e\x32-.perfetto_trace.protos.CounterDescriptor.Unit\x12\x11\n\tunit_name\x18\x06 \x01(\t\x12\x17\n\x0funit_multiplier\x18\x04 \x01(\x03\x12\x16\n\x0eis_incremental\x18\x05 \x01(\x08\"o\n\x12\x42uiltinCounterType\x12\x17\n\x13\x43OUNTER_UNSPECIFIED\x10\x00\x12\x1a\n\x16\x43OUNTER_THREAD_TIME_NS\x10\x01\x12$\n COUNTER_THREAD_INSTRUCTION_COUNT\x10\x02\"S\n\x04Unit\x12\x14\n\x10UNIT_UNSPECIFIED\x10\x00\x12\x10\n\x0cUNIT_TIME_NS\x10\x01\x12\x0e\n\nUNIT_COUNT\x10\x02\x12\x13\n\x0fUNIT_SIZE_BYTES\x10\x03\"\xa0\x02\n\x0cInternedData\x12>\n\x10\x65vent_categories\x18\x01 \x03(\x0b\x32$.perfetto_trace.protos.EventCategory\x12\x35\n\x0b\x65vent_names\x18\x02 \x03(\x0b\x32 .perfetto_trace.protos.EventName\x12J\n\x16\x64\x65\x62ug_annotation_names\x18\x03 \x03(\x0b\x32*.perfetto_trace.protos.DebugAnnotationName\x12M\n\x1e\x64\x65\x62ug_annotation_string_values\x18\x1d \x03(\x0b\x32%.perfetto_trace.protos.InternedString\"*\n\x0eInternedString\x12\x0b\n\x03iid\x18\x01 \x01(\x04\x12\x0b\n\x03str\x18\x02 \x01(\x0c\"*\n\rEventCategory\x12\x0b\n\x03iid\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\t\"&\n\tEventName\x12\x0b\n\x03iid\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\t\"0\n\x13\x44\x65\x62ugAnnotationName\x12\x0b\n\x03iid\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\tB\"\xea\x02\x1fGraphQL::Tracing::PerfettoTrace" pool = Google::Protobuf::DescriptorPool.generated_pool pool.add_serialized_file(descriptor_data) module GraphQL module Tracing module PerfettoTrace Trace = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("perfetto_trace.protos.Trace").msgclass TracePacket = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("perfetto_trace.protos.TracePacket").msgclass TrackEvent = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("perfetto_trace.protos.TrackEvent").msgclass TrackEvent::Type = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("perfetto_trace.protos.TrackEvent.Type").enummodule DebugAnnotation = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("perfetto_trace.protos.DebugAnnotation").msgclass TrackDescriptor = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("perfetto_trace.protos.TrackDescriptor").msgclass TrackDescriptor::ChildTracksOrdering = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("perfetto_trace.protos.TrackDescriptor.ChildTracksOrdering").enummodule CounterDescriptor = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("perfetto_trace.protos.CounterDescriptor").msgclass CounterDescriptor::BuiltinCounterType = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("perfetto_trace.protos.CounterDescriptor.BuiltinCounterType").enummodule CounterDescriptor::Unit = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("perfetto_trace.protos.CounterDescriptor.Unit").enummodule InternedData = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("perfetto_trace.protos.InternedData").msgclass InternedString = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("perfetto_trace.protos.InternedString").msgclass EventCategory = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("perfetto_trace.protos.EventCategory").msgclass EventName = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("perfetto_trace.protos.EventName").msgclass DebugAnnotationName = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("perfetto_trace.protos.DebugAnnotationName").msgclass end end end graphql-ruby-2.5.19/lib/graphql/tracing/platform_trace.rb000066400000000000000000000101641514115062600234120ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Tracing module PlatformTrace def initialize(trace_scalars: false, **_options) @trace_scalars = trace_scalars @platform_key_cache = Hash.new { |h, mod| h[mod] = mod::KeyCache.new } super end module BaseKeyCache def initialize @platform_field_key_cache = Hash.new { |h, k| h[k] = platform_field_key(k) } @platform_authorized_key_cache = Hash.new { |h, k| h[k] = platform_authorized_key(k) } @platform_resolve_type_key_cache = Hash.new { |h, k| h[k] = platform_resolve_type_key(k) } end attr_reader :platform_field_key_cache, :platform_authorized_key_cache, :platform_resolve_type_key_cache end def platform_execute_field_lazy(*args, &block) platform_execute_field(*args, &block) end def platform_authorized_lazy(key, &block) platform_authorized(key, &block) end def platform_resolve_type_lazy(key, &block) platform_resolve_type(key, &block) end def self.included(child_class) key_methods_class = Class.new { include(child_class) include(BaseKeyCache) } child_class.const_set(:KeyCache, key_methods_class) # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time [:execute_field, :execute_field_lazy].each do |field_trace_method| if !child_class.method_defined?(field_trace_method) child_class.module_eval <<-RUBY, __FILE__, __LINE__ def #{field_trace_method}(query:, field:, ast_node:, arguments:, object:) return_type = field.type.unwrap trace_field = if return_type.kind.scalar? || return_type.kind.enum? (field.trace.nil? && @trace_scalars) || field.trace else true end platform_key = if trace_field @platform_key_cache[#{child_class}].platform_field_key_cache[field] else nil end if platform_key && trace_field platform_#{field_trace_method}(platform_key) do super end else super end end RUBY end end [:authorized, :authorized_lazy].each do |auth_trace_method| if !child_class.method_defined?(auth_trace_method) child_class.module_eval <<-RUBY, __FILE__, __LINE__ def #{auth_trace_method}(type:, query:, object:) platform_key = @platform_key_cache[#{child_class}].platform_authorized_key_cache[type] platform_#{auth_trace_method}(platform_key) do super end end RUBY end end [:resolve_type, :resolve_type_lazy].each do |rt_trace_method| if !child_class.method_defined?(rt_trace_method) child_class.module_eval <<-RUBY, __FILE__, __LINE__ def #{rt_trace_method}(query:, type:, object:) platform_key = @platform_key_cache[#{child_class}].platform_resolve_type_key_cache[type] platform_#{rt_trace_method}(platform_key) do super end end RUBY end # rubocop:enable Development/NoEvalCop end end private # Get the transaction name based on the operation type and name if possible, or fall back to a user provided # one. Useful for anonymous queries. def transaction_name(query) selected_op = query.selected_operation txn_name = if selected_op op_type = selected_op.operation_type op_name = selected_op.name || fallback_transaction_name(query.context) || "anonymous" "#{op_type}.#{op_name}" else "query.anonymous" end "GraphQL/#{txn_name}" end def fallback_transaction_name(context) context[:tracing_fallback_transaction_name] end end end end graphql-ruby-2.5.19/lib/graphql/tracing/platform_tracing.rb000066400000000000000000000114631514115062600237460ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Tracing # Each platform provides: # - `.platform_keys` # - `#platform_trace` # - `#platform_field_key(type, field)` # @api private class PlatformTracing class << self attr_accessor :platform_keys def inherited(child_class) child_class.platform_keys = self.platform_keys end end def initialize(options = {}) @options = options @platform_keys = self.class.platform_keys @trace_scalars = options.fetch(:trace_scalars, false) end def trace(key, data) case key when "lex", "parse", "validate", "analyze_query", "analyze_multiplex", "execute_query", "execute_query_lazy", "execute_multiplex" platform_key = @platform_keys.fetch(key) platform_trace(platform_key, key, data) do yield end when "execute_field", "execute_field_lazy" field = data[:field] return_type = field.type.unwrap trace_field = if return_type.kind.scalar? || return_type.kind.enum? (field.trace.nil? && @trace_scalars) || field.trace else true end platform_key = if trace_field context = data.fetch(:query).context cached_platform_key(context, field, :field) { platform_field_key(field.owner, field) } else nil end if platform_key && trace_field platform_trace(platform_key, key, data) do yield end else yield end when "authorized", "authorized_lazy" type = data.fetch(:type) context = data.fetch(:context) platform_key = cached_platform_key(context, type, :authorized) { platform_authorized_key(type) } platform_trace(platform_key, key, data) do yield end when "resolve_type", "resolve_type_lazy" type = data.fetch(:type) context = data.fetch(:context) platform_key = cached_platform_key(context, type, :resolve_type) { platform_resolve_type_key(type) } platform_trace(platform_key, key, data) do yield end else # it's a custom key yield end end def self.use(schema_defn, options = {}) if options[:legacy_tracing] tracer = self.new(**options) schema_defn.tracer(tracer) else tracing_name = self.name.split("::").last trace_name = tracing_name.sub("Tracing", "Trace") if GraphQL::Tracing.const_defined?(trace_name, false) trace_module = GraphQL::Tracing.const_get(trace_name) warn("`use(#{self.name})` is deprecated, use the equivalent `trace_with(#{trace_module.name})` instead. More info: https://graphql-ruby.org/queries/tracing.html") schema_defn.trace_with(trace_module, **options) else warn("`use(#{self.name})` and `Tracing::PlatformTracing` are deprecated. Use a `trace_with(...)` module instead. More info: https://graphql-ruby.org/queries/tracing.html. Please open an issue on the GraphQL-Ruby repo if you want to discuss further!") tracer = self.new(**options) schema_defn.tracer(tracer, silence_deprecation_warning: true) end end end private # Get the transaction name based on the operation type and name if possible, or fall back to a user provided # one. Useful for anonymous queries. def transaction_name(query) selected_op = query.selected_operation txn_name = if selected_op op_type = selected_op.operation_type op_name = selected_op.name || fallback_transaction_name(query.context) || "anonymous" "#{op_type}.#{op_name}" else "query.anonymous" end "GraphQL/#{txn_name}" end def fallback_transaction_name(context) context[:tracing_fallback_transaction_name] end attr_reader :options # Different kind of schema objects have different kinds of keys: # # - Object types: `.authorized` # - Union/Interface types: `.resolve_type` # - Fields: execution # # So, they can all share one cache. # # If the key isn't present, the given block is called and the result is cached for `key`. # # @param ctx [GraphQL::Query::Context] # @param key [Class, GraphQL::Field] A part of the schema # @param trace_phase [Symbol] The stage of execution being traced (used by OpenTelementry tracing) # @return [String] def cached_platform_key(ctx, key, trace_phase) cache = ctx.namespace(self.class)[:platform_key_cache] ||= {} cache.fetch(key) { cache[key] = yield } end end end end graphql-ruby-2.5.19/lib/graphql/tracing/prometheus_trace.rb000066400000000000000000000055641514115062600237710ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing/monitor_trace" module GraphQL module Tracing # A tracer for reporting GraphQL-Ruby times to Prometheus. # # The PrometheusExporter server must be run with a custom type collector that extends `GraphQL::Tracing::PrometheusTracing::GraphQLCollector`. # # @example Adding this trace to your schema # require 'prometheus_exporter/client' # # class MySchema < GraphQL::Schema # trace_with GraphQL::Tracing::PrometheusTrace # end # # @example Running a custom type collector # # lib/graphql_collector.rb # if defined?(PrometheusExporter::Server) # require 'graphql/tracing' # # class GraphQLCollector < GraphQL::Tracing::PrometheusTrace::GraphQLCollector # end # end # # # Then run: # # bundle exec prometheus_exporter -a lib/graphql_collector.rb PrometheusTrace = MonitorTrace.create_module("prometheus") module PrometheusTrace if defined?(PrometheusExporter::Server) autoload :GraphQLCollector, "graphql/tracing/prometheus_trace/graphql_collector" end def initialize(client: PrometheusExporter::Client.default, keys_whitelist: [:execute_field], collector_type: "graphql", **rest) @prometheus_client = client @prometheus_keys_whitelist = keys_whitelist.map(&:to_sym) # handle previous string keys @prometheus_collector_type = collector_type setup_prometheus_monitor(**rest) super end attr_reader :prometheus_collector_type, :prometheus_client, :prometheus_keys_whitelist class PrometheusMonitor < MonitorTrace::Monitor def instrument(keyword, object) if active?(keyword) start = gettime result = yield duration = gettime - start send_json(duration, keyword, object) result else yield end end def active?(keyword) @trace.prometheus_keys_whitelist.include?(keyword) end def gettime ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) end def send_json(duration, keyword, object) event_name = name_for(keyword, object) @trace.prometheus_client.send_json( type: @trace.prometheus_collector_type, duration: duration, platform_key: event_name, key: keyword ) end include MonitorTrace::Monitor::GraphQLPrefixNames class Event < MonitorTrace::Monitor::Event def start @start_time = @monitor.gettime end def finish if @monitor.active?(keyword) duration = @monitor.gettime - @start_time @monitor.send_json(duration, keyword, object) end end end end end end end graphql-ruby-2.5.19/lib/graphql/tracing/prometheus_trace/000077500000000000000000000000001514115062600234325ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/tracing/prometheus_trace/graphql_collector.rb000066400000000000000000000017151514115062600274670ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing" module GraphQL module Tracing module PrometheusTrace class GraphQLCollector < ::PrometheusExporter::Server::TypeCollector def initialize @graphql_gauge = PrometheusExporter::Metric::Base.default_aggregation.new( 'graphql_duration_seconds', 'Time spent in GraphQL operations, in seconds' ) end def type 'graphql' end def collect(object) default_labels = { key: object['key'], platform_key: object['platform_key'] } custom = object['custom_labels'] labels = custom.nil? ? default_labels : default_labels.merge(custom) @graphql_gauge.observe object['duration'], labels end def metrics [@graphql_gauge] end end end # Backwards-compat: PrometheusTracing::GraphQLCollector = PrometheusTrace::GraphQLCollector end end graphql-ruby-2.5.19/lib/graphql/tracing/prometheus_tracing.rb000066400000000000000000000040231514115062600243070ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing/platform_tracing" module GraphQL module Tracing class PrometheusTracing < PlatformTracing DEFAULT_WHITELIST = ['execute_field', 'execute_field_lazy'].freeze DEFAULT_COLLECTOR_TYPE = 'graphql'.freeze self.platform_keys = { 'lex' => "graphql.lex", 'parse' => "graphql.parse", 'validate' => "graphql.validate", 'analyze_query' => "graphql.analyze", 'analyze_multiplex' => "graphql.analyze", 'execute_multiplex' => "graphql.execute", 'execute_query' => "graphql.execute", 'execute_query_lazy' => "graphql.execute", 'execute_field' => "graphql.execute", 'execute_field_lazy' => "graphql.execute" } def initialize(opts = {}) @client = opts[:client] || PrometheusExporter::Client.default @keys_whitelist = opts[:keys_whitelist] || DEFAULT_WHITELIST @collector_type = opts[:collector_type] || DEFAULT_COLLECTOR_TYPE super opts end def platform_trace(platform_key, key, _data, &block) return yield unless @keys_whitelist.include?(key) instrument_execution(platform_key, key, &block) end def platform_field_key(type, field) "#{type.graphql_name}.#{field.graphql_name}" end def platform_authorized_key(type) "#{type.graphql_name}.authorized" end def platform_resolve_type_key(type) "#{type.graphql_name}.resolve_type" end private def instrument_execution(platform_key, key, &block) start = ::Process.clock_gettime ::Process::CLOCK_MONOTONIC result = block.call duration = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start observe platform_key, key, duration result end def observe(platform_key, key, duration) @client.send_json( type: @collector_type, duration: duration, platform_key: platform_key, key: key ) end end end end graphql-ruby-2.5.19/lib/graphql/tracing/scout_trace.rb000066400000000000000000000027061514115062600227260ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing/monitor_trace" module GraphQL module Tracing # A tracer for sending GraphQL-Ruby times to Scout # # @example Adding this tracer to your schema # class MySchema < GraphQL::Schema # trace_with GraphQL::Tracing::ScoutTrace # end ScoutTrace = MonitorTrace.create_module("scout") module ScoutTrace class ScoutMonitor < MonitorTrace::Monitor def instrument(keyword, object) if keyword == :execute query = object.queries.first set_this_txn_name = query.context[:set_scout_transaction_name] if set_this_txn_name == true || (set_this_txn_name.nil? && @set_transaction_name) ScoutApm::Transaction.rename(transaction_name(query)) end end ScoutApm::Tracer.instrument("GraphQL", name_for(keyword, object), INSTRUMENT_OPTS) do yield end end INSTRUMENT_OPTS = { scope: true } include MonitorTrace::Monitor::GraphQLSuffixNames class Event < MonitorTrace::Monitor::Event def start layer = ScoutApm::Layer.new("GraphQL", @monitor.name_for(keyword, object)) layer.subscopable! @scout_req = ScoutApm::RequestManager.lookup @scout_req.start_layer(layer) end def finish @scout_req.stop_layer end end end end end end graphql-ruby-2.5.19/lib/graphql/tracing/scout_tracing.rb000066400000000000000000000035451514115062600232610ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing/platform_tracing" module GraphQL module Tracing class ScoutTracing < PlatformTracing INSTRUMENT_OPTS = { scope: true } self.platform_keys = { "lex" => "lex.graphql", "parse" => "parse.graphql", "validate" => "validate.graphql", "analyze_query" => "analyze.graphql", "analyze_multiplex" => "analyze.graphql", "execute_multiplex" => "execute.graphql", "execute_query" => "execute.graphql", "execute_query_lazy" => "execute.graphql", } # @param set_transaction_name [Boolean] If true, the GraphQL operation name will be used as the transaction name. # This is not advised if you run more than one query per HTTP request, for example, with `graphql-client` or multiplexing. # It can also be specified per-query with `context[:set_scout_transaction_name]`. def initialize(options = {}) self.class.include ScoutApm::Tracer @set_transaction_name = options.fetch(:set_transaction_name, false) super(options) end def platform_trace(platform_key, key, data) if key == "execute_query" set_this_txn_name = data[:query].context[:set_scout_transaction_name] if set_this_txn_name == true || (set_this_txn_name.nil? && @set_transaction_name) ScoutApm::Transaction.rename(transaction_name(data[:query])) end end self.class.instrument("GraphQL", platform_key, INSTRUMENT_OPTS) do yield end end def platform_field_key(type, field) "#{type.graphql_name}.#{field.graphql_name}" end def platform_authorized_key(type) "#{type.graphql_name}.authorized" end def platform_resolve_type_key(type) "#{type.graphql_name}.resolve_type" end end end end graphql-ruby-2.5.19/lib/graphql/tracing/sentry_trace.rb000066400000000000000000000051461514115062600231160ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing/monitor_trace" module GraphQL module Tracing # A tracer for reporting GraphQL-Ruby times to Sentry. # # @example Installing the tracer # class MySchema < GraphQL::Schema # trace_with GraphQL::Tracing::SentryTrace # end # @see MonitorTrace Configuration Options in the parent module SentryTrace = MonitorTrace.create_module("sentry") module SentryTrace class SentryMonitor < MonitorTrace::Monitor def instrument(keyword, object) return yield unless Sentry.initialized? platform_key = name_for(keyword, object) Sentry.with_child_span(op: platform_key, start_timestamp: Sentry.utc_now.to_f) do |span| result = yield return result unless span span.finish if keyword == :execute queries = object.queries operation_names = queries.map{|q| operation_name(q) } span.set_description(operation_names.join(", ")) if queries.size == 1 query = queries.first set_this_txn_name = query.context[:set_sentry_transaction_name] if set_this_txn_name == true || (set_this_txn_name.nil? && @set_transaction_name) Sentry.configure_scope do |scope| scope.set_transaction_name(transaction_name(query)) end end span.set_data('graphql.document', query.query_string) if query.selected_operation_name span.set_data('graphql.operation.name', query.selected_operation_name) end if query.selected_operation span.set_data('graphql.operation.type', query.selected_operation.operation_type) end end end result end end include MonitorTrace::Monitor::GraphQLPrefixNames private def operation_name(query) selected_op = query.selected_operation if selected_op [selected_op.operation_type, selected_op.name].compact.join(' ') else 'GraphQL Operation' end end class Event < MonitorTrace::Monitor::Event def start if Sentry.initialized? && (@span = Sentry.get_current_scope.get_span) span_name = @monitor.name_for(@keyword, @object) @span.start_child(op: span_name) end end def finish @span&.finish end end end end end end graphql-ruby-2.5.19/lib/graphql/tracing/statsd_trace.rb000066400000000000000000000025011514115062600230640ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing/monitor_trace" module GraphQL module Tracing # A tracer for reporting GraphQL-Ruby times to Statsd. # Passing any Statsd client that implements `.time(name) { ... }` # and `.timing(name, ms)` will work. # # @example Installing this tracer # # eg: # # $statsd = Statsd.new 'localhost', 9125 # class MySchema < GraphQL::Schema # use GraphQL::Tracing::StatsdTrace, statsd: $statsd # end StatsdTrace = MonitorTrace.create_module("statsd") module StatsdTrace class StatsdMonitor < MonitorTrace::Monitor def initialize(statsd:, **_rest) @statsd = statsd super end attr_reader :statsd def instrument(keyword, object) @statsd.time(name_for(keyword, object)) do yield end end include MonitorTrace::Monitor::GraphQLPrefixNames class Event < MonitorTrace::Monitor::Event def start @start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) end def finish elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start_time @monitor.statsd.timing(@monitor.name_for(keyword, object), elapsed) end end end end end end graphql-ruby-2.5.19/lib/graphql/tracing/statsd_tracing.rb000066400000000000000000000022461514115062600234230ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing/platform_tracing" module GraphQL module Tracing class StatsdTracing < PlatformTracing self.platform_keys = { 'lex' => "graphql.lex", 'parse' => "graphql.parse", 'validate' => "graphql.validate", 'analyze_query' => "graphql.analyze_query", 'analyze_multiplex' => "graphql.analyze_multiplex", 'execute_multiplex' => "graphql.execute_multiplex", 'execute_query' => "graphql.execute_query", 'execute_query_lazy' => "graphql.execute_query_lazy", } # @param statsd [Object] A statsd client def initialize(statsd:, **rest) @statsd = statsd super(**rest) end def platform_trace(platform_key, key, data) @statsd.time(platform_key) do yield end end def platform_field_key(type, field) "graphql.#{type.graphql_name}.#{field.graphql_name}" end def platform_authorized_key(type) "graphql.authorized.#{type.graphql_name}" end def platform_resolve_type_key(type) "graphql.resolve_type.#{type.graphql_name}" end end end end graphql-ruby-2.5.19/lib/graphql/tracing/trace.rb000066400000000000000000000137011514115062600215060ustar00rootroot00000000000000# frozen_string_literal: true require "graphql/tracing" module GraphQL module Tracing # This is the base class for a `trace` instance whose methods are called during query execution. # "Trace modes" are subclasses of this with custom tracing modules mixed in. # # A trace module may implement any of the methods on `Trace`, being sure to call `super` # to continue any tracing hooks and call the actual runtime behavior. # class Trace # @param multiplex [GraphQL::Execution::Multiplex, nil] # @param query [GraphQL::Query, nil] def initialize(multiplex: nil, query: nil, **_options) @multiplex = multiplex @query = query end # The Ruby parser doesn't call this method (`graphql/c_parser` does.) def lex(query_string:) yield end # @param query_string [String] # @return [void] def parse(query_string:) yield end def validate(query:, validate:) yield end def begin_validate(query, validate) end def end_validate(query, validate, errors) end # @param multiplex [GraphQL::Execution::Multiplex] # @param analyzers [Array] # @return [void] def begin_analyze_multiplex(multiplex, analyzers); end # @param multiplex [GraphQL::Execution::Multiplex] # @param analyzers [Array] # @return [void] def end_analyze_multiplex(multiplex, analyzers); end # @param multiplex [GraphQL::Execution::Multiplex] # @return [void] def analyze_multiplex(multiplex:) yield end def analyze_query(query:) yield end # This wraps an entire `.execute` call. # @param multiplex [GraphQL::Execution::Multiplex] # @return [void] def execute_multiplex(multiplex:) yield end def execute_query(query:) yield end def execute_query_lazy(query:, multiplex:) yield end # GraphQL is about to resolve this field # @param field [GraphQL::Schema::Field] # @param object [GraphQL::Schema::Object] # @param arguments [Hash] # @param query [GraphQL::Query] def begin_execute_field(field, object, arguments, query); end # GraphQL just finished resolving this field # @param field [GraphQL::Schema::Field] # @param object [GraphQL::Schema::Object] # @param arguments [Hash] # @param query [GraphQL::Query] # @param result [Object] def end_execute_field(field, object, arguments, query, result); end def execute_field(field:, query:, ast_node:, arguments:, object:) yield end def execute_field_lazy(field:, query:, ast_node:, arguments:, object:) yield end def authorized(query:, type:, object:) yield end # A call to `.authorized?` is starting # @param type [Class] # @param object [Object] # @param context [GraphQL::Query::Context] # @return [void] def begin_authorized(type, object, context) end # A call to `.authorized?` just finished # @param type [Class] # @param object [Object] # @param context [GraphQL::Query::Context] # @param authorized_result [Boolean] # @return [void] def end_authorized(type, object, context, authorized_result) end def authorized_lazy(query:, type:, object:) yield end def resolve_type(query:, type:, object:) yield end def resolve_type_lazy(query:, type:, object:) yield end # A call to `.resolve_type` is starting # @param type [Class, Module] # @param value [Object] # @param context [GraphQL::Query::Context] # @return [void] def begin_resolve_type(type, value, context) end # A call to `.resolve_type` just ended # @param type [Class, Module] # @param value [Object] # @param context [GraphQL::Query::Context] # @param resolved_type [Class] # @return [void] def end_resolve_type(type, value, context, resolved_type) end # A dataloader run is starting # @param dataloader [GraphQL::Dataloader] # @return [void] def begin_dataloader(dataloader); end # A dataloader run has ended # @param dataloder [GraphQL::Dataloader] # @return [void] def end_dataloader(dataloader); end # A source with pending keys is about to fetch # @param source [GraphQL::Dataloader::Source] # @return [void] def begin_dataloader_source(source); end # A fetch call has just ended # @param source [GraphQL::Dataloader::Source] # @return [void] def end_dataloader_source(source); end # Called when Dataloader spins up a new fiber for GraphQL execution # @param jobs [Array<#call>] Execution steps to run # @return [void] def dataloader_spawn_execution_fiber(jobs); end # Called when Dataloader spins up a new fiber for fetching data # @param pending_sources [GraphQL::Dataloader::Source] Instances with pending keys # @return [void] def dataloader_spawn_source_fiber(pending_sources); end # Called when an execution or source fiber terminates # @return [void] def dataloader_fiber_exit; end # Called when a Dataloader fiber is paused to wait for data # @param source [GraphQL::Dataloader::Source] The Source whose `load` call initiated this `yield` # @return [void] def dataloader_fiber_yield(source); end # Called when a Dataloader fiber is resumed because data has been loaded # @param source [GraphQL::Dataloader::Source] The Source whose `load` call previously caused this Fiber to wait # @return [void] def dataloader_fiber_resume(source); end end end end graphql-ruby-2.5.19/lib/graphql/type_kinds.rb000066400000000000000000000060321514115062600211310ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL # Type kinds are the basic categories which a type may belong to (`Object`, `Scalar`, `Union`...) module TypeKinds # These objects are singletons, eg `GraphQL::TypeKinds::UNION`, `GraphQL::TypeKinds::SCALAR`. class TypeKind attr_reader :name, :description def initialize(name, abstract: false, leaf: false, fields: false, wraps: false, input: false, description: nil) @name = name @abstract = abstract @fields = fields @wraps = wraps @input = input @leaf = leaf @composite = fields? || abstract? @description = description freeze end # Does this TypeKind have multiple possible implementers? # @deprecated Use `abstract?` instead of `resolves?`. def resolves?; @abstract; end # Is this TypeKind abstract? def abstract?; @abstract; end # Does this TypeKind have queryable fields? def fields?; @fields; end # Does this TypeKind modify another type? def wraps?; @wraps; end # Is this TypeKind a valid query input? def input?; @input; end def to_s; @name; end # Is this TypeKind a primitive value? def leaf?; @leaf; end # Is this TypeKind composed of many values? def composite?; @composite; end def scalar? self == TypeKinds::SCALAR end def object? self == TypeKinds::OBJECT end def interface? self == TypeKinds::INTERFACE end def union? self == TypeKinds::UNION end def enum? self == TypeKinds::ENUM end def input_object? self == TypeKinds::INPUT_OBJECT end def list? self == TypeKinds::LIST end def non_null? self == TypeKinds::NON_NULL end end TYPE_KINDS = [ SCALAR = TypeKind.new("SCALAR", input: true, leaf: true, description: 'Indicates this type is a scalar.'), OBJECT = TypeKind.new("OBJECT", fields: true, description: 'Indicates this type is an object. `fields` and `interfaces` are valid fields.'), INTERFACE = TypeKind.new("INTERFACE", abstract: true, fields: true, description: 'Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.'), UNION = TypeKind.new("UNION", abstract: true, description: 'Indicates this type is a union. `possibleTypes` is a valid field.'), ENUM = TypeKind.new("ENUM", input: true, leaf: true, description: 'Indicates this type is an enum. `enumValues` is a valid field.'), INPUT_OBJECT = TypeKind.new("INPUT_OBJECT", input: true, description: 'Indicates this type is an input object. `inputFields` is a valid field.'), LIST = TypeKind.new("LIST", wraps: true, description: 'Indicates this type is a list. `ofType` is a valid field.'), NON_NULL = TypeKind.new("NON_NULL", wraps: true, description: 'Indicates this type is a non-null. `ofType` is a valid field.'), ] end end graphql-ruby-2.5.19/lib/graphql/types.rb000066400000000000000000000011551514115062600201250ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Types extend Autoload autoload :Boolean, "graphql/types/boolean" autoload :BigInt, "graphql/types/big_int" autoload :Float, "graphql/types/float" autoload :ID, "graphql/types/id" autoload :Int, "graphql/types/int" autoload :JSON, "graphql/types/json" autoload :String, "graphql/types/string" autoload :ISO8601Date, "graphql/types/iso_8601_date" autoload :ISO8601DateTime, "graphql/types/iso_8601_date_time" autoload :ISO8601Duration, "graphql/types/iso_8601_duration" autoload :Relay, "graphql/types/relay" end end graphql-ruby-2.5.19/lib/graphql/types/000077500000000000000000000000001514115062600175765ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/types/big_int.rb000066400000000000000000000011071514115062600215350ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Types class BigInt < GraphQL::Schema::Scalar description "Represents non-fractional signed whole numeric values. Since the value may exceed the size of a 32-bit integer, it's encoded as a string." def self.coerce_input(value, _ctx) value && parse_int(value) rescue ArgumentError nil end def self.coerce_result(value, _ctx) value.to_i.to_s end def self.parse_int(value) value.is_a?(Numeric) ? value : Integer(value, 10) end end end end graphql-ruby-2.5.19/lib/graphql/types/boolean.rb000066400000000000000000000005771514115062600215530ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Types class Boolean < GraphQL::Schema::Scalar description "Represents `true` or `false` values." def self.coerce_input(value, _ctx) (value == true || value == false) ? value : nil end def self.coerce_result(value, _ctx) !!value end default_scalar true end end end graphql-ruby-2.5.19/lib/graphql/types/float.rb000066400000000000000000000007301514115062600212300ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Types class Float < GraphQL::Schema::Scalar description "Represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point)." def self.coerce_input(value, _ctx) value.is_a?(Numeric) ? value.to_f : nil end def self.coerce_result(value, _ctx) value.to_f end default_scalar true end end end graphql-ruby-2.5.19/lib/graphql/types/id.rb000066400000000000000000000015051514115062600205200ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Types class ID < GraphQL::Schema::Scalar graphql_name "ID" description "Represents a unique identifier that is Base64 obfuscated. It is often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"VXNlci0xMA==\"`) or integer (such as `4`) input value will be accepted as an ID." default_scalar true def self.coerce_result(value, _ctx) value.is_a?(::String) ? value : value.to_s end def self.coerce_input(value, _ctx) case value when ::String value when Integer value.to_s else nil end end end end end graphql-ruby-2.5.19/lib/graphql/types/int.rb000066400000000000000000000016521514115062600207210ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Types # @see {Types::BigInt} for handling integers outside 32-bit range. class Int < GraphQL::Schema::Scalar description "Represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1." MIN = -(2**31) MAX = (2**31) - 1 def self.coerce_input(value, ctx) return if !value.is_a?(Integer) if value >= MIN && value <= MAX value else err = GraphQL::IntegerDecodingError.new(value) ctx.schema.type_error(err, ctx) end end def self.coerce_result(value, ctx) value = value.to_i if value >= MIN && value <= MAX value else err = GraphQL::IntegerEncodingError.new(value, context: ctx) ctx.schema.type_error(err, ctx) end end default_scalar true end end end graphql-ruby-2.5.19/lib/graphql/types/iso_8601_date.rb000066400000000000000000000024551514115062600223760ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Types # This scalar takes `Date`s and transmits them as strings, # using ISO 8601 format. # # Use it for fields or arguments as follows: # # field :published_at, GraphQL::Types::ISO8601Date, null: false # # argument :deliver_at, GraphQL::Types::ISO8601Date, null: false # # Alternatively, use this built-in scalar as inspiration for your # own Date type. class ISO8601Date < GraphQL::Schema::Scalar description "An ISO 8601-encoded date" specified_by_url "https://tools.ietf.org/html/rfc3339" # @param value [Date,Time,DateTime,String] # @return [String] def self.coerce_result(value, _ctx) Date.parse(value.to_s).iso8601 end # @param str_value [String, Date, DateTime, Time] # @return [Date, nil] def self.coerce_input(value, ctx) if value.is_a?(::Date) value elsif value.is_a?(::DateTime) value.to_date elsif value.is_a?(::Time) value.to_date elsif value.nil? nil else Date.iso8601(value) end rescue ArgumentError, TypeError err = GraphQL::DateEncodingError.new(value) ctx.schema.type_error(err, ctx) end end end end graphql-ruby-2.5.19/lib/graphql/types/iso_8601_date_time.rb000066400000000000000000000047031514115062600234120ustar00rootroot00000000000000# frozen_string_literal: true require 'time' module GraphQL module Types # This scalar takes `Time`s and transmits them as strings, # using ISO 8601 format. # # Use it for fields or arguments as follows: # # field :created_at, GraphQL::Types::ISO8601DateTime, null: false # # argument :deliver_at, GraphQL::Types::ISO8601DateTime, null: false # # Alternatively, use this built-in scalar as inspiration for your # own DateTime type. class ISO8601DateTime < GraphQL::Schema::Scalar description "An ISO 8601-encoded datetime" specified_by_url "https://tools.ietf.org/html/rfc3339" # It's not compatible with Rails' default, # i.e. ActiveSupport::JSON::Encoder.time_precision (3 by default) DEFAULT_TIME_PRECISION = 0 # @return [Integer] def self.time_precision @time_precision || DEFAULT_TIME_PRECISION end # @param [Integer] value def self.time_precision=(value) @time_precision = value end # @param value [Time,Date,DateTime,String] # @return [String] def self.coerce_result(value, _ctx) case value when Date return value.to_time.iso8601(time_precision) when ::String return Time.parse(value).iso8601(time_precision) else # Time, DateTime or compatible is given: return value.iso8601(time_precision) end rescue StandardError => error raise GraphQL::Error, "An incompatible object (#{value.class}) was given to #{self}. Make sure that only Times, Dates, DateTimes, and well-formatted Strings are used with this type. (#{error.message})" end # @param str_value [String] # @return [Time] def self.coerce_input(str_value, _ctx) Time.iso8601(str_value) rescue ArgumentError, TypeError begin dt = Date.iso8601(str_value).to_time # For compatibility, continue accepting dates given without times # But without this, it would zero out given any time part of `str_value` (hours and/or minutes) if dt.iso8601.start_with?(str_value) dt elsif str_value.length == 8 && str_value.match?(/\A\d{8}\Z/) # Allow dates that are missing the "-". eg. "20220404" dt else nil end rescue ArgumentError, TypeError # Invalid input nil end end end end end graphql-ruby-2.5.19/lib/graphql/types/iso_8601_duration.rb000066400000000000000000000054701514115062600233060ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Types # This scalar takes `Duration`s and transmits them as strings, # using ISO 8601 format. ActiveSupport >= 5.0 must be loaded to use # this scalar. # # Use it for fields or arguments as follows: # # field :age, GraphQL::Types::ISO8601Duration, null: false # # argument :interval, GraphQL::Types::ISO8601Duration, null: false # # Alternatively, use this built-in scalar as inspiration for your # own Duration type. class ISO8601Duration < GraphQL::Schema::Scalar description "An ISO 8601-encoded duration" # @return [Integer, nil] def self.seconds_precision # ActiveSupport::Duration precision defaults to whatever input was given @seconds_precision end # @param [Integer, nil] value def self.seconds_precision=(value) @seconds_precision = value end # @param value [ActiveSupport::Duration, String] # @return [String] # @raise [GraphQL::Error] if ActiveSupport::Duration is not defined or if an incompatible object is passed def self.coerce_result(value, _ctx) unless defined?(ActiveSupport::Duration) raise GraphQL::Error, "ActiveSupport >= 5.0 must be loaded to use the built-in ISO8601Duration type." end begin case value when ActiveSupport::Duration value.iso8601(precision: seconds_precision) when ::String ActiveSupport::Duration.parse(value).iso8601(precision: seconds_precision) else # Try calling as ActiveSupport::Duration compatible as a fallback value.iso8601(precision: seconds_precision) end rescue StandardError => error raise GraphQL::Error, "An incompatible object (#{value.class}) was given to #{self}. Make sure that only ActiveSupport::Durations and well-formatted Strings are used with this type. (#{error.message})" end end # @param value [String, ActiveSupport::Duration] # @return [ActiveSupport::Duration, nil] # @raise [GraphQL::Error] if ActiveSupport::Duration is not defined # @raise [GraphQL::DurationEncodingError] if duration cannot be parsed def self.coerce_input(value, ctx) unless defined?(ActiveSupport::Duration) raise GraphQL::Error, "ActiveSupport >= 5.0 must be loaded to use the built-in ISO8601Duration type." end begin if value.is_a?(ActiveSupport::Duration) value elsif value.nil? nil else ActiveSupport::Duration.parse(value) end rescue ArgumentError, TypeError err = GraphQL::DurationEncodingError.new(value) ctx.schema.type_error(err, ctx) end end end end end graphql-ruby-2.5.19/lib/graphql/types/json.rb000066400000000000000000000013141514115062600210730ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Types # An untyped JSON scalar that maps to Ruby hashes, arrays, strings, integers, floats, booleans and nils. # This should be used judiciously because it subverts the GraphQL type system. # # Use it for fields or arguments as follows: # # field :template_parameters, GraphQL::Types::JSON, null: false # # argument :template_parameters, GraphQL::Types::JSON, null: false # class JSON < GraphQL::Schema::Scalar description "Represents untyped JSON" def self.coerce_input(value, _context) value end def self.coerce_result(value, _context) value end end end end graphql-ruby-2.5.19/lib/graphql/types/relay.rb000066400000000000000000000027251514115062600212450ustar00rootroot00000000000000# frozen_string_literal: true # behavior modules: require "graphql/types/relay/connection_behaviors" require "graphql/types/relay/edge_behaviors" require "graphql/types/relay/node_behaviors" require "graphql/types/relay/page_info_behaviors" require "graphql/types/relay/has_node_field" require "graphql/types/relay/has_nodes_field" # concrete classes based on the gem defaults: require "graphql/types/relay/page_info" require "graphql/types/relay/base_connection" require "graphql/types/relay/base_edge" require "graphql/types/relay/node" module GraphQL module Types # This module contains some types and fields that could support Relay conventions in GraphQL. # # You can use these classes out of the box if you want, but if you want to use your _own_ # GraphQL extensions along with the features in this code, you could also # open up the source files and copy the relevant methods and configuration into # your own classes. # # For example, the provided object types extend {Types::Relay::BaseObject}, # but you might want to: # # 1. Migrate the extensions from {Types::Relay::BaseObject} into _your app's_ base object # 2. Copy {Relay::BaseConnection}, {Relay::BaseEdge}, etc into _your app_, and # change them to extend _your_ base object. # # Similarly, `BaseField`'s extensions could be migrated to your app # and `Node` could be implemented to mix in your base interface module. module Relay end end end graphql-ruby-2.5.19/lib/graphql/types/relay/000077500000000000000000000000001514115062600207125ustar00rootroot00000000000000graphql-ruby-2.5.19/lib/graphql/types/relay/base_connection.rb000066400000000000000000000032121514115062600243660ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Types module Relay # Use this to implement Relay connections, or take it as inspiration # for Relay classes in your own app. # # You may wish to copy this code into your own base class, # so you can extend your own `BaseObject` instead of `GraphQL::Schema::Object`. # # @example Implementation a connection and edge # class BaseObject < GraphQL::Schema::Object; end # # # Given some object in your app ... # class Types::Post < BaseObject # end # # # Make a couple of base classes: # class Types::BaseEdge < GraphQL::Types::Relay::BaseEdge; end # class Types::BaseConnection < GraphQL::Types::Relay::BaseConnection; end # # # Then extend them for the object in your app # class Types::PostEdge < Types::BaseEdge # node_type Types::Post # end # # class Types::PostConnection < Types::BaseConnection # edge_type Types::PostEdge, # edges_nullable: true, # edge_nullable: true, # node_nullable: true, # nodes_field: true # # # Alternatively, you can call the class methods followed by your edge type # # edges_nullable true # # edge_nullable true # # node_nullable true # # has_nodes_field true # # edge_type Types::PostEdge # end # # @see Relay::BaseEdge for edge types class BaseConnection < Schema::Object include ConnectionBehaviors end end end end graphql-ruby-2.5.19/lib/graphql/types/relay/base_edge.rb000066400000000000000000000015701514115062600231400ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Types module Relay # A class-based definition for Relay edges. # # Use this as a parent class in your app, or use it as inspiration for your # own base `Edge` class. # # For example, you may want to extend your own `BaseObject` instead of the # built-in `GraphQL::Schema::Object`. # # @example Making a UserEdge type # # Make a base class for your app # class Types::BaseEdge < GraphQL::Types::Relay::BaseEdge # end # # # Then extend your own base class # class Types::UserEdge < Types::BaseEdge # node_type(Types::User) # end # # @see {Relay::BaseConnection} for connection types class BaseEdge < GraphQL::Schema::Object include Types::Relay::EdgeBehaviors end end end end graphql-ruby-2.5.19/lib/graphql/types/relay/connection_behaviors.rb000066400000000000000000000176561514115062600254570ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Types module Relay module ConnectionBehaviors extend Forwardable def_delegators :@object, :cursor_from_node, :parent def self.included(child_class) child_class.extend(ClassMethods) child_class.has_nodes_field(true) child_class.node_nullable(true) child_class.edges_nullable(true) child_class.edge_nullable(true) child_class.module_exec { self.edge_type = nil self.node_type = nil self.edge_class = nil } child_class.default_broadcastable(nil) add_page_info_field(child_class) end module ClassMethods def inherited(child_class) super child_class.has_nodes_field(has_nodes_field) child_class.node_nullable(node_nullable) child_class.edges_nullable(edges_nullable) child_class.edge_nullable(edge_nullable) child_class.edge_type = nil child_class.node_type = nil child_class.edge_class = nil child_class.default_broadcastable(default_broadcastable?) end def default_relay? true end def default_broadcastable? @default_broadcastable end def default_broadcastable(new_value) @default_broadcastable = new_value end # @return [Class] attr_reader :node_type # @return [Class] attr_reader :edge_class # Configure this connection to return `edges` and `nodes` based on `edge_type_class`. # # This method will use the inputs to create: # - `edges` field # - `nodes` field # - description # # It's called when you subclass this base connection, trying to use the # class name to set defaults. You can call it again in the class definition # to override the default (or provide a value, if the default lookup failed). # @param field_options [Hash] Any extra keyword arguments to pass to the `field :edges, ...` and `field :nodes, ...` configurations def edge_type(edge_type_class, edge_class: GraphQL::Pagination::Connection::Edge, node_type: edge_type_class.node_type, nodes_field: self.has_nodes_field, node_nullable: self.node_nullable, edges_nullable: self.edges_nullable, edge_nullable: self.edge_nullable, field_options: nil) # Set this connection's graphql name node_type_name = node_type.graphql_name @node_type = node_type @edge_type = edge_type_class @edge_class = edge_class base_field_options = { name: :edges, type: [edge_type_class, null: edge_nullable], null: edges_nullable, description: "A list of edges.", scope: false, # Assume that the connection was already scoped. connection: false, } if field_options base_field_options.merge!(field_options) end field(**base_field_options) define_nodes_field(node_nullable, field_options: field_options) if nodes_field description("The connection type for #{node_type_name}.") end # Filter this list according to the way its node type would scope them def scope_items(items, context) node_type.scope_items(items, context) end # The connection will skip auth on its nodes if the node_type is configured for that def reauthorize_scoped_objects(new_value = nil) if new_value.nil? if @reauthorize_scoped_objects != nil @reauthorize_scoped_objects else node_type.reauthorize_scoped_objects end else @reauthorize_scoped_objects = new_value end end # Add the shortcut `nodes` field to this connection and its subclasses def nodes_field(node_nullable: self.node_nullable, field_options: nil) define_nodes_field(node_nullable, field_options: field_options) end def authorized?(obj, ctx) true # Let nodes be filtered out end def visible?(ctx) # if this is an abstract base class, there may be no `node_type` node_type ? node_type.visible?(ctx) : super end # Set the default `node_nullable` for this class and its child classes. (Defaults to `true`.) # Use `node_nullable(false)` in your base class to make non-null `node` and `nodes` fields. def node_nullable(new_value = nil) if new_value.nil? defined?(@node_nullable) ? @node_nullable : superclass.node_nullable else @node_nullable = new_value end end # Set the default `edges_nullable` for this class and its child classes. (Defaults to `true`.) # Use `edges_nullable(false)` in your base class to make non-null `edges` fields. def edges_nullable(new_value = nil) if new_value.nil? defined?(@edges_nullable) ? @edges_nullable : superclass.edges_nullable else @edges_nullable = new_value end end # Set the default `edge_nullable` for this class and its child classes. (Defaults to `true`.) # Use `edge_nullable(false)` in your base class to make non-null `edge` fields. def edge_nullable(new_value = nil) if new_value.nil? defined?(@edge_nullable) ? @edge_nullable : superclass.edge_nullable else @edge_nullable = new_value end end # Set the default `nodes_field` for this class and its child classes. (Defaults to `true`.) # Use `nodes_field(false)` in your base class to prevent adding of a nodes field. def has_nodes_field(new_value = nil) if new_value.nil? defined?(@nodes_field) ? @nodes_field : superclass.has_nodes_field else @nodes_field = new_value end end protected attr_writer :edge_type, :node_type, :edge_class private def define_nodes_field(nullable, field_options: nil) base_field_options = { name: :nodes, type: [@node_type, null: nullable], null: nullable, description: "A list of nodes.", connection: false, # Assume that the connection was scoped before this step: scope: false, } if field_options base_field_options.merge!(field_options) end field(**base_field_options) end end class << self def add_page_info_field(obj_type) obj_type.field :page_info, GraphQL::Types::Relay::PageInfo, null: false, description: "Information to aid in pagination." end end def edges # Assume that whatever authorization needed to happen # already happened at the connection level. current_runtime_state = Fiber[:__graphql_runtime_info] query_runtime_state = current_runtime_state[context.query] query_runtime_state.was_authorized_by_scope_items = @object.was_authorized_by_scope_items? @object.edges end def nodes # Assume that whatever authorization needed to happen # already happened at the connection level. current_runtime_state = Fiber[:__graphql_runtime_info] query_runtime_state = current_runtime_state[context.query] query_runtime_state.was_authorized_by_scope_items = @object.was_authorized_by_scope_items? @object.nodes end end end end end graphql-ruby-2.5.19/lib/graphql/types/relay/edge_behaviors.rb000066400000000000000000000055501514115062600242120ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Types module Relay module EdgeBehaviors def self.included(child_class) child_class.description("An edge in a connection.") child_class.field(:cursor, String, null: false, description: "A cursor for use in pagination.") child_class.extend(ClassMethods) child_class.class_exec { self.node_type = nil } child_class.node_nullable(true) child_class.default_broadcastable(nil) end def node current_runtime_state = Fiber[:__graphql_runtime_info] query_runtime_state = current_runtime_state[context.query] query_runtime_state.was_authorized_by_scope_items = @object.was_authorized_by_scope_items? @object.node end module ClassMethods def inherited(child_class) super child_class.node_type = nil child_class.node_nullable = nil child_class.default_broadcastable(default_broadcastable?) end def default_relay? true end def default_broadcastable? @default_broadcastable end def default_broadcastable(new_value) @default_broadcastable = new_value end # Get or set the Object type that this edge wraps. # # @param node_type [Class] A `Schema::Object` subclass # @param null [Boolean] # @param field_options [Hash] Any extra arguments to pass to the `field :node` configuration def node_type(node_type = nil, null: self.node_nullable, field_options: nil) if node_type @node_type = node_type # Add a default `node` field base_field_options = { name: :node, type: node_type, null: null, description: "The item at the end of the edge.", connection: false, } if field_options base_field_options.merge!(field_options) end field(**base_field_options) end @node_type end def authorized?(obj, ctx) true end def visible?(ctx) node_type.visible?(ctx) end # Set the default `node_nullable` for this class and its child classes. (Defaults to `true`.) # Use `node_nullable(false)` in your base class to make non-null `node` field. def node_nullable(new_value = nil) if new_value.nil? @node_nullable != nil ? @node_nullable : superclass.node_nullable else @node_nullable = new_value end end protected attr_writer :node_type, :node_nullable end end end end end graphql-ruby-2.5.19/lib/graphql/types/relay/has_node_field.rb000066400000000000000000000021011514115062600241540ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Types module Relay # Include this module to your root Query type to get a Relay-compliant `node(id: ID!): Node` field that uses the schema's `object_from_id` hook. module HasNodeField def self.included(child_class) child_class.field(**field_options, &field_block) end class << self def field_options { name: "node", type: GraphQL::Types::Relay::Node, null: true, description: "Fetches an object given its ID.", relay_node_field: true, } end def field_block Proc.new { argument :id, "ID!", description: "ID of the object." def resolve(obj, args, ctx) ctx.schema.object_from_id(args[:id], ctx) end def resolve_field(obj, args, ctx) resolve(obj, args, ctx) end } end end end end end end graphql-ruby-2.5.19/lib/graphql/types/relay/has_nodes_field.rb000066400000000000000000000021701514115062600243450ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Types module Relay # Include this module to your root Query type to get a Relay-style `nodes(id: ID!): [Node]` field that uses the schema's `object_from_id` hook. module HasNodesField def self.included(child_class) child_class.field(**field_options, &field_block) end class << self def field_options { name: "nodes", type: [GraphQL::Types::Relay::Node, null: true], null: false, description: "Fetches a list of objects given a list of IDs.", relay_nodes_field: true, } end def field_block Proc.new { argument :ids, "[ID!]!", description: "IDs of the objects." def resolve(obj, args, ctx) args[:ids].map { |id| ctx.schema.object_from_id(id, ctx) } end def resolve_field(obj, args, ctx) resolve(obj, args, ctx) end } end end end end end end graphql-ruby-2.5.19/lib/graphql/types/relay/node.rb000066400000000000000000000005611514115062600221660ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Types module Relay # This can be used for Relay's `Node` interface, # or you can take it as inspiration for your own implementation # of the `Node` interface. module Node include GraphQL::Schema::Interface include Types::Relay::NodeBehaviors end end end end graphql-ruby-2.5.19/lib/graphql/types/relay/node_behaviors.rb000066400000000000000000000011601514115062600242240ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Types module Relay module NodeBehaviors def self.included(child_module) child_module.extend(ClassMethods) child_module.description("An object with an ID.") child_module.field(:id, ID, null: false, description: "ID of the object.", resolver_method: :default_global_id) end def default_global_id context.schema.id_from_object(object, self.class, context) end module ClassMethods def default_relay? true end end end end end end graphql-ruby-2.5.19/lib/graphql/types/relay/page_info.rb000066400000000000000000000003651514115062600231720ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Types module Relay # The return type of a connection's `pageInfo` field class PageInfo < GraphQL::Schema::Object include PageInfoBehaviors end end end end graphql-ruby-2.5.19/lib/graphql/types/relay/page_info_behaviors.rb000066400000000000000000000020121514115062600252230ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Types module Relay module PageInfoBehaviors def self.included(child_class) child_class.extend ClassMethods child_class.description "Information about pagination in a connection." child_class.field :has_next_page, Boolean, null: false, description: "When paginating forwards, are there more items?" child_class.field :has_previous_page, Boolean, null: false, description: "When paginating backwards, are there more items?" child_class.field :start_cursor, String, null: true, description: "When paginating backwards, the cursor to continue." child_class.field :end_cursor, String, null: true, description: "When paginating forwards, the cursor to continue." end end module ClassMethods def default_relay? true end def default_broadcastable? true end end end end end graphql-ruby-2.5.19/lib/graphql/types/string.rb000066400000000000000000000014611514115062600214330ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL module Types class String < GraphQL::Schema::Scalar description "Represents textual data as UTF-8 character sequences. This type is most often used by GraphQL to represent free-form human-readable text." def self.coerce_result(value, ctx) str = value.to_s if str.encoding == Encoding::UTF_8 || str.ascii_only? str elsif str.frozen? str.encode(Encoding::UTF_8) else str.encode!(Encoding::UTF_8) end rescue EncodingError err = GraphQL::StringEncodingError.new(str, context: ctx) ctx.schema.type_error(err, ctx) end def self.coerce_input(value, _ctx) value.is_a?(::String) ? value : nil end default_scalar true end end end graphql-ruby-2.5.19/lib/graphql/unauthorized_enum_value_error.rb000066400000000000000000000007261514115062600251360ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class UnauthorizedEnumValueError < GraphQL::UnauthorizedError # @return [GraphQL::Schema::EnumValue] The value whose `#authorized?` check returned false attr_accessor :enum_value def initialize(type:, context:, enum_value:) @enum_value = enum_value message ||= "#{enum_value.path} failed authorization" super(message, object: enum_value.value, type: type, context: context) end end end graphql-ruby-2.5.19/lib/graphql/unauthorized_error.rb000066400000000000000000000021161514115062600227110ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL # When an `authorized?` hook returns false, this error is used to communicate the failure. # It's passed to {Schema.unauthorized_object}. # # Alternatively, custom code in `authorized?` may raise this error. It will be routed the same way. class UnauthorizedError < GraphQL::Error # @return [Object] the application object that failed the authorization check attr_reader :object # @return [Class] the GraphQL object type whose `.authorized?` method was called (and returned false) attr_reader :type # @return [GraphQL::Query::Context] the context for the current query attr_accessor :context def initialize(message = nil, object: nil, type: nil, context: nil) if message.nil? && object.nil? && type.nil? raise ArgumentError, "#{self.class.name} requires either a message or keywords" end @object = object @type = type @context = context message ||= "An instance of #{object.class} failed #{type.graphql_name}'s authorization check" super(message) end end end graphql-ruby-2.5.19/lib/graphql/unauthorized_field_error.rb000066400000000000000000000014421514115062600240550ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL class UnauthorizedFieldError < GraphQL::UnauthorizedError # @return [Field] the field that failed the authorization check attr_accessor :field def initialize(message = nil, object: nil, type: nil, context: nil, field: nil) if message.nil? && [field, type].any?(&:nil?) raise ArgumentError, "#{self.class.name} requires either a message or keywords" end @field = field message ||= begin if object "An instance of #{object.class} failed #{type.name}'s authorization check on field #{field.name}" else "Failed #{type.name}'s authorization check on field #{field.name}" end end super(message, object: object, type: type, context: context) end end end graphql-ruby-2.5.19/lib/graphql/unresolved_type_error.rb000066400000000000000000000031501514115062600234160ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL # Error raised when the value provided for a field # can't be resolved to one of the possible types for the field. class UnresolvedTypeError < GraphQL::RuntimeTypeError # @return [Object] The runtime value which couldn't be successfully resolved with `resolve_type` attr_reader :value # @return [GraphQL::Field] The field whose value couldn't be resolved (`field.type` is type which couldn't be resolved) attr_reader :field # @return [GraphQL::BaseType] The owner of `field` attr_reader :parent_type # @return [Object] The return of {Schema#resolve_type} for `value` attr_reader :resolved_type # @return [Array] The allowed options for resolving `value` to `field.type` attr_reader :possible_types def initialize(value, field, parent_type, resolved_type, possible_types) @value = value @field = field @parent_type = parent_type @resolved_type = resolved_type @possible_types = possible_types message = "The value from \"#{field.graphql_name}\" on \"#{parent_type.graphql_name}\" could not be resolved to \"#{field.type.to_type_signature}\". " \ "(Received: `#{resolved_type.inspect}`, Expected: [#{possible_types.map(&:graphql_name).join(", ")}]) " \ "Make sure you have defined a `resolve_type` proc on your schema and that value `#{value.inspect}` " \ "gets resolved to a valid type. You may need to add your type to `orphan_types` if it implements an " \ "interface but isn't a return type of any other field." super(message) end end end graphql-ruby-2.5.19/lib/graphql/version.rb000066400000000000000000000001061514115062600204410ustar00rootroot00000000000000# frozen_string_literal: true module GraphQL VERSION = "2.5.19" end graphql-ruby-2.5.19/readme.md000066400000000000000000000047361514115062600160170ustar00rootroot00000000000000# graphql graphql-ruby [![CI Suite](https://github.com/rmosolgo/graphql-ruby/actions/workflows/ci.yaml/badge.svg)](https://github.com/rmosolgo/graphql-ruby/actions/workflows/ci.yaml) [![Gem Version](https://badge.fury.io/rb/graphql.svg)](https://rubygems.org/gems/graphql) A Ruby implementation of [GraphQL](https://graphql.org/). - [Website](https://graphql-ruby.org/) - [API Documentation](https://www.rubydoc.info/github/rmosolgo/graphql-ruby) - [Newsletter](https://buttondown.email/graphql-ruby) ## Installation Install from RubyGems by adding it to your `Gemfile`, then bundling. ```ruby # Gemfile gem 'graphql' ``` ``` $ bundle install ``` ## Getting Started ``` $ rails generate graphql:install ``` After this, you may need to run `bundle install` again, as by default graphiql-rails is added on installation. Or, see ["Getting Started"](https://graphql-ruby.org/getting_started.html). ## Upgrade I also sell [GraphQL::Pro](https://graphql.pro) which provides several features on top of the GraphQL runtime, including: - [Persisted queries](https://graphql-ruby.org/operation_store/overview) - [API versioning](https://graphql-ruby.org/changesets/overview) - [Streaming payloads](https://graphql-ruby.org/defer/overview) - [Server-side caching](https://graphql-ruby.org/object_cache/overview) - [Rate limiters](https://graphql-ruby.org/limiters/overview) - Subscriptions backends for [Pusher](https://graphql-ruby.org/subscriptions/pusher_implementation) and [Ably](https://graphql-ruby.org/subscriptions/ably_implementation) - Authorization plugins for [Pundit](https://graphql-ruby.org/authorization/pundit_integration) and [CanCan](https://graphql-ruby.org/authorization/can_can_integration) Besides that, Pro customers get email support and an opportunity to support graphql-ruby's development! ## Goals - Implement the GraphQL spec & support a Relay front end - Provide idiomatic, plain-Ruby API with similarities to reference implementation where possible - Support Ruby on Rails and Relay ## Getting Involved - __Say hi & ask questions__ in the #graphql-ruby channel on [Discord](https://discord.com/invite/xud7bH9). - __Report bugs__ by posting a description, full stack trace, and all relevant code in a [GitHub issue](https://github.com/rmosolgo/graphql-ruby/issues). - __Start hacking__ with the [Development guide](https://graphql-ruby.org/development). graphql-ruby-2.5.19/spec/000077500000000000000000000000001514115062600151605ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/000077500000000000000000000000001514115062600163135ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/.gitignore000066400000000000000000000006651514115062600203120ustar00rootroot00000000000000# See https://help.github.com/articles/ignoring-files for more about ignoring files. # # If you find yourself ignoring temporary files generated by your text editor # or operating system, you probably want to add a global ignore instead: # git config --global core.excludesfile '~/.gitignore_global' # Ignore bundler config. /.bundle # Ignore all logfiles and tempfiles. /log/* /tmp/* /node_modules /yarn-error.log .byebug_history graphql-ruby-2.5.19/spec/dummy/Rakefile000066400000000000000000000004011514115062600177530ustar00rootroot00000000000000# frozen_string_literal: true # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require_relative 'config/application' Rails.application.load_tasks graphql-ruby-2.5.19/spec/dummy/app/000077500000000000000000000000001514115062600170735ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/app/assets/000077500000000000000000000000001514115062600203755ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/app/assets/config/000077500000000000000000000000001514115062600216425ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/app/assets/config/manifest.js000066400000000000000000000000461514115062600240060ustar00rootroot00000000000000//= link_directory ../javascripts .js graphql-ruby-2.5.19/spec/dummy/app/assets/javascripts/000077500000000000000000000000001514115062600227265ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/app/assets/javascripts/application.js000066400000000000000000000061171514115062600255740ustar00rootroot00000000000000// This is a manifest file that'll be compiled into application.js, which will include all the files // listed below. // // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's // vendor/assets/javascripts directory can be referenced here using a relative path. // // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // compiled file. JavaScript code in this file should be added after the last require_* statement. // // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details // about supported directives. // //= require rails-ujs // Action Cable provides the framework to deal with WebSockets in Rails. // You can generate new channels where WebSocket features live using the `rails generate channel` command. // //= require action_cable //= require_self (function() { this.App || (this.App = {}); App.cable = ActionCable.createConsumer(); App.subscribe = function(options) { var query = options.query var variables = options.variables var receivedCallback = options.received // Unique-ish var uuid = Math.round(Date.now() + Math.random() * 100000).toString(16) var subscription = { _subscribed: false, subscription: App.cable.subscriptions.create({ channel: "GraphqlChannel", id: uuid, }, { connected: function() { this.perform("execute", { query: query, variables: variables, }) console.log("Connected", query, variables) }, received: function(payload) { subscription._subscribed = true App.logToBody("ActionCable received: " + JSON.stringify(payload)) if (payload.result) { receivedCallback(payload) } if (!payload.more) { this.unsubscribe() App.logToBody("Remaining ActionCable subscriptions: " + App.cable.subscriptions.subscriptions.length) } } } ), trigger: function(options) { if (!subscription._subscribed) { options.retries ||= 0 options.retries++ if (options.retries > 5) { throw new Error("Retried 5 times, failed to trigger: " + JSON.stringify(options)) } else { App.logToBody("Retrying trigger " + options.retries + " : " + JSON.stringify(options)) setTimeout(function() { subscription.trigger(options) }, 500) } } else { App.logToBody("Triggering " + JSON.stringify(options)) this.subscription.perform("make_trigger", options) } }, unsubscribe: function() { this.subscription.unsubscribe() }, } return subscription } // Add `text` to the HTML body, for debugging App.logToBody = function(text) { var bodyLog = document.getElementById("body-log") var logEntry = document.createElement("p") logEntry.innerText = text bodyLog.appendChild(logEntry) bodyLog.append("\n") } }).call(this); graphql-ruby-2.5.19/spec/dummy/app/channels/000077500000000000000000000000001514115062600206665ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/app/channels/application_cable/000077500000000000000000000000001514115062600243175ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/app/channels/application_cable/channel.rb000066400000000000000000000001551514115062600262550ustar00rootroot00000000000000# frozen_string_literal: true module ApplicationCable class Channel < ActionCable::Channel::Base end end graphql-ruby-2.5.19/spec/dummy/app/channels/application_cable/connection.rb000066400000000000000000000001631514115062600270030ustar00rootroot00000000000000# frozen_string_literal: true module ApplicationCable class Connection < ActionCable::Connection::Base end end graphql-ruby-2.5.19/spec/dummy/app/channels/graphql_channel.rb000066400000000000000000000064431514115062600243500ustar00rootroot00000000000000# frozen_string_literal: true class GraphqlChannel < ActionCable::Channel::Base class QueryType < GraphQL::Schema::Object field :value, Integer, null: false def value 3 end end class PayloadType < GraphQL::Schema::Object field :value, Integer, null: false end class CounterIncremented < GraphQL::Schema::Subscription def self.reset_call_count @@call_count = 0 end reset_call_count field :new_value, Integer, null: false def update if object if object.value == "server-unsubscribe" unsubscribe elsif object.value == "server-unsubscribe-with-message" unsubscribe({ new_value: 9999 }) end end result = { new_value: @@call_count += 1 } puts " -> CounterIncremented#update: #{result}" result end end class SubscriptionType < GraphQL::Schema::Object field :payload, PayloadType, null: false do argument :id, ID end field :counter_incremented, subscription: CounterIncremented end # Wacky behavior around the number 4 # so we can confirm it's used by the UI module CustomSerializer def self.load(value) if value == "4x" ExamplePayload.new(400) else GraphQL::Subscriptions::Serialize.load(value) end end def self.dump(obj) if obj.is_a?(ExamplePayload) && obj.value == 4 "4x" else GraphQL::Subscriptions::Serialize.dump(obj) end end end class GraphQLSchema < GraphQL::Schema query(QueryType) subscription(SubscriptionType) use GraphQL::Subscriptions::ActionCableSubscriptions, serializer: CustomSerializer, broadcast: true, default_broadcastable: true end def subscribed @subscription_ids = [] end def execute(data) query = data["query"] variables = data["variables"] || {} operation_name = data["operationName"] context = { # Make sure the channel is in the context channel: self, } puts "[GraphQLSchema.execute] #{query} || #{variables}" result = GraphQLSchema.execute( query: query, context: context, variables: variables, operation_name: operation_name ) payload = { result: result.to_h, more: result.subscription?, } # Track the subscription here so we can remove it # on unsubscribe. if result.context[:subscription_id] @subscription_ids << result.context[:subscription_id] end puts " -> [transmit(#{result.context[:subscription_id]})] #{payload.inspect}" transmit(payload) end def make_trigger(data) field = data["field"] args = data["arguments"] value = data["value"] value = value && ExamplePayload.new(value) puts "[make_trigger] #{[field, args, value]}" GraphQLSchema.subscriptions.trigger(field, args, value) end def unsubscribed @subscription_ids.each { |sid| puts "[delete_subscription] #{sid}" GraphQLSchema.subscriptions.delete_subscription(sid) } end # This is to make sure that GlobalID is used to load and dump this object class ExamplePayload include GlobalID::Identification def initialize(value) @value = value end def self.find(value) self.new(value) end attr_reader :value alias :id :value end end graphql-ruby-2.5.19/spec/dummy/app/controllers/000077500000000000000000000000001514115062600214415ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/app/controllers/application_controller.rb000066400000000000000000000001771514115062600265410ustar00rootroot00000000000000# frozen_string_literal: true class ApplicationController < ActionController::Base protect_from_forgery with: :exception end graphql-ruby-2.5.19/spec/dummy/app/controllers/pages_controller.rb000066400000000000000000000001411514115062600253240ustar00rootroot00000000000000# frozen_string_literal: true class PagesController < ApplicationController def show end end graphql-ruby-2.5.19/spec/dummy/app/graphql/000077500000000000000000000000001514115062600205315ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/app/graphql/dummy_schema.rb000066400000000000000000000037021514115062600235330ustar00rootroot00000000000000# frozen_string_literal: true begin require "graphql-pro" rescue LoadError => err puts "Skipping GraphQL::Pro: #{err.message}" end class DummySchema < GraphQL::Schema class Query < GraphQL::Schema::Object field :str, String, fallback_value: "hello" field :sleep, Float do argument :seconds, Float end def sleep(seconds:) Kernel.sleep(seconds) seconds end end query(Query) class Subscription < GraphQL::Schema::Object field :message, String do argument :channel, String end end subscription(Subscription) DB_NUMBER = Rails.env.test? ? 1 : 2 use GraphQL::Tracing::DetailedTrace, redis: Redis.new(db: DB_NUMBER) if defined?(GraphQL::Pro) use GraphQL::Pro::OperationStore, redis: Redis.new(db: DB_NUMBER) use GraphQL::Pro::PusherSubscriptions, redis: Redis.new(db: DummySchema::DB_NUMBER), pusher: MockPusher.new class KeyNotRequiredLimiter < GraphQL::Enterprise::RuntimeLimiter def limiter_key(query) query. context[:limiter_key] || "unlimited" end def limit_for(key, query) key == "unlimited" ? nil : super end end use KeyNotRequiredLimiter, redis: Redis.new(db: DummySchema::DB_NUMBER), limit_ms: 100 end def self.detailed_trace?(query) query.context[:profile] end end # To preview rate limiter # puts "Making Rate-limited requests..." # 3.times.map do # pp DummySchema.execute("{ sleep(seconds: 0.02) }", context: { limiter_key: "client-1" }).to_h # end # 3.times.map do # pp DummySchema.execute("{ sleep(seconds: 0.110) }", context: { limiter_key: "client-2" }).to_h # end # puts " ... done" # To preview subscription data in the dashboard: # DummySchema.subscriptions.clear # res1 = DummySchema.execute("subscription { message(channel: \"cats\") }") # res2 = DummySchema.execute("subscription { message(channel: \"dogs\") }") # DummySchema.subscriptions.trigger(:message, { channel: "cats" }, "meow") graphql-ruby-2.5.19/spec/dummy/app/graphql/mock_pusher.rb000066400000000000000000000032411514115062600233750ustar00rootroot00000000000000# frozen_string_literal: true class MockPusher class Channel attr_reader :id, :occupants def initialize(id) @id = id @occupants = 0 @inboxes = [[]] end def trigger(event_name, payload) if event_name != "update" raise "Invariant: GraphQL is only expected to call update, but received #{event_name.inspect}. Fix tests or implementation." end @inboxes.each do |ibx| ibx << payload end nil end def new_inbox ibx = [] @inboxes << ibx ibx end def updates @inboxes[0] end def occupant_left @occupants -= 1 if @occupants < 0 raise "Invariant: less than 0 occupants for #{self.inspect}" end nil end def occupant_entered @occupants += 1 nil end def occupied? @occupants > 0 end end def initialize @channels = Hash.new { |h, k| h[k] = Channel.new(k) } @key = "abcdef" @secret = "12345" @batch_sizes = [] end attr_reader :key, :secret, :batch_sizes # Mock pusher: def channel_info(channel_name, info: "") channel = @channels[channel_name] res = { occupied: channel.occupied? } if info.include?("subscription_count") res[:subscription_count] = channel.occupants end res end def trigger(channel_name, action, payload) @channels[channel_name].trigger(action, payload) end def trigger_batch(triggers) @batch_sizes << triggers.size triggers.each do |trigger| @channels[trigger[:channel]].trigger(trigger[:name], trigger[:data]) end end # Testing: def channel(channel_name) @channels[channel_name] end end graphql-ruby-2.5.19/spec/dummy/app/graphql/not_installed_schema.rb000066400000000000000000000002741514115062600252400ustar00rootroot00000000000000# frozen_string_literal: true class NotInstalledSchema < GraphQL::Schema class Query < GraphQL::Schema::Object field :str, String, fallback_value: "hello" end query(Query) end graphql-ruby-2.5.19/spec/dummy/app/helpers/000077500000000000000000000000001514115062600205355ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/app/helpers/application_helper.rb000066400000000000000000000000731514115062600247240ustar00rootroot00000000000000# frozen_string_literal: true module ApplicationHelper end graphql-ruby-2.5.19/spec/dummy/app/jobs/000077500000000000000000000000001514115062600200305ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/app/jobs/application_job.rb000066400000000000000000000001111514115062600235030ustar00rootroot00000000000000# frozen_string_literal: true class ApplicationJob < ActiveJob::Base end graphql-ruby-2.5.19/spec/dummy/app/views/000077500000000000000000000000001514115062600202305ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/app/views/layouts/000077500000000000000000000000001514115062600217305ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/app/views/layouts/application.html.erb000066400000000000000000000004151514115062600256700ustar00rootroot00000000000000 Dummy <%= csrf_meta_tags %> <%= javascript_include_tag 'application', host: "" # work around config.asset_host which is set to test dashboard %> <%= yield %> graphql-ruby-2.5.19/spec/dummy/app/views/pages/000077500000000000000000000000001514115062600213275ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/app/views/pages/show.html000066400000000000000000000107511514115062600232010ustar00rootroot00000000000000

ActionCable Test Page

ActionCable updates:


Fingerprint test


graphql-ruby-2.5.19/spec/dummy/bin/000077500000000000000000000000001514115062600170635ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/bin/bundle000077500000000000000000000002371514115062600202640ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) load Gem.bin_path('bundler', 'bundle') graphql-ruby-2.5.19/spec/dummy/bin/rails000077500000000000000000000002531514115062600201230ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' require 'rails/commands' graphql-ruby-2.5.19/spec/dummy/bin/rake000077500000000000000000000001701514115062600177310ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require_relative '../config/boot' require 'rake' Rake.application.run graphql-ruby-2.5.19/spec/dummy/bin/setup000077500000000000000000000014411514115062600201510ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require 'pathname' require 'fileutils' include FileUtils # path to your application root. APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end chdir APP_ROOT do # This script is a starting point to setup your application. # Add necessary setup steps to this file. puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system('bundle check') || system!('bundle install') # Install JavaScript dependencies if using Yarn # system('bin/yarn') puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' puts "\n== Restarting application server ==" system! 'bin/rails restart' end graphql-ruby-2.5.19/spec/dummy/bin/update000077500000000000000000000013471514115062600203000ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require 'pathname' require 'fileutils' include FileUtils # path to your application root. APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end chdir APP_ROOT do # This script is a way to update your development environment automatically. # Add necessary update steps to this file. puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system('bundle check') || system!('bundle install') puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' puts "\n== Restarting application server ==" system! 'bin/rails restart' end graphql-ruby-2.5.19/spec/dummy/bin/yarn000077500000000000000000000005361514115062600177660ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true VENDOR_PATH = File.expand_path('..', __dir__) Dir.chdir(VENDOR_PATH) do begin exec "yarnpkg #{ARGV.join(" ")}" rescue Errno::ENOENT $stderr.puts "Yarn executable was not detected in the system." $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" exit 1 end end graphql-ruby-2.5.19/spec/dummy/config.ru000066400000000000000000000002401514115062600201240ustar00rootroot00000000000000# frozen_string_literal: true # This file is used by Rack-based servers to start the application. require_relative 'config/environment' run Rails.application graphql-ruby-2.5.19/spec/dummy/config/000077500000000000000000000000001514115062600175605ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/config/application.rb000066400000000000000000000012171514115062600224110ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'boot' require "rails" # Pick the frameworks you want: require "active_model/railtie" require "active_job/railtie" require "action_controller/railtie" require "action_view/railtie" require "action_cable/engine" require "sprockets/railtie" require "rails/test_unit/railtie" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) module Dummy class Application < Rails::Application # Don't generate system test files. config.generators.system_tests = nil config.asset_host = "http://some.cdn" end end graphql-ruby-2.5.19/spec/dummy/config/boot.rb000066400000000000000000000002751514115062600210540ustar00rootroot00000000000000# frozen_string_literal: true ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) require 'bundler/setup' # Set up gems listed in the Gemfile. require 'bootsnap/setup' graphql-ruby-2.5.19/spec/dummy/config/cable.yml000066400000000000000000000002271514115062600213520ustar00rootroot00000000000000development: adapter: async test: adapter: async production: adapter: redis url: redis://localhost:6379/1 channel_prefix: dummy_production graphql-ruby-2.5.19/spec/dummy/config/database.yml000066400000000000000000000017401514115062600220510ustar00rootroot00000000000000# SQLite. Versions 3.8.0 and up are supported. # gem install sqlite3 # # Ensure the SQLite 3 gem is defined in your Gemfile # gem "sqlite3" # default: &default adapter: sqlite3 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> timeout: 5000 development: <<: *default database: storage/development.sqlite3 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: <<: *default database: storage/test.sqlite3 # SQLite3 write its data on the local filesystem, as such it requires # persistent disks. If you are deploying to a managed service, you should # make sure it provides disk persistence, as many don't. # # Similarly, if you deploy your application as a Docker container, you must # ensure the database is located in a persisted volume. production: <<: *default # database: path/to/persistent/storage/production.sqlite3 graphql-ruby-2.5.19/spec/dummy/config/environment.rb000066400000000000000000000002361514115062600224520ustar00rootroot00000000000000# frozen_string_literal: true # Load the Rails application. require_relative 'application' # Initialize the Rails application. Rails.application.initialize! graphql-ruby-2.5.19/spec/dummy/config/environments/000077500000000000000000000000001514115062600223075ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/config/environments/development.rb000066400000000000000000000025541514115062600251640ustar00rootroot00000000000000# frozen_string_literal: true Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Do not eager load code on boot. config.eager_load = false # Show full error reports. config.consider_all_requests_local = true # Enable/disable caching. By default caching is disabled. if Rails.root.join('tmp/caching-dev.txt').exist? config.action_controller.perform_caching = true config.cache_store = :memory_store config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}" } else config.action_controller.perform_caching = false config.cache_store = :null_store end # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raises error for missing translations # config.action_view.raise_on_missing_translations = true # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. config.file_watcher = ActiveSupport::EventedFileUpdateChecker end graphql-ruby-2.5.19/spec/dummy/config/environments/production.rb000066400000000000000000000060461514115062600250300ustar00rootroot00000000000000# frozen_string_literal: true Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. config.cache_classes = true # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both threaded web servers # and those relying on copy on write to perform better. # Rake tasks automatically ignore this option for performance. config.eager_load = true # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true # Attempt to read encrypted secrets from `config/secrets.yml.enc`. # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or # `config/secrets.yml.key`. config.read_encrypted_secrets = true # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = 'http://assets.example.com' # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX # Mount Action Cable outside main process or domain # config.action_cable.mount_path = nil # config.action_cable.url = 'wss://example.com/cable' # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true # Use the lowest log level to ensure availability of diagnostic information # when problems arise. config.log_level = :debug # Prepend all log lines with the following tags. config.log_tags = [ :request_id ] # Use a different cache store in production. # config.cache_store = :mem_cache_store # Use a real queuing backend for Active Job (and separate queues per environment) # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "dummy_#{Rails.env}" # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new # Use a different logger for distributed setups. # require 'syslog/logger' # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') if ENV["RAILS_LOG_TO_STDOUT"].present? logger = ActiveSupport::Logger.new(STDOUT) logger.formatter = config.log_formatter config.logger = ActiveSupport::TaggedLogging.new(logger) end end graphql-ruby-2.5.19/spec/dummy/config/environments/test.rb000066400000000000000000000027771514115062600236300ustar00rootroot00000000000000# frozen_string_literal: true Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! config.cache_classes = true # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that # preloads Rails for running tests, you may have to set it to true. config.eager_load = false # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}" } # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr # Raises error for missing translations # config.action_view.raise_on_missing_translations = true end graphql-ruby-2.5.19/spec/dummy/config/initializers/000077500000000000000000000000001514115062600222665ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/config/initializers/application_controller_renderer.rb000066400000000000000000000003661514115062600312540ustar00rootroot00000000000000# frozen_string_literal: true # Be sure to restart your server when you modify this file. # ActiveSupport::Reloader.to_prepare do # ApplicationController.renderer.defaults.merge!( # http_host: 'example.org', # https: false # ) # end graphql-ruby-2.5.19/spec/dummy/config/initializers/backtrace_silencers.rb000066400000000000000000000006621514115062600266050ustar00rootroot00000000000000# frozen_string_literal: true # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. # Rails.backtrace_cleaner.remove_silencers! graphql-ruby-2.5.19/spec/dummy/config/initializers/cookies_serializer.rb000066400000000000000000000004221514115062600264760ustar00rootroot00000000000000# frozen_string_literal: true # Be sure to restart your server when you modify this file. # Specify a serializer for the signed and encrypted cookie jars. # Valid options are :json, :marshal, and :hybrid. Rails.application.config.action_dispatch.cookies_serializer = :json graphql-ruby-2.5.19/spec/dummy/config/initializers/filter_parameter_logging.rb000066400000000000000000000003401514115062600276430ustar00rootroot00000000000000# frozen_string_literal: true # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. Rails.application.config.filter_parameters += [:password] graphql-ruby-2.5.19/spec/dummy/config/initializers/graphql_dashboard.rb000066400000000000000000000002221514115062600262540ustar00rootroot00000000000000# frozen_string_literal: true ActiveSupport.on_load(:graphql_dashboard_application_controller) do def self.hook_was_called? true end end graphql-ruby-2.5.19/spec/dummy/config/initializers/inflections.rb000066400000000000000000000012451514115062600251320ustar00rootroot00000000000000# frozen_string_literal: true # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym 'RESTful' # end graphql-ruby-2.5.19/spec/dummy/config/initializers/mime_types.rb000066400000000000000000000002721514115062600247670ustar00rootroot00000000000000# frozen_string_literal: true # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf graphql-ruby-2.5.19/spec/dummy/config/initializers/wrap_parameters.rb000066400000000000000000000005611514115062600260110ustar00rootroot00000000000000# frozen_string_literal: true # Be sure to restart your server when you modify this file. # This file contains settings for ActionController::ParamsWrapper which # is enabled by default. # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do wrap_parameters format: [:json] end graphql-ruby-2.5.19/spec/dummy/config/locales/000077500000000000000000000000001514115062600212025ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/config/locales/en.yml000066400000000000000000000015211514115062600223260ustar00rootroot00000000000000# Files in the config/locales directory are used for internationalization # and are automatically loaded by Rails. If you want to use locales other # than English, add the necessary files in this directory. # # To use the locales, use `I18n.t`: # # I18n.t 'hello' # # In views, this is aliased to just `t`: # # <%= t('hello') %> # # To use a different locale, set it with `I18n.locale`: # # I18n.locale = :es # # This would use the information in config/locales/es.yml. # # The following keys must be escaped otherwise they will not be retrieved by # the default I18n backend: # # true, false, on, off, yes, no # # Instead, surround them with single quotes. # # en: # 'true': 'foo' # # To learn more, please read the Rails Internationalization guide # available at https://guides.rubyonrails.org/i18n.html. en: hello: "Hello world" graphql-ruby-2.5.19/spec/dummy/config/puma.rb000066400000000000000000000044401514115062600210510ustar00rootroot00000000000000# frozen_string_literal: true # Puma can serve each request in a thread from an internal thread pool. # The `threads` method setting takes two numbers: a minimum and maximum. # Any libraries that use thread pools should be configured to match # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. # threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } threads threads_count, threads_count # Specifies the `port` that Puma will listen on to receive requests; default is 3000. # port ENV.fetch("PORT") { 3000 } # Specifies the `environment` that Puma will run in. # environment ENV.fetch("RAILS_ENV") { "development" } # Specifies the number of `workers` to boot in clustered mode. # Workers are forked webserver processes. If using threads and workers together # the concurrency of the application would be max `threads` * `workers`. # Workers do not work on JRuby or Windows (both of which do not support # processes). # # workers ENV.fetch("WEB_CONCURRENCY") { 2 } # Use the `preload_app!` method when specifying a `workers` number. # This directive tells Puma to first boot the application and load code # before forking the application. This takes advantage of Copy On Write # process behavior so workers use less memory. If you use this option # you need to make sure to reconnect any threads in the `on_worker_boot` # block. # # preload_app! # If you are preloading your application and using Active Record, it's # recommended that you close any connections to the database before workers # are forked to prevent connection leakage. # # before_fork do # ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord) # end # The code in the `on_worker_boot` will be called if you are using # clustered mode by specifying a number of `workers`. After each worker # process is booted, this block will be run. If you are using the `preload_app!` # option, you will want to use this block to reconnect to any threads # or connections that may have been created at application boot, as Ruby # cannot share connections between processes. # # on_worker_boot do # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) # end # # Allow puma to be restarted by `rails restart` command. plugin :tmp_restart graphql-ruby-2.5.19/spec/dummy/config/routes.rb000066400000000000000000000002321514115062600214230ustar00rootroot00000000000000# frozen_string_literal: true Rails.application.routes.draw do root to: "pages#show" mount GraphQL::Dashboard, at: "/dash", schema: "DummySchema" end graphql-ruby-2.5.19/spec/dummy/config/secrets.yml000066400000000000000000000023751514115062600217620ustar00rootroot00000000000000# Be sure to restart your server when you modify this file. # Your secret key is used for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. # You can use `rails secret` to generate a secure secret key. # Make sure the secrets in this file are kept private # if you're sharing your code publicly. # Shared secrets are available across all environments. # shared: # api_key: a1B2c3D4e5F6 # Environmental secrets are only available for that specific environment. development: secret_key_base: 32be83294ab8a3b21fce26b6e48db6c5d84cfa3c89e686dcb39369806e8daba53215caed302b89ce4a54b5c67ff2fa6be797f0f5c937a71fbb9f396ef108e867 test: secret_key_base: 88c4ea138f1ad39c9eaa3196361a539e8aba1fcbffa3c2383045799cefe150fadaa7256ea680a912d27a878fdff55debd3f920388de68aa4f81828b0bd2ac072 # Do not keep production secrets in the unencrypted secrets file. # Instead, either read values from the environment. # Or, use `bin/rails secrets:setup` to configure encrypted secrets # and move the `production:` environment over there. production: secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> graphql-ruby-2.5.19/spec/dummy/package.json000066400000000000000000000000771514115062600206050ustar00rootroot00000000000000{ "name": "dummy", "private": true, "dependencies": {} } graphql-ruby-2.5.19/spec/dummy/public/000077500000000000000000000000001514115062600175715ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/public/404.html000066400000000000000000000032721514115062600207720ustar00rootroot00000000000000 The page you were looking for doesn't exist (404)

The page you were looking for doesn't exist.

You may have mistyped the address or the page may have moved.

If you are the application owner check the logs for more information.

graphql-ruby-2.5.19/spec/dummy/public/422.html000066400000000000000000000032511514115062600207670ustar00rootroot00000000000000 The change you wanted was rejected (422)

The change you wanted was rejected.

Maybe you tried to change something you didn't have access to.

If you are the application owner check the logs for more information.

graphql-ruby-2.5.19/spec/dummy/public/500.html000066400000000000000000000031431514115062600207640ustar00rootroot00000000000000 We're sorry, but something went wrong (500)

We're sorry, but something went wrong.

If you are the application owner check the logs for more information.

graphql-ruby-2.5.19/spec/dummy/public/apple-touch-icon-precomposed.png000066400000000000000000000000001514115062600257520ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/public/apple-touch-icon.png000066400000000000000000000000001514115062600234340ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/public/favicon.ico000066400000000000000000000000001514115062600217000ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/public/robots.txt000066400000000000000000000001421514115062600216370ustar00rootroot00000000000000# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file graphql-ruby-2.5.19/spec/dummy/test/000077500000000000000000000000001514115062600172725ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/test/application_system_test_case.rb000066400000000000000000000003041514115062600255550ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" class ApplicationSystemTestCase < ActionDispatch::SystemTestCase driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400] end graphql-ruby-2.5.19/spec/dummy/test/channels/000077500000000000000000000000001514115062600210655ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/test/channels/graphql_channel_test.rb000066400000000000000000000113231514115062600255770ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" class GraphqlChannelTest < ActionCable::Channel::TestCase module RealChannelStub def confirmed? subscription_confirmation_sent? end def real_streams streams end end def assert_has_real_stream(stream_name) assert subscription.real_streams.key?(stream_name), "Expected Stream #{stream_name.inspect} to be present in #{subscription.real_streams.keys}" end def setup @prev_server = ActionCable.server @server = TestServer.new(subscription_adapter: ActionCable::SubscriptionAdapter::Async) @server.config.allowed_request_origins = [ 'http://rubyonrails.com' ] ActionCable.instance_variable_set(:@server, @server) end def teardown ActionCable.instance_variable_set(:@server, @prev_server) end def wait_for_async wait_for_executor Concurrent.global_io_executor end def run_in_eventmachine yield wait_for_async end def wait_for_executor(executor) # do not wait forever, wait 2s timeout = 2 until executor.completed_task_count == executor.scheduled_task_count sleep 0.1 timeout -= 0.1 raise "Executor could not complete all tasks in 2 seconds" unless timeout > 0 end end class Connection < ActionCable::Connection::Base attr_reader :websocket def send_async(method, *args) send method, *args end public :handle_close end module InterceptTransmit def transmit(msg) intercepted_messages << JSON.parse(msg) super end def intercepted_messages @intercepted_messages ||= [] end end test "it subscribes and unsubscribes" do run_in_eventmachine do env = Rack::MockRequest.env_for "/test", "HTTP_HOST" => "localhost", "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket", "HTTP_ORIGIN" => "http://rubyonrails.com" @connection = Connection.new(ActionCable.server, env).tap do |connection| connection.process assert_predicate connection.websocket, :possible? wait_for_async assert_predicate connection.websocket, :alive? connection.websocket.singleton_class.prepend(InterceptTransmit) end @connection.subscriptions.add({"identifier" => "{\"channel\": \"GraphqlChannel\"}"}) @subscription = @connection.subscriptions.instance_variable_get(:@subscriptions).values.first @subscription.singleton_class.prepend(RealChannelStub) assert subscription.confirmed? subscription.execute({ "query" => "subscription { payload(id: \"abc\") { value } }" }) wait_for_async sub_id = subscription.instance_variable_get(:@subscription_ids).first subscription_stream = "graphql-subscription:#{sub_id}" assert_has_real_stream subscription_stream topic_stream = "graphql-event::payload:id:abc" assert_has_real_stream topic_stream subscription.make_trigger({ "field" => "payload", "arguments" => { "id" => "abc"}, "value" => 19 }) wait_for_async @connection.handle_close wait_for_async expected_data = [ {"identifier"=>"{\"channel\": \"GraphqlChannel\"}", "type"=>"confirm_subscription"}, {"identifier"=>"{\"channel\": \"GraphqlChannel\"}", "message"=>{"result"=>{"data"=>{}}, "more"=>true}}, {"identifier"=>"{\"channel\": \"GraphqlChannel\"}", "message"=>{"result"=>{"data"=>{"payload"=>{"value"=>19}}}, "more"=>true}}, {"identifier"=>"{\"channel\": \"GraphqlChannel\"}", "message"=>{"more"=>false}}, ] assert_equal expected_data, @connection.websocket.intercepted_messages end end class TestServer include ActionCable::Server::Connections include ActionCable::Server::Broadcasting attr_reader :logger, :config, :mutex class FakeConfiguration < ActionCable::Server::Configuration attr_accessor :subscription_adapter, :log_tags, :filter_parameters def initialize(subscription_adapter:) @log_tags = [] @filter_parameters = [] @subscription_adapter = subscription_adapter end def pubsub_adapter @subscription_adapter end end def initialize(subscription_adapter: SuccessAdapter) @logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new) @config = FakeConfiguration.new(subscription_adapter: subscription_adapter) @mutex = Monitor.new end def pubsub @pubsub ||= @config.subscription_adapter.new(self) end def event_loop @event_loop ||= ActionCable::Connection::StreamEventLoop.new.tap do |loop| loop.instance_variable_set(:@executor, Concurrent.global_io_executor) end end def worker_pool @worker_pool ||= ActionCable::Server::Worker.new(max_size: 5) end end end graphql-ruby-2.5.19/spec/dummy/test/controllers/000077500000000000000000000000001514115062600216405ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/test/controllers/dashboard/000077500000000000000000000000001514115062600235675ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/test/controllers/dashboard/application_controller_test.rb000066400000000000000000000003751514115062600317260ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" class DashboardApplicationControllerTest < ActionDispatch::IntegrationTest def test_it_calls_on_load_hook assert_equal true, GraphQL::Dashboard::ApplicationController.hook_was_called? end end graphql-ruby-2.5.19/spec/dummy/test/controllers/dashboard/detailed_traces/000077500000000000000000000000001514115062600267035ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/test/controllers/dashboard/detailed_traces/traces_controller_test.rb000066400000000000000000000057751514115062600340310ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" class DashboardTracesControllerTest < ActionDispatch::IntegrationTest def teardown DummySchema.detailed_trace.delete_all_traces end def test_it_renders_not_installed get graphql_dashboard.detailed_traces_traces_path, params: { schema: "NotInstalledSchema" } assert_includes response.body, CGI::escapeHTML("Detailed traces aren't installed yet") assert_includes response.body, "NotInstalledSchema" end def test_it_renders_blank_state get graphql_dashboard.detailed_traces_traces_path assert_includes response.body, "No traces saved yet." assert_includes response.body, "DummySchema" end def test_it_renders_trace_listing_with_pagination 20.times do |n| sleep 0.05 DummySchema.execute("query Query#{n} { str }", context: { profile: true }) end assert_equal 20, DummySchema.detailed_trace.traces.size get graphql_dashboard.detailed_traces_traces_path, params: { last: 10 } assert_includes response.body, "Query19" assert_includes response.body, "Query10" refute_includes response.body, "Query9" last_trace = DummySchema.detailed_trace.traces[9] last_ts = last_trace.begin_ms assert_includes response.body, "#{Time.at(last_ts / 1000.0).strftime("%Y-%m-%d %H:%M:%S.%L")}" assert_includes response.body, "Previous >" get graphql_dashboard.detailed_traces_traces_path, params: { last: 10, before: last_ts } assert_includes response.body, "Query9" assert_includes response.body, "Query0" refute_includes response.body, "Query10" very_last_trace = DummySchema.detailed_trace.traces.last very_last_ts = very_last_trace.begin_ms very_last_td = "#{Time.at(very_last_ts / 1000.0).strftime("%Y-%m-%d %H:%M:%S.%L")}" assert_includes response.body, very_last_td very_last_previous_link = "Previous >" assert_includes response.body, very_last_previous_link # Go beyond last trace: get graphql_dashboard.detailed_traces_traces_path, params: { last: 11, before: last_ts } assert_includes response.body, very_last_td refute_includes response.body, very_last_previous_link end def test_it_deletes_one_trace DummySchema.execute("{ str }", context: { profile: true }) assert_equal 1, DummySchema.detailed_trace.traces.size id = DummySchema.detailed_trace.traces.first.id delete graphql_dashboard.detailed_traces_trace_path(id) assert_equal 0, DummySchema.detailed_trace.traces.size end def test_it_deletes_all_traces DummySchema.execute("{ str }", context: { profile: true }) assert_equal 1, DummySchema.detailed_trace.traces.size delete graphql_dashboard.delete_all_detailed_traces_traces_path assert_equal 0, DummySchema.detailed_trace.traces.size end end graphql-ruby-2.5.19/spec/dummy/test/controllers/dashboard/landings_controller_test.rb000066400000000000000000000015051514115062600312160ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" class DashboardLandingsControllerTest < ActionDispatch::IntegrationTest def test_it_shows_a_landing_page_with_local_static_asset_links get graphql_dashboard.root_path assert_includes response.body, "Welcome to the GraphQL-Ruby Dashboard" assert_includes response.body, '', "it doesn't use config.asset_host" end def test_it_shows_version_and_schema_info get graphql_dashboard.root_path assert_includes response.body, "GraphQL-Ruby v#{GraphQL::VERSION}" assert_includes response.body, "DummySchema" get graphql_dashboard.root_path, params: { schema: "NotInstalledSchema" } assert_includes response.body, "NotInstalledSchema" end end graphql-ruby-2.5.19/spec/dummy/test/controllers/dashboard/limiters/000077500000000000000000000000001514115062600254175ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/test/controllers/dashboard/limiters/limiters_controller_test.rb000066400000000000000000000027531514115062600331050ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" if defined?(GraphQL::Pro) class DashboardLimitersLimitersControllerTest < ActionDispatch::IntegrationTest def test_it_checks_installed get graphql_dashboard.limiters_limiter_path("runtime", { schema: "GraphQL::Schema" }) assert_includes response.body, CGI::escapeHTML("Rate limiters aren't installed on this schema yet.") refute_includes response.headers["Content-Security-Policy"], "nonce-" end def test_it_shows_limiters Redis.new(db: DummySchema::DB_NUMBER).flushdb 3.times do DummySchema.execute("{ sleep(seconds: 0.02) }", context: { limiter_key: "client-1" }).to_h end 4.times do DummySchema.execute("{ sleep(seconds: 0.110) }", context: { limiter_key: "client-2" }).to_h end get graphql_dashboard.limiters_limiter_path("runtime") assert_includes response.body, "4" assert_includes response.body, "3" assert_includes response.body, "Disable Soft Limiting" assert_includes response.headers["Content-Security-Policy"], "nonce-" patch graphql_dashboard.limiters_limiter_path("runtime") get graphql_dashboard.limiters_limiter_path("runtime") assert_includes response.body, "Enable Soft Limiting" get graphql_dashboard.limiters_limiter_path("active_operations") assert_includes response.body, "It looks like this limiter isn't installed yet." end end end graphql-ruby-2.5.19/spec/dummy/test/controllers/dashboard/operation_store/000077500000000000000000000000001514115062600270035ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/test/controllers/dashboard/operation_store/clients_controller_test.rb000066400000000000000000000050601514115062600342740ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" if defined?(GraphQL::Pro) class DashboardOperationStoreClientsControllerTest < ActionDispatch::IntegrationTest def test_it_manages_clients assert_equal 0, DummySchema.operation_store.all_clients(page: 1, per_page: 1).total_count get graphql_dashboard.operation_store_clients_path assert_includes response.body, "0 Clients" assert_includes response.body, "To get started, create" get graphql_dashboard.new_operation_store_client_path assert_includes response.body, "New Client" post graphql_dashboard.operation_store_clients_path, params: { client: { name: "client-1", secret: "abcdefedcba" } } get graphql_dashboard.operation_store_clients_path assert_includes response.body, "1 Client" get graphql_dashboard.edit_operation_store_client_path(name: "client-1") assert_includes response.body, "abcdefedcba" patch graphql_dashboard.operation_store_client_path(name: "client-1"), params: { client: { secret: "123456789" } } get graphql_dashboard.edit_operation_store_client_path(name: "client-1") assert_includes response.body, "123456789" delete graphql_dashboard.operation_store_client_path(name: "client-1") assert_equal 0, DummySchema.operation_store.all_clients(page: 1, per_page: 1).total_count ensure DummySchema.operation_store.delete_client("client-1") end def test_it_paginates 5.times do |i| DummySchema.operation_store.upsert_client("client-#{i}", "abcdef") end get graphql_dashboard.operation_store_clients_path(per_page: 2) assert_includes response.body, "5 Clients" assert_includes response.body, "?page=2&per_page=2" assert_includes response.body, "disabled>« prev" get graphql_dashboard.operation_store_clients_path(per_page: 2, page: 2) assert_includes response.body, "?page=1&per_page=2" assert_includes response.body, "?page=3&per_page=2" get graphql_dashboard.operation_store_clients_path(per_page: 2, page: 3) assert_includes response.body, "disabled>next »" assert_includes response.body, "?page=2&per_page=2" ensure 5.times do |i| DummySchema.operation_store.delete_client("client-#{i}") end end def test_it_checks_installed get graphql_dashboard.new_operation_store_client_path, params: { schema: GraphQL::Schema } assert_includes response.body, "isn't installed for this schema yet" end end end index_entries_controller_test.rb000066400000000000000000000026571514115062600354250ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/test/controllers/dashboard/operation_store# frozen_string_literal: true require "test_helper" if defined?(GraphQL::Pro) class DashboardOperationStoreIndexEntriesControllerTest < ActionDispatch::IntegrationTest def test_it_shows_entries DummySchema.operation_store.upsert_client("client-1", "abcdef") DummySchema.operation_store.add(body: "query GetTypename { __type(name: \"Query\") { name @skip(if: true) } }", operation_alias: "GetTypename", client_name: "client-1") get graphql_dashboard.operation_store_index_entries_path assert_includes response.body, "Query.__type.name" assert_includes response.body, "7 entries" get graphql_dashboard.operation_store_index_entries_path(q: "Query") assert_includes response.body, "3 results" assert_includes response.body, ">Query" assert_includes response.body, ">Query.__type" assert_includes response.body, ">Query.__type.name" get graphql_dashboard.operation_store_index_entries_path(q: "Query", per_page: 1, page: 2) assert_includes response.body, "3 results" refute_includes response.body, ">Query" assert_includes response.body, ">Query.__type" refute_includes response.body, ">Query.__type.name" get graphql_dashboard.operation_store_index_entry_path(name: "Query.__type.name") assert_includes response.body, "GetTypename" ensure DummySchema.operation_store.delete_client("client-1") end end end operations_controller_test.rb000066400000000000000000000071331514115062600347420ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/test/controllers/dashboard/operation_store# frozen_string_literal: true require "test_helper" if defined?(GraphQL::Pro) class DashboardOperationStoreOperationsControllerTest < ActionDispatch::IntegrationTest def teardown DummySchema.operation_store.delete_client("client-1") DummySchema.operation_store.delete_client("client-2") super end def test_it_lists_shows_and_archives_operations get graphql_dashboard.operation_store_operations_path assert_includes response.body, "Add your first stored operations with" get graphql_dashboard.operation_store_operations_path(client_name: "client-5000") assert_includes response.body, "Add your first stored operations with" get graphql_dashboard.archived_operation_store_operations_path assert_includes response.body, "Archived operations will appear here." get graphql_dashboard.archived_operation_store_client_operations_path(client_name: "client-5000") assert_includes response.body, "Archived operations will appear here." os = DummySchema.operation_store os.upsert_client("client-1", "abcdef") os.add(body: "query GetTypename { __typename }", operation_alias: "GetTypename", client_name: "client-1") os.add(body: "query GetAliasedTypename { t: __typename }", operation_alias: "get-aliased-typename", client_name: "client-1") os.upsert_client("client-2", "abcdef") os.add(body: "query GetTypename { __typename }", operation_alias: "GetTypename2", client_name: "client-2") get graphql_dashboard.operation_store_operations_path assert_includes response.body, "2 Active" assert_includes response.body, "GetTypename" assert_includes response.body, "GetAliasedTypename" get graphql_dashboard.operation_store_operations_path(sort_by: "name", order_dir: "asc", per_page: 1) refute_includes response.body, "GetTypename" assert_includes response.body, "GetAliasedTypename" get graphql_dashboard.operation_store_operations_path(sort_by: "name", order_dir: "desc", per_page: 1) assert_includes response.body, "GetTypename" refute_includes response.body, "GetAliasedTypename" get graphql_dashboard.operation_store_operations_path(client_name: "client-2") assert_includes response.body, "1 Active" assert_includes response.body, "GetTypename" refute_includes response.body, "GetAliasedTypename" get graphql_dashboard.operation_store_operation_path(digest: "4cd12cc333c91f78e8f781933ecc783d") assert_includes response.body, "GetAliasedTypename" assert_includes response.body, "client-1" assert_includes response.body, "Query.__typename" post graphql_dashboard.archive_operation_store_client_operations_path(client_name: "client-1", operation_aliases: ["get-aliased-typename"]) post graphql_dashboard.archive_operation_store_operations_path(digests: ["b161214b11847649e7f36cc50e1257a1"]) get graphql_dashboard.operation_store_operations_path assert_includes response.body, "0 Active" assert_includes response.body, "2 Archived" get graphql_dashboard.archived_operation_store_operations_path assert_includes response.body, "2 Archived" assert_includes response.body, "0 Active" get graphql_dashboard.operation_store_operations_path(client_name: "client-2") assert_includes response.body, "0 Active" assert_includes response.body, "1 Archived" end def test_it_checks_installed get graphql_dashboard.new_operation_store_client_path, params: { schema: GraphQL::Schema } assert_includes response.body, "isn't installed for this schema yet" end end end graphql-ruby-2.5.19/spec/dummy/test/controllers/dashboard/statics_controller_test.rb000066400000000000000000000021061514115062600310670ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" class DashboardStaticsControllerTest < ActionDispatch::IntegrationTest def test_it_serves_assets get graphql_dashboard.static_path("dashboard.css") assert_includes response.body, "#header-icon {" assert_equal response.headers["Cache-Control"], "max-age=31556952, public" end def test_it_doesnt_trigger_csrf_failure original_forgery_protection = ActionController::Base.allow_forgery_protection ActionController::Base.allow_forgery_protection = true get graphql_dashboard.static_path("dashboard.js") assert_equal 200, response.status ensure ActionController::Base.allow_forgery_protection = original_forgery_protection end def test_it_responds_404_for_others get graphql_dashboard.static_path("other.rb") assert_equal 404, response.status assert_raises ActionController::UrlGenerationError do graphql_dashboard.static_path("invalid~char.js") end get graphql_dashboard.static_path("invalid-char.js").sub("-char", "~char") assert_equal 404, response.status end end graphql-ruby-2.5.19/spec/dummy/test/controllers/dashboard/subscriptions/000077500000000000000000000000001514115062600264765ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/test/controllers/dashboard/subscriptions/topics_controller_test.rb000066400000000000000000000045321514115062600336320ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" require "ostruct" # TODO use a real class in RedisBackend if defined?(GraphQL::Pro) class DashboardSubscriptionsTopicsControllerTest < ActionDispatch::IntegrationTest def test_it_checks_installed get graphql_dashboard.subscriptions_topics_path, params: { schema: GraphQL::Schema } assert_includes response.body, "GraphQL-Pro Subscriptions aren't installed on this schema yet." end def test_it_renders_empty_state_and_not_found_states get graphql_dashboard.subscriptions_topics_path assert_includes response.body, "There aren't any subscriptions right now." get graphql_dashboard.subscriptions_topic_path(":something:") assert_includes response.body, ":something:" assert_includes response.body, "Last triggered: none" assert_includes response.body, "0 Subscriptions" get graphql_dashboard.subscriptions_subscription_path("abcd-efg") assert_includes response.body, "abcd-efg" assert_includes response.body, "This subscription was not found or is no longer active." end def test_it_lists_topics_and_shows_detail DummySchema.subscriptions.clear _res1 = DummySchema.execute("subscription { message(channel: \"cats\") }") res2 = DummySchema.execute("subscription { message(channel: \"dogs\") }") DummySchema.subscriptions.trigger(:message, { channel: "dogs"}, "Woof!") get graphql_dashboard.subscriptions_topics_path assert_includes response.body, ":message:channel:cats" assert_includes response.body, ":message:channel:dogs" assert_includes response.body, Time.now.strftime("%Y-%m-%d %H:%M:%S") get graphql_dashboard.subscriptions_topic_path(":message:channel:dogs") assert_includes response.body, res2.context[:subscription_id] assert_includes response.body, Time.now.strftime("%Y-%m-%d %H:%M:%S") get graphql_dashboard.subscriptions_subscription_path(res2.context[:subscription_id]) assert_includes response.body, res2.context[:subscription_id] assert_includes response.body, CGI::escapeHTML('subscription { message(channel: "dogs") }') post graphql_dashboard.subscriptions_clear_all_path get graphql_dashboard.subscriptions_topics_path refute_includes response.body, ":message:" ensure DummySchema.subscriptions.clear end end end graphql-ruby-2.5.19/spec/dummy/test/system/000077500000000000000000000000001514115062600206165ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/dummy/test/system/action_cable_subscription_test.rb000066400000000000000000000155271514115062600274230ustar00rootroot00000000000000# frozen_string_literal: true require "application_system_test_case" class ActionCableSubscriptionsTest < ApplicationSystemTestCase setup do ActionCable.server.config.logger = Logger.new(STDOUT) end # This test covers a lot of ground! test "it handles subscriptions" do # Load the page and let the subscriptions happen visit "/" # make sure they connect successfully assert_selector "#updates-1-connected" assert_selector "#updates-2-connected" # Trigger a few updates, make sure we get a client update: click_on("Trigger 1") click_on("Trigger 1") click_on("Trigger 1") assert_selector "#updates-1-3", text: "3" # Make sure there aren't any unexpected elements: refute_selector "#updates-1-4" refute_selector "#updates-2-1" # Now, trigger updates to a different stream # and make sure the previous stream is not affected click_on("Trigger 2") click_on("Trigger 2") assert_selector "#updates-2-1", text: "1" assert_selector "#updates-2-2", text: "2" refute_selector "#updates-2-3" refute_selector "#updates-1-4" # Now unsubscribe one, it should not receive updates but the other should click_on("Unsubscribe 1") click_on("Trigger 1") # This should not have changed refute_selector "#updates-1-4" click_on("Trigger 2") assert_selector "#updates-2-3", text: "3" refute_selector "#updates-1-4" # wacky behavior to make sure the custom serializer is used: click_on("Trigger 2") assert_selector "#updates-2-400", text: "400" end # Wrap an `assert_selector` call in a debugging check def debug_assert_selector(selector) if !page.has_css?(selector) puts "[debug_assert_selector(#{selector.inspect})] Failed to find #{selector.inspect} in:" puts page.html else puts "[debug_assert_selector(#{selector.inspect})] Found #{selector.inspect}" end assert_selector(selector) end # It seems like ActionCable's order of evaluation here is non-deterministic, # so detect which order to make the assertions. # (They still both have to pass, but we don't know exactly what order the evaluations went in.) def detect_update_values(possibility_1, possibility_2) if page.has_css?("#fingerprint-updates-1-update-1-value-#{possibility_1}") [possibility_1, possibility_2] else [possibility_2, possibility_1] end end test "it only re-runs queries once for subscriptions with matching fingerprints" do GraphqlChannel::CounterIncremented.reset_call_count visit "/" using_wait_time 10 do sleep 1 # Make 3 subscriptions to the same payload click_on("Subscribe with fingerprint 1") debug_assert_selector "#fingerprint-updates-1-connected-1" click_on("Subscribe with fingerprint 1") debug_assert_selector "#fingerprint-updates-1-connected-2" click_on("Subscribe with fingerprint 1") debug_assert_selector "#fingerprint-updates-1-connected-3" # And two to the next payload click_on("Subscribe with fingerprint 2") debug_assert_selector "#fingerprint-updates-2-connected-1" click_on("Subscribe with fingerprint 2") debug_assert_selector "#fingerprint-updates-2-connected-2" # Now trigger. We expect a total of two updates: # - One is built & delivered to the first three subscribers # - Another is built & delivered to the next two click_on("Trigger with fingerprint 2") # The order here is random, I think depending on ActionCable's internal storage: fingerprint_1_value, fingerprint_2_value = detect_update_values(1, 2) # These all share the first value: debug_assert_selector "#fingerprint-updates-1-update-1-value-#{fingerprint_1_value}" debug_assert_selector "#fingerprint-updates-1-update-2-value-#{fingerprint_1_value}" debug_assert_selector "#fingerprint-updates-1-update-3-value-#{fingerprint_1_value}" # and these share the second value: debug_assert_selector "#fingerprint-updates-2-update-1-value-#{fingerprint_2_value}" debug_assert_selector "#fingerprint-updates-2-update-2-value-#{fingerprint_2_value}" click_on("Unsubscribe with fingerprint 2") click_on("Trigger with fingerprint 1") fingerprint_1_value_2, fingerprint_2_value_2 = detect_update_values(3, 4) # These get an update debug_assert_selector "#fingerprint-updates-1-update-1-value-#{fingerprint_1_value_2}" debug_assert_selector "#fingerprint-updates-1-update-2-value-#{fingerprint_1_value_2}" debug_assert_selector "#fingerprint-updates-1-update-3-value-#{fingerprint_1_value_2}" # But these are unsubscribed: refute_selector "#fingerprint-updates-2-update-1-value-#{fingerprint_2_value_2}" refute_selector "#fingerprint-updates-2-update-2-value-#{fingerprint_2_value_2}" click_on("Unsubscribe with fingerprint 1") # Make a new subscription and make sure it's updated: click_on("Subscribe with fingerprint 2") click_on("Trigger with fingerprint 2") debug_assert_selector "#fingerprint-updates-2-update-1-value-#{fingerprint_2_value_2}" # But this one was unsubscribed: refute_selector "#fingerprint-updates-1-update-1-value-#{fingerprint_1_value_2 + 1}" refute_selector "#fingerprint-updates-1-update-1-value-#{fingerprint_1_value_2 + 2}" end end test "it unsubscribes from the server" do GraphqlChannel::CounterIncremented.reset_call_count visit "/" using_wait_time 10 do sleep 1 # Establish the connection click_on("Subscribe with fingerprint 1") debug_assert_selector "#fingerprint-updates-1-connected-1" # Trigger once click_on("Trigger with fingerprint 1") debug_assert_selector "#fingerprint-updates-1-update-1-value-1" # Server unsubscribe click_on("Server-side unsubscribe with fingerprint 1") # Subsequent updates should fail click_on("Trigger with fingerprint 1") refute_selector "#fingerprint-updates-1-update-2-value-2" # The client has only 2 connections (from the initial 2) assert_text "Remaining ActionCable subscriptions: 2" end end test "it unsubscribes with a message" do GraphqlChannel::CounterIncremented.reset_call_count visit "/" using_wait_time 10 do sleep 1 # Establish the connection click_on("Subscribe with fingerprint 1") debug_assert_selector "#fingerprint-updates-1-connected-1" # Trigger once click_on("Trigger with fingerprint 1") debug_assert_selector "#fingerprint-updates-1-update-1-value-1" # Server unsubscribe click_on("Unsubscribe with message with fingerprint 1") # Magic value from unsubscribe hook: debug_assert_selector "#fingerprint-updates-1-update-1-value-9999" # The client has only 2 connections (from the initial 2) assert_text "Remaining ActionCable subscriptions: 2" end end end graphql-ruby-2.5.19/spec/dummy/test/test_helper.rb000066400000000000000000000001671514115062600221410ustar00rootroot00000000000000# frozen_string_literal: true require File.expand_path('../../config/environment', __FILE__) require 'rails/test_help' graphql-ruby-2.5.19/spec/fixtures/000077500000000000000000000000001514115062600170315ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/fixtures/cop/000077500000000000000000000000001514115062600176125ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/fixtures/cop/.rubocop.yml000066400000000000000000000010501514115062600220600ustar00rootroot00000000000000require: - graphql/rubocop AllCops: TargetRubyVersion: 2.4 DisabledByDefault: true GraphQL/DefaultNullTrue: Enabled: true GraphQL/DefaultRequiredTrue: Enabled: true GraphQL/FieldTypeInBlock: Enabled: true Include: - field_type.rb - small_field_type.rb - field_type_autocorrect.rb - field_type_array.rb - field_type_array_autocorrect.rb - field_type_interface.rb - field_type_interface_autocorrect.rb GraphQL/RootTypesInBlock: Enabled: true Include: - root_types.rb - root_types_autocorrect.rb graphql-ruby-2.5.19/spec/fixtures/cop/field_type.rb000066400000000000000000000014041514115062600222620ustar00rootroot00000000000000class Types::Query field :current_account, Types::Account, null: false, description: "The account of the current viewer" field :find_account, Types::Account do argument :id, ID end # Don't modify these: field :current_time, String, description: "The current time in the viewer's timezone" field :current_time, Integer, description: "The current time in the viewer's timezone" field :current_time, Int, description: "The current time in the viewer's timezone" field :current_time, Float, description: "The current time in the viewer's timezone" field :current_time, Boolean, description: "The current time in the viewer's timezone" field(:all_accounts, [Types::Account, null: false]) { argument :active, Boolean, default_value: false } end graphql-ruby-2.5.19/spec/fixtures/cop/field_type_array.rb000066400000000000000000000002141514115062600234560ustar00rootroot00000000000000class Types::FooType < Types::BaseObject field :other, [String] field :bar, [Thing], null: false do argument :baz, String end end graphql-ruby-2.5.19/spec/fixtures/cop/field_type_array_corrected.rb000066400000000000000000000002241514115062600255110ustar00rootroot00000000000000class Types::FooType < Types::BaseObject field :other, [String] field :bar, null: false do type [Thing] argument :baz, String end end graphql-ruby-2.5.19/spec/fixtures/cop/field_type_corrected.rb000066400000000000000000000014451514115062600243210ustar00rootroot00000000000000class Types::Query field :current_account, null: false, description: "The account of the current viewer" do type Types::Account end field :find_account do type Types::Account argument :id, ID end # Don't modify these: field :current_time, String, description: "The current time in the viewer's timezone" field :current_time, Integer, description: "The current time in the viewer's timezone" field :current_time, Int, description: "The current time in the viewer's timezone" field :current_time, Float, description: "The current time in the viewer's timezone" field :current_time, Boolean, description: "The current time in the viewer's timezone" field(:all_accounts) { type [Types::Account, null: false] argument :active, Boolean, default_value: false } end graphql-ruby-2.5.19/spec/fixtures/cop/field_type_interface.rb000066400000000000000000000001201514115062600242740ustar00rootroot00000000000000module Types::FooType include Types::BaseInterface field :thing, Thing end graphql-ruby-2.5.19/spec/fixtures/cop/field_type_interface_corrected.rb000066400000000000000000000001411514115062600263310ustar00rootroot00000000000000module Types::FooType include Types::BaseInterface field :thing do type Thing end end graphql-ruby-2.5.19/spec/fixtures/cop/null_true.rb000066400000000000000000000004771514115062600221600ustar00rootroot00000000000000class Types::Something < Types::BaseObject field :name, String, null: true field :other_name, String, null: true, description: "Here's a description" field :described, [String, null: true], null: true, description: "Something" field :ok_field, String field :also_ok_field, String, null: false end graphql-ruby-2.5.19/spec/fixtures/cop/null_true_corrected.rb000066400000000000000000000004271514115062600242050ustar00rootroot00000000000000class Types::Something < Types::BaseObject field :name, String field :other_name, String, description: "Here's a description" field :described, [String, null: true], description: "Something" field :ok_field, String field :also_ok_field, String, null: false end graphql-ruby-2.5.19/spec/fixtures/cop/required_true.rb000066400000000000000000000007171514115062600230230ustar00rootroot00000000000000class Types::Something < Types::BaseObject field :name, String do argument :id_1, ID, required: true argument :id_2, ID, required: true, description: "Described" argument :id_3, ID, other_config: { something: false, required: true }, required: true, description: "Something" argument :id_4, ID, required: false argument :id_5, ID end field :name2, String do |f| f.argument(:id_1, ID, required: true) end end graphql-ruby-2.5.19/spec/fixtures/cop/required_true_corrected.rb000066400000000000000000000006111514115062600250460ustar00rootroot00000000000000class Types::Something < Types::BaseObject field :name, String do argument :id_1, ID argument :id_2, ID, description: "Described" argument :id_3, ID, other_config: { something: false, required: true }, description: "Something" argument :id_4, ID, required: false argument :id_5, ID end field :name2, String do |f| f.argument(:id_1, ID) end end graphql-ruby-2.5.19/spec/fixtures/cop/root_types.rb000066400000000000000000000001731514115062600223470ustar00rootroot00000000000000class MyAppSchema < GraphQL::Schema query Types::Query mutation Types::Mutation subscription Types::Subscription end graphql-ruby-2.5.19/spec/fixtures/cop/root_types_corrected.rb000066400000000000000000000002071514115062600243770ustar00rootroot00000000000000class MyAppSchema < GraphQL::Schema query { Types::Query } mutation { Types::Mutation } subscription { Types::Subscription } end graphql-ruby-2.5.19/spec/fixtures/cop/small_field_type.rb000066400000000000000000000001161514115062600234510ustar00rootroot00000000000000class Types::Admin::FooType < Types::FooType field :bar, Types::BarType end graphql-ruby-2.5.19/spec/fixtures/eager_module/000077500000000000000000000000001514115062600214615ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/fixtures/eager_module/eager_class.rb000066400000000000000000000001201514115062600242470ustar00rootroot00000000000000# frozen_string_literal: true module EagerModule module EagerClass end end graphql-ruby-2.5.19/spec/fixtures/eager_module/nested_eager_module.rb000066400000000000000000000003231514115062600257760ustar00rootroot00000000000000# frozen_string_literal: true module EagerModule module NestedEagerModule extend GraphQL::Autoload autoload(:NestedEagerClass, "fixtures/eager_module/nested_eager_module/nested_eager_class") end end graphql-ruby-2.5.19/spec/fixtures/eager_module/nested_eager_module/000077500000000000000000000000001514115062600254535ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/fixtures/eager_module/nested_eager_module/nested_eager_class.rb000066400000000000000000000001711514115062600316110ustar00rootroot00000000000000# frozen_string_literal: true module EagerModule module NestedEagerModule class NestedEagerClass end end end graphql-ruby-2.5.19/spec/fixtures/eager_module/other_eager_class.rb000066400000000000000000000001251514115062600254550ustar00rootroot00000000000000# frozen_string_literal: true module EagerModule module OtherEagerClass end end graphql-ruby-2.5.19/spec/fixtures/lazy_module/000077500000000000000000000000001514115062600213555ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/fixtures/lazy_module/lazy_class.rb000066400000000000000000000001161514115062600240440ustar00rootroot00000000000000# frozen_string_literal: true module LazyModule module LazyClass end end graphql-ruby-2.5.19/spec/fixtures/unicode_escapes/000077500000000000000000000000001514115062600221625ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/fixtures/unicode_escapes/query1.graphql000066400000000000000000000004001514115062600247620ustar00rootroot00000000000000{ example1: getString(string: "\u0064") # should return "d" example2: getString(string: "\\u0064") # should return "\\u0064" # example3: getString(string: "\u006") # validation error example4: getString(string: "\\u006") # should return "\\u006" } graphql-ruby-2.5.19/spec/fixtures/unicode_escapes/query2.graphql000066400000000000000000000004051514115062600247700ustar00rootroot00000000000000query bug2 { example1: getString(string: """\a""") # should be "\\a" example2: getString(string: """\u006""") # should be "\\u006" example3: getString(string: """\n""") # should be "\\n" example4: getString(string: """\u0064""") # should be "\\u0064" } graphql-ruby-2.5.19/spec/graphql/000077500000000000000000000000001514115062600166165ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/graphql/analysis/000077500000000000000000000000001514115062600204415ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/graphql/analysis/field_usage_spec.rb000066400000000000000000000166011514115062600242530ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Analysis::FieldUsage do let(:result) { GraphQL::Analysis.analyze_query(query, [GraphQL::Analysis::FieldUsage]).first } let(:query) { GraphQL::Query.new(Dummy::Schema, query_string, variables: variables) } let(:variables) { {} } describe "query with deprecated fields" do let(:query_string) {%| query { cheese(id: 1) { id fatContent } } |} it "keeps track of used fields" do assert_equal ['Cheese.id', 'Cheese.fatContent', 'Query.cheese'], result[:used_fields] end it "keeps track of deprecated fields" do assert_equal ['Cheese.fatContent'], result[:used_deprecated_fields] end end describe "query with deprecated fields used more than once" do let(:query_string) {%| query { cheese1: cheese(id: 1) { id fatContent } cheese2: cheese(id: 2) { id fatContent } } |} it "omits duplicate usage of a field" do assert_equal ['Cheese.id', 'Cheese.fatContent', 'Query.cheese'], result[:used_fields] end it "omits duplicate usage of a deprecated field" do assert_equal ['Cheese.fatContent'], result[:used_deprecated_fields] end end describe "query with deprecated fields in a fragment" do let(:query_string) {%| query { cheese(id: 1) { id ...CheeseSelections } } fragment CheeseSelections on Cheese { fatContent } |} it "keeps track of fields used in the fragment" do assert_equal ['Cheese.id', 'Cheese.fatContent', 'Query.cheese'], result[:used_fields] end it "keeps track of deprecated fields used in the fragment" do assert_equal ['Cheese.fatContent'], result[:used_deprecated_fields] end end describe "query with deprecated fields in an inline fragment" do let(:query_string) {%| query { cheese(id: 1) { id ... on Cheese { fatContent } } } |} it "keeps track of fields used in the fragment" do assert_equal ['Cheese.id', 'Cheese.fatContent', 'Query.cheese'], result[:used_fields] end it "keeps track of deprecated fields used in the fragment" do assert_equal ['Cheese.fatContent'], result[:used_deprecated_fields] end end describe "query with deprecated arguments" do let(:query_string) {%| query { fromSource(oldSource: "deprecated") { id } } |} it "keeps track of deprecated arguments" do assert_equal ['Query.fromSource.oldSource'], result[:used_deprecated_arguments] end end describe "query with deprecated arguments used more than once" do let(:query_string) {%| query { fromSource(oldSource: "deprecated1") { id } fromSource(oldSource: "deprecated2") { id } } |} it "omits duplicate usage of a deprecated argument" do assert_equal ['Query.fromSource.oldSource'], result[:used_deprecated_arguments] end end describe "query with deprecated arguments nested in an array argument" do let(:query_string) {%| query { searchDairy(product: [{ oldSource: "deprecated" }]) { __typename } } |} it "keeps track of nested deprecated arguments" do assert_equal ['DairyProductInput.oldSource'], result[:used_deprecated_arguments] end end describe "query with deprecated enum argument" do let(:query_string) {%| query { fromSource(source: YAK) { id } } |} it "keeps track of deprecated arguments" do assert_equal ['DairyAnimal.YAK'], result[:used_deprecated_enum_values] end describe "tracks non-null/list enums" do let(:query_string) {%| query { cheese(id: 1) { similarCheese(source: [YAK]) { id } } } |} it "keeps track of deprecated arguments" do assert_equal ['DairyAnimal.YAK'], result[:used_deprecated_enum_values] end end end describe "query with an array argument sent as null" do let(:query_string) {%| query { searchDairy(product: null) { __typename } } |} it "tolerates null for array argument" do result end end describe "query with an input object sent in as null" do let(:query_string) {%| query { cheese(id: 1) { id dairyProduct(input: null) { __typename } } } |} it "tolerates null for object argument" do result end end describe "query with deprecated arguments nested in an argument" do let(:query_string) {%| query { searchDairy(singleProduct: { oldSource: "deprecated" }) { __typename } } |} it "keeps track of nested deprecated arguments" do assert_equal ['DairyProductInput.oldSource'], result[:used_deprecated_arguments] end end describe "query with arguments nested in a deprecated argument" do let(:query_string) {%| query { searchDairy(oldProduct: [{ source: "sheep" }]) { __typename } } |} it "keeps track of top-level deprecated arguments" do assert_equal ['Query.searchDairy.oldProduct'], result[:used_deprecated_arguments] end end describe "query with scalar arguments nested in a deprecated argument" do let(:query_string) {%| query { searchDairy(productIds: ["123"]) { __typename } } |} it "keeps track of top-level deprecated arguments" do assert_equal ['Query.searchDairy.productIds'], result[:used_deprecated_arguments] end end describe "mutation with deprecated argument" do let(:query_string) {%| mutation { pushValue(deprecatedTestInput: { oldSource: "deprecated" }) } |} it "keeps track of nested deprecated arguments" do assert_equal ['DairyProductInput.oldSource'], result[:used_deprecated_arguments] end end describe "mutation with deprecated arguments with prepared values" do let(:query_string) {%| mutation { pushValue(preparedTestInput: { deprecatedDate: "2020-10-10" }) } |} it "keeps track of nested deprecated arguments" do assert_equal ['PreparedDateInput.deprecatedDate'], result[:used_deprecated_arguments] end end describe "when an argument prepare raises a GraphQL::ExecutionError" do class ArgumentErrorFieldUsageSchema < GraphQL::Schema class FieldUsage < GraphQL::Analysis::FieldUsage def result values = super query.context[:field_usage] = values nil end end class Query < GraphQL::Schema::Object field :f, Int do argument :i, Int, prepare: ->(*) { raise GraphQL::ExecutionError.new("boom!") } end end query(Query) query_analyzer(FieldUsage) end it "skips analysis of those arguments" do res = ArgumentErrorFieldUsageSchema.execute("{ f(i: 1) }") assert_equal ["boom!"], res["errors"].map { |e| e["message"] } assert_equal({used_fields: ["Query.f"], used_deprecated_arguments: [], used_deprecated_fields: [], used_deprecated_enum_values: []}, res.context[:field_usage]) end end end graphql-ruby-2.5.19/spec/graphql/analysis/max_query_complexity_spec.rb000066400000000000000000000142661514115062600263000ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Analysis::MaxQueryComplexity do let(:schema) { Class.new(Dummy::Schema) } let(:query_string) {%| { a: cheese(id: 1) { id } b: cheese(id: 1) { id } c: cheese(id: 1) { id } d: cheese(id: 1) { id } e: cheese(id: 1) { id } } |} let(:query) { GraphQL::Query.new(schema, query_string, variables: {}, max_complexity: max_complexity) } let(:result) { GraphQL::Analysis.analyze_query(query, [GraphQL::Analysis::MaxQueryComplexity]).first } describe "when a query goes over max complexity" do let(:max_complexity) { 9 } it "returns an error" do assert_equal GraphQL::AnalysisError, result.class assert_equal "Query has complexity of 10, which exceeds max complexity of 9", result.message end end describe "when there is no max complexity" do let(:max_complexity) { nil } it "doesn't error" do assert_nil result end end describe "when the query is less than the max complexity" do let(:max_complexity) { 99 } it "doesn't error" do assert_nil result end end describe "when max_complexity is decreased at query-level" do before do schema.max_complexity(100) end let(:max_complexity) { 7 } it "is applied" do assert_equal GraphQL::AnalysisError, result.class assert_equal "Query has complexity of 10, which exceeds max complexity of 7", result.message end end describe "when max_complexity is increased at query-level" do before do schema.max_complexity(1) end let(:max_complexity) { 10 } it "doesn't error" do assert_nil result end end describe "when max_complexity is nil at query-level" do let(:max_complexity) { nil } before do schema.max_complexity(1) end it "is applied" do assert_nil result end end describe "when used with the max_depth plugin" do let(:schema) do Class.new(GraphQL::Schema) do query Dummy::DairyAppQuery max_depth 3 max_complexity 1 end end let(:query_string) {%| { a: cheese(id: 1) { ...cheeseFields } b: cheese(id: 1) { ...cheeseFields } c: cheese(id: 1) { ...cheeseFields } d: cheese(id: 1) { ...cheeseFields } e: cheese(id: 1) { ...cheeseFields } } fragment cheeseFields on Cheese { id } |} let(:result) { schema.execute(query_string) } it "returns a complexity error" do assert_equal "Query has complexity of 10, which exceeds max complexity of 1", result["errors"].first["message"] end end describe "count_introspection_fields: false" do let(:schema) { Class.new(Dummy::Schema) { max_complexity(5) } } let(:skip_introspection_schema) { Class.new(Dummy::Schema) do max_complexity 5, count_introspection_fields: false end } it "skips introspection fields when configured" do query_string = "{ c1: cheese(id: 1) { id __typename } c2: cheese(id: 2) { id __typename } }" res = schema.execute(query_string) expected_msg = "Query has complexity of 6, which exceeds max complexity of 5" assert_equal [expected_msg], res["errors"].map { |e| e["message"]} res2 = skip_introspection_schema.execute(query_string) assert_equal 2, res2["data"].size refute res2.key?("errors") end end describe "across a multiplex" do before do schema.analysis_engine = GraphQL::Analysis::AST end let(:queries) { 5.times.map { |n| GraphQL::Query.new(schema, "{ cheese(id: #{n}) { id } }", variables: {}) } } let(:max_complexity) { 9 } let(:multiplex) { GraphQL::Execution::Multiplex.new(schema: schema, queries: queries, context: {}, max_complexity: max_complexity) } let(:analyze_multiplex) { GraphQL::Analysis.analyze_multiplex(multiplex, [GraphQL::Analysis::MaxQueryComplexity]) } it "returns errors for all queries" do analyze_multiplex err_msg = "Query has complexity of 10, which exceeds max complexity of 9" queries.each do |query| assert_equal err_msg, query.analysis_errors[0].message end end describe "with a local override" do let(:max_complexity) { 10 } it "uses the override" do analyze_multiplex queries.each do |query| assert query.analysis_errors.empty? end end end end describe "when an argument is unauthorized by type" do class AuthorizedTypeSchema < GraphQL::Schema class Thing < GraphQL::Schema::Object def self.authorized?(obj, ctx) !!ctx[:authorized] && super end field :name, String end class Query < GraphQL::Schema::Object field :things, Thing.connection_type do argument :thing_id, ID, loads: Thing end def things(thing:) [thing] end end query(Query) def self.resolve_type(abs_type, object, ctx) Thing end def self.object_from_id(id, ctx) if id == "13" raise GraphQL::ExecutionError, "No Thing ##{id}" else { name: "Loaded thing #{id}" } end end def self.unauthorized_object(err) raise GraphQL::ExecutionError, "Unauthorized Object: #{err.object[:name].inspect}" end default_max_page_size 30 max_complexity 10 end it "when the arg is unauthorized, returns an authorization error, not a complexity error" do query_str = "{ things(thingId: \"123\", first: 1) { nodes { name } } }" res = AuthorizedTypeSchema.execute(query_str, context: { authorized: true }) assert_equal "Loaded thing 123", res["data"]["things"]["nodes"].first["name"] res2 = AuthorizedTypeSchema.execute(query_str) assert_equal ["Unauthorized Object: \"Loaded thing 123\""], res2["errors"].map { |e| e["message"] } end it "returns the right error when the loaded object raises an error" do query_str = "{ things(thingId: \"13\", first: 1) { nodes { name } } }" res = AuthorizedTypeSchema.execute(query_str, context: { authorized: true }) assert_equal ["No Thing #13"], res["errors"].map { |e| e["message"] } end end end graphql-ruby-2.5.19/spec/graphql/analysis/max_query_depth_spec.rb000066400000000000000000000104321514115062600251760ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Analysis::MaxQueryDepth do let(:schema) { schema = Class.new(Dummy::Schema) schema.analysis_engine = GraphQL::Analysis::AST schema } let(:query_string) { " { cheese(id: 1) { similarCheese(source: SHEEP) { similarCheese(source: SHEEP) { similarCheese(source: SHEEP) { similarCheese(source: SHEEP) { similarCheese(source: SHEEP) { id } } } } } } } "} let(:max_depth) { nil } let(:query) { # Don't override `schema.max_depth` with `nil` options = max_depth ? { max_depth: max_depth } : {} GraphQL::Query.new( schema, query_string, variables: {}, **options ) } let(:result) { GraphQL::Analysis.analyze_query(query, [GraphQL::Analysis::MaxQueryDepth]).first } let(:multiplex) { GraphQL::Execution::Multiplex.new( schema: schema, queries: [query.dup, query.dup], context: {}, max_complexity: nil ) } let(:multiplex_result) { GraphQL::Analysis.analyze_multiplex(multiplex, [GraphQL::Analysis::MaxQueryDepth]).first } describe "when the query is deeper than max depth" do let(:max_depth) { 5 } it "adds an error message for a too-deep query" do assert_equal "Query has depth of 7, which exceeds max depth of 5", result.message end end describe "when a multiplex queries is deeper than max depth" do before do schema.max_depth = 5 end it "adds an error message for a too-deep query on from multiplex analyzer" do assert_equal "Query has depth of 7, which exceeds max depth of 5", multiplex_result.message end end describe "when the query specifies a different max_depth" do let(:max_depth) { 100 } it "obeys that max_depth" do assert_nil result end end describe "When the query is not deeper than max_depth" do before do schema.max_depth = 100 end it "doesn't add an error" do assert_nil result end end describe "when the max depth isn't set" do before do schema.max_depth = nil end it "doesn't add an error message" do assert_nil result end end describe "when a fragment exceeds max depth" do before do schema.max_depth = 4 end let(:query_string) { " { cheese(id: 1) { ...moreFields } } fragment moreFields on Cheese { similarCheese(source: SHEEP) { similarCheese(source: SHEEP) { similarCheese(source: SHEEP) { ...evenMoreFields } } } } fragment evenMoreFields on Cheese { similarCheese(source: SHEEP) { similarCheese(source: SHEEP) { id } } } "} it "adds an error message for a too-deep query" do assert_equal "Query has depth of 7, which exceeds max depth of 4", result.message end end describe "when the query would cause a stack error" do let(:query_string) { str = "query { cheese(id: 1) { ".dup n = 10_000 n.times { str << "similarCheese(source: SHEEP) { " } str << "id " n.times { str << "} " } str << "} }" str } it "returns an error" do assert_equal ["This query is too large to execute."], query.result["errors"].map { |err| err["message"] } # Make sure `Schema.execute` works too execute_result = schema.execute(query_string) assert_equal ["This query is too large to execute."], execute_result["errors"].map { |err| err["message"] } end end it "counts introspection fields by default, but can be set to skip" do schema.max_depth = 3 query_str = <<-GRAPHQL { __type(name: \"Abc\") { fields { type { ofType { name } } } } } GRAPHQL result = schema.execute(query_str) assert_equal ["Query has depth of 5, which exceeds max depth of 3"], result["errors"].map { |e| e["message"] } schema.max_depth(3, count_introspection_fields: false) result = schema.execute(query_str) assert_equal({ "__type" => nil }, result["data"]) end end graphql-ruby-2.5.19/spec/graphql/analysis/query_complexity_spec.rb000066400000000000000000000622211514115062600254250ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Analysis::QueryComplexity do let(:schema) { Class.new(Dummy::Schema) { complexity_cost_calculation_mode(:future) } } let(:reduce_result) { GraphQL::Analysis.analyze_query(query, [GraphQL::Analysis::QueryComplexity]) } let(:reduce_multiplex_result) { GraphQL::Analysis.analyze_multiplex(multiplex, [GraphQL::Analysis::QueryComplexity]) } let(:variables) { {} } let(:query_context) { {} } let(:query) { GraphQL::Query.new(schema, query_string, context: query_context, variables: variables) } let(:multiplex) { GraphQL::Execution::Multiplex.new( schema: schema, queries: [query.dup, query.dup], context: {}, max_complexity: 10 ) } describe "simple queries" do let(:query_string) {%| query cheeses { # complexity of 3 cheese1: cheese(id: 1) { id flavor __typename } # complexity of 4 cheese2: cheese(id: 2) { similarCheese(source: SHEEP) { ... on Cheese { similarCheese(source: SHEEP) { id } } } } } |} it "sums the complexity" do complexities = reduce_result.first assert_equal 8, complexities end end describe "with skip/include" do let(:query_string) {%| query cheeses($skip: Boolean = false, $include: Boolean = true) { fields: cheese(id: 1) { flavor origin @skip(if: $skip) source @include(if: $include) } inlineFragments: cheese(id: 1) { ...on Cheese { flavor } ...on Cheese @skip(if: $skip) { origin } ...on Cheese @include(if: $include) { source } } fragmentSpreads: cheese(id: 1) { ...Flavorful ...Original @skip(if: $skip) ...Sourced @include(if: $include) } } fragment Flavorful on Cheese { flavor } fragment Original on Cheese { origin } fragment Sourced on Cheese { source } |} it "sums up all included complexities" do assert_equal 12, reduce_result.first end describe "when skipped by directives" do let(:variables) { { "skip" => true, "include" => false } } it "doesn't include skipped fields and fragments" do assert_equal 6, reduce_result.first end end end describe "query with fragments" do let(:query_string) {%| { # complexity of 3 cheese1: cheese(id: 1) { id flavor } # complexity of 7 cheese2: cheese(id: 2) { ... cheeseFields1 ... cheeseFields2 } } fragment cheeseFields1 on Cheese { similarCow: similarCheese(source: COW) { id ... cheeseFields2 } } fragment cheeseFields2 on Cheese { similarSheep: similarCheese(source: SHEEP) { id } } |} it "counts all fragment usages, not the definitions" do complexity = reduce_result.first assert_equal 10, complexity end describe "mutually exclusive types" do let(:query_string) {%| { favoriteEdible { # 1 for everybody fatContent # 1 for everybody ... on Edible { origin } # 1 for honey, aspartame ... on Sweetener { sweetness } # 2 for milk ... milkFields # 1 for cheese ... cheeseFields # 1 for honey ... honeyFields # 1 for milk + cheese ... dairyProductFields } } fragment milkFields on Milk { id source } fragment cheeseFields on Cheese { source } fragment honeyFields on Honey { flowerType } fragment dairyProductFields on DairyProduct { ... on Cheese { flavor } ... on Milk { flavors } } |} it "gets the max among options" do complexity = reduce_result.first assert_equal 6, complexity end end describe "when there are no selections on any object types" do let(:query_string) {%| { # 1 for everybody favoriteEdible { # 1 for everybody fatContent # 1 for everybody ... on Edible { origin } # 1 for honey, aspartame ... on Sweetener { sweetness } } } |} it "gets the max among interface types" do complexity = reduce_result.first assert_equal 4, complexity end end describe "redundant fields" do let(:query_string) {%| { favoriteEdible { fatContent # this is executed separately and counts separately: aliasedFatContent: fatContent ... on Edible { fatContent } ... edibleFields } } fragment edibleFields on Edible { fatContent } |} it "only counts them once" do complexity = reduce_result.first assert_equal 3, complexity end end describe "redundant fields not within a fragment" do let(:query_string) {%| { cheese { id } cheese { id } } |} it "only counts them once" do complexity = reduce_result.first assert_equal 2, complexity end end end describe "relay types" do let(:schema) { Class.new(StarWars::Schema) { complexity_cost_calculation_mode(:future) } } let(:query) { GraphQL::Query.new(schema, query_string) } let(:query_string) {%| { rebels { ships(first: 1) { edges { node { id } } nodes { id } pageInfo { hasNextPage } } } } |} it "gets the complexity" do complexity = reduce_result.first expected_complexity = 1 + # rebels 1 + # ships 1 + # edges 1 + # nodes 1 + 1 + # pageInfo, hasNextPage 1 + 1 + 1 # node, id, id assert_equal expected_complexity, complexity end describe "first/last" do let(:query_string) {%| { rebels { s1: ships(first: 5) { edges { node { id } } pageInfo { hasNextPage } } s2: ships(last: 3) { nodes { id } } } } |} it "uses first/last for calculating complexity" do complexity = reduce_result.first expected_complexity = ( 1 + # rebels (1 + 1 + (5 * 2) + 2) + # s1 (1 + 1 + (3 * 1) + 0) # s2 ) assert_equal expected_complexity, complexity end end describe "Field-level max_page_size" do let(:query_string) {%| { rebels { ships { nodes { id } } } } |} it "uses field max_page_size" do complexity = reduce_result.first assert_equal 1 + 1 + 1 + (1000 * 1), complexity end end describe "Schema-level default_max_page_size" do let(:query_string) {%| { rebels { bases { nodes { id } totalCount } } } |} it "uses schema default_max_page_size" do complexity = reduce_result.first assert_equal 1 + 1 + 1 + (3 * 1) + 1, complexity end end describe "Field-level default_page_size" do let(:query_string) {%| { rebels { shipsWithDefaultPageSize { nodes { id } } } } |} it "uses field default_page_size" do complexity = reduce_result.first assert_equal 1 + 1 + 1 + (500 * 1), complexity end end describe "Schema-level default_page_size" do let(:schema) { Class.new(StarWars::SchemaWithDefaultPageSize) { complexity_cost_calculation_mode(:future) } } let(:query) { GraphQL::Query.new(schema, query_string) } let(:query_string) {%| { rebels { bases { nodes { id } totalCount } } } |} it "uses schema default_page_size" do complexity = reduce_result.first assert_equal 1 + 1 + 1 + (2 * 1) + 1, complexity end end end describe "calculation complexity for a multiplex" do let(:query_string) {%| query cheeses { cheese(id: 1) { id flavor source } } |} it "sums complexity for both queries" do complexity = reduce_multiplex_result.first assert_equal 8, complexity end describe "abstract type" do let(:query_string) {%| query Edible { allEdible { origin fatContent } } |} it "sums complexity for both queries" do complexity = reduce_multiplex_result.first assert_equal 6, complexity end end end describe "custom complexities" do class CustomComplexitySchema < GraphQL::Schema module ComplexityInterface include GraphQL::Schema::Interface field :value, Int end class SingleComplexity < GraphQL::Schema::Object field :value, Int, complexity: 0.1 field :complexity, SingleComplexity do argument :int_value, Int, required: false complexity(->(ctx, args, child_complexity) { args[:int_value] + child_complexity }) end implements ComplexityInterface end class DoubleComplexity < GraphQL::Schema::Object field :value, Int, complexity: 4 implements ComplexityInterface end class Query < GraphQL::Schema::Object field :complexity, SingleComplexity do argument :int_value, Int, required: false, prepare: ->(val, ctx) { if ctx[:raise_prepare_error] raise GraphQL::ExecutionError, "Boom" else val end } complexity ->(ctx, args, child_complexity) { args[:int_value] + child_complexity } end def complexity(int_value:) { value: int_value } end field :inner_complexity, ComplexityInterface do argument :value, Int, required: false end end query(Query) orphan_types(DoubleComplexity) complexity_cost_calculation_mode(:future) module CustomIntrospection class DynamicFields < GraphQL::Introspection::DynamicFields field :__typename, String, complexity: 100 end class EntryPoints < GraphQL::Introspection::EntryPoints class CustomIntrospectionField < GraphQL::Schema::Field def calculate_complexity(query:, nodes:, child_complexity:) child_complexity + 0.6 end end field_class CustomIntrospectionField field :__schema, GraphQL::Schema::LateBoundType.new("__Schema") end end introspection(CustomIntrospection) end let(:query) { GraphQL::Query.new(complexity_schema, query_string, context: query_context) } let(:complexity_schema) { CustomComplexitySchema } let(:query_string) {%| { a: complexity(intValue: 3) { value } b: complexity(intValue: 6) { value complexity(intValue: 1) { value } } } |} it "sums the complexity" do complexity = reduce_result.first # 10 from `complexity`, `0.3` from `value` assert_equal 10.3, complexity end describe "introspection" do let(:query_string) { "{ __typename __schema { queryType } }"} it "does custom complexity for introspection" do complexity = reduce_result.first # 100 + 1 + 0.6 assert_equal 101.6, complexity end end describe "same field on multiple types" do let(:query_string) {%| { innerComplexity(intValue: 2) { ... on SingleComplexity { value } ... on DoubleComplexity { value } } } |} it "picks them max for those fields" do complexity = reduce_result.first # 1 for innerComplexity + 4 for DoubleComplexity.value assert_equal 5, complexity end end describe "when prepare raises an error" do let(:query_string) { "{ complexity(intValue: 3) { value } }"} let(:query_context) { { raise_prepare_error: true } } it "handles it nicely" do result = query.result assert_equal ["Boom"], result["errors"].map { |e| e["message"] } complexity = reduce_result.first assert_equal 0.1, complexity end end end describe "custom complexities by complexity_for(...)" do class CustomComplexityByMethodSchema < GraphQL::Schema module ComplexityInterface include GraphQL::Schema::Interface field :value, Int end class SingleComplexity < GraphQL::Schema::Object field :value, Int, complexity: 0.1 field :complexity, SingleComplexity do argument :int_value, Int, required: false def complexity_for(query:, child_complexity:, lookahead:) lookahead.arguments[:int_value] + child_complexity end end implements ComplexityInterface end class ComplexityFourField < GraphQL::Schema::Field def complexity_for(query:, lookahead:, child_complexity:) 4 end end class DoubleComplexity < GraphQL::Schema::Object field_class ComplexityFourField field :value, Int implements ComplexityInterface end class Thing < GraphQL::Schema::Object field :name, String end class CustomThingConnection < GraphQL::Types::Relay::BaseConnection edge_type Thing.edge_type field :something_special, String, complexity: 3 end class Query < GraphQL::Schema::Object field :complexity, SingleComplexity do argument :int_value, Int, required: false def complexity_for(query:, child_complexity:, lookahead:) lookahead.arguments[:int_value] + child_complexity end end field :inner_complexity, ComplexityInterface do argument :value, Int, required: false end field :things, Thing.connection_type, max_page_size: 100 do argument :count, Int, validates: { numericality: { less_than: 50 } } end def things(count:) count.times.map {|t| {name: t.to_s}} end class ThingsCustom < GraphQL::Schema::Resolver type CustomThingConnection, null: false complexity 100 def resolve 5.times { |t| { name: "Thing #{t}" } } end end field :things_custom, resolver: ThingsCustom end query(Query) orphan_types(DoubleComplexity) complexity_cost_calculation_mode(:future) end let(:query) { GraphQL::Query.new(complexity_schema, query_string) } let(:complexity_schema) { CustomComplexityByMethodSchema } let(:query_string) {%| { a: complexity(intValue: 3) { value } b: complexity(intValue: 6) { value complexity(intValue: 1) { value } } } |} it "inherits complexity_cost_calculation_mode" do schema = Class.new(CustomComplexityByMethodSchema) assert_equal CustomComplexityByMethodSchema.complexity_cost_calculation_mode, schema.complexity_cost_calculation_mode end it "sums the complexity" do complexity = reduce_result.first # 10 from `complexity`, `0.3` from `value` assert_equal 10.3, complexity end describe "same field on multiple types" do let(:query_string) {%| { innerComplexity(value: 2) { ... on SingleComplexity { value } ... on DoubleComplexity { value } } } |} it "picks them max for those fields" do complexity = reduce_result.first # 1 for innerComplexity + 4 for DoubleComplexity.value assert_equal 5, complexity end end describe "when the query fails validation" do let(:query_string) {%| { things(count: 200, first: 5) { nodes { name } } } |} it "handles the error" do res = GraphQL::Query.new(complexity_schema, query_string).result assert_equal ["count must be less than 50"], res["errors"].map { |e| e["message"] } assert_equal [], reduce_result, "It doesn't finish calculation" end end describe "when connection fields have custom complexity" do let(:query_string) { "{ thingsCustom(first: 2) { somethingSpecial nodes { name } } }"} it "uses the custom configured value" do complexity = reduce_result.first assert_equal 106, complexity end end end describe "field_complexity hook" do class CustomComplexityAnalyzer < GraphQL::Analysis::QueryComplexity def initialize(query) super @field_complexities_by_query = {} end def result super @field_complexities_by_query[@query] end private def field_complexity(scoped_type_complexity, max_complexity:, child_complexity:) @field_complexities_by_query[scoped_type_complexity.query] ||= {} @field_complexities_by_query[scoped_type_complexity.query][scoped_type_complexity.response_path] = { max_complexity: max_complexity, child_complexity: child_complexity, } end end let(:reduce_result) { GraphQL::Analysis.analyze_query(query, [CustomComplexityAnalyzer]) } let(:query_string) {%| { cheese { id } cheese { id flavor } } |} it "gets called for each field with complexity data" do field_complexities = reduce_result.first assert_equal({ ['cheese', 'id'] => { max_complexity: 1, child_complexity: 0 }, ['cheese', 'flavor'] => { max_complexity: 1, child_complexity: 0 }, ['cheese'] => { max_complexity: 3, child_complexity: 2 }, }, field_complexities) end end describe "maximum of possible scopes regardless of selection order" do class MaxOfPossibleScopes < GraphQL::Schema class Cheese < GraphQL::Schema::Object field :kind, String end module Producer include GraphQL::Schema::Interface field :cheese, Cheese, complexity: 5 field :name, String, complexity: 5 end class Farm < GraphQL::Schema::Object implements Producer field :cheese, Cheese, complexity: 10 field :name, String, complexity: 10 end class Entity < GraphQL::Schema::Union possible_types Farm end class Query < GraphQL::Schema::Object field :entity, Entity, fallback_value: nil end def self.resolve_type Farm end def self.cost(query_string_or_query) query = if query_string_or_query.is_a?(String) GraphQL::Query.new(self, query_string_or_query) else query_string_or_query end GraphQL::Analysis::AST.analyze_query( query, [GraphQL::Analysis::AST::QueryComplexity], ).first end query(Query) end describe "in :future mode" do let(:schema) { Class.new(MaxOfPossibleScopes) { complexity_cost_calculation_mode(:future) }} it "uses maximum of merged composite fields, regardless of selection order" do a = schema.cost(%| { entity { ...on Producer { cheese { kind } } ...on Farm { cheese { kind } } } } |) b = schema.cost(%| { entity { ...on Farm { cheese { kind } } ...on Producer { cheese { kind } } } } |) assert_equal 0, a - b end it "uses maximum of merged leaf fields, regardless of selection order" do a = schema.cost(%| { entity { ...on Producer { name } ...on Farm { name } } } |) b = schema.cost(%| { entity { ...on Farm { name } ...on Producer { name } } } |) assert_equal 0, a - b end end describe "in :legacy mode" do let(:schema) { Class.new(MaxOfPossibleScopes) { complexity_cost_calculation_mode(:legacy) }} it "uses the last of merged composite fields" do a = schema.cost(%| { entity { ...on Producer { cheese { kind } } ...on Farm { cheese { kind } } } } |) b = schema.cost(%| { entity { ...on Farm { cheese { kind } } ...on Producer { cheese { kind } } } } |) assert_equal 5, a - b end it "uses the last-occurring leaf field" do a = schema.cost(%| { entity { ...on Producer { name } ...on Farm { name } } } |) b = schema.cost(%| { entity { ...on Farm { name } ...on Producer { name } } } |) assert_equal 5, a - b end end describe "In dynamic mode with :compare" do let(:schema) { Class.new(MaxOfPossibleScopes) do def self.complexity_cost_calculation_mode_for(context) :compare end def self.legacy_complexity_cost_calculation_mismatch(query, future_cpx, legacy_cpx) query.context.response_extensions["complexity_warning"] = { "current" => legacy_cpx, "future" => future_cpx } 1003 end end } it "calls the handler and uses the returned value" do query = GraphQL::Query.new(schema, %| { entity { ...on Producer { cheese { kind } } ...on Farm { cheese { kind } } } } |) a = schema.cost(query) assert_equal 12, a refute query.result.to_h.key?("extensions") queryb = GraphQL::Query.new(schema, %| { entity { ...on Farm { cheese { kind } } ...on Producer { cheese { kind } } } } |) b = schema.cost(queryb) assert_equal 1003, b assert_equal({"complexity_warning" => {"current" => 7, "future" => 12}}, queryb.result.to_h["extensions"]) end it "calls the custom handler when leaf fields don't match" do a = schema.cost(%| { entity { ...on Producer { name } ...on Farm { name } } } |) assert_equal 11, a b = schema.cost(%| { entity { ...on Farm { name } ...on Producer { name } } } |) assert_equal 1003, b end end describe "without a mode setting" do it "warns, and invalid mismatched scope types will still compute without error" do cost = nil stdout, _stderr = capture_io do cost = MaxOfPossibleScopes.cost(%| { entity { ...on Farm { cheese { kind } } ...on Producer { cheese: name } } } |) end assert_equal 12, cost assert_includes stdout, "GraphQL-Ruby's complexity cost system is getting some \"breaking fixes\" in a future version. See the migration notes at https://graphql-ruby.org/api-doc/#{GraphQL::VERSION}/GraphQL/Schema.html#complexity_cost_calculation_mode_for-class_method To opt into the future behavior, configure your schema (MaxOfPossibleScopes) with: complexity_cost_calculation_mode(:future) # or `:legacy`, `:compare`" end it "uses legacy mode" do cost = nil assert_nil MaxOfPossibleScopes.complexity_cost_calculation_mode stdout, _stderr = capture_io do cost = MaxOfPossibleScopes.cost(%| { entity { ...on Farm { name } ...on Producer { name } } } |) end puts stdout assert_equal 6, cost assert_includes stdout, "GraphQL-Ruby's complexity cost system is getting some \"breaking fixes\" in a future version. See the migration notes at https://graphql-ruby.org/api-doc/#{GraphQL::VERSION}/GraphQL/Schema.html#complexity_cost_calculation_mode_for-class_method To opt into the future behavior, configure your schema (MaxOfPossibleScopes) with: complexity_cost_calculation_mode(:future) # or `:legacy`, `:compare`" end end end end graphql-ruby-2.5.19/spec/graphql/analysis/query_depth_spec.rb000066400000000000000000000044351514115062600243370ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Analysis::QueryDepth do let(:result) { GraphQL::Analysis.analyze_query(query, [GraphQL::Analysis::QueryDepth]) } let(:query) { GraphQL::Query.new(Dummy::Schema, query_string, variables: variables) } let(:variables) { {} } describe "multiple operations" do let(:query_string) {%| query Cheese1 { cheese1: cheese(id: 1) { id flavor } } query Cheese2 { cheese(id: 2) { similarCheese(source: SHEEP) { ... on Cheese { similarCheese(source: SHEEP) { id } } } } } |} let(:query) { GraphQL::Query.new(Dummy::Schema, query_string, variables: variables, operation_name: "Cheese1") } it "analyzes the selected operation only" do depth = result.first assert_equal 2, depth end end describe "simple queries" do let(:query_string) {%| query cheeses($isIncluded: Boolean = true){ # depth of 2 cheese1: cheese(id: 1) { id flavor } # depth of 4 cheese2: cheese(id: 2) @include(if: $isIncluded) { similarCheese(source: SHEEP) { ... on Cheese { similarCheese(source: SHEEP) { id } } } } } |} it "finds the max depth" do depth = result.first assert_equal 4, depth end describe "with directives" do let(:variables) { { "isIncluded" => false } } it "doesn't count skipped fields" do assert_equal 2, result.first end end end describe "query with fragments" do let(:query_string) {%| { # depth of 2 cheese1: cheese(id: 1) { id flavor } # depth of 4 cheese2: cheese(id: 2) { ... cheeseFields1 } } fragment cheeseFields1 on Cheese { similarCheese(source: COW) { id ... cheeseFields2 } } fragment cheeseFields2 on Cheese { similarCheese(source: SHEEP) { id } } |} it "finds the max depth" do assert_equal 4, result.first end end end graphql-ruby-2.5.19/spec/graphql/analysis_spec.rb000066400000000000000000000464651514115062600220170ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Analysis do class AstTypeCollector < GraphQL::Analysis::Analyzer def initialize(query) super @types = [] end def on_enter_operation_definition(node, parent, visitor) @types << visitor.type_definition end def on_enter_field(memo, node, visitor) @types << visitor.field_definition.type.unwrap end def result @types end end class AstNodeCounter < GraphQL::Analysis::Analyzer def initialize(query) super @nodes = Hash.new { |h,k| h[k] = 0 } end def on_enter_abstract_node(node, parent, _visitor) @nodes[node.class] += 1 end alias :on_enter_operation_definition :on_enter_abstract_node alias :on_enter_field :on_enter_abstract_node alias :on_enter_argument :on_enter_abstract_node def result @nodes end end class AstConditionalAnalyzer < GraphQL::Analysis::Analyzer def initialize(query) super @i_have_been_called = false end def analyze? !!query.context[:analyze] end def on_operation_definition(node, parent, visitor) @i_have_been_called = true end def result @i_have_been_called end end class AstPrecomputedAnalyzer < GraphQL::Analysis::Analyzer def initialize(query) super @i_have_been_visited = false end def visit? query.context[:precomputed_result].nil? end def on_enter_field(node, parent, visitor) @i_have_been_visited = true end def result return query.context[:precomputed_result], @i_have_been_visited end end class AstErrorAnalyzer < GraphQL::Analysis::Analyzer def result GraphQL::AnalysisError.new("An Error!") end end class AstPreviousField < GraphQL::Analysis::Analyzer def on_enter_field(node, parent, visitor) @previous_field = visitor.previous_field_definition end def result @previous_field end end class AstArguments < GraphQL::Analysis::Analyzer def on_enter_argument(node, parent, visitor) @argument = visitor.argument_definition @previous_argument = visitor.previous_argument_definition end def result [@argument, @previous_argument] end end class AstSkipInclude < GraphQL::Analysis::Analyzer def initialize(query) super @included = [] end def on_enter_field(node, parent, visitor) @included << "enter #{node.name}" unless visitor.skipping? end def on_leave_field(node, parent, visitor) @included << "leave #{node.name}" unless visitor.skipping? end def on_enter_inline_fragment(node, parent, visitor) @included << "enter ...on #{node.type.name}" unless visitor.skipping? end def on_leave_inline_fragment(node, parent, visitor) @included << "leave ...on #{node.type.name}" unless visitor.skipping? end def on_enter_fragment_spread(node, parent, visitor) @included << "enter ...#{node.name}" unless visitor.skipping? end def on_leave_fragment_spread(node, parent, visitor) @included << "leave ...#{node.name}" unless visitor.skipping? end def result @included end end describe "skip and include behaviors" do let(:reduce_result) { GraphQL::Analysis.analyze_query(query, [AstSkipInclude]) } let(:query) { GraphQL::Query.new(Dummy::Schema, query_string) } let(:query_string) {%|{}|} describe "for fields" do let(:query_string) {%| { cheese { flavor origin @skip(if: true) source @include(if: false) } cheese @skip(if: true) { flavor } cheese @include(if: false) { flavor } } |} it "tracks inclusions" do expected = [ "enter cheese", "enter flavor", "leave flavor", "leave cheese", ] assert_equal expected, reduce_result.first end end describe "for inline fragments" do let(:query_string) {%| { cheese { ...on Cheese @skip(if: true) { origin } ...on Cheese { flavor } ...on Cheese @include(if: false) { source } } } |} it "tracks inclusions" do expected = [ "enter cheese", "enter ...on Cheese", "enter flavor", "leave flavor", "leave ...on Cheese", "leave cheese", ] assert_equal expected, reduce_result.first end end describe "for fragment spreads" do let(:query_string) {%| { cheese { ...Original @skip(if: true) ...Flavorful ...Sourced @include(if: false) } } fragment Flavorful on Cheese { flavor } fragment Original on Cheese { origin } fragment Sourced on Cheese { source } |} it "tracks inclusions" do expected = [ "enter cheese", "enter ...Flavorful", "enter flavor", "leave flavor", "leave ...Flavorful", "leave cheese", ] assert_equal expected, reduce_result.first end end end describe "using the AST analysis engine" do let(:schema) do query_type = Class.new(GraphQL::Schema::Object) do graphql_name 'Query' field :foobar, Integer, null: false def foobar 1337 end end Class.new(GraphQL::Schema) do query query_type query_analyzer AstErrorAnalyzer end end let(:query_string) {%| query { foobar } |} let(:query) { GraphQL::Query.new(schema, query_string, variables: {}) } it "runs the AST analyzers correctly" do res = query.result refute res.key?("data") assert_equal ["An Error!"], res["errors"].map { |e| e["message"] } end end describe ".analyze_query" do let(:analyzers) { [AstTypeCollector, AstNodeCounter] } let(:reduce_result) { GraphQL::Analysis.analyze_query(query, analyzers) } let(:variables) { {} } let(:query) { GraphQL::Query.new(Dummy::Schema, query_string, variables: variables) } let(:query_string) {%| { cheese(id: 1) { id flavor } } |} describe "without a valid operation" do let(:query_string) {%| # A comment # is an invalid operation # Should break |} it "bails early when there is no selected operation to be executed" do assert_equal 2, reduce_result.size end end describe "conditional analysis" do let(:analyzers) { [AstTypeCollector, AstConditionalAnalyzer] } describe "when analyze? returns false" do let(:query) { GraphQL::Query.new(Dummy::Schema, query_string, variables: variables, context: { analyze: false }) } it "does not run the analyzer" do # Only type_collector ran assert_equal 1, reduce_result.size end end describe "when analyze? returns true" do let(:query) { GraphQL::Query.new(Dummy::Schema, query_string, variables: variables, context: { analyze: true }) } it "it runs the analyzer" do # Both analyzers ran assert_equal 2, reduce_result.size end end describe "Visitor#previous_field_definition" do let(:analyzers) { [AstPreviousField] } let(:query) { GraphQL::Query.new(Dummy::Schema, "{ __schema { types { name } } }") } it "it runs the analyzer" do prev_field = reduce_result.first assert_equal "__Schema.types", prev_field.path end end describe "Visitor#argument_definition" do let(:analyzers) { [AstArguments] } let(:query) do GraphQL::Query.new( Dummy::Schema, '{ searchDairy(product: [{ source: "SHEEP" }]) { ... on Cheese { id } } }' ) end it "it runs the analyzer" do argument, prev_argument = reduce_result.first assert_equal "DairyProductInput.source", argument.path assert_equal "Query.searchDairy.product", prev_argument.path end end end describe "precomputed analysis" do let(:analyzers) { [AstPrecomputedAnalyzer] } describe "when visit? returns true" do let(:query) { GraphQL::Query.new(Dummy::Schema, query_string, variables: variables, context: {}) } it "runs the analyzer with visitation" do assert_equal [nil, true], reduce_result.first end end describe "when visit? returns false" do let(:query) { GraphQL::Query.new(Dummy::Schema, query_string, variables: variables, context: { precomputed_result: 23 }) } it "runs the analyzer without visitation" do assert_equal [23, false], reduce_result.first end end end it "calls the defined analyzers" do collected_types, node_counts = reduce_result expected_visited_types = [ Dummy::DairyAppQuery, Dummy::Cheese, GraphQL::Types::Int, GraphQL::Types::String ] assert_equal expected_visited_types, collected_types expected_node_counts = { GraphQL::Language::Nodes::OperationDefinition => 1, GraphQL::Language::Nodes::Field => 3, GraphQL::Language::Nodes::Argument => 1 } assert_equal expected_node_counts, node_counts end class FinishedSchema < GraphQL::Schema class FinishedAnalyzer < GraphQL::Analysis::Analyzer def on_enter_field(node, parent, visitor) if query.context[:force_prepare] visitor.arguments_for(node, visitor.field_definition) end end def result query.context[:analysis_finished] = true end end class Query < GraphQL::Schema::Object field :f1, Int do argument :arg, String, prepare: ->(val, ctx) { ctx[:analysis_finished] ? val.to_i : raise("Prepared too soon!") } end def f1(arg:) arg end end query(Query) query_analyzer(FinishedAnalyzer) end it "doesn't call prepare hooks by default" do res = FinishedSchema.execute("{ f1(arg: \"5\") }") assert_equal 5, res["data"]["f1"] err = assert_raises RuntimeError do FinishedSchema.execute("{ f1(arg: \"5\") }", context: { force_prepare: true }) end assert_equal "Prepared too soon!", err.message end describe "tracing" do let(:query_string) { "{ t: __typename }"} it "emits traces" do traces = TestTracing.with_trace do ctx = { tracers: [TestTracing] } Dummy::Schema.execute(query_string, context: ctx) end # The query_trace is on the list _first_ because it finished first if USING_C_PARSER _lex, _parse, _validate, query_trace, multiplex_trace, *_rest = traces else _parse, _validate, query_trace, multiplex_trace, *_rest = traces end assert_equal "analyze_multiplex", multiplex_trace[:key] assert_instance_of GraphQL::Execution::Multiplex, multiplex_trace[:multiplex] assert_equal "analyze_query", query_trace[:key] assert_instance_of GraphQL::Query, query_trace[:query] end end class AstConnectionCounter < GraphQL::Analysis::Analyzer def initialize(query) super @fields = 0 @connections = 0 end def on_enter_field(node, parent, visitor) if visitor.field_definition.connection? @connections += 1 else @fields += 1 end end def result { fields: @fields, connections: @connections } end end describe "when processing fields" do let(:analyzers) { [AstConnectionCounter] } let(:reduce_result) { GraphQL::Analysis.analyze_query(query, analyzers) } let(:query) { GraphQL::Query.new(StarWars::Schema, query_string, variables: variables) } let(:query_string) {%| query getBases { empire { basesByName(first: 30) { edges { cursor } } bases(first: 30) { edges { cursor } } } } |} it "knows which fields are connections" do connection_counts = reduce_result.first expected_connection_counts = { :fields => 5, :connections => 2 } assert_equal expected_connection_counts, connection_counts end end end describe "Detecting all-introspection queries" do class AllIntrospectionSchema < GraphQL::Schema class Query < GraphQL::Schema::Object field :int, Int end query(Query) end class AllIntrospectionAnalyzer < GraphQL::Analysis::Analyzer def initialize(query) @is_introspection = true super end def on_enter_field(node, parent, visitor) @is_introspection &= (visitor.field_definition.introspection? || ((owner = visitor.field_definition.owner) && owner.introspection?)) end def result @is_introspection end end def is_introspection?(query_str) query = GraphQL::Query.new(AllIntrospectionSchema, query_str) result = GraphQL::Analysis.analyze_query(query, [AllIntrospectionAnalyzer]) result.first end it "returns true for queries containing only introspection types and fields" do assert is_introspection?("{ __typename }") refute is_introspection?("{ int }") assert is_introspection?(GraphQL::Introspection::INTROSPECTION_QUERY) assert is_introspection?("{ __type(name: \"Something\") { fields { name } } }") refute is_introspection?("{ int __type(name: \"Thing\") { name } }") end end describe "when there's a hidden field" do class HiddenAnalyzedFieldSchema < GraphQL::Schema use GraphQL::Schema::Warden if ADD_WARDEN class DoNothingAnalyzer < GraphQL::Analysis::Analyzer def on_enter_field(node, parent, visitor) @result ||= [] @result << [node.name, visitor.field_definition.class] super end attr_reader :result end class BaseField < GraphQL::Schema::Field def initialize(*args, visible: true, **kwargs, &block) @visible = visible super(*args, **kwargs, &block) end def visible?(context) return @visible end end class BaseObject < GraphQL::Schema::Object field_class BaseField end class Article < BaseObject field :title, String, null: false end class Query < BaseObject field :article, String, visible: false do |f| f.argument(:id, Integer) end def article(id:) { title: "hello world" } end end query Query end it "uses nil for the field definition" do gql = <<~GQL { article(id: 1) { title } } GQL query = GraphQL::Query.new(HiddenAnalyzedFieldSchema, gql) result = GraphQL::Analysis.analyze_query(query, [HiddenAnalyzedFieldSchema::DoNothingAnalyzer]) assert_equal [[["article", NilClass], ["title", NilClass]]], result end end describe ".validate_timeout" do class AnalysisTimeoutSchema < GraphQL::Schema class SlowAnalyzer < GraphQL::Analysis::Analyzer def initialize(...) super if query.context[:initialize_sleep] sleep 0.6 end end def on_enter_field(node, parent, visitor) if node.name != "__typename" sleep 0.1 end super end def on_enter_directive(...) sleep 0.1 super end def on_enter_argument(...) sleep 0.1 super end def on_enter_inline_fragment(...) sleep 0.1 super end def on_enter_fragment_spread(node, _parent, _visitor) if !node.name.include?("NoSleep") sleep 0.1 end super end def result nil end end class Query < GraphQL::Schema::Object field :f1, Int do argument :a, String, required: false argument :b, String, required: false argument :c, String, required: false argument :d, String, required: false argument :e, String, required: false argument :f, String, required: false end def f1(...) context[:int] ||= 0 context[:int] += 1 end end class Nothing < GraphQL::Schema::Directive locations(GraphQL::Schema::Directive::FIELD) repeatable(true) end directive(Nothing) query(Query) query_analyzer(SlowAnalyzer) validate_timeout 0.5 end it "covers analysis too" do res = AnalysisTimeoutSchema.execute("{ f1: f1 f2: f1 }") assert_equal({ "f1" => 1, "f2" => 2}, res["data"]) res2 = AnalysisTimeoutSchema.execute("{ f1: f1, f2: f1, f3: f1, f4: f1, f5: f1, f6: f1}") assert_equal ["Timeout on validation of query"], res2["errors"].map { |e| e["message"]} end it "covers directives" do res = AnalysisTimeoutSchema.execute("{ f1 @nothing @nothing }") assert_equal({ "f1" => 1 }, res["data"]) res2 = AnalysisTimeoutSchema.execute("{ f1 @nothing @nothing @nothing @nothing @nothing }") assert_equal ["Timeout on validation of query"], res2["errors"].map { |e| e["message"]} end it "covers arguments" do res = AnalysisTimeoutSchema.execute("{ f1(a: \"a\", b: \"b\")}") assert_equal({ "f1" => 1 }, res["data"]) res2 = AnalysisTimeoutSchema.execute('{ f1(a: "a", b: "b", c: "c", d: "d", e: "e", f: "f") }') assert_equal ["Timeout on validation of query"], res2["errors"].map { |e| e["message"]} end it "covers inline fragments" do res = AnalysisTimeoutSchema.execute("{ ... { f1 } ... { f1 } }") assert_equal({ "f1" => 1 }, res["data"]) res2 = AnalysisTimeoutSchema.execute("{ ... { f1 } ... { f1 } ... { f1 } ... { f1 } ... { f1 } ... { f1 } }") assert_equal ["Timeout on validation of query"], res2["errors"].map { |e| e["message"]} end it "covers operation definitions" do res = AnalysisTimeoutSchema.execute('query Q1 { __typename }', operation_name: "Q1") assert_equal({ "__typename" => "Query" }, res["data"]) res = AnalysisTimeoutSchema.execute('query Q1 { __typename }', operation_name: "Q1", context: { initialize_sleep: true }) assert_equal({ "__typename" => "Query" }, res["data"]) end it "covers fragment spreads" do res = AnalysisTimeoutSchema.execute("{ ...F } fragment F on Query { f1 }") assert_equal({ "f1" => 1 }, res["data"]) res2 = AnalysisTimeoutSchema.execute('{ ...F ...F ...F ...F ...F ...F } fragment F on Query { f1 }') assert_equal ["Timeout on validation of query"], res2["errors"].map { |e| e["message"]} end it "can be ignored" do no_timeout_schema = Class.new(AnalysisTimeoutSchema) do validate_timeout(nil) end res = no_timeout_schema.execute("{ f1 @nothing @nothing @nothing @nothing @nothing }") assert_equal({ "f1" => 1 }, res["data"]) res2 = no_timeout_schema.execute('{ ...F ...F ...F ...F ...F ...F } fragment F on Query { f1 }') assert_equal({ "f1" => 1 }, res2["data"]) end end end graphql-ruby-2.5.19/spec/graphql/authorization_spec.rb000066400000000000000000001031421514115062600230560ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe "GraphQL::Authorization" do module AuthTest class Box attr_reader :value def initialize(value:) @value = value end end class BaseArgument < GraphQL::Schema::Argument def visible?(context) super && (context[:hide] ? @name != "hidden" : true) end def authorized?(parent_object, value, context) super && parent_object != :hide2 end end class BaseInputObjectArgument < BaseArgument def authorized?(parent_object, value, context) super && parent_object != :hide3 end end class BaseInputObject < GraphQL::Schema::InputObject argument_class BaseInputObjectArgument end class BaseField < GraphQL::Schema::Field argument_class BaseArgument def visible?(context) super && (context[:hide] ? @name != "hidden" : true) end def authorized?(object, args, context) if object == :raise raise GraphQL::UnauthorizedFieldError.new("raised authorized field error", object: object) end return Box.new(value: context[:lazy_field_authorized]) if context.key?(:lazy_field_authorized) super && object != :hide && object != :replace end end class BaseObject < GraphQL::Schema::Object field_class BaseField end module BaseInterface include GraphQL::Schema::Interface end class BaseEnumValue < GraphQL::Schema::EnumValue def initialize(*args, role: nil, **kwargs) @role = role super(*args, **kwargs) end def visible?(context) super && (context[:hide] ? @role != :hidden : true) end def authorized?(context) super && (context[:authorized] ? true : @role != :unauthorized) end end class BaseEnum < GraphQL::Schema::Enum enum_value_class(BaseEnumValue) end module HiddenInterface include BaseInterface definition_methods do def visible?(ctx) super && !ctx[:hide] end def resolve_type(obj, ctx) HiddenObject end end end module HiddenDefaultInterface if GraphQL::Schema.use_visibility_profile? include HiddenInterface else # Warden will detect no possible types include BaseInterface end def self.resolve_type(obj, ctx) HiddenObject end end class HiddenObject < BaseObject implements HiddenInterface implements HiddenDefaultInterface def self.visible?(ctx) super && !ctx[:hide] end field :some_field, String end class RelayObject < BaseObject def self.visible?(ctx) super && !ctx[:hidden_relay] end def self.authorized?(_val, ctx) super && !ctx[:unauthorized_relay] end field :some_field, String end class UnauthorizedObject < BaseObject def self.authorized?(value, context) if context[:raise] raise GraphQL::UnauthorizedError.new("raised authorized object error", object: value.object) end super && !context[:hide] end field :value, String, null: false, method: :itself end class UnauthorizedBox < BaseObject # Hide `"a"` def self.authorized?(value, context) super && value != "a" end field :value, String, null: false, method: :itself end module UnauthorizedInterface include BaseInterface def self.resolve_type(obj, ctx) if obj.is_a?(String) UnauthorizedCheckBox else raise "Unexpected value: #{obj.inspect}" end end end class UnauthorizedCheckBox < BaseObject implements UnauthorizedInterface # This authorized check returns a lazy object, it should be synced by the runtime. def self.authorized?(value, context) if !value.is_a?(String) raise "Unexpected box value: #{value.inspect}" end is_authed = super && value != "a" # Make it many levels nested just to make sure we support nested lazy objects Box.new(value: Box.new(value: Box.new(value: Box.new(value: is_authed)))) end field :value, String, null: false, method: :itself end class IntegerObject < BaseObject def self.authorized?(obj, ctx) if !obj.is_a?(Integer) raise "Unexpected IntegerObject: #{obj}" end is_allowed = !(ctx[:unauthorized_relay] || obj == ctx[:exclude_integer]) Box.new(value: Box.new(value: is_allowed)) end field :value, Integer, null: false, method: :itself end class IntegerObjectEdge < GraphQL::Types::Relay::BaseEdge node_type(IntegerObject) end class IntegerObjectConnection < GraphQL::Types::Relay::BaseConnection edge_type(IntegerObjectEdge) end # This object responds with `replaced => false`, # but if its replacement value is used, it gives `replaced => true` class Replaceable def replacement { replaced: true } end def replaced false end end class ReplacedObject < BaseObject def self.authorized?(obj, ctx) super && !ctx[:replace_me] end field :replaced, Boolean, null: false end class LandscapeFeature < BaseEnum value "MOUNTAIN" value "STREAM", role: :unauthorized value "FIELD" value "TAR_PIT", role: :hidden end class Query < BaseObject def self.authorized?(obj, ctx) !ctx[:query_unauthorized] end field :hidden, Integer, null: false field :unauthorized, Integer, method: :itself field :int2, Integer do argument :int, Integer, required: false argument :hidden, Integer, required: false argument :unauthorized, Integer, required: false end def int2(**args) args[:unauthorized] || 1 end field :landscape_feature, LandscapeFeature, null: false do argument :string, String, required: false argument :enum, LandscapeFeature, required: false end def landscape_feature(string: nil, enum: nil) string || enum end field :landscape_features, [LandscapeFeature], null: false do argument :strings, [String], required: false argument :enums, [LandscapeFeature], required: false end def landscape_features(strings: [], enums: []) strings + enums end def empty_array; []; end field :hidden_object, HiddenObject, null: false, resolver_method: :itself field :hidden_interface, HiddenInterface, null: false, resolver_method: :itself field :hidden_default_interface, HiddenDefaultInterface, null: false, resolver_method: :itself field :hidden_connection, RelayObject.connection_type, null: :false, resolver_method: :empty_array field :hidden_edge, RelayObject.edge_type, null: :false, resolver_method: :edge_object field :unauthorized_object, UnauthorizedObject, resolver_method: :itself field :unauthorized_connection, RelayObject.connection_type, null: false, resolver_method: :array_with_item field :unauthorized_edge, RelayObject.edge_type, null: false, resolver_method: :edge_object def edge_object OpenStruct.new(node: 100) end def array_with_item [1] end field :unauthorized_lazy_box, UnauthorizedBox do argument :value, String end def unauthorized_lazy_box(value:) # Make it extra nested, just for good measure. Box.new(value: Box.new(value: value)) end field :unauthorized_list_items, [UnauthorizedObject] def unauthorized_list_items [self, self] end field :unauthorized_lazy_check_box, UnauthorizedCheckBox, resolver_method: :unauthorized_lazy_box do argument :value, String end field :unauthorized_interface, UnauthorizedInterface, resolver_method: :unauthorized_lazy_box do argument :value, String end field :unauthorized_lazy_list_interface, [UnauthorizedInterface, null: true] def unauthorized_lazy_list_interface ["z", Box.new(value: Box.new(value: "z2")), "a", Box.new(value: "a")] end field :integers, IntegerObjectConnection, null: false def integers [1,2,3] end field :lazy_integers, IntegerObjectConnection, null: false def lazy_integers Box.new(value: Box.new(value: [1,2,3])) end field :replaced_object, ReplacedObject, null: false def replaced_object Replaceable.new end end class DoHiddenStuff < GraphQL::Schema::RelayClassicMutation def self.visible?(ctx) super && (ctx[:hidden_mutation] ? false : true) end end class DoHiddenStuff2 < GraphQL::Schema::Mutation def self.visible?(ctx) super && !ctx[:hidden_mutation] end field :some_return_field, String end class DoUnauthorizedStuff < GraphQL::Schema::RelayClassicMutation def self.authorized?(obj, ctx) super && (ctx[:unauthorized_mutation] ? false : true) end end class Mutation < BaseObject field :do_hidden_stuff, mutation: DoHiddenStuff field :do_hidden_stuff2, mutation: DoHiddenStuff2 field :do_unauthorized_stuff, mutation: DoUnauthorizedStuff end class Nothing < GraphQL::Schema::Directive locations(FIELD) def self.visible?(ctx) !!ctx[:show_nothing_directive] end end class Schema < GraphQL::Schema query(Query) mutation(Mutation) directive(Nothing) use GraphQL::Schema::Warden if ADD_WARDEN lazy_resolve(Box, :value) def self.unauthorized_object(err) if err.object.respond_to?(:replacement) err.object.replacement elsif err.object == :replace 33 elsif err.object == :raise_from_object raise GraphQL::ExecutionError, err.message else raise GraphQL::ExecutionError, "Unauthorized #{err.type.graphql_name}: #{err.object.inspect}" end end end class SchemaWithFieldHook < GraphQL::Schema query(Query) use GraphQL::Schema::Warden if ADD_WARDEN lazy_resolve(Box, :value) def self.unauthorized_field(err) if err.object == :replace 42 elsif err.object == :raise raise GraphQL::ExecutionError, "#{err.message} in field #{err.field.graphql_name}" else raise GraphQL::ExecutionError, "Unauthorized field #{err.field.graphql_name} on #{err.type.graphql_name}: #{err.object}" end end end end def auth_execute(*args, **kwargs) AuthTest::Schema.execute(*args, **kwargs) end describe "applying the visible? method" do it "works in queries" do res = auth_execute(" { int int2 } ", context: { hide: true }) assert_equal 1, res["errors"].size end it "applies return type visibility to fields" do error_queries = { "hiddenObject" => "{ hiddenObject { __typename } }", "hiddenInterface" => "{ hiddenInterface { __typename } }", "hiddenDefaultInterface" => "{ hiddenDefaultInterface { __typename } }", } error_queries.each do |name, q| hidden_res = auth_execute(q, context: { hide: true}) assert_equal ["Field '#{name}' doesn't exist on type 'Query'#{name == "hiddenDefaultInterface" ? "" : " (Did you mean `hiddenConnection`?)"}"], hidden_res["errors"].map { |e| e["message"] } visible_res = auth_execute(q) # Both fields exist; the interface resolves to the object type, though assert_equal "HiddenObject", visible_res["data"][name]["__typename"] end end it "uses the mutation for derived fields, inputs and outputs" do query = "mutation { doHiddenStuff(input: {}) { __typename } }" res = auth_execute(query, context: { hidden_mutation: true }) assert_equal ["Field 'doHiddenStuff' doesn't exist on type 'Mutation'"], res["errors"].map { |e| e["message"] } # `#resolve` isn't implemented, so this errors out: assert_raises GraphQL::RequiredImplementationMissingError do auth_execute(query) end introspection_q = <<-GRAPHQL { t1: __type(name: "DoHiddenStuffInput") { name } t2: __type(name: "DoHiddenStuffPayload") { name } } GRAPHQL hidden_introspection_res = auth_execute(introspection_q, context: { hidden_mutation: true }) assert_nil hidden_introspection_res["data"]["t1"] assert_nil hidden_introspection_res["data"]["t2"] visible_introspection_res = auth_execute(introspection_q) assert_equal "DoHiddenStuffInput", visible_introspection_res["data"]["t1"]["name"] assert_equal "DoHiddenStuffPayload", visible_introspection_res["data"]["t2"]["name"] end it "works with Schema::Mutation" do query = "mutation { doHiddenStuff2 { __typename } }" res = auth_execute(query, context: { hidden_mutation: true }) assert_equal ["Field 'doHiddenStuff2' doesn't exist on type 'Mutation'"], res["errors"].map { |e| e["message"] } # `#resolve` isn't implemented, so this errors out: assert_raises GraphQL::RequiredImplementationMissingError do auth_execute(query) end end it "uses the base type for edges and connections" do query = <<-GRAPHQL { hiddenConnection { __typename } hiddenEdge { __typename } } GRAPHQL hidden_res = auth_execute(query, context: { hidden_relay: true }) assert_equal 2, hidden_res["errors"].size visible_res = auth_execute(query) assert_equal "RelayObjectConnection", visible_res["data"]["hiddenConnection"]["__typename"] assert_equal "RelayObjectEdge", visible_res["data"]["hiddenEdge"]["__typename"] end it "treats hidden enum values as non-existent, even in lists" do hidden_res_1 = auth_execute <<-GRAPHQL, context: { hide: true } { landscapeFeature(enum: TAR_PIT) } GRAPHQL assert_equal ["Argument 'enum' on Field 'landscapeFeature' has an invalid value (TAR_PIT). Expected type 'LandscapeFeature'."], hidden_res_1["errors"].map { |e| e["message"] } hidden_res_2 = auth_execute <<-GRAPHQL, context: { hide: true } { landscapeFeatures(enums: [STREAM, TAR_PIT]) } GRAPHQL assert_equal ["Argument 'enums' on Field 'landscapeFeatures' has an invalid value ([STREAM, TAR_PIT]). Expected type '[LandscapeFeature!]'."], hidden_res_2["errors"].map { |e| e["message"] } success_res = auth_execute <<-GRAPHQL, context: { hide: false, authorized: true } { landscapeFeature(enum: TAR_PIT) landscapeFeatures(enums: [STREAM, TAR_PIT]) } GRAPHQL assert_equal "TAR_PIT", success_res["data"]["landscapeFeature"] assert_equal ["STREAM", "TAR_PIT"], success_res["data"]["landscapeFeatures"] end it "refuses to resolve to hidden enum values" do expected_class = AuthTest::LandscapeFeature::UnresolvedValueError assert_raises(expected_class) do auth_execute <<-GRAPHQL, context: { hide: true } { landscapeFeature(string: "TAR_PIT") } GRAPHQL end assert_raises(expected_class) do auth_execute <<-GRAPHQL, context: { hide: true } { landscapeFeatures(strings: ["STREAM", "TAR_PIT"]) } GRAPHQL end end it "rejects incoming unauthorized enum values" do res = auth_execute <<-GRAPHQL, context: { } { landscapeFeature(enum: STREAM) } GRAPHQL assert_equal ["Unauthorized LandscapeFeature: \"STREAM\""], res["errors"].map { |e| e["message"] } end it "rejects outgoing unauthorized enum values" do err = assert_raises(AuthTest::LandscapeFeature::UnresolvedValueError) do auth_execute <<-GRAPHQL, context: { } { landscapeFeature(string: "STREAM") } GRAPHQL end assert_equal "`Query.landscapeFeature` returned `\"STREAM\"` at `landscapeFeature`, but this value was unauthorized. Update the field or resolver to return a different value in this case (or return `nil`).", err.message end it "works in introspection" do res = auth_execute <<-GRAPHQL, context: { hide: true, hidden_mutation: true } { query: __type(name: "Query") { fields { name args { name } } } hiddenObject: __type(name: "HiddenObject") { name } hiddenInterface: __type(name: "HiddenInterface") { name } landscapeFeatures: __type(name: "LandscapeFeature") { enumValues { name } } } GRAPHQL query_field_names = res["data"]["query"]["fields"].map { |f| f["name"] } refute_includes query_field_names, "int" int2_arg_names = res["data"]["query"]["fields"].find { |f| f["name"] == "int2" }["args"].map { |a| a["name"] } assert_equal ["int", "unauthorized"], int2_arg_names assert_nil res["data"]["hiddenObject"] assert_nil res["data"]["hiddenInterface"] visible_landscape_features = res["data"]["landscapeFeatures"]["enumValues"].map { |v| v["name"] } assert_equal ["MOUNTAIN", "STREAM", "FIELD"], visible_landscape_features end it "works when printing the SDL" do full_sdl_lines = AuthTest::Schema.to_definition.split("\n") restricted_sdl_lines = AuthTest::Schema.to_definition(context: { hide: true, hidden_mutation: true, hidden_relay: true }).split("\n") expected_hidden_lines = [ "Autogenerated return type of DoHiddenStuff2.", "type DoHiddenStuff2Payload {", "Autogenerated input type of DoHiddenStuff", "input DoHiddenStuffInput {", "Autogenerated return type of DoHiddenStuff.", "type DoHiddenStuffPayload {", "interface HiddenDefaultInterface", "interface HiddenInterface", "type HiddenObject implements HiddenDefaultInterface & HiddenInterface {", " doHiddenStuff(", " Parameters for DoHiddenStuff", " input: DoHiddenStuffInput!", " ): DoHiddenStuffPayload", " doHiddenStuff2: DoHiddenStuff2Payload", " hidden: Int!", " hiddenConnection(", " hiddenDefaultInterface: HiddenDefaultInterface!", " hiddenEdge: RelayObjectEdge", " hiddenInterface: HiddenInterface!", " hiddenObject: HiddenObject!", " int2(hidden: Int, int: Int, unauthorized: Int): Int" ] assert_equal expected_hidden_lines, full_sdl_lines.select { |l| l.include?("Hidden") || l.include?("hidden") } assert_equal [], restricted_sdl_lines.select { |l| l.include?("Hidden") || l.include?("hidden") } end it "works with directives" do query_str = "{ __typename @nothing }" visible_response = auth_execute(query_str, context: { show_nothing_directive: true }) assert_equal "Query", visible_response["data"]["__typename"] hidden_response = auth_execute(query_str) assert_equal ["Directive @nothing is not defined"], hidden_response["errors"].map { |e| e["message"] } end end describe "applying the authorized? method" do it "halts on unauthorized objects, replacing the object with nil" do query = "{ unauthorizedObject { __typename } }" hidden_response = auth_execute(query, context: { hide: true }) assert_nil hidden_response["data"].fetch("unauthorizedObject") visible_response = auth_execute(query, context: {}) assert_equal({ "__typename" => "UnauthorizedObject" }, visible_response["data"]["unauthorizedObject"]) end it "halts on unauthorized mutations" do query = "mutation { doUnauthorizedStuff(input: {}) { __typename } }" res = auth_execute(query, context: { unauthorized_mutation: true }) assert_nil res["data"].fetch("doUnauthorizedStuff") assert_raises GraphQL::RequiredImplementationMissingError do auth_execute(query) end end describe "field level authorization" do describe "unauthorized field" do describe "with an unauthorized field hook configured" do describe "when the hook returns a value" do it "replaces the response with the return value of the unauthorized field hook" do query = "{ unauthorized }" response = AuthTest::SchemaWithFieldHook.execute(query, root_value: :replace) assert_equal 42, response["data"].fetch("unauthorized") end end describe "when the field hook raises an error" do it "returns nil" do query = "{ unauthorized }" response = AuthTest::SchemaWithFieldHook.execute(query, root_value: :hide) assert_nil response["data"].fetch("unauthorized") end it "adds the error to the errors key" do query = "{ unauthorized }" response = AuthTest::SchemaWithFieldHook.execute(query, root_value: :hide) assert_equal ["Unauthorized field unauthorized on Query: hide"], response["errors"].map { |e| e["message"] } end end describe "when the field authorization resolves lazily" do it "returns value if authorized" do query = "{ unauthorized }" response = AuthTest::SchemaWithFieldHook.execute(query, root_value: 34, context: { lazy_field_authorized: true }) assert_equal 34, response["data"].fetch("unauthorized") end it "returns nil if not authorized" do query = "{ unauthorized }" response = AuthTest::SchemaWithFieldHook.execute(query, root_value: 34, context: { lazy_field_authorized: false }) assert_nil response["data"].fetch("unauthorized") assert_equal ["Unauthorized field unauthorized on Query: 34"], response["errors"].map { |e| e["message"] } end end describe "when the field authorization raises an UnauthorizedFieldError" do it "receives the raised error" do query = "{ unauthorized }" response = AuthTest::SchemaWithFieldHook.execute(query, root_value: :raise) assert_equal ["raised authorized field error in field unauthorized"], response["errors"].map { |e| e["message"] } end end end describe "with an unauthorized field hook not configured" do describe "When the object hook replaces the field" do it "delegates to the unauthorized object hook, which replaces the object" do query = "{ unauthorized }" response = AuthTest::Schema.execute(query, root_value: :replace) assert_equal 33, response["data"].fetch("unauthorized") end end describe "When the object hook raises an error" do it "returns nil" do query = "{ unauthorized }" response = AuthTest::Schema.execute(query, root_value: :hide) assert_nil response["data"].fetch("unauthorized") end it "adds the error to the errors key" do query = "{ unauthorized }" response = AuthTest::Schema.execute(query, root_value: :hide) assert_equal ["Unauthorized Query: :hide"], response["errors"].map { |e| e["message"] } end end end end describe "authorized field" do it "returns the field data" do query = "{ unauthorized }" response = AuthTest::SchemaWithFieldHook.execute(query, root_value: 1) assert_equal 1, response["data"].fetch("unauthorized") end end end it "halts on unauthorized fields, using the parent object" do query = "{ unauthorized }" hidden_response = auth_execute(query, root_value: :hide) assert_nil hidden_response["data"].fetch("unauthorized") visible_response = auth_execute(query, root_value: 1) assert_equal 1, visible_response["data"]["unauthorized"] end it "halts on unauthorized arguments, using the parent object" do query = "{ int2(unauthorized: 5) }" hidden_response = auth_execute(query, root_value: :hide2) assert_nil hidden_response["data"].fetch("int2") visible_response = auth_execute(query) assert_equal 5, visible_response["data"]["int2"] end it "works with edges and connections" do query = <<-GRAPHQL { unauthorizedConnection { __typename edges { __typename node { __typename } } nodes { __typename } } unauthorizedEdge { __typename node { __typename } } } GRAPHQL unauthorized_res = auth_execute(query, context: { unauthorized_relay: true }) conn = unauthorized_res["data"].fetch("unauthorizedConnection") assert_equal "RelayObjectConnection", conn.fetch("__typename") # This is tricky: the previous behavior was to replace the _whole_ # list with `nil`. This was due to an implementation detail: # The list field's return value (an array of integers) was wrapped # _before_ returning, and during this wrapping, a cascading error # caused the entire field to be nilled out. # # In the interpreter, each list item is contained and the error doesn't propagate # up to the whole list. # # Originally, I thought that this was a _feature_ that obscured list entries. # But really, look at the test below: you don't get this "feature" if # you use `edges { node }`, so it can't be relied on in any way. # # All that to say, in the interpreter, `nodes` and `edges { node }` behave # the same. # # TODO revisit the docs for this. failed_nodes_value = [nil] assert_equal failed_nodes_value, conn.fetch("nodes") assert_equal [{"node" => nil, "__typename" => "RelayObjectEdge"}], conn.fetch("edges") edge = unauthorized_res["data"].fetch("unauthorizedEdge") assert_nil edge.fetch("node") assert_equal "RelayObjectEdge", edge["__typename"] unauthorized_object_paths = [ ["unauthorizedConnection", "edges", 0, "node"], ["unauthorizedConnection", "nodes", 0], ["unauthorizedEdge", "node"] ] assert_equal unauthorized_object_paths, unauthorized_res["errors"].map { |e| e["path"] } authorized_res = auth_execute(query) conn = authorized_res["data"].fetch("unauthorizedConnection") assert_equal "RelayObjectConnection", conn.fetch("__typename") assert_equal [{"__typename"=>"RelayObject"}], conn.fetch("nodes") assert_equal [{"node" => {"__typename" => "RelayObject"}, "__typename" => "RelayObjectEdge"}], conn.fetch("edges") edge = authorized_res["data"].fetch("unauthorizedEdge") assert_equal "RelayObject", edge.fetch("node").fetch("__typename") assert_equal "RelayObjectEdge", edge["__typename"] end it "authorizes _after_ resolving lazy objects" do query = <<-GRAPHQL { a: unauthorizedLazyBox(value: "a") { value } b: unauthorizedLazyBox(value: "b") { value } } GRAPHQL unauthorized_res = auth_execute(query) assert_nil unauthorized_res["data"].fetch("a") assert_equal "b", unauthorized_res["data"]["b"]["value"] end it "authorizes items in a list" do query = <<-GRAPHQL { unauthorizedListItems { __typename } } GRAPHQL unauthorized_res = auth_execute(query, context: { hide: true }) assert_nil unauthorized_res["data"]["unauthorizedListItems"] authorized_res = auth_execute(query, context: { hide: false }) assert_equal 2, authorized_res["data"]["unauthorizedListItems"].size end it "syncs lazy objects from authorized? checks" do query = <<-GRAPHQL { a: unauthorizedLazyCheckBox(value: "a") { value } b: unauthorizedLazyCheckBox(value: "b") { value } } GRAPHQL unauthorized_res = auth_execute(query) assert_nil unauthorized_res["data"].fetch("a") assert_equal "b", unauthorized_res["data"]["b"]["value"] # Also, the custom handler was called: assert_equal ["Unauthorized UnauthorizedCheckBox: \"a\""], unauthorized_res["errors"].map { |e| e["message"] } end it "Works for lazy connections" do query = <<-GRAPHQL { lazyIntegers { edges { node { value } } } } GRAPHQL res = auth_execute(query) assert_equal [1,2,3], res["data"]["lazyIntegers"]["edges"].map { |e| e["node"]["value"] } end it "Works for eager connections" do query = <<-GRAPHQL { integers { edges { node { value } } } } GRAPHQL res = auth_execute(query) assert_equal [1,2,3], res["data"]["integers"]["edges"].map { |e| e["node"]["value"] } end it "filters out individual nodes by value" do query = <<-GRAPHQL { integers { edges { node { value } } } } GRAPHQL res = auth_execute(query, context: { exclude_integer: 1 }) assert_equal [nil,2,3], res["data"]["integers"]["edges"].map { |e| e["node"] && e["node"]["value"] } assert_equal ["Unauthorized IntegerObject: 1"], res["errors"].map { |e| e["message"] } end it "works with lazy values / interfaces" do query = <<-GRAPHQL query($value: String!){ unauthorizedInterface(value: $value) { ... on UnauthorizedCheckBox { value } } } GRAPHQL res = auth_execute(query, variables: { value: "a"}) assert_nil res["data"]["unauthorizedInterface"] res2 = auth_execute(query, variables: { value: "b"}) assert_equal "b", res2["data"]["unauthorizedInterface"]["value"] end it "works with lazy values / lists of interfaces" do query = <<-GRAPHQL { unauthorizedLazyListInterface { ... on UnauthorizedCheckBox { value } } } GRAPHQL res = auth_execute(query) # An error from two, values from the others assert_equal ["Unauthorized UnauthorizedCheckBox: \"a\"", "Unauthorized UnauthorizedCheckBox: \"a\""], res["errors"].map { |e| e["message"] } assert_equal [{"value" => "z"}, {"value" => "z2"}, nil, nil], res["data"]["unauthorizedLazyListInterface"] end describe "with an unauthorized field hook configured" do it "replaces objects from the unauthorized_object hook" do query = "{ replacedObject { replaced } }" res = auth_execute(query, context: { replace_me: true }) assert_equal true, res["data"]["replacedObject"]["replaced"] res = auth_execute(query, context: { replace_me: false }) assert_equal false, res["data"]["replacedObject"]["replaced"] end it "works when the query hook returns false and there's no root object" do query = "{ __typename }" res = auth_execute(query) assert_equal "Query", res["data"]["__typename"] unauth_res = auth_execute(query, context: { query_unauthorized: true }) assert_equal({ "errors" => [{"message"=>"Unauthorized Query: nil"}], "data" => nil, }, unauth_res.to_h) end describe "when the object authorization raises an UnauthorizedFieldError" do it "receives the raised error" do query = "{ unauthorizedObject { value } }" response = auth_execute(query, context: { raise: true }, root_value: :raise_from_object) assert_equal ["raised authorized object error"], response["errors"].map { |e| e["message"] } end end end end describe "returning false" do class FalseSchema < GraphQL::Schema class Query < GraphQL::Schema::Object def self.authorized?(obj, ctx) false end field :int, Integer, null: false def int 1 end end query(Query) end it "works out-of-the-box" do res = FalseSchema.execute("{ int }") assert_nil res.fetch("data") refute res.key?("errors") end end describe "overriding authorized_new" do class AuthorizedNewOverrideSchema < GraphQL::Schema module LogTrace def trace(key, data) if ((q = data[:query]) && (c = q.context)) c[:log] << key end yield end ["parse", "lex", "validate", "analyze_query", "analyze_multiplex", "execute_query", "execute_multiplex", "execute_field", "execute_field_lazy", "authorized", "authorized_lazy", "resolve_type", "resolve_type_lazy", "execute_query_lazy"].each do |method_name| define_method(method_name) do |**data, &block| trace(method_name, data, &block) end end end module CustomIntrospection class DynamicFields < GraphQL::Introspection::DynamicFields def self.authorized_new(obj, ctx) new(obj, ctx) end end end class Query < GraphQL::Schema::Object def self.authorized_new(obj, ctx) new(obj, ctx) end field :int, Integer, null: false def int; 1; end end query(Query) introspection(CustomIntrospection) trace_with(LogTrace) end it "avoids calls to Object.authorized?" do log = [] res = AuthorizedNewOverrideSchema.execute("{ __typename int }", context: { log: log }) assert_equal "Query", res["data"]["__typename"] assert_equal 1, res["data"]["int"] expected_log = [ "validate", "analyze_query", "execute_query", "execute_field", "execute_field", "execute_query_lazy" ] assert_equal expected_log, log end end end graphql-ruby-2.5.19/spec/graphql/autoload_spec.rb000066400000000000000000000042251514115062600217700ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" require "open3" describe GraphQL::Autoload do module LazyModule extend GraphQL::Autoload autoload(:LazyClass, "fixtures/lazy_module/lazy_class") end module EagerModule extend GraphQL::Autoload autoload(:EagerClass, "fixtures/eager_module/eager_class") autoload(:OtherEagerClass, "fixtures/eager_module/other_eager_class") autoload(:NestedEagerModule, "fixtures/eager_module/nested_eager_module") def self.eager_load! super NestedEagerModule.eager_load! end end describe "#autoload" do it "sets autoload" do assert LazyModule.const_defined?(:LazyClass) assert_equal("fixtures/lazy_module/lazy_class", LazyModule.autoload?(:LazyClass)) LazyModule::LazyClass assert_nil(LazyModule.autoload?(:LazyClass)) end end describe "#eager_load!" do it "eagerly loads autoload entries" do assert EagerModule.autoload?(:EagerClass) assert EagerModule.autoload?(:OtherEagerClass) assert EagerModule.autoload?(:NestedEagerModule) EagerModule.eager_load! assert_nil(EagerModule.autoload?(:EagerClass)) assert_nil(EagerModule.autoload?(:OtherEagerClass)) assert_nil(EagerModule.autoload?(:NestedEagerModule)) assert_nil(EagerModule::NestedEagerModule.autoload?(:NestedEagerClass)) assert EagerModule::NestedEagerModule::NestedEagerClass end end describe "loading nested files in the repo" do it "can load them individually" do files_to_load = Dir.glob("lib/**/tracing/*.rb") assert_equal 29, files_to_load.size, "It found all the expected files" files_to_load.each do |file| require_path = file.sub("lib/", "").sub(".rb", "") stderr_and_stdout, _status = Open3.capture2e("ruby -Ilib -e 'require \"#{require_path}\"'") assert_equal "", stderr_and_stdout, "It loads #{require_path.inspect} in isolation" stderr_and_stdout, _status = Open3.capture2e("ruby -Ilib -e 'require \"graphql\"; require \"#{require_path}\"'") assert_equal "", stderr_and_stdout, "It loads #{require_path.inspect} after loading graphql" end end end end graphql-ruby-2.5.19/spec/graphql/backtrace_spec.rb000066400000000000000000000311671514115062600221040ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Backtrace do class LazyError def raise_err raise "Lazy Boom" end end class ErrorAnalyzer < GraphQL::Analysis::Analyzer def on_enter_operation_definition(node, parent_node, visitor) if node.name == "raiseError" raise GraphQL::AnalysisError, "this should not be wrapped by a backtrace, but instead, returned to the client" end end def result end end class NilInspectObject # Oops, this is evil, but it happens and we should handle it. def inspect; nil; end end module ErrorTrace def initialize(required_arg:, **_rest) super(**_rest) end def execute_multiplex(multiplex:) super raise "Instrumentation Boom" end end let(:resolvers) { { "Query" => { "field1" => Proc.new { :something }, "field2" => Proc.new { :something }, "nilInspect" => Proc.new { NilInspectObject.new }, "nestedList" => Proc.new { [ { thing: { name: "abc" } }, { thing: { name: :boom } } ] }, }, "Thing" => { "name" => Proc.new { |obj| obj[:name] == :boom ? raise("Boom!") : obj[:name] }, "listField" => Proc.new { :not_a_list }, "raiseField" => Proc.new { |o, a| raise("This is broken: #{a[:message]}") }, "executionError" => Proc.new { raise GraphQL::ExecutionError, "Client-facing error" } }, "ThingWrapper" => { "thing" => Proc.new { |obj| obj[:thing] }, }, "OtherThing" => { "strField" => Proc.new { LazyError.new }, }, } } let(:schema) { defn = <<-GRAPHQL type Query { field1: Thing field2: OtherThing nilInspect: Thing nestedList: [ThingWrapper] } type Thing { name: String listField: [OtherThing] raiseField(message: String!): Int executionError: Int } type ThingWrapper { thing: Thing } type OtherThing { strField: String } GRAPHQL schema_class = GraphQL::Schema.from_definition(defn, default_resolve: resolvers) schema_class.class_exec { lazy_resolve(LazyError, :raise_err) query_analyzer(ErrorAnalyzer) } schema_class } let(:backtrace_schema) { Class.new(schema) do use GraphQL::Backtrace end } describe "GraphQL backtrace helpers" do it "raises a TracedError when enabled" do assert_raises(GraphQL::Backtrace::TracedError) { backtrace_schema.execute("query BrokenList { field1 { listField { strField } } }") } assert_raises(GraphQL::Execution::Interpreter::ListResultFailedError) { schema.execute("query BrokenList { field1 { listField { strField } } }") } end it "works for objects inside lists" do assert_raises(GraphQL::Backtrace::TracedError) do backtrace_schema.execute("{ nestedList { thing { name } } }") end end it "doesn't wrap GraphQL::ExecutionError" do assert_equal ["Client-facing error"], backtrace_schema.execute("{ field1 { executionError } }")["errors"].map { |e| e["message"] } end it "annotates crashes from user code" do err = assert_raises(GraphQL::Backtrace::TracedError) { backtrace_schema.execute <<-GRAPHQL, root_value: "Root" query($msg: String = \"Boom\") { field1 { boomError: raiseField(message: $msg) } } GRAPHQL } # The original error info is present assert_instance_of RuntimeError, err.cause b = err.cause.backtrace assert_backtrace_includes(b, file: "backtrace_spec.rb", method: "block") assert_backtrace_includes(b, file: "field.rb", method: "resolve") assert_backtrace_includes(b, file: "runtime.rb", method: "evaluate_selection") assert_backtrace_includes(b, file: "interpreter.rb", method: "run_all") # GraphQL backtrace is present expected_graphql_backtrace = [ "3:13: Thing.raiseField as boomError", "2:11: Query.field1", "1:9: query", ] assert_equal expected_graphql_backtrace, err.graphql_backtrace hash_inspect = { message: "Boom" }.inspect # The message includes the GraphQL context rendered_table = [ 'Loc | Field | Object | ' + "Arguments".ljust(hash_inspect.size) + ' | Result', '3:13 | Thing.raiseField as boomError | :something | ' + hash_inspect + ' | #', '2:11 | Query.field1 | "Root" | ' + "{}".ljust(hash_inspect.size) + ' | {}', '1:9 | query | "Root" | ' + {"msg" => "Boom"}.inspect.ljust(hash_inspect.size) + ' | {field1: {...}}', ].join("\n") assert_includes err.message, "\n" + rendered_table # The message includes the original error message assert_includes err.message, "This is broken: Boom" assert_includes err.message, "spec/graphql/backtrace_spec.rb:49", "It includes the original backtrace" assert_includes err.message, "more lines" end it "annotates crashes from user code when using inline fragments" do err = assert_raises(GraphQL::Backtrace::TracedError) { backtrace_schema.execute <<-GRAPHQL, root_value: "Root" query($msg: String = \"Boom\") { field1 { ... on Thing { boomError: raiseField(message: $msg) } } } GRAPHQL } # GraphQL backtrace is present expected_graphql_backtrace = [ "4:15: Thing.raiseField as boomError", "2:11: Query.field1", "1:9: query", ] assert_equal expected_graphql_backtrace, err.graphql_backtrace hash_inspect = { message: "Boom" }.inspect # The message includes the GraphQL context rendered_table = [ 'Loc | Field | Object | ' + "Arguments".ljust(hash_inspect.size) + ' | Result', '4:15 | Thing.raiseField as boomError | :something | ' + hash_inspect + ' | #', '2:11 | Query.field1 | "Root" | ' + "{}".ljust(hash_inspect.size) + ' | {}', '1:9 | query | "Root" | ' + {"msg" => "Boom"}.inspect.ljust(hash_inspect.size) + ' | {field1: {...}}', ].join("\n") assert_includes err.message, "\n" + rendered_table # The message includes the original error message assert_includes err.message, "This is broken: Boom" assert_includes err.message, "spec/graphql/backtrace_spec.rb:49", "It includes the original backtrace" assert_includes err.message, "more lines" end it "annotates errors from Query#result" do query_str = "query StrField { field2 { strField } __typename }" context = { backtrace: true } query = GraphQL::Query.new(schema, query_str, context: context) err = assert_raises(GraphQL::Backtrace::TracedError) { query.result } assert_instance_of RuntimeError, err.cause end it "annotates errors inside lazy resolution" do # Test context-based flag err = assert_raises(GraphQL::Backtrace::TracedError) { schema.execute("query StrField { field2 { strField } __typename }", context: { backtrace: true }) } assert_instance_of RuntimeError, err.cause b = err.cause.backtrace assert_backtrace_includes(b, file: "backtrace_spec.rb", method: "raise_err") assert_backtrace_includes(b, file: "schema.rb", method: "sync_lazy") assert_backtrace_includes(b, file: "interpreter.rb", method: "run_all") expected_graphql_backtrace = [ "1:27: OtherThing.strField", "1:18: Query.field2", "1:1: query StrField", ] assert_equal(expected_graphql_backtrace, err.graphql_backtrace) rendered_table = [ 'Loc | Field | Object | Arguments | Result', '1:27 | OtherThing.strField | :something | {} | #', '1:18 | Query.field2 | nil | {} | {strField: (unresolved)}', '1:1 | query StrField | nil | {} | {field2: {...}, __typename: "Query"}', ].join("\n") assert_includes err.message, rendered_table end it "returns analysis errors to the client" do res = backtrace_schema.execute("query raiseError { __typename }") assert_equal "this should not be wrapped by a backtrace, but instead, returned to the client", res["errors"].first["message"] end it "always stringifies the #inspect response" do # test the schema plugin err = assert_raises(GraphQL::Backtrace::TracedError) { backtrace_schema.execute("query { nilInspect { raiseField(message: \"pop!\") } }") } hash_inspect = {message: "pop!"}.inspect # `=>` on Ruby < 3.4 rendered_table = [ 'Loc | Field | Object | ' + "Arguments".ljust(hash_inspect.size) + ' | Result', '1:22 | Thing.raiseField | | ' + hash_inspect + ' | #', '1:9 | Query.nilInspect | nil | ' + "{}".ljust(hash_inspect.size) + ' | {}', '1:1 | query | nil | ' + "{}".ljust(hash_inspect.size) + ' | {nilInspect: {...}}', '', '' ].join("\n") table = err.message.split("GraphQL Backtrace:\n").last assert_equal rendered_table, table end it "raises original exception instead of a TracedError when error does not occur during resolving" do instrumentation_schema = Class.new(schema) do trace_with(ErrorTrace, required_arg: true) end assert_raises(RuntimeError) { instrumentation_schema.execute(GraphQL::Introspection::INTROSPECTION_QUERY, context: { backtrace: true }) } end end # This will get brittle when execution code moves between files # but I'm not sure how to be sure that the backtrace contains the right stuff! def assert_backtrace_includes(backtrace, file:, method:) includes_tag = if RUBY_VERSION < "3.4" backtrace.any? { |s| s.include?(file) && s.include?("`" + method) } elsif method == "block" backtrace.any? { |s| s.include?(file) && s.include?("'block") } else backtrace.any? { |s| s.include?(file) && s.include?("#{method}'") } end assert includes_tag, "Backtrace should include #{file} inside method #{method}\n\n#{backtrace.join("\n")}" end it "works with stand-alone validation" do res = backtrace_schema.validate("{ __typename }") assert_equal [], res end it "works with stand-alone analysis" do example_analyzer = Class.new(GraphQL::Analysis::Analyzer) do def result :finished end end query = GraphQL::Query.new(backtrace_schema, "{ __typename }") result = GraphQL::Analysis.analyze_query(query, [example_analyzer]) assert_equal [:finished], result end it "works with multiplex analysis" do example_analyzer = Class.new(GraphQL::Analysis::Analyzer) do def result :finished end end query = GraphQL::Query.new(backtrace_schema, "{ __typename }") multiplex = GraphQL::Execution::Multiplex.new( schema: schema, queries: [query], context: {}, max_complexity: nil, ) result = GraphQL::Analysis.analyze_multiplex(multiplex, [example_analyzer]) assert_equal [:finished], result end it "works with multiplex queries" do res = backtrace_schema.multiplex([ { query: 'query { __typename }' }, { query: 'query { __typename }' }, ]) expected_res = [ {"data" => { "__typename" => "Query" }}, {"data" => { "__typename" => "Query" }}, ] assert_graphql_equal expected_res, res end it "includes other trace modules when backtrace is active" do custom_trace = Module.new schema = Class.new(GraphQL::Schema) do trace_with(custom_trace) end query = GraphQL::Query.new(schema, "{ __typename }", context: { backtrace: true }) assert_includes query.current_trace.class.ancestors, custom_trace end describe "When validators are used" do class ValidatorBacktraceSchema < GraphQL::Schema class Query < GraphQL::Schema::Object field :greeting, String do argument :name, String, validates: { length: { minimum: 5 }} end def greeting(name:) "Hello, #{name}!" end end query(Query) use GraphQL::Backtrace end it "works properly" do assert_equal "Hello, Albert!", ValidatorBacktraceSchema.execute("{ greeting(name: \"Albert\") }")["data"]["greeting"] assert_equal ["name is too short (minimum is 5)"], ValidatorBacktraceSchema.execute("{ greeting(name: \"Tim\") }")["errors"].map { |e| e["message"] } end end end graphql-ruby-2.5.19/spec/graphql/cop/000077500000000000000000000000001514115062600173775ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/graphql/cop/default_null_true_spec.rb000066400000000000000000000013551514115062600244570ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe "GraphQL::Cop::DefaultNullTrue" do include RubocopTestHelpers it "finds and autocorrects `null: true` field configurations" do result = run_rubocop_on("spec/fixtures/cop/null_true.rb") assert_equal 3, rubocop_errors(result) assert_includes result, <<-RUBY field :name, String, null: true ^^^^^^^^^^ RUBY assert_includes result, <<-RUBY null: true, ^^^^^^^^^^ RUBY assert_includes result, <<-RUBY field :described, [String, null: true], null: true, description: "Something" ^^^^^^^^^^ RUBY assert_rubocop_autocorrects_all("spec/fixtures/cop/null_true.rb") end end graphql-ruby-2.5.19/spec/graphql/cop/default_required_true_spec.rb000066400000000000000000000017451514115062600253300ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe "GraphQL::Cop::DefaultRequiredTrue" do include RubocopTestHelpers it "finds and autocorrects `required: true` argument configurations" do result = run_rubocop_on("spec/fixtures/cop/required_true.rb") assert_equal 4, rubocop_errors(result) assert_includes result, <<-RUBY argument :id_1, ID, required: true ^^^^^^^^^^^^^^ RUBY assert_includes result, <<-RUBY required: true, ^^^^^^^^^^^^^^ RUBY assert_includes result, <<-RUBY argument :id_3, ID, other_config: { something: false, required: true }, required: true, description: \"Something\" ^^^^^^^^^^^^^^ RUBY assert_includes result, <<-RUBY f.argument(:id_1, ID, required: true) ^^^^^^^^^^^^^^ RUBY assert_rubocop_autocorrects_all("spec/fixtures/cop/required_true.rb") end end graphql-ruby-2.5.19/spec/graphql/cop/field_type_in_block_spec.rb000066400000000000000000000032071514115062600247240ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe "GraphQL::Cop::FieldTypeInBlock" do include RubocopTestHelpers it "finds and autocorrects field corrections with inline types" do result = run_rubocop_on("spec/fixtures/cop/field_type.rb") assert_equal 3, rubocop_errors(result) assert_includes result, <<-RUBY field :current_account, Types::Account, null: false, description: "The account of the current viewer" ^^^^^^^^^^^^^^ RUBY assert_includes result, <<-RUBY field :find_account, Types::Account do ^^^^^^^^^^^^^^ RUBY assert_includes result, <<-RUBY field(:all_accounts, [Types::Account, null: false]) { ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUBY assert_rubocop_autocorrects_all("spec/fixtures/cop/field_type.rb") end it "works on small classes" do result = run_rubocop_on("spec/fixtures/cop/small_field_type.rb") assert_equal 1, rubocop_errors(result) end it "works with array types" do result = run_rubocop_on("spec/fixtures/cop/field_type_array.rb") assert_equal 1, rubocop_errors(result) assert_includes result, <<-RUBY field :bar, [Thing], null: false do ^^^^^^^ RUBY assert_rubocop_autocorrects_all("spec/fixtures/cop/field_type_array.rb") end it "Works with interfaces" do result = run_rubocop_on("spec/fixtures/cop/field_type_interface.rb") assert_equal 1, rubocop_errors(result) assert_includes result, <<-RUBY field :thing, Thing ^^^^^ RUBY assert_rubocop_autocorrects_all("spec/fixtures/cop/field_type_interface.rb") end end graphql-ruby-2.5.19/spec/graphql/cop/root_types_in_block_spec.rb000066400000000000000000000012611514115062600250050ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe "GraphQL::Cop::RootTypesInBlock" do include RubocopTestHelpers it "finds and autocorrects field corrections with inline types" do result = run_rubocop_on("spec/fixtures/cop/root_types.rb") assert_equal 3, rubocop_errors(result) assert_includes result, <<-RUBY query Types::Query ^^^^^^^^^^^^^^^^^^ RUBY assert_includes result, <<-RUBY mutation Types::Mutation ^^^^^^^^^^^^^^^^^^^^^^^^ RUBY assert_includes result, <<-RUBY subscription Types::Subscription ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUBY assert_rubocop_autocorrects_all("spec/fixtures/cop/root_types.rb") end end graphql-ruby-2.5.19/spec/graphql/current_spec.rb000066400000000000000000000033461514115062600216450ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Current do describe "when no query is running" do it "returns nil for things" do assert_nil GraphQL::Current.operation_name assert_nil GraphQL::Current.field assert_nil GraphQL::Current.dataloader_source_class end end describe "in queries" do class CurrentSchema < GraphQL::Schema class ThingSource < GraphQL::Dataloader::Source def initialize(context) @context = context end def fetch(names) @context[:current_operation_name] << GraphQL::Current.operation_name @context[:current_source] << GraphQL::Current.dataloader_source_class names end end class Thing < GraphQL::Schema::Object field :name, String def name context[:current_field] << GraphQL::Current.field.path context.dataloader.with(ThingSource, context).load("thing") end end class Query < GraphQL::Schema::Object field :thing, Thing def thing context[:current_field] << GraphQL::Current.field.path :thing end end query(Query) use GraphQL::Dataloader end it "returns execution information" do ctx = { current_field: [], current_source: [], current_operation_name: [] } res = CurrentSchema.execute("query GetThingName { thing { name } }", context: ctx) assert_equal "thing", res["data"]["thing"]["name"] assert_equal ["GetThingName"], ctx[:current_operation_name] assert_equal [CurrentSchema::ThingSource], ctx[:current_source] assert_equal ["Query.thing", "Thing.name"], ctx[:current_field] end end end graphql-ruby-2.5.19/spec/graphql/dataloader/000077500000000000000000000000001514115062600207165ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/graphql/dataloader/active_record_association_source_spec.rb000066400000000000000000000225231514115062600310460ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Dataloader::ActiveRecordAssociationSource do if testing_rails? class VulfpeckSchema < GraphQL::Schema class Album < GraphQL::Schema::Object field :name, String end class Band < GraphQL::Schema::Object field :albums, [Album] do argument :genre, String, required: false argument :reverse, Boolean, required: false, default_value: false argument :unscoped, Boolean, required: false, default_value: false end def albums(genre: nil, reverse:, unscoped:) if unscoped scope = nil else scope = ::Album if genre scope = scope.where(band_genre: genre) end scope = if reverse scope.order(name: :desc) else scope.order(:name) end end dataload_association(:albums, scope: scope) end end class Query < GraphQL::Schema::Object field :band, Band do argument :name, String end def band(name:) ::Band.find_by(name: name) end end query(Query) use GraphQL::Dataloader end it "works with different scopes on the same object at runtime" do query_str = <<~GRAPHQL { band(name: "Vulfpeck") { allAlbums: albums { name } unscopedAlbums: albums(unscoped: true) { name } reverseAlbums: albums(reverse: true) { name } countryAlbums: albums(genre: "country") { name } } } GRAPHQL result = VulfpeckSchema.execute(query_str) assert_equal ["Mit Peck", "My First Car"], result["data"]["band"]["allAlbums"].map { |a| a["name"] } assert_equal ["Mit Peck", "My First Car"], result["data"]["band"]["unscopedAlbums"].map { |a| a["name"] } assert_equal ["My First Car", "Mit Peck"], result["data"]["band"]["reverseAlbums"].map { |a| a["name"] } assert_equal [], result["data"]["band"]["countryAlbums"] end it_dataloads "queries for associated records when the association isn't already loaded" do |d| my_first_car = ::Album.find(2) homey = ::Album.find(4) log = with_active_record_log(colorize: false) do vulfpeck, chon = d.with(GraphQL::Dataloader::ActiveRecordAssociationSource, :band).load_all([my_first_car, homey]) assert_equal "Vulfpeck", vulfpeck.name assert_equal "Chon", chon.name end assert_includes log, '[["id", 1], ["id", 3]]' toms_story = ::Album.find(3) log = with_active_record_log(colorize: false) do vulfpeck, chon, toms_story_band = d.with(GraphQL::Dataloader::ActiveRecordAssociationSource, :band).load_all([my_first_car, homey, toms_story]) assert_equal "Vulfpeck", vulfpeck.name assert_equal "Chon", chon.name assert_equal "Tom's Story", toms_story_band.name end assert_includes log, '[["id", 2]]' end it_dataloads "doesn't load records that are already cached by ActiveRecordSource" do |d| d.with(GraphQL::Dataloader::ActiveRecordSource, Band).load_all([1,2,3]) my_first_car = ::Album.find(2) homey = ::Album.find(4) toms_story = ::Album.find(3) log = with_active_record_log(colorize: false) do vulfpeck, chon, toms_story_band = d.with(GraphQL::Dataloader::ActiveRecordAssociationSource, :band).load_all([my_first_car, homey, toms_story]) assert_equal "Vulfpeck", vulfpeck.name assert_equal "Chon", chon.name assert_equal "Tom's Story", toms_story_band.name end assert_equal "", log end it_dataloads "warms the cache for ActiveRecordSource" do |d| my_first_car = ::Album.find(2) homey = ::Album.find(4) toms_story = ::Album.find(3) d.with(GraphQL::Dataloader::ActiveRecordAssociationSource, :band).load_all([my_first_car, homey, toms_story]) log = with_active_record_log(colorize: false) do d.with(GraphQL::Dataloader::ActiveRecordSource, Band).load_all([1,2,3]) end assert_equal "", log end it_dataloads "doesn't warm the cache when a scope is given" do |d| my_first_car = ::Album.find(2) homey = ::Album.find(4) summerteeth = ::Album.find(6) results = d.with(GraphQL::Dataloader::ActiveRecordAssociationSource, :band, ::Band.country).load_all([my_first_car, homey, summerteeth]) assert_equal [nil, nil, ::Band.find(4)], results log = with_active_record_log(colorize: false) do d.with(GraphQL::Dataloader::ActiveRecordSource, Band).load_all([1,2,4]) end assert_includes log, "SELECT \"bands\".* FROM \"bands\" WHERE \"bands\".\"id\" IN (?, ?, ?) [[\"id\", 1], [\"id\", 2], [\"id\", 4]]" end it_dataloads "doesn't pause when the association is already loaded" do |d| source = d.with(GraphQL::Dataloader::ActiveRecordAssociationSource, :band) assert_equal 0, source.results.size assert_equal 0, source.pending.size my_first_car = ::Album.find(2) vulfpeck = my_first_car.band vulfpeck2 = source.load(my_first_car) assert_equal vulfpeck, vulfpeck2 assert_equal 0, source.results.size assert_equal 0, source.pending.size my_first_car.reload vulfpeck3 = source.load(my_first_car) assert_equal vulfpeck, vulfpeck3 assert_equal 1, source.results.size assert_equal 0, source.pending.size end it_dataloads "raises an error with a non-existent association" do |d| my_first_car = ::Album.find(2) source = d.with(GraphQL::Dataloader::ActiveRecordAssociationSource, :tour_bus) assert_raises ActiveRecord::AssociationNotFoundError do source.load(my_first_car) end end it_dataloads "works with polymorphic associations" do |d| wilco = ::Band.find(4) vulfpeck = d.with(GraphQL::Dataloader::ActiveRecordAssociationSource, :thing).load(wilco) assert_equal ::Band.find(1), vulfpeck end it_dataloads "works with collection associations" do |d| wilco = ::Band.find(4) chon = ::Band.find(3) albums_by_band = nil log = with_active_record_log(colorize: false) do albums_by_band = d.with(GraphQL::Dataloader::ActiveRecordAssociationSource, :albums).load_all([wilco, chon]) end assert_equal [[6], [4, 5]], albums_by_band.map { |al| al.map(&:id) } assert_includes log, 'SELECT "albums".* FROM "albums" WHERE "albums"."band_id" IN (?, ?) [["band_id", 4], ["band_id", 3]]' albums = nil log = with_active_record_log(colorize: false) do albums = d.with(GraphQL::Dataloader::ActiveRecordSource, Album).load_all([3,4,5,6]) end assert_equal [3,4,5,6], albums.map(&:id) assert_includes log, 'WHERE "albums"."id" = ? [["id", 3]]' end it_dataloads "works with collection associations with scope" do |d| wilco = ::Band.find(4) chon = ::Band.find(3) albums_by_band = nil one_month_ago = nil log = with_active_record_log(colorize: false) do one_month_ago = 1.month.ago.end_of_day albums_by_band_1 = d.with(GraphQL::Dataloader::ActiveRecordAssociationSource, :albums, Album.where("created_at >= ?", one_month_ago)).request(wilco) albums_by_band_2 = d.with(GraphQL::Dataloader::ActiveRecordAssociationSource, :albums, Album.where("created_at >= ?", one_month_ago)).request(chon) albums_by_band_3 = d.with(GraphQL::Dataloader::ActiveRecordAssociationSource, :albums, Album.where("created_at <= ?", one_month_ago)).request(wilco) albums_by_band = [albums_by_band_1.load, albums_by_band_2.load, albums_by_band_3.load] end assert_equal [[6], [4, 5], []], albums_by_band.map { |al| al.map(&:id) } expected_log = if Rails::VERSION::STRING > "8" 'SELECT "albums".* FROM "albums" WHERE (created_at >= ?) AND "albums"."band_id" IN (?, ?)' else 'SELECT "albums".* FROM "albums" WHERE (created_at >= ' + one_month_ago.utc.strftime("'%Y-%m-%d %H:%M:%S.%6N'") + ') AND "albums"."band_id" IN (?, ?)' end assert_includes log, expected_log albums = nil log = with_active_record_log(colorize: false) do albums = d.with(GraphQL::Dataloader::ActiveRecordSource, Album).load_all([3,4,5,6]) end assert_equal [3,4,5,6], albums.map(&:id) assert_includes log, 'WHERE "albums"."id" IN (?, ?, ?, ?) [["id", 3], ["id", 4], ["id", 5], ["id", 6]]' end if Rails::VERSION::STRING > "7.1" # not supported in <7.1 it_dataloads "loads with composite primary keys and warms the cache" do |d| my_first_car = ::Album.find(2) homey = ::Album.find(4) log = with_active_record_log(colorize: false) do vulfpeck, chon = d.with(GraphQL::Dataloader::ActiveRecordAssociationSource, :composite_band).load_all([my_first_car, homey]) assert_equal "Vulfpeck", vulfpeck.name assert_equal "Chon", chon.name end assert_includes log, '[["name", "Vulfpeck"], ["name", "Chon"], ["genre", 0]]' log = with_active_record_log(colorize: false) do d.with(GraphQL::Dataloader::ActiveRecordSource, CompositeBand).load_all([["Vulfpeck", "rock"], ["Chon", :rock]]) end assert_equal "", log end end end end graphql-ruby-2.5.19/spec/graphql/dataloader/active_record_source_spec.rb000066400000000000000000000106421514115062600264510ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Dataloader::ActiveRecordSource do if testing_rails? describe "finding by ID" do it_dataloads "loads once, then returns from a cache when available" do |d| log = with_active_record_log(colorize: false) do r1 = d.with(GraphQL::Dataloader::ActiveRecordSource, Band).load(1) assert_equal "Vulfpeck", r1.name end assert_includes log, 'SELECT "bands".* FROM "bands" WHERE "bands"."id" = ? [["id", 1]]' log = with_active_record_log(colorize: false) do r1 = d.with(GraphQL::Dataloader::ActiveRecordSource, Band).load(1) assert_equal "Vulfpeck", r1.name end assert_equal "", log log = with_active_record_log(colorize: false) do records = d.with(GraphQL::Dataloader::ActiveRecordSource, Band).load_all([1, 99, 2, 3]) assert_equal ["Vulfpeck", nil, "Tom's Story", "Chon"], records.map { |r| r&.name } end assert_includes log, '[["id", 99], ["id", 2], ["id", 3]]' end it_dataloads "casts load values to the column type" do |d| log = with_active_record_log(colorize: false) do r1 = d.with(GraphQL::Dataloader::ActiveRecordSource, Band).load("1") assert_equal "Vulfpeck", r1.name end assert_includes log, 'SELECT "bands".* FROM "bands" WHERE "bands"."id" = ? [["id", 1]]' log = with_active_record_log(colorize: false) do d.with(GraphQL::Dataloader::ActiveRecordSource, Band).load(1) end assert_equal "", log log = with_active_record_log(colorize: false) do d.with(GraphQL::Dataloader::ActiveRecordSource, Band).load("1") end assert_equal "", log end end describe "finding by other columns" do it_dataloads "uses the alternative primary key" do |d| log = with_active_record_log(colorize: false) do r1 = d.with(GraphQL::Dataloader::ActiveRecordSource, AlternativeBand).load("Vulfpeck") assert_equal "Vulfpeck", r1.name if Rails::VERSION::STRING > "8" assert_equal 1, r1["id"] else assert_equal 1, r1._read_attribute("id") end end assert_includes log, 'SELECT "bands".* FROM "bands" WHERE "bands"."name" = ? [["name", "Vulfpeck"]]' end if Rails::VERSION::STRING > "7.1" # not supported in <7.1 it_dataloads "uses composite primary keys" do |d| log = with_active_record_log(colorize: false) do r1 = d.with(GraphQL::Dataloader::ActiveRecordSource, CompositeBand).load(["Chon", :rock]) assert_equal "Chon", r1.name assert_equal ["Chon", "rock"], r1.id if Rails::VERSION::STRING > "8" assert_equal 3, r1["id"] else assert_equal 3, r1._read_attribute("id") end end assert_includes log, 'SELECT "bands".* FROM "bands" WHERE "bands"."name" = ? AND "bands"."genre" = ? [["name", "Chon"], ["genre", 0]]' end end it_dataloads "uses specified find_by columns" do |d| log = with_active_record_log(colorize: false) do r1 = d.with(GraphQL::Dataloader::ActiveRecordSource, Band, find_by: :name).load("Chon") assert_equal "Chon", r1.name assert_equal 3, r1.id end assert_includes log, 'SELECT "bands".* FROM "bands" WHERE "bands"."name" = ? [["name", "Chon"]]' end end describe "warming the cache" do it_dataloads "can receive passed-in objects with a class" do |d| d.with(GraphQL::Dataloader::ActiveRecordSource, Band).merge({ 100 => Band.find(3) }) log = with_active_record_log(colorize: false) do band3 = d.with(GraphQL::Dataloader::ActiveRecordSource, Band).load(100) assert_equal "Chon", band3.name assert_equal 3, band3.id end assert_equal "", log end it_dataloads "can infer class of passed-in objects" do |d| d.merge_records([Band.find(3), Album.find(4)]) log = with_active_record_log(colorize: false) do band3 = d.with(GraphQL::Dataloader::ActiveRecordSource, Band).load(3) assert_equal "Chon", band3.name album4 = d.with(GraphQL::Dataloader::ActiveRecordSource, Album).load(4) assert_equal "Homey", album4.name end assert_equal "", log end end end end graphql-ruby-2.5.19/spec/graphql/dataloader/async_dataloader_spec.rb000066400000000000000000000251251514115062600255570ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" if RUBY_VERSION >= "3.2.0" require "async" describe GraphQL::Dataloader::AsyncDataloader do class AsyncSchema < GraphQL::Schema class SleepSource < GraphQL::Dataloader::Source def initialize(tag = nil) @tag = tag end def fetch(keys) max_sleep = keys.max # t1 = Time.now # puts "----- SleepSource => #{max_sleep} (from: #{keys})" sleep(max_sleep) # puts "----- SleepSource done #{max_sleep} after #{Time.now - t1}" keys.map { |_k| max_sleep } end end class WaitForSource < GraphQL::Dataloader::Source def initialize(tag) @tag = tag end def fetch(waits) max_wait = waits.max # puts "[#{Time.now.to_f}] Waiting #{max_wait} for #{@tag}" `sleep #{max_wait}` # puts "[#{Time.now.to_f}] Finished for #{@tag}" waits.map { |_w| @tag } end end class KeyWaitForSource < GraphQL::Dataloader::Source class << self attr_accessor :fetches def reset @fetches = [] end end def initialize(wait) @wait = wait end def fetch(keys) self.class.fetches << keys sleep(@wait) keys end end class FiberLocalContextSource < GraphQL::Dataloader::Source def fetch(keys) keys.map { |key| Thread.current[key] } end end class Sleeper < GraphQL::Schema::Object field :sleeper, Sleeper, null: false, resolver_method: :sleep do argument :duration, Float end def sleep(duration:) context[:key_i] ||= 0 new_key = context[:key_i] += 1 dataloader.with(SleepSource, new_key).load(duration) duration end field :duration, Float, null: false def duration; object; end end class Waiter < GraphQL::Schema::Object field :wait_for, Waiter, null: false do argument :tag, String argument :wait, Float end def wait_for(tag:, wait:) dataloader.with(WaitForSource, tag).load(wait) end field :tag, String, null: false def tag object end end class Query < GraphQL::Schema::Object field :sleep, Float, null: false do argument :duration, Float end field :sleeper, Sleeper, null: false, resolver_method: :sleep do argument :duration, Float end def sleep(duration:) context[:key_i] ||= 0 new_key = context[:key_i] += 1 dataloader.with(SleepSource, new_key).load(duration) duration end field :wait_for, Waiter, null: false do argument :tag, String argument :wait, Float end def wait_for(tag:, wait:) dataloader.with(WaitForSource, tag).load(wait) end class ListWaiter < GraphQL::Schema::Object field :waiter, Waiter def waiter dataloader.with(KeyWaitForSource, object[:wait]).load(object[:tag]) end end field :list_waiters, [ListWaiter] do argument :wait, Float argument :tags, [String] end def list_waiters(wait:, tags:) Kernel.sleep(0.1) tags.map { |t| { tag: t, wait: wait }} end field :fiber_local_context, String do argument :key, String end def fiber_local_context(key:) dataloader.with(FiberLocalContextSource).load(key) end end query(Query) use GraphQL::Dataloader::AsyncDataloader end module AsyncDataloaderAssertions def self.included(child_class) child_class.class_eval do it "works with sources" do dataloader = GraphQL::Dataloader::AsyncDataloader.new r1 = dataloader.with(AsyncSchema::SleepSource, :s1).request(0.1) r2 = dataloader.with(AsyncSchema::SleepSource, :s2).request(0.2) r3 = dataloader.with(AsyncSchema::SleepSource, :s3).request(0.3) v1 = nil dataloader.append_job { v1 = r1.load } started_at = Time.now dataloader.run ended_at = Time.now assert_equal 0.1, v1 started_at_2 = Time.now # These should take no time at all since they're already resolved v2 = r2.load v3 = r3.load ended_at_2 = Time.now assert_equal 0.2, v2 assert_equal 0.3, v3 assert_in_delta 0.0, started_at_2 - ended_at_2, 0.06, "Already-loaded values returned instantly" assert_in_delta 0.3, ended_at - started_at, 0.06, "IO ran in parallel" end it "works with GraphQL" do started_at = Time.now res = @schema.execute("{ s1: sleep(duration: 0.1) s2: sleep(duration: 0.2) s3: sleep(duration: 0.3) }") ended_at = Time.now assert_equal({"s1"=>0.1, "s2"=>0.2, "s3"=>0.3}, res["data"]) assert_in_delta 0.3, ended_at - started_at, 0.06, "IO ran in parallel" end it "runs fields by depth" do query_str = <<-GRAPHQL { s1: sleeper(duration: 0.1) { sleeper(duration: 0.1) { sleeper(duration: 0.1) { duration } } } s2: sleeper(duration: 0.2) { sleeper(duration: 0.1) { duration } } s3: sleeper(duration: 0.3) { duration } } GRAPHQL started_at = Time.now res = @schema.execute(query_str) ended_at = Time.now expected_data = { "s1" => { "sleeper" => { "sleeper" => { "duration" => 0.1 } } }, "s2" => { "sleeper" => { "duration" => 0.1 } }, "s3" => { "duration" => 0.3 } } assert_graphql_equal expected_data, res["data"] assert_in_delta 0.5, ended_at - started_at, 0.06, "Each depth ran in parallel" end it "runs dataloaders in parallel across branches" do query_str = <<-GRAPHQL { w1: waitFor(tag: "a", wait: 0.2) { waitFor(tag: "b", wait: 0.2) { waitFor(tag: "c", wait: 0.2) { tag } } } # After the first, these are returned eagerly from cache w2: waitFor(tag: "a", wait: 0.2) { waitFor(tag: "a", wait: 0.2) { waitFor(tag: "a", wait: 0.2) { tag } } } w3: waitFor(tag: "a", wait: 0.2) { waitFor(tag: "b", wait: 0.2) { waitFor(tag: "d", wait: 0.2) { tag } } } w4: waitFor(tag: "e", wait: 0.6) { tag } } GRAPHQL started_at = Time.now res = @schema.execute(query_str) ended_at = Time.now expected_data = { "w1" => { "waitFor" => { "waitFor" => { "tag" => "c" } } }, "w2" => { "waitFor" => { "waitFor" => { "tag" => "a" } } }, "w3" => { "waitFor" => { "waitFor" => { "tag" => "d" } } }, "w4" => { "tag" => "e" } } assert_graphql_equal expected_data, res["data"] # We've basically got two options here: # - Put all jobs in the same queue (fields and sources), but then you don't get predictable batching. # - Work one-layer-at-a-time, but then layers can get stuck behind one another. That's what's implemented here. assert_in_delta 1.0, ended_at - started_at, 0.06, "Sources were executed in parallel" end it "groups across list items" do query_str = <<-GRAPHQL { listWaiters(wait: 0.2, tags: ["a", "b", "c"]) { waiter { tag } } } GRAPHQL t1 = Time.now result = @schema.execute(query_str) t2 = Time.now assert_equal ["a", "b", "c"], result["data"]["listWaiters"].map { |lw| lw["waiter"]["tag"]} # The field itself waits 0.1 assert_in_delta 0.3, t2 - t1, 0.06, "Wait was parallel" assert_equal [["a", "b", "c"]], AsyncSchema::KeyWaitForSource.fetches, "All keys were fetched at once" end it 'copies fiber-local variables over to sources' do key = 'arbitrary_context' value = 'test' Thread.current[key] = value query_str = <<-GRAPHQL { fiberLocalContext(key: "#{key}") } GRAPHQL result = @schema.execute(query_str) assert_equal value, result['data']['fiberLocalContext'] end end end end describe "with async" do before do @schema = AsyncSchema AsyncSchema::KeyWaitForSource.reset end include AsyncDataloaderAssertions end describe "with perfetto trace turned on" do class TraceAsyncSchema < AsyncSchema trace_with GraphQL::Tracing::PerfettoTrace use GraphQL::Dataloader::AsyncDataloader end before do @schema = TraceAsyncSchema AsyncSchema::KeyWaitForSource.reset end include AsyncDataloaderAssertions include PerfettoSnapshot it "produces a trace" do query_str = <<-GRAPHQL { s1: sleeper(duration: 0.1) { sleeper(duration: 0.1) { sleeper(duration: 0.1) { duration } } } s2: sleeper(duration: 0.2) { sleeper(duration: 0.1) { duration } } s3: sleeper(duration: 0.3) { duration } } GRAPHQL res = @schema.execute(query_str) if ENV["DUMP_PERFETTO"] res.context.query.current_trace.write(file: "perfetto.dump") end json = res.context.query.current_trace.write(file: nil, debug_json: true) data = JSON.parse(json) check_snapshot(data, "example.json") end end end end graphql-ruby-2.5.19/spec/graphql/dataloader/nonblocking_dataloader_spec.rb000066400000000000000000000203741514115062600267460ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" if Fiber.respond_to?(:scheduler) # Ruby 3+ describe "GraphQL::Dataloader::NonblockingDataloader" do class NonblockingSchema < GraphQL::Schema class SleepSource < GraphQL::Dataloader::Source def fetch(keys) max_sleep = keys.max # t1 = Time.now # puts "----- SleepSource => #{max_sleep} " sleep(max_sleep) # puts "----- SleepSource done #{max_sleep} after #{Time.now - t1}" keys.map { |_k| max_sleep } end end class WaitForSource < GraphQL::Dataloader::Source def initialize(tag) @tag = tag end def fetch(waits) max_wait = waits.max # puts "[#{Time.now.to_f}] Waiting #{max_wait} for #{@tag}" `sleep #{max_wait}` # puts "[#{Time.now.to_f}] Finished for #{@tag}" waits.map { |_w| @tag } end end class Sleeper < GraphQL::Schema::Object field :sleeper, Sleeper, null: false, resolver_method: :sleep do argument :duration, Float end def sleep(duration:) `sleep #{duration}` duration end field :duration, Float, null: false def duration; object; end end class Waiter < GraphQL::Schema::Object field :wait_for, Waiter, null: false do argument :tag, String argument :wait, Float end def wait_for(tag:, wait:) dataloader.with(WaitForSource, tag).load(wait) end field :tag, String, null: false def tag object end end class Query < GraphQL::Schema::Object field :sleep, Float, null: false do argument :duration, Float end field :sleeper, Sleeper, null: false, resolver_method: :sleep do argument :duration, Float end def sleep(duration:) `sleep #{duration}` duration end field :wait_for, Waiter, null: false do argument :tag, String argument :wait, Float end def wait_for(tag:, wait:) dataloader.with(WaitForSource, tag).load(wait) end end query(Query) use GraphQL::Dataloader, nonblocking: true end def with_scheduler Fiber.set_scheduler(scheduler_class.new) yield ensure Fiber.set_scheduler(nil) end module NonblockingDataloaderAssertions def self.included(child_class) child_class.class_eval do it "runs IO in parallel by default" do dataloader = GraphQL::Dataloader.new(nonblocking: true) results = {} dataloader.append_job { sleep(0.1); results[:a] = 1 } dataloader.append_job { sleep(0.2); results[:b] = 2 } dataloader.append_job { sleep(0.3); results[:c] = 3 } assert_equal({}, results, "Nothing ran yet") started_at = Time.now with_scheduler { dataloader.run } ended_at = Time.now assert_equal({ a: 1, b: 2, c: 3 }, results, "All the jobs ran") assert_in_delta 0.3, ended_at - started_at, 0.06, "IO ran in parallel" end it "works with sources" do dataloader = GraphQL::Dataloader.new(nonblocking: true) r1 = dataloader.with(NonblockingSchema::SleepSource).request(0.1) r2 = dataloader.with(NonblockingSchema::SleepSource).request(0.2) r3 = dataloader.with(NonblockingSchema::SleepSource).request(0.3) v1 = nil dataloader.append_job { v1 = r1.load } started_at = Time.now with_scheduler { dataloader.run } ended_at = Time.now assert_equal 0.3, v1 started_at_2 = Time.now # These should take no time at all since they're already resolved v2 = r2.load v3 = r3.load ended_at_2 = Time.now assert_equal 0.3, v2 assert_equal 0.3, v3 assert_in_delta 0.0, started_at_2 - ended_at_2, 0.06, "Already-loaded values returned instantly" assert_in_delta 0.3, ended_at - started_at, 0.06, "IO ran in parallel" end it "works with GraphQL" do started_at = Time.now res = with_scheduler { NonblockingSchema.execute("{ s1: sleep(duration: 0.1) s2: sleep(duration: 0.2) s3: sleep(duration: 0.3) }") } ended_at = Time.now assert_equal({"s1"=>0.1, "s2"=>0.2, "s3"=>0.3}, res["data"]) assert_in_delta 0.3, ended_at - started_at, 0.06, "IO ran in parallel" end it "nested fields don't wait for slower higher-level fields" do query_str = <<-GRAPHQL { s1: sleeper(duration: 0.1) { sleeper(duration: 0.1) { sleeper(duration: 0.1) { duration } } } s2: sleeper(duration: 0.2) { sleeper(duration: 0.1) { duration } } s3: sleeper(duration: 0.3) { duration } } GRAPHQL started_at = Time.now res = with_scheduler { NonblockingSchema.execute(query_str) } ended_at = Time.now expected_data = { "s1" => { "sleeper" => { "sleeper" => { "duration" => 0.1 } } }, "s2" => { "sleeper" => { "duration" => 0.1 } }, "s3" => { "duration" => 0.3 } } assert_graphql_equal expected_data, res["data"] assert_in_delta 0.3, ended_at - started_at, 0.06, "Fields ran without any waiting" end it "runs dataloaders in parallel across branches" do query_str = <<-GRAPHQL { w1: waitFor(tag: "a", wait: 0.2) { waitFor(tag: "b", wait: 0.2) { waitFor(tag: "c", wait: 0.2) { tag } } } # After the first, these are returned eagerly from cache w2: waitFor(tag: "a", wait: 0.2) { waitFor(tag: "a", wait: 0.2) { waitFor(tag: "a", wait: 0.2) { tag } } } w3: waitFor(tag: "a", wait: 0.2) { waitFor(tag: "b", wait: 0.2) { waitFor(tag: "d", wait: 0.2) { tag } } } w4: waitFor(tag: "e", wait: 0.6) { tag } } GRAPHQL started_at = Time.now res = with_scheduler do NonblockingSchema.execute(query_str) end ended_at = Time.now expected_data = { "w1" => { "waitFor" => { "waitFor" => { "tag" => "c" } } }, "w2" => { "waitFor" => { "waitFor" => { "tag" => "a" } } }, "w3" => { "waitFor" => { "waitFor" => { "tag" => "d" } } }, "w4" => { "tag" => "e" } } assert_graphql_equal expected_data, res["data"] # We've basically got two options here: # - Put all jobs in the same queue (fields and sources), but then you don't get predictable batching. # - Work one-layer-at-a-time, but then layers can get stuck behind one another. That's what's implemented here. assert_in_delta 1.0, ended_at - started_at, 0.5, "Sources were executed in parallel" end end end end describe "With the toy scheduler from Ruby's tests" do let(:scheduler_class) { ::DummyScheduler } include NonblockingDataloaderAssertions end if RUBY_ENGINE == "ruby" && !ENV["GITHUB_ACTIONS"] describe "With libev_scheduler" do require "libev_scheduler" let(:scheduler_class) { Libev::Scheduler } include NonblockingDataloaderAssertions end describe "with evt" do require "evt" let(:scheduler_class) { Evt::Scheduler } include NonblockingDataloaderAssertions end end end end graphql-ruby-2.5.19/spec/graphql/dataloader/snapshots/000077500000000000000000000000001514115062600227405ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/graphql/dataloader/snapshots/example.json000066400000000000000000002015661514115062600253000ustar00rootroot00000000000000{ "packet": [ { "trustedPacketSequenceId": 101010101010, "sequenceFlags": 101010101010, "previousPacketDropped": true, "trackDescriptor": { "uuid": "10101010101010", "name": "Main Thread", "childOrdering": "CHRONOLOGICAL" }, "firstPacketOnSequence": true }, { "trustedPacketSequenceId": 101010101010, "internedData": { "eventCategories": [ { "iid": "10101010101010", "name": "Dataloader" }, { "iid": "10101010101010", "name": "Field Execution" }, { "iid": "10101010101010", "name": "ActiveSupport::Notifications" }, { "iid": "10101010101010", "name": "Authorized" }, { "iid": "10101010101010", "name": "Resolve Type" }, { "iid": "10101010101010", "name": "Debug Inspect" } ], "eventNames": [ { "iid": "10101010101010", "name": "GraphQL::Tracing::DetailedTrace#inspect_object" } ], "debugAnnotationNames": [ { "iid": "10101010101010", "name": "object" }, { "iid": "10101010101010", "name": "result" }, { "iid": "10101010101010", "name": "arguments" }, { "iid": "10101010101010", "name": "fetch keys" }, { "iid": "10101010101010", "name": "inspect instance of" }, { "iid": "10101010101010", "name": "inspecting for" } ], "debugAnnotationStringValues": [ { "iid": "10101010101010", "str": "KG5pbCk=\n" } ] }, "sequenceFlags": 101010101010 }, { "trustedPacketSequenceId": 101010101010, "sequenceFlags": 101010101010, "trackDescriptor": { "uuid": "10101010101010", "name": "Main Fiber", "parentUuid": "10101010101010", "childOrdering": "CHRONOLOGICAL" } }, { "trustedPacketSequenceId": 101010101010, "sequenceFlags": 101010101010, "trackDescriptor": { "uuid": "10101010101010", "name": "Allocated Objects", "parentUuid": "10101010101010", "counter": {} } }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_COUNTER", "trackUuid": "10101010101010", "counterValue": "10101010101010" }, "sequenceFlags": 101010101010 }, { "trustedPacketSequenceId": 101010101010, "sequenceFlags": 101010101010, "trackDescriptor": { "uuid": "10101010101010", "name": "Active Fibers", "parentUuid": "10101010101010", "counter": {} } }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_COUNTER", "trackUuid": "10101010101010", "counterValue": "10101010101010" }, "sequenceFlags": 101010101010 }, { "trustedPacketSequenceId": 101010101010, "sequenceFlags": 101010101010, "trackDescriptor": { "uuid": "10101010101010", "name": "Resolved Fields", "parentUuid": "10101010101010", "counter": {} } }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_COUNTER", "trackUuid": "10101010101010", "counterValue": "10101010101010" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_BEGIN", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "name": "Parse", "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "debugAnnotations": [ { "nameIid": "10101010101010", "boolValue": true, "name": "valid?" }, { "nameIid": "10101010101010", "boolValue": true, "name": "validate?" } ], "type": "TYPE_SLICE_BEGIN", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "name": "Validate\n", "extraCounterTrackUuids": [ "10101010101010" ] }, "internedData": { "debugAnnotationNames": [ { "iid": "10101010101010", "name": "validate?" } ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "debugAnnotations": [ { "nameIid": "10101010101010", "stringValue": "query {\n s1: sleeper(duration: 0.1) {\n sleeper(duration: 0.1) {\n sleeper(duration: 0.1) {\n duration\n }\n }\n }\n s2: sleeper(duration: 0.2) {\n sleeper(duration: 0.1) {\n duration\n }\n }\n s3: sleeper(duration: 0.3) {\n duration\n }\n}", "name": "query_string" } ], "type": "TYPE_SLICE_BEGIN", "trackUuid": "10101010101010", "name": "Multiplex" }, "internedData": { "debugAnnotationNames": [ { "iid": "10101010101010", "name": "valid?" }, { "iid": "10101010101010", "name": "query_string" } ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "debugAnnotations": [ { "nameIid": "10101010101010", "name": "analyzers" }, { "nameIid": "10101010101010", "intValue": "10101010101010", "name": "analyzers_count" } ], "type": "TYPE_SLICE_BEGIN", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "name": "Analysis\n", "extraCounterTrackUuids": [ "10101010101010" ] }, "internedData": { "debugAnnotationNames": [ { "iid": "10101010101010", "name": "analyzers_count" }, { "iid": "10101010101010", "name": "analyzers" } ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_COUNTER", "trackUuid": "10101010101010", "counterValue": "10101010101010" }, "sequenceFlags": 101010101010 }, { "trustedPacketSequenceId": 101010101010, "sequenceFlags": 101010101010, "trackDescriptor": { "uuid": "10101010101010", "name": "Dataloader Fiber #1010", "parentUuid": "10101010101010", "childOrdering": "CHRONOLOGICAL" } }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010", "10101010101010" ], "name": "Create Execution Fiber", "extraCounterTrackUuids": [ "10101010101010", "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "trustedPacketSequenceId": 101010101010, "sequenceFlags": 101010101010, "trackDescriptor": { "uuid": "10101010101010", "name": "Exec Fiber #1010", "parentUuid": "10101010101010", "childOrdering": "CHRONOLOGICAL" } }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Authorized" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "boolValue": true, "name": "authorized?" } ], "type": "TYPE_SLICE_BEGIN", "nameIid": "10101010101010", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ], "name": "Authorize: Query" }, "internedData": { "eventNames": [ { "iid": "10101010101010", "name": "Authorize: Query" } ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Field Execution" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_SLICE_BEGIN", "trackUuid": "10101010101010", "name": "s1", "flowIds": [ "10101010101010" ] }, "internedData": { "debugAnnotationNames": [ { "iid": "10101010101010", "name": "authorized?" } ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "name": "Fiber Yield" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010", "10101010101010" ], "name": "Create Execution Fiber", "extraCounterTrackUuids": [ "10101010101010", "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "trustedPacketSequenceId": 101010101010, "sequenceFlags": 101010101010, "trackDescriptor": { "uuid": "10101010101010", "name": "Exec Fiber #1010", "parentUuid": "10101010101010", "childOrdering": "CHRONOLOGICAL" } }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Field Execution" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_SLICE_BEGIN", "trackUuid": "10101010101010", "name": "s2", "flowIds": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "name": "Fiber Yield" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010", "10101010101010" ], "name": "Create Execution Fiber", "extraCounterTrackUuids": [ "10101010101010", "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "trustedPacketSequenceId": 101010101010, "sequenceFlags": 101010101010, "trackDescriptor": { "uuid": "10101010101010", "name": "Exec Fiber #1010", "parentUuid": "10101010101010", "childOrdering": "CHRONOLOGICAL" } }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Field Execution" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_SLICE_BEGIN", "trackUuid": "10101010101010", "name": "s3", "flowIds": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "name": "Fiber Yield" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010", "10101010101010" ], "name": "Create Source Fiber", "extraCounterTrackUuids": [ "10101010101010", "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "trustedPacketSequenceId": 101010101010, "sequenceFlags": 101010101010, "trackDescriptor": { "uuid": "10101010101010", "name": "Source Fiber #1010", "parentUuid": "10101010101010", "childOrdering": "CHRONOLOGICAL" } }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "intValue": "10101010101010", "name": "@tag" }, { "nameIid": "10101010101010", "arrayValues": [ { "nameIid": "10101010101010", "doubleValue": 101010101010 } ], "name": "fetch keys" } ], "type": "TYPE_SLICE_BEGIN", "nameIid": "10101010101010", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ], "flowIds": [ "10101010101010" ], "name": "AsyncSchema::SleepSource" }, "internedData": { "eventNames": [ { "iid": "10101010101010", "name": "AsyncSchema::SleepSource" } ], "debugAnnotationNames": [ { "iid": "10101010101010", "name": "10101010101010" }, { "iid": "10101010101010", "name": "@tag" } ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010", "10101010101010" ], "name": "Create Source Fiber", "extraCounterTrackUuids": [ "10101010101010", "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "trustedPacketSequenceId": 101010101010, "sequenceFlags": 101010101010, "trackDescriptor": { "uuid": "10101010101010", "name": "Source Fiber #1010", "parentUuid": "10101010101010", "childOrdering": "CHRONOLOGICAL" } }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "intValue": "10101010101010", "name": "@tag" }, { "nameIid": "10101010101010", "arrayValues": [ { "nameIid": "10101010101010", "doubleValue": 101010101010 } ], "name": "fetch keys" } ], "type": "TYPE_SLICE_BEGIN", "nameIid": "10101010101010", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ], "flowIds": [ "10101010101010" ], "name": "AsyncSchema::SleepSource" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010", "10101010101010" ], "name": "Create Source Fiber", "extraCounterTrackUuids": [ "10101010101010", "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "trustedPacketSequenceId": 101010101010, "sequenceFlags": 101010101010, "trackDescriptor": { "uuid": "10101010101010", "name": "Source Fiber #1010", "parentUuid": "10101010101010", "childOrdering": "CHRONOLOGICAL" } }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "intValue": "10101010101010", "name": "@tag" }, { "nameIid": "10101010101010", "arrayValues": [ { "nameIid": "10101010101010", "doubleValue": 101010101010 } ], "name": "fetch keys" } ], "type": "TYPE_SLICE_BEGIN", "nameIid": "10101010101010", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ], "flowIds": [ "10101010101010" ], "name": "AsyncSchema::SleepSource" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "intValue": "10101010101010", "name": "@tag" }, { "nameIid": "10101010101010", "name": "fetch keys" } ], "type": "TYPE_SLICE_BEGIN", "nameIid": "10101010101010", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ], "name": "AsyncSchema::SleepSource" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "intValue": "10101010101010", "name": "@tag" }, { "nameIid": "10101010101010", "name": "fetch keys" } ], "type": "TYPE_SLICE_BEGIN", "nameIid": "10101010101010", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ], "name": "AsyncSchema::SleepSource" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "name": "Fiber Exit", "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "intValue": "10101010101010", "name": "@tag" }, { "nameIid": "10101010101010", "name": "fetch keys" } ], "type": "TYPE_SLICE_BEGIN", "nameIid": "10101010101010", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ], "name": "AsyncSchema::SleepSource" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "name": "Fiber Exit", "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "name": "Fiber Exit", "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "name": "Fiber Resume" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Field Execution" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "dictEntries": [ { "nameIid": "10101010101010", "doubleValue": 101010101010 } ], "name": "arguments" }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "object", "stringValue": null }, { "nameIid": "10101010101010", "doubleValue": 101010101010, "name": "result" } ], "type": "TYPE_SLICE_BEGIN", "trackUuid": "10101010101010", "name": "s1", "flowIds": [ "10101010101010" ] } }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010", "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010", "10101010101010" ] }, "internedData": { "debugAnnotationNames": [ { "iid": "10101010101010", "name": "duration\n" } ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Authorized" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "boolValue": true, "name": "authorized?" } ], "type": "TYPE_SLICE_BEGIN", "nameIid": "10101010101010", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ], "name": "Authorize: Sleeper" }, "internedData": { "eventNames": [ { "iid": "10101010101010", "name": "Authorize: Sleeper" } ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Field Execution" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_SLICE_BEGIN", "trackUuid": "10101010101010", "name": "s1.sleeper", "flowIds": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "name": "Fiber Yield" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "name": "Fiber Resume" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Field Execution" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "dictEntries": [ { "nameIid": "10101010101010", "doubleValue": 101010101010 } ], "name": "arguments" }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "object", "stringValue": null }, { "nameIid": "10101010101010", "doubleValue": 101010101010, "name": "result" } ], "type": "TYPE_SLICE_BEGIN", "trackUuid": "10101010101010", "name": "s2", "flowIds": [ "10101010101010" ] } }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010", "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010", "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Authorized" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "boolValue": true, "name": "authorized?" } ], "type": "TYPE_SLICE_BEGIN", "nameIid": "10101010101010", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ], "name": "Authorize: Sleeper" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Field Execution" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_SLICE_BEGIN", "trackUuid": "10101010101010", "name": "s2.sleeper", "flowIds": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "name": "Fiber Yield" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "name": "Fiber Resume" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Field Execution" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "dictEntries": [ { "nameIid": "10101010101010", "doubleValue": 101010101010 } ], "name": "arguments" }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "object", "stringValue": null }, { "nameIid": "10101010101010", "doubleValue": 101010101010, "name": "result" } ], "type": "TYPE_SLICE_BEGIN", "trackUuid": "10101010101010", "name": "s3", "flowIds": [ "10101010101010" ] } }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010", "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010", "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Authorized" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "boolValue": true, "name": "authorized?" } ], "type": "TYPE_SLICE_BEGIN", "nameIid": "10101010101010", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ], "name": "Authorize: Sleeper" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Field Execution" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "name": "arguments" }, { "nameIid": "10101010101010", "doubleValue": 101010101010, "name": "object" }, { "nameIid": "10101010101010", "doubleValue": 101010101010, "name": "result" } ], "type": "TYPE_SLICE_BEGIN", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "name": "s3.duration", "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010", "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010", "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "name": "Fiber Exit", "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010", "10101010101010" ], "name": "Create Source Fiber", "extraCounterTrackUuids": [ "10101010101010", "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "trustedPacketSequenceId": 101010101010, "sequenceFlags": 101010101010, "trackDescriptor": { "uuid": "10101010101010", "name": "Source Fiber #1010", "parentUuid": "10101010101010", "childOrdering": "CHRONOLOGICAL" } }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "intValue": "10101010101010", "name": "@tag" }, { "nameIid": "10101010101010", "arrayValues": [ { "nameIid": "10101010101010", "doubleValue": 101010101010 } ], "name": "fetch keys" } ], "type": "TYPE_SLICE_BEGIN", "nameIid": "10101010101010", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ], "flowIds": [ "10101010101010" ], "name": "AsyncSchema::SleepSource" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010", "10101010101010" ], "name": "Create Source Fiber", "extraCounterTrackUuids": [ "10101010101010", "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "trustedPacketSequenceId": 101010101010, "sequenceFlags": 101010101010, "trackDescriptor": { "uuid": "10101010101010", "name": "Source Fiber #1010", "parentUuid": "10101010101010", "childOrdering": "CHRONOLOGICAL" } }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "intValue": "10101010101010", "name": "@tag" }, { "nameIid": "10101010101010", "arrayValues": [ { "nameIid": "10101010101010", "doubleValue": 101010101010 } ], "name": "fetch keys" } ], "type": "TYPE_SLICE_BEGIN", "nameIid": "10101010101010", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ], "flowIds": [ "10101010101010" ], "name": "AsyncSchema::SleepSource" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "intValue": "10101010101010", "name": "@tag" }, { "nameIid": "10101010101010", "name": "fetch keys" } ], "type": "TYPE_SLICE_BEGIN", "nameIid": "10101010101010", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ], "name": "AsyncSchema::SleepSource" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "name": "Fiber Exit", "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "name": "Fiber Exit", "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "name": "Fiber Resume" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Field Execution" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "dictEntries": [ { "nameIid": "10101010101010", "doubleValue": 101010101010 } ], "name": "arguments" }, { "nameIid": "10101010101010", "doubleValue": 101010101010, "name": "object" }, { "nameIid": "10101010101010", "doubleValue": 101010101010, "name": "result" } ], "type": "TYPE_SLICE_BEGIN", "trackUuid": "10101010101010", "name": "s1.sleeper", "flowIds": [ "10101010101010" ] } }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010", "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010", "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Authorized" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "boolValue": true, "name": "authorized?" } ], "type": "TYPE_SLICE_BEGIN", "nameIid": "10101010101010", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ], "name": "Authorize: Sleeper" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Field Execution" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_SLICE_BEGIN", "trackUuid": "10101010101010", "name": "s1.sleeper.sleeper", "flowIds": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "name": "Fiber Yield" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "name": "Fiber Resume" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Field Execution" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "dictEntries": [ { "nameIid": "10101010101010", "doubleValue": 101010101010 } ], "name": "arguments" }, { "nameIid": "10101010101010", "doubleValue": 101010101010, "name": "object" }, { "nameIid": "10101010101010", "doubleValue": 101010101010, "name": "result" } ], "type": "TYPE_SLICE_BEGIN", "trackUuid": "10101010101010", "name": "s2.sleeper", "flowIds": [ "10101010101010" ] } }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010", "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010", "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Authorized" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "boolValue": true, "name": "authorized?" } ], "type": "TYPE_SLICE_BEGIN", "nameIid": "10101010101010", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ], "name": "Authorize: Sleeper" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Field Execution" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "name": "arguments" }, { "nameIid": "10101010101010", "doubleValue": 101010101010, "name": "object" }, { "nameIid": "10101010101010", "doubleValue": 101010101010, "name": "result" } ], "type": "TYPE_SLICE_BEGIN", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "name": "s2.sleeper.duration", "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010", "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010", "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "name": "Fiber Exit", "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010", "10101010101010" ], "name": "Create Source Fiber", "extraCounterTrackUuids": [ "10101010101010", "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "trustedPacketSequenceId": 101010101010, "sequenceFlags": 101010101010, "trackDescriptor": { "uuid": "10101010101010", "name": "Source Fiber #1010", "parentUuid": "10101010101010", "childOrdering": "CHRONOLOGICAL" } }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "intValue": "10101010101010", "name": "@tag" }, { "nameIid": "10101010101010", "arrayValues": [ { "nameIid": "10101010101010", "doubleValue": 101010101010 } ], "name": "fetch keys" } ], "type": "TYPE_SLICE_BEGIN", "nameIid": "10101010101010", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ], "flowIds": [ "10101010101010" ], "name": "AsyncSchema::SleepSource" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "name": "Fiber Exit", "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "name": "Fiber Resume" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Field Execution" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "dictEntries": [ { "nameIid": "10101010101010", "doubleValue": 101010101010 } ], "name": "arguments" }, { "nameIid": "10101010101010", "doubleValue": 101010101010, "name": "object" }, { "nameIid": "10101010101010", "doubleValue": 101010101010, "name": "result" } ], "type": "TYPE_SLICE_BEGIN", "trackUuid": "10101010101010", "name": "s1.sleeper.sleeper", "flowIds": [ "10101010101010" ] } }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010", "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010", "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Authorized" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "boolValue": true, "name": "authorized?" } ], "type": "TYPE_SLICE_BEGIN", "nameIid": "10101010101010", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ], "name": "Authorize: Sleeper" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Field Execution" ], "categoryIids": [ "10101010101010" ], "debugAnnotations": [ { "nameIid": "10101010101010", "name": "arguments" }, { "nameIid": "10101010101010", "doubleValue": 101010101010, "name": "object" }, { "nameIid": "10101010101010", "doubleValue": 101010101010, "name": "result" } ], "type": "TYPE_SLICE_BEGIN", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "name": "s1.sleeper.sleeper.duration", "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010", "10101010101010" ], "extraCounterTrackUuids": [ "10101010101010", "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "categories": [ "Dataloader" ], "categoryIids": [ "10101010101010" ], "type": "TYPE_INSTANT", "trackUuid": "10101010101010", "extraCounterValues": [ "10101010101010" ], "name": "Fiber Exit", "extraCounterTrackUuids": [ "10101010101010" ] }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_COUNTER", "trackUuid": "10101010101010", "counterValue": "10101010101010" }, "sequenceFlags": 101010101010 }, { "timestamp": "10101010101010", "trustedPacketSequenceId": 101010101010, "trackEvent": { "type": "TYPE_SLICE_END", "trackUuid": "10101010101010" }, "sequenceFlags": 101010101010 } ] }graphql-ruby-2.5.19/spec/graphql/dataloader/source_spec.rb000066400000000000000000000064601514115062600235630ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Dataloader::Source do class FailsToLoadSource < GraphQL::Dataloader::Source def fetch(keys) dataloader.with(FailsToLoadSource).load_all(keys) end end it "raises an error when it tries too many times to sync" do dl = GraphQL::Dataloader.new dl.append_job { dl.with(FailsToLoadSource).load(1) } err = assert_raises RuntimeError do dl.run end expected_message = "FailsToLoadSource#sync tried 1000 times to load pending keys ([1]), but they still weren't loaded. There is likely a circular dependency." assert_equal expected_message, err.message dl = GraphQL::Dataloader.new(fiber_limit: 10000) dl.append_job { dl.with(FailsToLoadSource).load(1) } err = assert_raises RuntimeError do dl.run end expected_message = "FailsToLoadSource#sync tried 1000 times to load pending keys ([1]), but they still weren't loaded. There is likely a circular dependency or `fiber_limit: 10000` is set too low." assert_equal expected_message, err.message end it "is pending when waiting for false and nil" do dl = GraphQL::Dataloader.new dl.with(FailsToLoadSource).request(nil) source_cache = dl.instance_variable_get(:@source_cache) source_cache_for_source = source_cache[FailsToLoadSource] # The value of this changed in Ruby 3.3.3, see https://bugs.ruby-lang.org/issues/20180 # In previous versions, it was `[{}]`, but now it's `[]` empty_batch_key = [*[], **{}] source_inst = source_cache_for_source[empty_batch_key] assert_instance_of FailsToLoadSource, source_inst, "The cache includes a pending source (#{source_cache_for_source.inspect})" assert source_inst.pending? end class CustomKeySource < GraphQL::Dataloader::Source def result_key_for(record) record[:id] end def fetch(records) records.map { |r| r[:value] * 10 } end end it "uses a custom key when configured" do values = nil GraphQL::Dataloader.with_dataloading do |dl| first_req = dl.with(CustomKeySource).request({ id: 1, value: 10 }) second_rec = dl.with(CustomKeySource).request({ id: 2, value: 20 }) third_rec = dl.with(CustomKeySource).request({id: 1, value: 30 }) values = [ first_req.load, second_rec.load, third_rec.load ] end # There wasn't a `300` because the third requested value was de-duped to the first one. assert_equal [100, 200, 100], values end class NoDataloaderSchema < GraphQL::Schema class ThingSource < GraphQL::Dataloader::Source def fetch(ids) ids.map { |id| { name: "Thing-#{id}" } } end end class Thing < GraphQL::Schema::Object field :name, String end class Query < GraphQL::Schema::Object field :thing, Thing do argument :id, ID end def thing(id:) context.dataloader.with(ThingSource).load(id) end end query(Query) end it "raises an error when used without a dataloader" do err = assert_raises GraphQL::Error do NoDataloaderSchema.execute("{ thing(id: 1) { name } }") end expected_message = "GraphQL::Dataloader is not running -- add `use GraphQL::Dataloader` to your schema to use Dataloader sources." assert_equal expected_message, err.message end end graphql-ruby-2.5.19/spec/graphql/dataloader_spec.rb000066400000000000000000001435421514115062600222660ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" require "fiber" if defined?(Console) && defined?(Async) Console.logger.disable(Async::Task) end describe GraphQL::Dataloader do class BatchedCallsCounter def initialize @count = 0 end def increment @count += 1 end attr_reader :count end class FiberSchema < GraphQL::Schema module Database extend self DATA = {} [ { id: "1", name: "Wheat", type: "Grain" }, { id: "2", name: "Corn", type: "Grain" }, { id: "3", name: "Butter", type: "Dairy" }, { id: "4", name: "Baking Soda", type: "LeaveningAgent" }, { id: "5", name: "Cornbread", type: "Recipe", ingredient_ids: ["1", "2", "3", "4"] }, { id: "6", name: "Grits", type: "Recipe", ingredient_ids: ["2", "3", "7"] }, { id: "7", name: "Cheese", type: "Dairy" }, ].each { |d| DATA[d[:id]] = d } def log @log ||= [] end def mget(ids) log << [:mget, ids.sort] ids.map { |id| DATA[id] } end def find_by(attribute, values) log << [:find_by, attribute, values.sort] values.map { |v| DATA.each_value.find { |dv| dv[attribute] == v } } end end class DataObject < GraphQL::Dataloader::Source def initialize(column = :id) @column = column end def fetch(keys) if @column == :id Database.mget(keys) else Database.find_by(@column, keys) end end end class ToString < GraphQL::Dataloader::Source def fetch(keys) keys.map(&:to_s) end end class NestedDataObject < GraphQL::Dataloader::Source def fetch(ids) @dataloader.with(DataObject).load_all(ids) end end class SlowDataObject < GraphQL::Dataloader::Source def initialize(batch_key) # This is just so that I can force different instances in test @batch_key = batch_key end def fetch(keys) t = Thread.new { sleep 0.5 Database.mget(keys) } dataloader.yield t.value end end class CustomBatchKeySource < GraphQL::Dataloader::Source def initialize(batch_key) @batch_key = batch_key end def self.batch_key_for(batch_key) Database.log << [:batch_key_for, batch_key] # Ignore it altogether :all_the_same end def fetch(keys) Database.mget(keys) end end class KeywordArgumentSource < GraphQL::Dataloader::Source def initialize(column:) @column = column end def fetch(keys) if @column == :id Database.mget(keys) else Database.find_by(@column, keys) end end end class AuthorizedSource < GraphQL::Dataloader::Source def initialize(counter) @counter = counter end def fetch(recipes) @counter&.increment recipes.map { true } end end class ErrorSource < GraphQL::Dataloader::Source def fetch(ids) raise GraphQL::Error, "Source error on: #{ids.inspect}" end end module Ingredient include GraphQL::Schema::Interface field :name, String, null: false field :id, ID, null: false field :name_by_scoped_context, String def name_by_scoped_context context[:ingredient_name] end end class Grain < GraphQL::Schema::Object implements Ingredient end class LeaveningAgent < GraphQL::Schema::Object implements Ingredient end class Dairy < GraphQL::Schema::Object implements Ingredient end class Recipe < GraphQL::Schema::Object def self.authorized?(obj, ctx) ctx.dataloader.with(AuthorizedSource, ctx[:batched_calls_counter]).load(obj) end field :name, String, null: false field :ingredients, [Ingredient], null: false def ingredients ingredients = dataloader.with(DataObject).load_all(object[:ingredient_ids]) ingredients end field :slow_ingredients, [Ingredient], null: false def slow_ingredients # Use `object[:id]` here to force two different instances of the loader in the test dataloader.with(SlowDataObject, object[:id]).load_all(object[:ingredient_ids]) end end class Cookbook < GraphQL::Schema::Object field :featured_recipe, Recipe def featured_recipe -> { Database.mget([object[:featured_recipe]]).first } end end class Query < GraphQL::Schema::Object field :recipes, [Recipe], null: false def recipes Database.mget(["5", "6"]) end field :ingredient, Ingredient do argument :id, ID end def ingredient(id:) dataloader.with(DataObject).load(id) end field :ingredient_by_name, Ingredient do argument :name, String end def ingredient_by_name(name:) ing = dataloader.with(DataObject, :name).load(name) context.scoped_set!(:ingredient_name, "Scoped:#{name}") ing end field :nested_ingredient, Ingredient do argument :id, ID end def nested_ingredient(id:) dataloader.with(NestedDataObject).load(id) end field :slow_recipe, Recipe do argument :id, ID end def slow_recipe(id:) dataloader.with(SlowDataObject, id).load(id) end field :recipe, Recipe do argument :id, ID, loads: Recipe, as: :recipe end def recipe(recipe:) recipe end field :recipe_by_id_using_load, Recipe do argument :id, ID, required: false end def recipe_by_id_using_load(id:) dataloader.with(DataObject).load(id) end field :recipes_by_id_using_load_all, [Recipe] do argument :ids, [ID, null: true] end def recipes_by_id_using_load_all(ids:) dataloader.with(DataObject).load_all(ids) end field :recipes_by_id, [Recipe] do argument :ids, [ID], loads: Recipe, as: :recipes end def recipes_by_id(recipes:) recipes end field :key_ingredient, Ingredient do argument :id, ID end def key_ingredient(id:) dataloader.with(KeywordArgumentSource, column: :id).load(id) end class RecipeIngredientInput < GraphQL::Schema::InputObject argument :id, ID argument :ingredient_number, Int end field :recipe_ingredient, Ingredient do argument :recipe, RecipeIngredientInput end def recipe_ingredient(recipe:) recipe_object = dataloader.with(DataObject).load(recipe[:id]) ingredient_idx = recipe[:ingredient_number] - 1 ingredient_id = recipe_object[:ingredient_ids][ingredient_idx] dataloader.with(DataObject).load(ingredient_id) end field :common_ingredients, [Ingredient] do argument :recipe_1_id, ID argument :recipe_2_id, ID end def common_ingredients(recipe_1_id:, recipe_2_id:) req1 = dataloader.with(DataObject).request(recipe_1_id) req2 = dataloader.with(DataObject).request(recipe_2_id) recipe1 = req1.load recipe2 = req2.load common_ids = recipe1[:ingredient_ids] & recipe2[:ingredient_ids] dataloader.with(DataObject).load_all(common_ids) end field :common_ingredients_with_load, [Ingredient], null: false do argument :recipe_1_id, ID, loads: Recipe argument :recipe_2_id, ID, loads: Recipe end def common_ingredients_with_load(recipe_1:, recipe_2:) common_ids = recipe_1[:ingredient_ids] & recipe_2[:ingredient_ids] dataloader.with(DataObject).load_all(common_ids) end field :common_ingredients_from_input_object, [Ingredient], null: false do class CommonIngredientsInput < GraphQL::Schema::InputObject argument :recipe_1_id, ID, loads: Recipe argument :recipe_2_id, ID, loads: Recipe end argument :input, CommonIngredientsInput end def common_ingredients_from_input_object(input:) recipe_1 = input[:recipe_1] recipe_2 = input[:recipe_2] common_ids = recipe_1[:ingredient_ids] & recipe_2[:ingredient_ids] dataloader.with(DataObject).load_all(common_ids) end field :ingredient_with_custom_batch_key, Ingredient do argument :id, ID argument :batch_key, String end def ingredient_with_custom_batch_key(id:, batch_key:) dataloader.with(CustomBatchKeySource, batch_key).load(id) end field :recursive_ingredient_name, String do argument :id, ID end def recursive_ingredient_name(id:) res = context.schema.execute("{ ingredient(id: #{id}) { name } }") res["data"]["ingredient"]["name"] end field :test_error, String do argument :source, Boolean, required: false, default_value: false end def test_error(source:) if source dataloader.with(ErrorSource).load(1) else raise GraphQL::Error, "Field error" end end class LookaheadInput < GraphQL::Schema::InputObject argument :id, ID argument :batch_key, String end field :lookahead_ingredient, Ingredient, extras: [:lookahead] do argument :input, LookaheadInput end def lookahead_ingredient(input:, lookahead:) lookahead.arguments # forces a dataloader.run_isolated call dataloader.with(CustomBatchKeySource, input[:batch_key]).load(input[:id]) end field :cookbooks, [Cookbook] def cookbooks [ { featured_recipe: "5" }, { featured_recipe: "6" }, ] end end query(Query) class Mutation1 < GraphQL::Schema::Mutation argument :argument_1, String, prepare: ->(val, ctx) { raise FieldTestError } field :value, String def resolve(argument_1:) { value: argument_1 } end end class Mutation2 < GraphQL::Schema::Mutation argument :argument_2, String, prepare: ->(val, ctx) { raise FieldTestError } field :value, String def resolve(argument_2:) { value: argument_2 } end end class Mutation3 < GraphQL::Schema::Mutation argument :label, String type String def resolve(label:) log = context[:mutation_log] ||= [] log << "begin #{label}" dataloader.with(DataObject).load(1) log << "end #{label}" label end end class GetCache < GraphQL::Schema::Mutation type String def resolve dataloader.with(ToString).load(1) end end class Mutation < GraphQL::Schema::Object field :mutation_1, mutation: Mutation1 field :mutation_2, mutation: Mutation2 field :mutation_3, mutation: Mutation3 field :set_cache, String do argument :input, String end def set_cache(input:) dataloader.with(ToString).merge({ 1 => input }) input end field :get_cache, mutation: GetCache end mutation(Mutation) def self.object_from_id(id, ctx) ctx.dataloader.with(DataObject).load(id) end def self.resolve_type(type, obj, ctx) get_type(obj[:type]) end orphan_types(Grain, Dairy, Recipe, LeaveningAgent) use GraphQL::Dataloader lazy_resolve Proc, :call class FieldTestError < StandardError; end rescue_from(FieldTestError) do |err, obj, args, ctx, field| errs = ctx[:errors] ||= [] errs << "FieldTestError @ #{ctx[:current_path]}, #{field.path} / #{ctx[:current_field].path}" nil end end class UsageAnalyzer < GraphQL::Analysis::Analyzer def initialize(query) @query = query @fields = Set.new end def on_enter_field(node, parent, visitor) args = @query.arguments_for(node, visitor.field_definition) # This bug has been around for a while, # see https://github.com/rmosolgo/graphql-ruby/issues/3321 if args.is_a?(GraphQL::Execution::Lazy) args = args.value end @fields << [node.name, args.keys] end def result @fields end end def database_log FiberSchema::Database.log end before do database_log.clear end ALL_FIBERS = [] class PartsSchema < GraphQL::Schema class FieldSource < GraphQL::Dataloader::Source DATA = [ {"id" => 1, "name" => "a"}, {"id" => 2, "name" => "b"}, {"id" => 3, "name" => "c"}, {"id" => 4, "name" => "d"}, ] def fetch(fields) @previously_fetched ||= Set.new fields.each do |f| if !@previously_fetched.add?(f) raise "Duplicate fetch for #{f.inspect}" end end Array.new(fields.size, DATA) end end class StringFilter < GraphQL::Schema::InputObject argument :equal_to_any_of, [String] end class ComponentFilter < GraphQL::Schema::InputObject argument :name, StringFilter end class FetchObjects < GraphQL::Schema::Resolver argument :filter, ComponentFilter, required: false def resolve(**_kwargs) context.dataloader.with(FieldSource).load("#{field.path}/#{object&.fetch("id")}") end end class Component < GraphQL::Schema::Object field :name, String end class Part < GraphQL::Schema::Object field :components, [Component], resolver: FetchObjects end class Manufacturer < GraphQL::Schema::Object field :parts, [Part], resolver: FetchObjects end class Query < GraphQL::Schema::Object field :manufacturers, [Manufacturer], resolver: FetchObjects end query(Query) use GraphQL::Dataloader end module DataloaderAssertions module FiberCounting class << self attr_accessor :starting_fiber_count, :last_spawn_fiber_count, :last_max_fiber_count def current_fiber_count count_active_fibers - starting_fiber_count end def count_active_fibers GC.start ObjectSpace.each_object(Fiber).count end end def initialize(*args, **kwargs, &block) super FiberCounting.starting_fiber_count = FiberCounting.count_active_fibers FiberCounting.last_max_fiber_count = 0 FiberCounting.last_spawn_fiber_count = 0 end def spawn_fiber result = super update_fiber_counts result end def spawn_source_task(parent_task, condition, trace) result = super if result update_fiber_counts end result end private def update_fiber_counts FiberCounting.last_spawn_fiber_count += 1 current_count = FiberCounting.current_fiber_count if current_count > FiberCounting.last_max_fiber_count FiberCounting.last_max_fiber_count = current_count end end end def self.included(child_class) child_class.class_eval do let(:schema) { make_schema_from(FiberSchema) } let(:parts_schema) { make_schema_from(PartsSchema) } it "Works with request(...)" do res = schema.execute <<-GRAPHQL { commonIngredients(recipe1Id: 5, recipe2Id: 6) { name } } GRAPHQL expected_data = { "data" => { "commonIngredients" => [ { "name" => "Corn" }, { "name" => "Butter" }, ] } } assert_graphql_equal expected_data, res assert_equal [[:mget, ["5", "6"]], [:mget, ["2", "3"]]], database_log end it "runs mutations sequentially" do res = schema.execute <<-GRAPHQL mutation { first: mutation3(label: "first") second: mutation3(label: "second") } GRAPHQL assert_equal({ "first" => "first", "second" => "second" }, res["data"]) assert_equal ["begin first", "end first", "begin second", "end second"], res.context[:mutation_log] end it "clears the cache between mutations" do res = schema.execute <<-GRAPHQL mutation { setCache(input: "Salad") getCache } GRAPHQL assert_equal({"setCache" => "Salad", "getCache" => "1"}, res["data"]) end it "batch-loads" do res = schema.execute <<-GRAPHQL { i1: ingredient(id: 1) { id name } i2: ingredient(id: 2) { name } __typename r1: recipe(id: 5) { # This loads Ingredients 3 and 4 ingredients { name } } # This loads Ingredient 7 ri1: recipeIngredient(recipe: { id: 6, ingredientNumber: 3 }) { name } } GRAPHQL expected_data = { "i1" => { "id" => "1", "name" => "Wheat" }, "i2" => { "name" => "Corn" }, "__typename" => "Query", "r1" => { "ingredients" => [ { "name" => "Wheat" }, { "name" => "Corn" }, { "name" => "Butter" }, { "name" => "Baking Soda" }, ], }, "ri1" => { "name" => "Cheese", }, } assert_graphql_equal(expected_data, res["data"]) expected_log = [ [:mget, [ "1", "2", # The first 2 ingredients "5", # The first recipe "6", # recipeIngredient recipeId ]], [:mget, [ "7", # recipeIngredient ingredient_id ]], [:mget, [ "3", "4", # The two unfetched ingredients the first recipe ]], ] assert_equal expected_log, database_log end it "caches and batch-loads across a multiplex" do context = {} result = schema.multiplex([ { query: "{ i1: ingredient(id: 1) { name } i2: ingredient(id: 2) { name } }", }, { query: "{ i2: ingredient(id: 2) { name } r1: recipe(id: 5) { ingredients { name } } }", }, { query: "{ i1: ingredient(id: 1) { name } ri1: recipeIngredient(recipe: { id: 5, ingredientNumber: 2 }) { name } }", }, ], context: context) expected_result = [ {"data"=>{"i1"=>{"name"=>"Wheat"}, "i2"=>{"name"=>"Corn"}}}, {"data"=>{"i2"=>{"name"=>"Corn"}, "r1"=>{"ingredients"=>[{"name"=>"Wheat"}, {"name"=>"Corn"}, {"name"=>"Butter"}, {"name"=>"Baking Soda"}]}}}, {"data"=>{"i1"=>{"name"=>"Wheat"}, "ri1"=>{"name"=>"Corn"}}}, ] assert_graphql_equal expected_result, result expected_log = [ [:mget, ["1", "2", "5"]], [:mget, ["3", "4"]], ] assert_equal expected_log, database_log end it "works with calls within sources" do res = schema.execute <<-GRAPHQL { i1: nestedIngredient(id: 1) { name } i2: nestedIngredient(id: 2) { name } } GRAPHQL expected_data = { "i1" => { "name" => "Wheat" }, "i2" => { "name" => "Corn" } } assert_graphql_equal expected_data, res["data"] assert_equal [[:mget, ["1", "2"]]], database_log end it "works with batch parameters" do res = schema.execute <<-GRAPHQL { i1: ingredientByName(name: "Butter") { id } i2: ingredientByName(name: "Corn") { id } i3: ingredientByName(name: "Gummi Bears") { id } } GRAPHQL expected_data = { "i1" => { "id" => "3" }, "i2" => { "id" => "2" }, "i3" => nil, } assert_graphql_equal expected_data, res["data"] assert_equal [[:find_by, :name, ["Butter", "Corn", "Gummi Bears"]]], database_log end it "works with manual parallelism" do start = Time.now.to_f schema.execute <<-GRAPHQL { i1: slowRecipe(id: 5) { slowIngredients { name } } i2: slowRecipe(id: 6) { slowIngredients { name } } } GRAPHQL finish = Time.now.to_f # For some reason Async adds some overhead to this manual parallelism. # But who cares, you wouldn't use Thread#join in that case delta = schema.dataloader_class == GraphQL::Dataloader ? 0.1 : 0.5 # Each load slept for 0.5 second, so sequentially, this would have been 2s sequentially assert_in_delta 1, finish - start, delta, "Load threads are executed in parallel" expected_log = [ # These were separated because of different recipe IDs: [:mget, ["5"]], [:mget, ["6"]], # These were cached separately because of different recipe IDs: [:mget, ["2", "3", "7"]], [:mget, ["1", "2", "3", "4"]], ] # Sort them because threads may have returned in slightly different order assert_equal expected_log.sort, database_log.sort end it "Works with multiple-field selections and __typename" do query_str = <<-GRAPHQL { ingredient(id: 1) { __typename name } } GRAPHQL res = schema.execute(query_str) expected_data = { "ingredient" => { "__typename" => "Grain", "name" => "Wheat", } } assert_graphql_equal expected_data, res["data"] end it "Works when the parent field didn't yield" do query_str = <<-GRAPHQL { recipes { ingredients { name } } } GRAPHQL res = schema.execute(query_str) expected_data = { "recipes" =>[ { "ingredients" => [ {"name"=>"Wheat"}, {"name"=>"Corn"}, {"name"=>"Butter"}, {"name"=>"Baking Soda"} ]}, { "ingredients" => [ {"name"=>"Corn"}, {"name"=>"Butter"}, {"name"=>"Cheese"} ]}, ] } assert_graphql_equal expected_data, res["data"] expected_log = [ [:mget, ["5", "6"]], [:mget, ["1", "2", "3", "4", "7"]], ] assert_equal expected_log, database_log end it "loads arguments in batches, even with request" do query_str = <<-GRAPHQL { commonIngredientsWithLoad(recipe1Id: 5, recipe2Id: 6) { name } } GRAPHQL res = schema.execute(query_str) expected_data = { "commonIngredientsWithLoad" => [ {"name"=>"Corn"}, {"name"=>"Butter"}, ] } assert_graphql_equal expected_data, res["data"] expected_log = [ [:mget, ["5", "6"]], [:mget, ["2", "3"]], ] assert_equal expected_log, database_log end it "works with sources that use keyword arguments in the initializer" do query_str = <<-GRAPHQL { keyIngredient(id: 1) { __typename name } } GRAPHQL res = schema.execute(query_str) expected_data = { "keyIngredient" => { "__typename" => "Grain", "name" => "Wheat", } } assert_graphql_equal expected_data, res["data"] end it "Works with analyzing arguments with `loads:`, even with .request" do query_str = <<-GRAPHQL { commonIngredientsWithLoad(recipe1Id: 5, recipe2Id: 6) { name } } GRAPHQL query = GraphQL::Query.new(schema, query_str) results = GraphQL::Analysis.analyze_query(query, [UsageAnalyzer]) expected_results = [ ["commonIngredientsWithLoad", [:recipe_1, :recipe_2]], ["name", []], ] normalized_results = results.first.to_a normalized_results.each do |key, values| values.sort! end assert_equal expected_results, results.first.to_a end it "Works with input objects, load and request" do query_str = <<-GRAPHQL { commonIngredientsFromInputObject(input: { recipe1Id: 5, recipe2Id: 6 }) { name } } GRAPHQL res = schema.execute(query_str) expected_data = { "commonIngredientsFromInputObject" => [ {"name"=>"Corn"}, {"name"=>"Butter"}, ] } assert_graphql_equal expected_data, res["data"] expected_log = [ [:mget, ["5", "6"]], [:mget, ["2", "3"]], ] assert_equal expected_log, database_log end it "batches calls in .authorized?" do query_str = "{ r1: recipe(id: 5) { name } r2: recipe(id: 6) { name } }" context = { batched_calls_counter: BatchedCallsCounter.new } schema.execute(query_str, context: context) assert_equal 1, context[:batched_calls_counter].count query_str = "{ recipes { name } }" context = { batched_calls_counter: BatchedCallsCounter.new } schema.execute(query_str, context: context) assert_equal 1, context[:batched_calls_counter].count query_str = "{ recipesById(ids: [5, 6]) { name } }" context = { batched_calls_counter: BatchedCallsCounter.new } schema.execute(query_str, context: context) assert_equal 1, context[:batched_calls_counter].count end it "batches nested object calls in .authorized? after using lazy_resolve" do query_str = "{ cookbooks { featuredRecipe { name } } }" context = { batched_calls_counter: BatchedCallsCounter.new } result = schema.execute(query_str, context: context) assert_equal ["Cornbread", "Grits"], result["data"]["cookbooks"].map { |c| c["featuredRecipe"]["name"] } refute result.key?("errors") assert_equal 1, context[:batched_calls_counter].count end it "works when passing nil into source" do query_str = <<-GRAPHQL query($id: ID) { recipe: recipeByIdUsingLoad(id: $id) { name } } GRAPHQL res = schema.execute(query_str, variables: { id: nil }) expected_data = { "recipe" => nil } assert_graphql_equal expected_data, res["data"] query_str = <<-GRAPHQL query($ids: [ID]!) { recipes: recipesByIdUsingLoadAll(ids: $ids) { name } } GRAPHQL res = schema.execute(query_str, variables: { ids: [nil] }) expected_data = { "recipes" => nil } assert_graphql_equal expected_data, res["data"] end it "Works with input objects using variables, load and request" do query_str = <<-GRAPHQL query($input: CommonIngredientsInput!) { commonIngredientsFromInputObject(input: $input) { name } } GRAPHQL res = schema.execute(query_str, variables: { input: { recipe1Id: 5, recipe2Id: 6 }}) expected_data = { "commonIngredientsFromInputObject" => [ {"name"=>"Corn"}, {"name"=>"Butter"}, ] } assert_graphql_equal expected_data, res["data"] expected_log = [ [:mget, ["5", "6"]], [:mget, ["2", "3"]], ] assert_equal expected_log, database_log end it "supports general usage" do a = b = c = nil res = GraphQL::Dataloader.with_dataloading { |dataloader| dataloader.append_job { a = dataloader.with(FiberSchema::DataObject).load("1") } dataloader.append_job { b = dataloader.with(FiberSchema::DataObject).load("1") } dataloader.append_job { r1 = dataloader.with(FiberSchema::DataObject).request("2") r2 = dataloader.with(FiberSchema::DataObject).request("3") c = [ r1.load, r2.load ] } :finished } assert_equal :finished, res assert_equal [[:mget, ["1", "2", "3"]]], database_log assert_equal "Wheat", a[:name] assert_equal "Wheat", b[:name] assert_equal ["Corn", "Butter"], c.map { |d| d[:name] } end it "works with scoped context" do query_str = <<-GRAPHQL { i1: ingredientByName(name: "Corn") { nameByScopedContext } i2: ingredientByName(name: "Wheat") { nameByScopedContext } i3: ingredientByName(name: "Butter") { nameByScopedContext } } GRAPHQL expected_data = { "i1" => { "nameByScopedContext" => "Scoped:Corn" }, "i2" => { "nameByScopedContext" => "Scoped:Wheat" }, "i3" => { "nameByScopedContext" => "Scoped:Butter" }, } result = schema.execute(query_str) assert_graphql_equal expected_data, result["data"] end it "works when the schema calls itself" do result = schema.execute("{ recursiveIngredientName(id: 1) }") assert_equal "Wheat", result["data"]["recursiveIngredientName"] end it "uses .batch_key_for in source classes" do query_str = <<-GRAPHQL { i1: ingredientWithCustomBatchKey(id: 1, batchKey: "abc") { name } i2: ingredientWithCustomBatchKey(id: 2, batchKey: "def") { name } i3: ingredientWithCustomBatchKey(id: 3, batchKey: "ghi") { name } } GRAPHQL res = schema.execute(query_str) expected_data = { "i1" => { "name" => "Wheat" }, "i2" => { "name" => "Corn" }, "i3" => { "name" => "Butter" } } assert_graphql_equal expected_data, res["data"] expected_log = [ # Each batch key is given to the source class: [:batch_key_for, "abc"], [:batch_key_for, "def"], [:batch_key_for, "ghi"], # But since they return the same value, # all keys are fetched in the same call: [:mget, ["1", "2", "3"]] ] assert_equal expected_log, database_log end it "uses cached values from .merge" do query_str = "{ ingredient(id: 1) { id name } }" assert_equal "Wheat", schema.execute(query_str)["data"]["ingredient"]["name"] assert_equal [[:mget, ["1"]]], database_log database_log.clear dataloader = schema.dataloader_class.new data_source = dataloader.with(FiberSchema::DataObject) data_source.merge({ "1" => { name: "Kamut", id: "1", type: "Grain" } }) assert_equal "Kamut", data_source.load("1")[:name] res = schema.execute(query_str, context: { dataloader: dataloader }) assert_equal [], database_log assert_equal "Kamut", res["data"]["ingredient"]["name"] end it "raises errors from fields" do err = assert_raises GraphQL::Error do schema.execute("{ testError }") end assert_equal "Field error", err.message end it "raises errors from sources" do err = assert_raises GraphQL::Error do schema.execute("{ testError(source: true) }") end assert_equal "Source error on: [1]", err.message end it "works with very very large queries" do query_str = "{".dup fields = 1100 fields.times do |i| query_str << "\n field#{i}: lookaheadIngredient(input: { id: 1, batchKey: \"key-#{i}\"}) { name }" end query_str << "\n}" GC.start GC.disable old_fibers = [] ObjectSpace.each_object(Fiber) do |f| old_fibers << f end res = schema.execute(query_str) assert_equal fields, res["data"].keys.size skip("Doesn't work after Ractor.new (https://bugs.ruby-lang.org/issues/19387)") if RUN_RACTOR_TESTS all_fibers = [] ObjectSpace.each_object(Fiber) do |f| all_fibers << f end new_fibers = all_fibers - old_fibers if new_fibers.any?(&:alive?) message = "Alive fibers:\n\n".dup new_fibers.select(&:alive?).each do |f| message << " - #{f.inspect}\n" f.backtrace.each do |line| message << " #{line}\n" end end puts message end assert_equal [false], new_fibers.map(&:alive?).uniq ensure GC.enable end it "doesn't perform duplicate source fetches" do query = <<~QUERY query { manufacturers { parts { components(filter: {name: {equalToAnyOf: ["c1", "c2", "c3"]}}) { name } } } } QUERY response = parts_schema.execute(query).to_h assert_equal [4, 4, 4, 4], response["data"]["manufacturers"].map { |parts_obj| parts_obj["parts"].size } end describe "fiber_limit" do def assert_last_max_fiber_count(expected_last_max_fiber_count, message = nil) if FiberCounting.last_max_fiber_count == (expected_last_max_fiber_count + 1) # TODO why does this happen sometimes? warn "AsyncDataloader had +1 last_max_fiber_count" assert_equal (expected_last_max_fiber_count + 1), FiberCounting.last_max_fiber_count, message else assert_equal expected_last_max_fiber_count, FiberCounting.last_max_fiber_count, message end end it "respects a configured fiber_limit" do skip("Doesn't work after Ractor.new (https://bugs.ruby-lang.org/issues/19387)") if RUN_RACTOR_TESTS query_str = <<-GRAPHQL { recipes { ingredients { name } } nestedIngredient(id: 2) { name } keyIngredient(id: 4) { name } commonIngredientsWithLoad(recipe1Id: 5, recipe2Id: 6) { name } } GRAPHQL fiber_counting_dataloader_class = Class.new(schema.dataloader_class) fiber_counting_dataloader_class.include(FiberCounting) res = schema.execute(query_str, context: { dataloader: fiber_counting_dataloader_class.new }) assert_nil res.context.dataloader.fiber_limit assert_equal 10, FiberCounting.last_spawn_fiber_count assert_last_max_fiber_count(9, "No limit works as expected") res = schema.execute(query_str, context: { dataloader: fiber_counting_dataloader_class.new(fiber_limit: 4) }) assert_equal 4, res.context.dataloader.fiber_limit assert_equal 12, FiberCounting.last_spawn_fiber_count assert_last_max_fiber_count(4, "Limit of 4 works as expected") res = schema.execute(query_str, context: { dataloader: fiber_counting_dataloader_class.new(fiber_limit: 6) }) assert_equal 6, res.context.dataloader.fiber_limit assert_equal 8, FiberCounting.last_spawn_fiber_count assert_last_max_fiber_count(6, "Limit of 6 works as expected") end it "accepts a default fiber_limit config" do skip("Doesn't work after Ractor.new (https://bugs.ruby-lang.org/issues/19387)") if RUN_RACTOR_TESTS schema = Class.new(FiberSchema) do use GraphQL::Dataloader, fiber_limit: 4 end query_str = <<-GRAPHQL { recipes { ingredients { name } } nestedIngredient(id: 2) { name } keyIngredient(id: 4) { name } commonIngredientsWithLoad(recipe1Id: 5, recipe2Id: 6) { name } } GRAPHQL res = schema.execute(query_str) assert_equal 4, res.context.dataloader.fiber_limit assert_nil res["errors"] end it "requires at least three fibers" do dl = GraphQL::Dataloader.new(fiber_limit: 2) err = assert_raises ArgumentError do dl.run end assert_equal "Dataloader fiber limit is too low (2), it must be at least 4", err.message end end end end end def make_schema_from(schema) schema end include DataloaderAssertions if RUBY_VERSION >= "3.1.1" require "async" describe "AsyncDataloader" do def make_schema_from(schema) Class.new(schema) { use GraphQL::Dataloader::AsyncDataloader } end include DataloaderAssertions end end if Fiber.respond_to?(:scheduler) describe "nonblocking: true" do def make_schema_from(schema) Class.new(schema) do use GraphQL::Dataloader, nonblocking: true end end before do Fiber.set_scheduler(::DummyScheduler.new) end after do Fiber.set_scheduler(nil) end include DataloaderAssertions end if RUBY_ENGINE == "ruby" && !ENV["GITHUB_ACTIONS"] describe "nonblocking: true with libev" do require "libev_scheduler" def make_schema_from(schema) Class.new(schema) do use GraphQL::Dataloader, nonblocking: true end end before do Fiber.set_scheduler(Libev::Scheduler.new) end after do Fiber.set_scheduler(nil) end include DataloaderAssertions end end end describe "example from #3314" do module Example class FooType < GraphQL::Schema::Object field :id, ID, null: false end class FooSource < GraphQL::Dataloader::Source def fetch(ids) ids.map { |id| OpenStruct.new(id: id) } end end class QueryType < GraphQL::Schema::Object field :foo, Example::FooType do argument :foo_id, GraphQL::Types::ID, required: false, loads: Example::FooType end def foo(foo: nil) dataloader.with(Example::FooSource).load("load") end end class Schema < GraphQL::Schema query Example::QueryType use GraphQL::Dataloader def self.object_from_id(id, ctx) ctx.dataloader.with(Example::FooSource).load(id) end def self.resolve_type(type, obj, ctx) type end end end it "loads properly" do result = Example::Schema.execute(<<-GRAPHQL) { fooWithLoad: foo(fooId: "Other") { __typename id } } GRAPHQL # This should not have a Lazy in it expected_result = { "data" => { "fooWithLoad" => { "id" => "load", "__typename" => "Foo" }, } } assert_graphql_equal expected_result, result.to_h end end class FiberErrorSchema < GraphQL::Schema class ErrorObject < GraphQL::Dataloader::Source def fetch(_) raise ArgumentError, "Nope" end end class Query < GraphQL::Schema::Object field :load, String, null: false field :load_all, String, null: false field :request, String, null: false field :request_all, String, null: false def load dataloader.with(ErrorObject).load(123) end def load_all dataloader.with(ErrorObject).load_all([123]) end def request req = dataloader.with(ErrorObject).request(123) req.load end def request_all req = dataloader.with(ErrorObject).request_all([123]) req.load end end use GraphQL::Dataloader query(Query) rescue_from(StandardError) do |err, obj, args, ctx, field| ctx[:errors] << "#{err.message} (#{field.owner.name}.#{field.graphql_name}, #{obj.inspect}, #{args.inspect})" nil end end it "Works with error handlers" do context = { errors: [] } res = FiberErrorSchema.execute("{ load loadAll request requestAll }", context: context) expected_errors = [ "Nope (FiberErrorSchema::Query.load, nil, {})", "Nope (FiberErrorSchema::Query.loadAll, nil, {})", "Nope (FiberErrorSchema::Query.request, nil, {})", "Nope (FiberErrorSchema::Query.requestAll, nil, {})", ] assert_nil(res["data"]) assert_equal(expected_errors, context[:errors].sort) end it "has proper context[:current_field]" do res = FiberSchema.execute("mutation { mutation1(argument1: \"abc\") { __typename } mutation2(argument2: \"def\") { __typename } }") assert_equal({"mutation1"=>{ "__typename" => "Mutation1Payload" }, "mutation2"=>{ "__typename" => "Mutation2Payload"} }, res["data"]) expected_errors = [ "FieldTestError @ [\"mutation1\"], Mutation.mutation1 / Mutation.mutation1", "FieldTestError @ [\"mutation2\"], Mutation.mutation2 / Mutation.mutation2", ] assert_equal expected_errors, res.context[:errors] end it "passes along throws" do value = catch(:hello) do dataloader = GraphQL::Dataloader.new dataloader.append_job do throw(:hello, :world) end dataloader.run end assert :world, value end class CanaryDataloader < GraphQL::Dataloader::NullDataloader end it "uses context[:dataloader] when given" do res = Class.new(GraphQL::Schema) do query_type = Class.new(GraphQL::Schema::Object) do graphql_name "Query" end query(query_type) end.execute("{ __typename }") assert_instance_of GraphQL::Dataloader::NullDataloader, res.context.dataloader res = FiberSchema.execute("{ __typename }") assert_instance_of GraphQL::Dataloader, res.context.dataloader refute res.context.dataloader.nonblocking? res = FiberSchema.execute("{ __typename }", context: { dataloader: CanaryDataloader.new } ) assert_instance_of CanaryDataloader, res.context.dataloader if Fiber.respond_to?(:scheduler) Fiber.set_scheduler(::DummyScheduler.new) res = FiberSchema.execute("{ __typename }", context: { dataloader: GraphQL::Dataloader.new(nonblocking: true) }) assert res.context.dataloader.nonblocking? res = FiberSchema.multiplex([{ query: "{ __typename }" }], context: { dataloader: GraphQL::Dataloader.new(nonblocking: true) }) assert res[0].context.dataloader.nonblocking? Fiber.set_scheduler(nil) end end describe "#run_isolated" do module RunIsolated class CountSource < GraphQL::Dataloader::Source def fetch(ids) @count ||= 0 @count += ids.size ids.map { |_id| @count } end end end it "uses its own queue" do dl = GraphQL::Dataloader.new result = {} dl.append_job { result[:a] = 1 } dl.append_job { result[:b] = 2 } dl.append_job { result[:c] = 3 } dl.run_isolated { result[:d] = 4 } assert_equal({ d: 4 }, result) dl.run_isolated { _r1 = dl.with(RunIsolated::CountSource).request(1) _r2 = dl.with(RunIsolated::CountSource).request(2) r3 = dl.with(RunIsolated::CountSource).request(3) # This is going to `Fiber.yield` result[:e] = r3.load } assert_equal({ d: 4, e: 3 }, result) dl.run assert_equal({ a: 1, b: 2, c: 3, d: 4, e: 3 }, result) end it "shares a cache" do dl = GraphQL::Dataloader.new result = {} dl.run_isolated { _r1 = dl.with(RunIsolated::CountSource).request(1) _r2 = dl.with(RunIsolated::CountSource).request(2) r3 = dl.with(RunIsolated::CountSource).request(3) # Run all three of the above requests: result[:a] = r3.load } dl.append_job { # This should return cached from above result[:b] = dl.with(RunIsolated::CountSource).load(1) } dl.append_job { # This one is run by itself result[:c] = dl.with(RunIsolated::CountSource).load(4) } assert_equal({ a: 3 }, result) dl.run assert_equal({ a: 3, b: 3, c: 4 }, result) end end describe "thread local variables" do module ThreadVariable class Type < GraphQL::Schema::Object field :key, String, null: false field :value, String, null: false end class Source < GraphQL::Dataloader::Source def fetch(keys) keys.map { |key| OpenStruct.new(key: key, value: Thread.current[key.to_sym]) } end end class QueryType < GraphQL::Schema::Object field :thread_var, ThreadVariable::Type do argument :key, GraphQL::Types::String end def thread_var(key:) dataloader.with(ThreadVariable::Source).load(key) end end class Schema < GraphQL::Schema query ThreadVariable::QueryType use GraphQL::Dataloader end end it "sets the parent thread locals in the execution fiber" do Thread.current[:test_thread_var] = 'foobarbaz' result = ThreadVariable::Schema.execute(<<-GRAPHQL) { threadVar(key: "test_thread_var") { key value } } GRAPHQL expected_result = { "data" => { "threadVar" => { "key" => "test_thread_var", "value" => "foobarbaz" } } } assert_graphql_equal expected_result, result.to_h end end describe "thread-local variables with custom dataloader" do module CustomThreadVariable class Type < GraphQL::Schema::Object field :key, String, null: false field :value, String, null: false end class CustomDataloader < GraphQL::Dataloader def get_fiber_variables { test_thread_var: "bazbarfoo" } end end class Source < GraphQL::Dataloader::Source def fetch(keys) keys.map { |key| OpenStruct.new(key: key, value: Thread.current[key.to_sym]) } end end class QueryType < GraphQL::Schema::Object field :thread_var, CustomThreadVariable::Type do argument :key, GraphQL::Types::String end def thread_var(key:) dataloader.with(CustomThreadVariable::Source).load(key) end end class Schema < GraphQL::Schema query CustomThreadVariable::QueryType use CustomDataloader end end it "sets the parent thread locals in the execution fiber" do result = CustomThreadVariable::Schema.execute(<<-GRAPHQL) { threadVar(key: "test_thread_var") { key value } } GRAPHQL expected_result = { "data" => { "threadVar" => { "key" => "test_thread_var", "value" => "bazbarfoo" } } } assert_graphql_equal expected_result, result.to_h end end describe "dataloader calls from inside sources" do class NestedDataloaderCallsSchema < GraphQL::Schema class Echo < GraphQL::Dataloader::Source def fetch(keys) keys end end class Nested < GraphQL::Dataloader::Source def fetch(keys) dataloader.with(Echo).load_all(keys) end end class Nested2 < GraphQL::Dataloader::Source def fetch(keys) dataloader.with(Nested).load_all(keys) end end class QueryType < GraphQL::Schema::Object field :nested, String field :nested2, String def nested dataloader.with(Nested).load("nested") end def nested2 dataloader.with(Nested2).load("nested2") end end query QueryType use GraphQL::Dataloader end end it "loads data from inside source methods" do assert_equal({ "data" => { "nested" => "nested" } }, NestedDataloaderCallsSchema.execute("{ nested }")) assert_equal({ "data" => { "nested2" => "nested2" } }, NestedDataloaderCallsSchema.execute("{ nested2 }")) assert_equal({ "data" => { "nested" => "nested", "nested2" => "nested2" } }, NestedDataloaderCallsSchema.execute("{ nested nested2 }")) end describe "with lazy authorization hooks" do class LazyAuthHookSchema < GraphQL::Schema class Source < ::GraphQL::Dataloader::Source def fetch(ids) return ids.map {|i| i * 2} end end class BarType < GraphQL::Schema::Object field :id, Integer def id object end def self.authorized?(object, context) -> { true } end end class FooType < GraphQL::Schema::Object field :dataloader_value, BarType def self.authorized?(object, context) -> { true } end def dataloader_value dataloader.with(Source).load(1) end end class QueryType < GraphQL::Schema::Object field :foo, FooType def foo {} end end use GraphQL::Dataloader query QueryType lazy_resolve Proc, :call end it "resolves everything" do dataloader_query = """ query { foo { dataloaderValue { id } } } """ dataloader_result = LazyAuthHookSchema.execute(dataloader_query) assert_equal 2, dataloader_result["data"]["foo"]["dataloaderValue"]["id"] end end end graphql-ruby-2.5.19/spec/graphql/directive_spec.rb000066400000000000000000000216141514115062600221370ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe "GraphQL::Directive" do let(:variables) { {"t" => true, "f" => false} } let(:result) { Dummy::Schema.execute(query_string, variables: variables) } describe "on fields" do let(:query_string) { %|query directives($t: Boolean!, $f: Boolean!) { cheese(id: 1) { # plain fields: skipFlavor: flavor @skip(if: true) dontSkipFlavor: flavor @skip(if: false) dontSkipDontIncludeFlavor: flavor @skip(if: false), @include(if: false) skipAndInclude: flavor @skip(if: true), @include(if: true) includeFlavor: flavor @include(if: $t) dontIncludeFlavor: flavor @include(if: $f) # fields in fragments ... includeIdField ... dontIncludeIdField ... skipIdField ... dontSkipIdField } } fragment includeIdField on Cheese { includeId: id @include(if: true) } fragment dontIncludeIdField on Cheese { dontIncludeId: id @include(if: false) } fragment skipIdField on Cheese { skipId: id @skip(if: true) } fragment dontSkipIdField on Cheese { dontSkipId: id @skip(if: false) } | } describe "child fields" do let(:query_string) { <<-GRAPHQL { __type(name: """ Cheese """) { fields { name } fields @skip(if: true) { isDeprecated } } } GRAPHQL } it "skips child fields too" do first_field = result["data"]["__type"]["fields"].first assert first_field.key?("name") assert !first_field.key?("isDeprecated") end end describe "when directive uses argument with default value" do describe "with false" do let(:query_string) { <<-GRAPHQL query($f: Boolean = false) { cheese(id: 1) { dontIncludeFlavor: flavor @include(if: $f) dontSkipFlavor: flavor @skip(if: $f) } } GRAPHQL } it "is not included" do assert !result["data"]["cheese"].key?("dontIncludeFlavor") end it "is not skipped" do assert result["data"]["cheese"].key?("dontSkipFlavor") end end describe "with true" do let(:query_string) { <<-GRAPHQL query($t: Boolean = true) { cheese(id: 1) { includeFlavor: flavor @include(if: $t) skipFlavor: flavor @skip(if: $t) } } GRAPHQL } it "is included" do assert result["data"]["cheese"].key?("includeFlavor") end it "is skipped" do assert !result["data"]["cheese"].key?("skipFlavor") end end end it "intercepts fields" do expected = { "data" =>{ "cheese" => { "dontSkipFlavor" => "Brie", "includeFlavor" => "Brie", "includeId" => 1, "dontSkipId" => 1, }, }} assert_equal(expected, result) end end describe "on fragments spreads and inline fragments" do let(:query_string) { %|query directives { cheese(id: 1) { ... skipFlavorField @skip(if: true) ... dontSkipFlavorField @skip(if: false) ... includeFlavorField @include(if: true) ... dontIncludeFlavorField @include(if: false) ... on Cheese @skip(if: true) { skipInlineId: id } ... on Cheese @skip(if: false) { dontSkipInlineId: id } ... on Cheese @include(if: true) { includeInlineId: id } ... on Cheese @include(if: false) { dontIncludeInlineId: id } ... @skip(if: true) { skipNoType: id } ... @skip(if: false) { dontSkipNoType: id } } } fragment includeFlavorField on Cheese { includeFlavor: flavor } fragment dontIncludeFlavorField on Cheese { dontIncludeFlavor: flavor } fragment skipFlavorField on Cheese { skipFlavor: flavor } fragment dontSkipFlavorField on Cheese { dontSkipFlavor: flavor } |} it "intercepts fragment spreads" do expected = { "data" => { "cheese" => { "dontSkipFlavor" => "Brie", "includeFlavor" => "Brie", "dontSkipInlineId" => 1, "includeInlineId" => 1, "dontSkipNoType" => 1, }, }} assert_equal(expected, result) end end describe "merging @skip and @include" do let(:field_included?) { r = result["data"]["cheese"]; r.has_key?('flavor') && r.has_key?('withVariables') } let(:skip?) { false } let(:include?) { true } let(:variables) { {"skip" => skip?, "include" => include?} } let(:query_string) {" query getCheese ($include: Boolean!, $skip: Boolean!) { cheese(id: 1) { flavor @include(if: #{include?}) @skip(if: #{skip?}), withVariables: flavor @include(if: $include) @skip(if: $skip) } } "} # behavior as defined in # https://github.com/facebook/graphql/blob/master/spec/Section%203%20--%20Type%20System.md#include describe "when @skip=false and @include=true" do let(:skip?) { false } let(:include?) { true } it "is included" do assert field_included? end end describe "when @skip=false and @include=false" do let(:skip?) { false } let(:include?) { false } it "is not included" do assert !field_included? end end describe "when @skip=true and @include=true" do let(:skip?) { true } let(:include?) { true } it "is not included" do assert !field_included? end end describe "when @skip=true and @include=false" do let(:skip?) { true } let(:include?) { false } it "is not included" do assert !field_included? end end describe "when evaluating skip on query selection and fragment" do describe "with @skip" do let(:query_string) {" query getCheese ($skip: Boolean!) { cheese(id: 1) { flavor, withVariables: flavor, ...F0 } } fragment F0 on Cheese { flavor @skip(if: #{skip?}) withVariables: flavor @skip(if: $skip) } "} describe "and @skip=false" do let(:skip?) { false } it "is included" do assert field_included? end end describe "and @skip=true" do let(:skip?) { true } it "is included" do assert field_included? end end end end describe "when evaluating conflicting @skip and @include on query selection and fragment" do let(:query_string) {" query getCheese ($include: Boolean!, $skip: Boolean!) { cheese(id: 1) { ... on Cheese @include(if: #{include?}) { flavor } withVariables: flavor @include(if: $include), ...F0 } } fragment F0 on Cheese { flavor @skip(if: #{skip?}), withVariables: flavor @skip(if: $skip) } "} describe "when @skip=false and @include=true" do let(:skip?) { false } let(:include?) { true } it "is included" do assert field_included? end end describe "when @skip=false and @include=false" do let(:skip?) { false } let(:include?) { false } it "is included" do assert field_included? end end describe "when @skip=true and @include=true" do let(:skip?) { true } let(:include?) { true } it "is included" do assert field_included? end end describe "when @skip=true and @include=false" do let(:skip?) { true } let(:include?) { false } it "is not included" do assert !field_included? end end end describe "when handling multiple fields at the same level" do describe "when at least one occurrence would be included" do let(:query_string) {" query getCheese ($include: Boolean!, $skip: Boolean!) { cheese(id: 1) { ... on Cheese { flavor } flavor @include(if: #{include?}), flavor @skip(if: #{skip?}), withVariables: flavor, withVariables: flavor @include(if: $include), withVariables: flavor @skip(if: $skip) } } "} let(:skip?) { true } let(:include?) { false } it "is included" do assert field_included? end end describe "when no occurrence would be included" do let(:query_string) {" query getCheese ($include: Boolean!, $skip: Boolean!) { cheese(id: 1) { flavor @include(if: #{include?}), flavor @skip(if: #{skip?}), withVariables: flavor @include(if: $include), withVariables: flavor @skip(if: $skip) } } "} let(:skip?) { true } let(:include?) { false } it "is not included" do assert !field_included? end end end end end graphql-ruby-2.5.19/spec/graphql/execution/000077500000000000000000000000001514115062600206215ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/graphql/execution/breadth_runtime_spec.rb000066400000000000000000000222061514115062600253360ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe "GraphQL::Execution::Interpreter for breadth-first execution" do # A breadth-first interpreter uses the following runtime interface: # - evaluate_selection(result_key, ast_nodes, selections_result) # - exit_with_inner_result? class SimpleBreadthRuntime < GraphQL::Execution::Interpreter::Runtime class BreadthObject < GraphQL::Execution::Interpreter::Runtime::GraphQLResultHash attr_accessor :breadth_index attr_accessor :results_by_key def collect_result(result_name, result_value) results_by_key[result_name][breadth_index] = result_value true end end def initialize(query:) query.multiplex = GraphQL::Execution::Multiplex.new( schema: query.schema, queries: [query], context: query.context, max_complexity: nil, ) super(query: query) @breadth_results_by_key = {} end def run(&block) query.current_trace.execute_multiplex(multiplex: query.multiplex) do query.current_trace.execute_query(query: query, &block) end ensure delete_all_interpreter_context end def evaluate_breadth_selection(objects, parent_type, node) result_key = node.alias || node.name @breadth_results_by_key[result_key] = Array.new(objects.size) objects.each_with_index do |object, index| app_value = parent_type.wrap(object, query.context) breadth_object = BreadthObject.new(nil, parent_type, app_value, nil, false, node.selections, false, node, nil, nil) breadth_object.ordered_result_keys = [] breadth_object.breadth_index = index breadth_object.results_by_key = @breadth_results_by_key state = get_current_runtime_state state.current_result_name = nil state.current_result = breadth_object @dataloader.append_job { evaluate_selection(result_key, node, breadth_object) } end @dataloader.run @breadth_results_by_key[result_key] end end class PassthroughLoader < GraphQL::Batch::Loader def perform(objects) objects.each { |obj| fulfill(obj, obj) } end end class SimpleHashBatchLoader < GraphQL::Batch::Loader def initialize(key) super() @key = key end def perform(objects) objects.each { |obj| fulfill(obj, obj.fetch(@key)) } end end class UpcaseExtension < GraphQL::Schema::FieldExtension def after_resolve(value:, **rest) value&.upcase end end class RangeInput < GraphQL::Schema::InputObject argument :min, Int argument :max, Int def prepare min..max end end class BreadthBaseField < GraphQL::Schema::Field def authorized?(obj, args, ctx) if !ctx[:field_auth].nil? ctx[:field_auth] elsif !ctx[:lazy_field_auth].nil? PassthroughLoader.load(ctx[:lazy_field_auth]) elsif !ctx[:field_auth_with_error].nil? raise GraphQL::ExecutionError, "Not authorized" unless ctx[:field_auth_with_error] else true end end end class BreadthBaseObject < GraphQL::Schema::Object field_class BreadthBaseField end class BreadthTestQuery < BreadthBaseObject field :foo, String def foo object[:foo] end field :lazy_foo, String def lazy_foo SimpleHashBatchLoader.for(:foo).load(object) end field :maybe_lazy_foo, String def maybe_lazy_foo if object[:foo] == "beep" SimpleHashBatchLoader.for(:foo).load(object) else object[:foo] end end field :nested_lazy_foo, String def nested_lazy_foo PassthroughLoader .load(object) .then { |obj| SimpleHashBatchLoader.for(:foo).load(obj) } .then { |str| str } end field :upcase_foo, String, extensions: [UpcaseExtension] def upcase_foo object[:foo] end field :lazy_upcase_foo, String, extensions: [UpcaseExtension] def lazy_upcase_foo SimpleHashBatchLoader.for(:foo).load(object) end field :go_boom, String def go_boom raise GraphQL::ExecutionError, "boom" end field :args, String do |f| f.argument :a, String f.argument :b, String end def args(a:, b:) "#{a}#{b}" end field :valid_args, String do |f| f.argument :a, String, validates: { length: { is: 1 } } end def valid_args(a:) a end field :range, String do |f| f.argument :input, RangeInput end def range(input:) "#{input.min}-#{input.max}" end field :extras, String, extras: [:lookahead] def extras(lookahead:) lookahead.field.name end # uses default resolver... field :fizz, String end class BreadthTestSchema < GraphQL::Schema use(GraphQL::Batch) query BreadthTestQuery end SCHEMA_FROM_DEF = GraphQL::Schema.from_definition( %|type Query { a: String }|, default_resolve: { "Query" => { "a" => ->(obj, _args, _ctx) { obj["a"] } }, }, ) OBJECTS = [{ foo: "fizz" }, { foo: "buzz" }, { foo: "beep" }, { foo: "boom" }].freeze EXPECTED_RESULTS = ["fizz", "buzz", "beep", "boom"].freeze def test_maps_sync_results result = map_breadth_objects(OBJECTS, "{ foo }") assert_equal EXPECTED_RESULTS, result end def test_maps_lazy_results result = map_breadth_objects(OBJECTS, "{ lazyFoo }") assert_equal EXPECTED_RESULTS, result end def test_maps_sometimes_lazy_results result = map_breadth_objects(OBJECTS, "{ maybeLazyFoo }") assert_equal EXPECTED_RESULTS, result end def test_maps_nested_lazy_results result = map_breadth_objects(OBJECTS, "{ nestedLazyFoo }") assert_equal EXPECTED_RESULTS, result end def test_maps_field_extension_results result = map_breadth_objects(OBJECTS, "{ upcaseFoo }") assert_equal ["FIZZ", "BUZZ", "BEEP", "BOOM"], result end def test_maps_lazy_field_extension_results result = map_breadth_objects(OBJECTS, "{ lazyUpcaseFoo }") assert_equal ["FIZZ", "BUZZ", "BEEP", "BOOM"], result end def test_maps_fields_with_authorization context = { field_auth: false } result = map_breadth_objects(OBJECTS, "{ foo }", context: context) assert_equal [nil, nil, nil, nil], result end def test_maps_fields_with_lazy_authorization context = { lazy_field_auth: false } result = map_breadth_objects(OBJECTS, "{ foo }", context: context) assert result.all? { |r| r.is_a?(GraphQL::UnauthorizedFieldError) } end def test_maps_fields_with_authorization_errors context = { field_auth_with_error: false } result = map_breadth_objects(OBJECTS, "{ foo }", context: context) assert result.all? { |r| r.is_a?(GraphQL::ExecutionError) } end def test_maps_field_errors result = map_breadth_objects(OBJECTS, "{ goBoom }") assert result.all? { |r| r.is_a?(GraphQL::ExecutionError) } assert_equal ["boom", "boom", "boom", "boom"], result.map(&:message) end def test_maps_basic_arguments doc = %|{ args(a:"fizz", b:"buzz") }| result = map_breadth_objects([{}], doc) assert_equal ["fizzbuzz"], result end def test_maps_basic_arguments_with_variables doc = %|query($b: String) { args(a:"fizz", b: $b) }| result = map_breadth_objects([{}], doc, variables: { b: "buzz" }) assert_equal ["fizzbuzz"], result end def test_maps_invalidated_arguments doc = %|query { validArgs(a: "boo") }| result = map_breadth_objects([{}], doc) assert result.first.is_a?(GraphQL::ExecutionError) assert_equal "a is the wrong length (should be 1)", result.first.message end def test_maps_prepared_input_object doc = %|{ range(input: { min: 1, max: 2 }) }| result = map_breadth_objects([{}], doc) assert_equal ["1-2"], result end def test_maps_prepared_input_object_with_variables doc = %|query($b: Int) { range(input: { min: 1, max: $b }) }| result = map_breadth_objects([{}], doc, variables: { b: 2 }) assert_equal ["1-2"], result end def test_maps_extras_arguments result = map_breadth_objects([{}], "{ extras }") assert_equal ["extras"], result end def test_uses_default_resolver_for_hash_keys result = map_breadth_objects([{ fizz: "buzz" }], "{ fizz }") assert_equal ["buzz"], result end def test_uses_default_resolver_for_method_calls entity = Struct.new(:fizz) result = map_breadth_objects([entity.new("buzz")], "{ fizz }") assert_equal ["buzz"], result end def test_maps_schemas_from_definition objects = [{ "a" => "1" }, { "a" => "2" }] result = map_breadth_objects(objects, "{ a }", schema: SCHEMA_FROM_DEF) assert_equal ["1", "2"], result end def test_maps_results_with_multiple_nodes result = map_breadth_objects(OBJECTS, "{ foo foo }") assert_equal EXPECTED_RESULTS, result end private def map_breadth_objects(objects, doc, schema: BreadthTestSchema, variables: {}, context: {}) query = GraphQL::Query.new( schema, document: GraphQL.parse(doc), variables: variables, context: context, ) node = query.document.definitions.first.selections.first runtime = SimpleBreadthRuntime.new(query: query) runtime.run { runtime.evaluate_breadth_selection(objects, schema.query, node) } end end graphql-ruby-2.5.19/spec/graphql/execution/errors_spec.rb000066400000000000000000000234011514115062600234740ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe "GraphQL::Execution::Errors" do class ParentErrorsTestSchema < GraphQL::Schema class ErrorD < RuntimeError; end rescue_from(ErrorD) do |err, obj, args, ctx, field| raise GraphQL::ExecutionError, "ErrorD on #{obj.inspect} at #{field ? "#{field.path}(#{args})" : "boot"}" end end class ErrorsTestSchema < ParentErrorsTestSchema ErrorD = ParentErrorsTestSchema::ErrorD class ErrorA < RuntimeError; end class ErrorB < RuntimeError; end class ErrorC < RuntimeError attr_reader :value def initialize(value:) @value = value super end end class ErrorASubclass < ErrorA; end class ErrorBChildClass < ErrorB; end class ErrorBGrandchildClass < ErrorBChildClass; end rescue_from(ErrorA) do |err, obj, args, ctx, field| ctx[:errors] << "#{err.message} (#{field.owner.name}.#{field.graphql_name}, #{obj.inspect}, #{args.inspect})" nil end rescue_from(ErrorBChildClass) do |*| "Handled ErrorBChildClass" end # Trying to assert that _specificity_ takes priority # over sequence, but the stability of that assertion # depends on the underlying implementation. rescue_from(ErrorBGrandchildClass) do |*| "Handled ErrorBGrandchildClass" end rescue_from(ErrorB) do |*| raise GraphQL::ExecutionError, "boom!" end rescue_from(ErrorC) do |err, *| err.value end class ErrorList < Array def each raise ErrorB end end class Thing < GraphQL::Schema::Object def self.authorized?(obj, ctx) if ctx[:authorized] == false raise ErrorD end true end field :string, String, null: false def string "a string" end end class ValuesInput < GraphQL::Schema::InputObject argument :value, Int, loads: Thing def self.object_from_id(type, value, ctx) if value == 1 :thing else raise ErrorD end end end class PickyString < GraphQL::Schema::Scalar def self.coerce_input(value, ctx) if value == "picky" value else raise ErrorB, "The string wasn't \"picky\"" end end end class Query < GraphQL::Schema::Object field :f1, Int do argument :a1, Int, required: false end def f1(a1: nil) raise ErrorA, "f1 broke" end field :f2, Int def f2 -> { raise ErrorA, "f2 broke" } end field :f3, Int def f3 raise ErrorB end field :f4, Int, null: false def f4 raise ErrorC.new(value: 20) end field :f5, Int def f5 raise ErrorASubclass, "raised subclass" end field :f6, Int def f6 -> { raise ErrorB } end field :f7, String def f7 raise ErrorBGrandchildClass end field :f8, String do argument :input, PickyString end def f8(input:) input end field :f9, String do argument :thing_id, ID, loads: Thing end def f9(thing:) thing[:id] end field :thing, Thing def thing :thing end field :input_field, Int do argument :values, ValuesInput end field :non_nullable_array, [String], null: false def non_nullable_array [nil] end field :error_in_each, [Int] def error_in_each ErrorList.new end end query(Query) lazy_resolve(Proc, :call) def self.object_from_id(id, ctx) if id == "boom" raise ErrorB end { thing: true, id: id } end def self.resolve_type(type, obj, ctx) Thing end end class ErrorsTestSchemaWithoutInterpreter < GraphQL::Schema class Query < GraphQL::Schema::Object field :non_nullable_array, [String], null: false def non_nullable_array [nil] end end query(Query) end describe "rescue_from handling" do it "can replace values with `nil`" do ctx = { errors: [] } res = ErrorsTestSchema.execute "{ f1(a1: 1) }", context: ctx, root_value: :abc assert_equal({ "data" => { "f1" => nil } }, res) assert_equal ["f1 broke (ErrorsTestSchema::Query.f1, :abc, #{{a1: 1}.inspect})"], ctx[:errors] end it "rescues errors from lazy code" do ctx = { errors: [] } res = ErrorsTestSchema.execute("{ f2 }", context: ctx) assert_equal({ "data" => { "f2" => nil } }, res) assert_equal ["f2 broke (ErrorsTestSchema::Query.f2, nil, {})"], ctx[:errors] end it "picks the most specific handler and uses the return value from it" do res = ErrorsTestSchema.execute("{ f7 }") assert_equal({ "data" => { "f7" => "Handled ErrorBGrandchildClass" } }, res) end it "rescues errors from lazy code with handlers that re-raise" do res = ErrorsTestSchema.execute("{ f6 }") expected_error = { "message"=>"boom!", "locations"=>[{"line"=>1, "column"=>3}], "path"=>["f6"] } assert_equal({ "data" => { "f6" => nil }, "errors" => [expected_error] }, res) end it "can raise new errors" do res = ErrorsTestSchema.execute("{ f3 }") expected_error = { "message"=>"boom!", "locations"=>[{"line"=>1, "column"=>3}], "path"=>["f3"] } assert_equal({ "data" => { "f3" => nil }, "errors" => [expected_error] }, res) end it "can replace values with non-nil" do res = ErrorsTestSchema.execute("{ f4 }") assert_equal({ "data" => { "f4" => 20 } }, res) end it "rescues subclasses" do context = { errors: [] } res = ErrorsTestSchema.execute("{ f5 }", context: context) assert_equal({ "data" => { "f5" => nil } }, res) assert_equal ["raised subclass (ErrorsTestSchema::Query.f5, nil, {})"], context[:errors] end describe "errors raised when coercing inputs" do it "rescues them" do res1 = ErrorsTestSchema.execute("{ f8(input: \"picky\") }") assert_equal "picky", res1["data"]["f8"] res2 = ErrorsTestSchema.execute("{ f8(input: \"blah\") }") assert_equal ["errors"], res2.keys assert_equal ["boom!"], res2["errors"].map { |e| e["message"] } res3 = ErrorsTestSchema.execute("query($v: PickyString!) { f8(input: $v) }", variables: { v: "blah" }) assert_equal ["errors"], res3.keys assert_equal ["Variable $v of type PickyString! was provided invalid value"], res3["errors"].map { |e| e["message"] } assert_equal [["boom!"]], res3["errors"].map { |e| e["extensions"]["problems"].map { |pr| pr["explanation"] } } end end describe "errors raised when loading objects from ID" do it "rescues them" do res1 = ErrorsTestSchema.execute("{ f9(thingId: \"abc\") }") assert_equal "abc", res1["data"]["f9"] res2 = ErrorsTestSchema.execute("{ f9(thingId: \"boom\") }") assert_equal ["boom!"], res2["errors"].map { |e| e["message"] } end end describe "errors raised in authorized hooks" do it "rescues them" do context = { authorized: false } res = ErrorsTestSchema.execute(" { thing { string } } ", context: context) assert_equal ["ErrorD on nil at Query.thing({})"], res["errors"].map { |e| e["message"] } end end describe "errors raised in input_object loads" do it "rescues them from literal values" do context = { authorized: false } res = ErrorsTestSchema.execute(" { inputField(values: { value: 2 }) } ", root_value: :root, context: context) # It would be better to have the arguments here, but since this error was raised during _creation_ of keywords, # so the runtime arguments aren't available now. assert_equal ["ErrorD on :root at Query.inputField()"], res["errors"].map { |e| e["message"] } end it "rescues them from variable values" do context = { authorized: false } res = ErrorsTestSchema.execute( "query($values: ValuesInput!) { inputField(values: $values) } ", variables: { values: { "value" => 2 } }, context: context, ) assert_equal ["ErrorD on nil at Query.inputField()"], res["errors"].map { |e| e["message"] } end end describe "errors raised in non_nullable_array loads" do it "outputs the appropriate error message when using non-interpreter schema" do res = ErrorsTestSchemaWithoutInterpreter.execute("{ nonNullableArray }") expected_error = { "message" => "Cannot return null for non-nullable element of type 'String!' for Query.nonNullableArray", "path" => ["nonNullableArray", 0], "locations" => [{ "line" => 1, "column" => 3 }] } assert_equal({ "data" => nil, "errors" => [expected_error] }, res) end it "outputs the appropriate error message when using interpreter schema" do res = ErrorsTestSchema.execute("{ nonNullableArray }") expected_error = { "message" => "Cannot return null for non-nullable element of type 'String!' for Query.nonNullableArray", "path" => ["nonNullableArray", 0], "locations" => [{ "line" => 1, "column" => 3 }] } assert_equal({ "data" => nil, "errors" => [expected_error] }, res) end end describe "when .each on a list type raises an error" do it "rescues it properly" do res = ErrorsTestSchema.execute("{ __typename errorInEach }") expected_error = { "message" => "boom!", "locations"=>[{"line"=>1, "column"=>14}], "path"=>["errorInEach"] } assert_equal({ "data" => { "__typename" => "Query", "errorInEach" => nil }, "errors" => [expected_error] }, res) end end end end graphql-ruby-2.5.19/spec/graphql/execution/instrumentation_spec.rb000066400000000000000000000157021514115062600254300ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Schema do describe "instrumentation teardown bug" do # This instrumenter records that it ran, # or raises an error if instructed to do so class InstrumenterError < StandardError attr_reader :key def initialize(key) @key = key super() end end module LogInstrumenter def self.generate(context_key_sym) hook_method = :"#{context_key_sym}_run_hook" mod = Module.new mod.define_method(:execute_query) do |query:, &block| public_send(hook_method, query, "begin") result = nil begin result = super(query: query, &block) ensure public_send(hook_method, query, "end") end result end mod.define_method(:execute_multiplex) do |multiplex:, &block| public_send(hook_method, multiplex, "begin") result = nil begin result = super(multiplex: multiplex, &block) ensure public_send(hook_method, multiplex, "end") end result end mod.define_method(hook_method) do |unit_of_work, event_name| log_key = :"#{context_key_sym}_did_#{event_name}" error_key = :"#{context_key_sym}_should_raise_#{event_name}" unit_of_work.context[log_key] = true if unit_of_work.context[error_key] raise InstrumenterError.new(log_key) end end mod end end module ExecutionErrorTrace def execute_query(query:) if query.context[:raise_execution_error] raise GraphQL::ExecutionError, "Raised from trace execute_query" end super end end # This is how you might add queries from a persisted query backend module QueryStringTrace def execute_multiplex(multiplex:) multiplex.queries.each do |query| if query.context[:extra_query_string] && query.query_string.nil? query.query_string = query.context[:extra_query_string] end end super end end let(:query_type) { Class.new(GraphQL::Schema::Object) do graphql_name "Query" field :int, Integer do argument :value, Integer, required: false end def int(value:) value end end } let(:schema) { spec = self Class.new(GraphQL::Schema) do query(spec.query_type) trace_with(LogInstrumenter.generate(:second_instrumenter)) trace_with(LogInstrumenter.generate(:first_instrumenter)) trace_with(ExecutionErrorTrace) trace_with(QueryStringTrace) end } describe "query instrumenters" do it "before_query of the 2nd instrumenter does not run but after_query does" do context = {second_instrumenter_should_raise_begin: true} assert_raises InstrumenterError do schema.execute(" { int(value: 2) } ", context: context) end assert context[:first_instrumenter_did_begin] assert context[:first_instrumenter_did_end] assert context[:second_instrumenter_did_begin] refute context[:second_instrumenter_did_end] end it "runs after_query even if a previous after_query raised an error" do context = {second_instrumenter_should_raise_end: true} err = assert_raises InstrumenterError do schema.execute(" { int(value: 2) } ", context: context) end # The error came from the second instrumenter: assert_equal :second_instrumenter_did_end, err.key # But the first instrumenter still got a chance to teardown assert context[:first_instrumenter_did_begin] assert context[:first_instrumenter_did_end] assert context[:second_instrumenter_did_begin] assert context[:second_instrumenter_did_end] end it "rescues execution errors from execute_query" do context = {raise_execution_error: true} res = schema.execute(" { int(value: 2) } ", context: context) assert_equal({ "data" => nil, "errors" => [ { "message" => "Raised from trace execute_query" }, ] }, res.to_h) end it "can assign a query string there" do context = { extra_query_string: "{ __typename }"} res = schema.execute(nil, context: context) assert_equal "Query", res["data"]["__typename"] end end describe "within a multiplex" do let(:multiplex_schema) { Class.new(schema) { trace_with(LogInstrumenter.generate(:second_instrumenter)) trace_with(LogInstrumenter.generate(:first_instrumenter)) } } it "only runs after_multiplex if before_multiplex finished" do multiplex_ctx = {second_instrumenter_should_raise_begin: true} query_1_ctx = {} query_2_ctx = {} assert_raises InstrumenterError do multiplex_schema.multiplex( [ {query: "{int(value: 1)}", context: query_1_ctx}, {query: "{int(value: 2)}", context: query_2_ctx}, ], context: multiplex_ctx ) end assert multiplex_ctx[:first_instrumenter_did_begin] assert multiplex_ctx[:first_instrumenter_did_end] assert multiplex_ctx[:second_instrumenter_did_begin] refute multiplex_ctx[:second_instrumenter_did_end] # No query instrumentation was run at all expected_ctx_size = GraphQL::Schema.use_visibility_profile? ? 1 : 0 assert_equal expected_ctx_size, query_1_ctx.size assert_equal expected_ctx_size, query_2_ctx.size end it "does full and partial query runs" do multiplex_ctx = {} query_1_ctx = {} query_2_ctx = {second_instrumenter_should_raise_begin: true} assert_raises InstrumenterError do multiplex_schema.multiplex( [ { query: " { int(value: 2) } ", context: query_1_ctx }, { query: " { int(value: 2) } ", context: query_2_ctx }, ], context: multiplex_ctx ) end # multiplex got a full run assert multiplex_ctx[:first_instrumenter_did_begin] assert multiplex_ctx[:first_instrumenter_did_end] assert multiplex_ctx[:second_instrumenter_did_begin] assert multiplex_ctx[:second_instrumenter_did_end] # query 1 got a full run assert query_1_ctx[:first_instrumenter_did_begin] assert query_1_ctx[:first_instrumenter_did_end] assert query_1_ctx[:second_instrumenter_did_begin] assert query_1_ctx[:second_instrumenter_did_end] # query 2 got a partial run assert query_2_ctx[:first_instrumenter_did_begin] assert query_2_ctx[:first_instrumenter_did_end] assert query_2_ctx[:second_instrumenter_did_begin] refute query_2_ctx[:second_instrumenter_did_end] end end end end graphql-ruby-2.5.19/spec/graphql/execution/interpreter/000077500000000000000000000000001514115062600231645ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/graphql/execution/interpreter/arguments_spec.rb000066400000000000000000000027141514115062600265340ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe "GraphQL::Execution::Interpreter::Arguments" do class InterpreterArgsTestSchema < GraphQL::Schema class SearchParams < GraphQL::Schema::InputObject argument :query, String, required: false end class Query < GraphQL::Schema::Object field :search, [String], null: false do argument :params, SearchParams, required: false argument :limit, Int end end query(Query) end def arguments(query_str) query = GraphQL::Query.new(InterpreterArgsTestSchema, query_str) ast_node = query.document.definitions.first.selections.first query_type = query.get_type("Query") field = query.get_field(query_type, "search") query.arguments_for(ast_node, field) end it "provides .dig" do query_str = <<-GRAPHQL { search(limit: 10, params: { query: "abcde" } ) } GRAPHQL args = arguments(query_str) assert_equal 10, args.dig(:limit) assert_equal "abcde", args.dig(:params, :query) assert_nil args.dig(:nothing) assert_nil args.dig(:params, :nothing) assert_nil args.dig(:nothing, :nothing, :nothing) end it "is frozen, and so are its constituent hashes" do query_str = <<-GRAPHQL { search(limit: 10, params: { query: "abcde" } ) } GRAPHQL args = arguments(query_str) assert args.frozen? assert args.argument_values.frozen? assert args.keyword_arguments.frozen? end end graphql-ruby-2.5.19/spec/graphql/execution/interpreter_spec.rb000066400000000000000000000634171514115062600245360ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" require_relative "../subscriptions_spec" describe GraphQL::Execution::Interpreter do module InterpreterTest class Box def initialize(value: nil, &block) @value = value @block = block end def value if @block @value = @block.call @block = nil end @value end end class Expansion < GraphQL::Schema::Object field :sym, String, null: false field :lazy_sym, String, null: false field :name, String, null: false field :cards, ["InterpreterTest::Card"], null: false def self.authorized?(expansion, ctx) if expansion.sym == "NOPE" false else true end end def cards Query::CARDS.select { |c| c.expansion_sym == @object.sym } end def lazy_sym Box.new(value: object.sym) end field :always_cached_value, Integer, null: false def always_cached_value raise "should never be called" end end class Card < GraphQL::Schema::Object field :name, String, null: false field :colors, "[InterpreterTest::Color]", null: false field :expansion, Expansion, null: false def expansion Query::EXPANSIONS.find { |e| e.sym == @object.expansion_sym } end field :parent_class_name, String, null: false, extras: [:parent] def parent_class_name(parent:) parent.class.name end end class Color < GraphQL::Schema::Enum value "WHITE" value "BLUE" value "BLACK" value "RED" value "GREEN" end class Entity < GraphQL::Schema::Union possible_types Card, Expansion def self.resolve_type(obj, ctx) obj.sym ? Expansion : Card end end class FieldCounter < GraphQL::Schema::Object implements GraphQL::Types::Relay::Node field :field_counter, FieldCounter, null: false def field_counter; self.class.generate_tag(context); end field :calls, Integer, null: false do argument :expected, Integer end def calls(expected:) c = context[:calls] += 1 if c != expected raise "Expected #{expected} calls but had #{c} so far" else c end end field :runtime_info, String, null: false do argument :a, Integer, required: false argument :b, Integer, required: false end def runtime_info(a: nil, b: nil) inspect_context end field :lazy_runtime_info, String, null: false do argument :a, Integer, required: false argument :b, Integer, required: false end def lazy_runtime_info(a: nil, b: nil) Box.new { inspect_context } end def self.generate_tag(context) context[:field_counters_count] ||= 0 current_count = context[:field_counters_count] += 1 "field_counter_#{current_count}" end private def inspect_context "<#{interpreter_context_for(:current_object).object.inspect}> #{interpreter_context_for(:current_path)} -> #{interpreter_context_for(:current_field).path}(#{interpreter_context_for(:current_arguments).size})" end def interpreter_context_for(key) # Make sure it's set in query context and interpreter namespace. base_ctx_value = context[key] interpreter_ctx_value = context.namespace(:interpreter)[key] if base_ctx_value != interpreter_ctx_value raise "Context mismatch for #{key} -> #{base_ctx_value} / interpreter: #{interpreter_ctx_value}" else base_ctx_value end end end class Query < GraphQL::Schema::Object # Try a root-level authorized hook that returns a lazy value def self.authorized?(obj, ctx) Box.new(value: true) end field :card, Card do argument :name, String end def card(name:) Box.new(value: CARDS.find { |c| c.name == name }) end field :expansion, Expansion do argument :sym, String end def expansion(sym:) EXPANSIONS.find { |e| e.sym == sym } end field :expansion_raw, Expansion, null: false def expansion_raw raw_value(sym: "RAW", name: "Raw expansion", always_cached_value: 42) end field :expansion_mixed, [Expansion], null: false def expansion_mixed expansions + [expansion_raw] end field :expansions, [Expansion], null: false def expansions EXPANSIONS end class ExpansionData < OpenStruct end CARDS = [ OpenStruct.new(name: "Dark Confidant", colors: ["BLACK"], expansion_sym: "RAV"), ] EXPANSIONS = [ ExpansionData.new(name: "Ravnica, City of Guilds", sym: "RAV"), # This data has an error, for testing null propagation ExpansionData.new(name: nil, sym: "XYZ"), # This is not allowed by .authorized?, ExpansionData.new(name: nil, sym: "NOPE"), ] field :find, [Entity], null: false do argument :id, [ID] end def find(id:) id.map do |ent_id| Query::EXPANSIONS.find { |e| e.sym == ent_id } || Query::CARDS.find { |c| c.name == ent_id } end end field :find_many, [Entity, null: true], null: false do argument :ids, [ID] end def find_many(ids:) find(id: ids).map { |e| Box.new(value: e) } end field :field_counter, FieldCounter, null: false def field_counter; FieldCounter.generate_tag(context) ; end include GraphQL::Types::Relay::HasNodeField include GraphQL::Types::Relay::HasNodesField class NestedQueryResult < GraphQL::Schema::Object field :result, String field :current_path, [String] end field :nested_query, NestedQueryResult do argument :query, String end def nested_query(query:) result = context.schema.multiplex([{query: query}], context: { allow_pending_thread_state: true }).first { result: JSON.dump(result), current_path: context[:current_path], } end end class Counter < GraphQL::Schema::Object field :value, Integer, null: false def value counter.value end field :lazy_value, Integer, null: false def lazy_value Box.new { counter.value } end field :incremented_value, Integer, hash_key: :incremented_value field :increment, Counter, null: false def increment v = counter.value += 1 { counter: counter, incremented_value: v, } end private def counter if object.is_a?(Hash) && object.key?(:counter) object[:counter] else object end end end class Mutation < GraphQL::Schema::Object field :increment_counter, Counter, null: false def increment_counter counter = context[:counter] v = counter.value += 1 { counter: counter, incremented_value: v } end end class Schema < GraphQL::Schema query(Query) mutation(Mutation) lazy_resolve(Box, :value) use GraphQL::Schema::AlwaysVisible def self.object_from_id(id, ctx) OpenStruct.new(id: id) end def self.id_from_object(obj, type, ctx) obj.id end def self.resolve_type(type, obj, ctx) FieldCounter end class EnsureArgsAreObject def self.trace(event, data) case event when "execute_field", "execute_field_lazy" args = data[:query].context[:current_arguments] if !args.is_a?(GraphQL::Execution::Interpreter::Arguments) raise "Expected arguments object, got #{args.class}: #{args.inspect}" end end yield end end tracer EnsureArgsAreObject module EnsureThreadCleanedUp def execute_multiplex(multiplex:) res = super runtime_info = Fiber[:__graphql_runtime_info] if !runtime_info.nil? && runtime_info != {} if !multiplex.context[:allow_pending_thread_state] # `nestedQuery` can allow this raise "Query did not clean up runtime state, found: #{runtime_info.inspect}" end end res end end trace_with(EnsureThreadCleanedUp) end end it "runs a query" do query_string = <<-GRAPHQL query($expansion: String!, $id1: ID!, $id2: ID!){ card(name: "Dark Confidant") { colors expansion { ... { name } cards { name } } } expansion(sym: $expansion) { ... ExpansionFields } find(id: [$id1, $id2]) { __typename ... on Card { name } ... on Expansion { sym } } } fragment ExpansionFields on Expansion { cards { name } } GRAPHQL vars = {expansion: "RAV", id1: "Dark Confidant", id2: "RAV"} result = InterpreterTest::Schema.execute(query_string, variables: vars) assert_equal ["BLACK"], result["data"]["card"]["colors"] assert_equal "Ravnica, City of Guilds", result["data"]["card"]["expansion"]["name"] assert_equal [{"name" => "Dark Confidant"}], result["data"]["card"]["expansion"]["cards"] assert_equal [{"name" => "Dark Confidant"}], result["data"]["expansion"]["cards"] expected_abstract_list = [ {"__typename" => "Card", "name" => "Dark Confidant"}, {"__typename" => "Expansion", "sym" => "RAV"}, ] assert_equal expected_abstract_list, result["data"]["find"] assert_nil Fiber[:__graphql_runtime_info] end it "runs a nested query and maintains proper state" do query_str = "query($queryStr: String!) { nestedQuery(query: $queryStr) { result currentPath } }" result = InterpreterTest::Schema.execute(query_str, variables: { queryStr: "{ __typename }" }) assert_equal '{"data":{"__typename":"Query"}}', result["data"]["nestedQuery"]["result"] assert_equal ["nestedQuery"], result["data"]["nestedQuery"]["currentPath"] assert_nil Fiber[:__graphql_runtime_info] end it "runs mutation roots atomically and sequentially" do query_str = <<-GRAPHQL mutation { i1: incrementCounter { value lazyValue i2: increment { value incrementedValue lazyValue } i3: increment { value incrementedValue lazyValue } } i4: incrementCounter { value incrementedValue lazyValue } i5: incrementCounter { value incrementedValue lazyValue } } GRAPHQL result = InterpreterTest::Schema.execute(query_str, context: { counter: OpenStruct.new(value: 0) }) expected_data = { "i1" => { "value" => 1, # All of these get `3` as lazy value. They're resolved together, # since they aren't _root_ mutation fields. "lazyValue" => 3, "i2" => { "value" => 2, "incrementedValue" => 2, "lazyValue" => 3 }, "i3" => { "value" => 3, "incrementedValue" => 3, "lazyValue" => 3 }, }, "i4" => { "value" => 4, "incrementedValue" => 4, "lazyValue" => 4}, "i5" => { "value" => 5, "incrementedValue" => 5, "lazyValue" => 5}, } assert_graphql_equal expected_data, result["data"] end it "runs skip and include" do query_str = <<-GRAPHQL query($truthy: Boolean!, $falsey: Boolean!){ exp1: expansion(sym: "RAV") @skip(if: true) { name } exp2: expansion(sym: "RAV") @skip(if: false) { name } exp3: expansion(sym: "RAV") @include(if: true) { name } exp4: expansion(sym: "RAV") @include(if: false) { name } exp5: expansion(sym: "RAV") @include(if: $truthy) { name } exp6: expansion(sym: "RAV") @include(if: $falsey) { name } } GRAPHQL vars = {truthy: true, falsey: false} result = InterpreterTest::Schema.execute(query_str, variables: vars) expected_data = { "exp2" => {"name" => "Ravnica, City of Guilds"}, "exp3" => {"name" => "Ravnica, City of Guilds"}, "exp5" => {"name" => "Ravnica, City of Guilds"}, } assert_graphql_equal expected_data, result["data"] assert_nil Fiber[:__graphql_runtime_info] end describe "runtime info in context" do it "is available" do res = InterpreterTest::Schema.execute <<-GRAPHQL { fieldCounter { runtimeInfo(a: 1, b: 2) fieldCounter { runtimeInfo lazyRuntimeInfo(a: 1) } } } GRAPHQL assert_equal '<"field_counter_1"> ["fieldCounter", "runtimeInfo"] -> FieldCounter.runtimeInfo(2)', res["data"]["fieldCounter"]["runtimeInfo"] # These are both `field_counter_2`, but one is lazy assert_equal '<"field_counter_2"> ["fieldCounter", "fieldCounter", "runtimeInfo"] -> FieldCounter.runtimeInfo(0)', res["data"]["fieldCounter"]["fieldCounter"]["runtimeInfo"] assert_equal '<"field_counter_2"> ["fieldCounter", "fieldCounter", "lazyRuntimeInfo"] -> FieldCounter.lazyRuntimeInfo(1)', res["data"]["fieldCounter"]["fieldCounter"]["lazyRuntimeInfo"] end end describe "null propagation" do it "propagates nulls" do query_str = <<-GRAPHQL { expansion(sym: "XYZ") { name sym lazySym } } GRAPHQL res = InterpreterTest::Schema.execute(query_str) # Although the expansion was found, its name of `nil` # propagated to here assert_nil res["data"].fetch("expansion") assert_equal ["Cannot return null for non-nullable field Expansion.name"], res["errors"].map { |e| e["message"] } assert_nil Fiber[:__graphql_runtime_info] end it "places errors ahead of data in the response" do query_str = <<-GRAPHQL { expansion(sym: "XYZ") { name } } GRAPHQL res = InterpreterTest::Schema.execute(query_str) assert_equal ["errors", "data"], res.keys end it "propagates nulls in lists" do query_str = <<-GRAPHQL { expansions { name sym lazySym } } GRAPHQL res = InterpreterTest::Schema.execute(query_str) # A null in one of the list items removed the whole list assert_nil(res["data"]) end it "works with unions that fail .authorized?" do res = InterpreterTest::Schema.execute <<-GRAPHQL { find(id: "NOPE") { ... on Expansion { sym } } } GRAPHQL assert_equal ["Cannot return null for non-nullable element of type 'Entity!' for Query.find"], res["errors"].map { |e| e["message"] } end it "works with lists of unions" do res = InterpreterTest::Schema.execute <<-GRAPHQL { findMany(ids: ["RAV", "NOPE", "BOGUS"]) { ... on Expansion { sym } } } GRAPHQL assert_equal 3, res["data"]["findMany"].size assert_equal "RAV", res["data"]["findMany"][0]["sym"] assert_nil res["data"]["findMany"][1] assert_nil res["data"]["findMany"][2] assert_equal false, res.key?("errors") assert_equal Hash, res["data"].class assert_equal Array, res["data"]["findMany"].class end end describe "duplicated fields" do it "doesn't run them multiple times" do query_str = <<-GRAPHQL { fieldCounter { calls(expected: 1) # This should not be called since it matches the above calls(expected: 1) fieldCounter { calls(expected: 2) } ...ExtraFields } } fragment ExtraFields on FieldCounter { fieldCounter { # This should not be called since it matches the inline field: calls(expected: 2) # This _should_ be called c3: calls(expected: 3) } } GRAPHQL # It will raise an error if it doesn't match the expectation res = InterpreterTest::Schema.execute(query_str, context: { calls: 0 }) assert_equal 3, res["data"]["fieldCounter"]["fieldCounter"]["c3"] end end describe "backwards compatibility" do it "handles a legacy nodes field" do res = InterpreterTest::Schema.execute('{ node(id: "abc") { id } }') assert_equal "abc", res["data"]["node"]["id"] res = InterpreterTest::Schema.execute('{ nodes(ids: ["abc", "xyz"]) { id } }') assert_equal ["abc", "xyz"], res["data"]["nodes"].map { |n| n["id"] } end end describe "returning raw values" do it "returns raw value" do query_str = <<-GRAPHQL { expansionRaw { name sym alwaysCachedValue } } GRAPHQL res = InterpreterTest::Schema.execute(query_str) assert_equal({ sym: "RAW", name: "Raw expansion", always_cached_value: 42 }, res["data"]["expansionRaw"]) end end describe "returning raw values and resolved fields" do it "returns raw value" do query_str = <<-GRAPHQL { expansionRaw { name sym alwaysCachedValue } } GRAPHQL res = InterpreterTest::Schema.execute(query_str) assert_equal({ sym: "RAW", name: "Raw expansion", always_cached_value: 42 }, res["data"]["expansionRaw"]) end end describe "Lazy skips" do class LazySkipSchema < GraphQL::Schema class Query < GraphQL::Schema::Object def self.authorized?(obj, ctx) -> { true } end field :skip, String def skip context.skip end field :lazy_skip, String def lazy_skip -> { context.skip } end field :mixed_skips, [String] def mixed_skips [ "a", context.skip, "c", -> { context.skip }, "e", ] end end class NothingSubscription < GraphQL::Schema::Subscription field :nothing, String def authorized?(*) -> { true } end def update { nothing: object } end end class Subscription < GraphQL::Schema::Object field :nothing, subscription: NothingSubscription end query Query subscription Subscription use InMemoryBackend::Subscriptions, extra: nil lazy_resolve Proc, :call end it "skips properly" do res = LazySkipSchema.execute("{ skip }") assert_equal({}, res["data"]) refute res.key?("errors") res = LazySkipSchema.execute("{ mixedSkips }") assert_equal({ "mixedSkips" => ["a", "c", "e"] }, res["data"]) refute res.key?("errors") res = LazySkipSchema.execute("{ lazySkip }") assert_equal({}, res["data"]) refute res.key?("errors") res = LazySkipSchema.execute("subscription { nothing { nothing } }") assert_equal({}, res["data"]) refute res.key?("errors") # Make sure an update works properly LazySkipSchema.subscriptions.trigger(:nothing, {}, :nothing_at_all) _key, updates = LazySkipSchema.subscriptions.deliveries.first assert_equal "nothing_at_all", updates[0]["data"]["nothing"]["nothing"] end end describe "GraphQL::ExecutionErrors from connection fields" do module ConnectionErrorTest class BaseField < GraphQL::Schema::Field def authorized?(obj, args, ctx) ctx[:authorized_calls] ||= 0 ctx[:authorized_calls] += 1 raise GraphQL::ExecutionError, "#{name} is not authorized" end end class BaseConnection < GraphQL::Types::Relay::BaseConnection node_nullable(false) edge_nullable(false) edges_nullable(false) end class BaseEdge < GraphQL::Types::Relay::BaseEdge node_nullable(false) end class Thing < GraphQL::Schema::Object field_class BaseField connection_type_class BaseConnection edge_type_class BaseEdge field :title, String, null: false field :body, String, null: false end class Query < GraphQL::Schema::Object field :things, Thing.connection_type, null: false def things [{title: "a"}, {title: "b"}, {title: "c"}] end field :thing, Thing, null: false def thing { title: "a", body: "b", } end end class Schema < GraphQL::Schema query Query end end it "returns only 1 error and stops resolving fields after that" do res = ConnectionErrorTest::Schema.execute("{ things { nodes { title } } }") assert_equal 1, res["errors"].size assert_equal 1, res.context[:authorized_calls] res = ConnectionErrorTest::Schema.execute("{ things { edges { node { title } } } }") assert_equal 1, res["errors"].size assert_equal 1, res.context[:authorized_calls] res = ConnectionErrorTest::Schema.execute("{ thing { title body } }") assert_equal 1, res["errors"].size assert_equal 1, res.context[:authorized_calls] end end describe "GraphQL::ExecutionErrors from non-null list fields" do module ListErrorTest class BaseField < GraphQL::Schema::Field def authorized?(*) raise GraphQL::ExecutionError, "#{name} is not authorized" end end class Thing < GraphQL::Schema::Object field_class BaseField field :title, String, null: false end class Query < GraphQL::Schema::Object field :things, [Thing], null: false def things [{title: "a"}, {title: "b"}, {title: "c"}] end end class Schema < GraphQL::Schema query Query end end it "returns only 1 error" do res = ListErrorTest::Schema.execute("{ things { title } }") assert_equal 1, res["errors"].size end end describe "Invalid null from raised execution error doesn't halt parent fields" do class RaisedErrorSchema < GraphQL::Schema module Iface include GraphQL::Schema::Interface field :bar, String, null: false end class Txn < GraphQL::Schema::Object field :fails, String, null: false def fails raise GraphQL::ExecutionError, "boom" end end class Concrete < GraphQL::Schema::Object implements Iface field :txn, Txn def txn {} end field :msg, String def msg "THIS SHOULD SHOW UP" end end class Query < GraphQL::Schema::Object field :iface, Iface def iface {} end end query(Query) orphan_types([Concrete]) def self.resolve_type(type, obj, ctx) Concrete end end it "resolves fields on the parent object" do querystring = """ { iface { ... on Concrete { txn { fails } msg } } } """ result = RaisedErrorSchema.execute(querystring) expected_result = { "errors" => [ { "message"=>"boom", "locations"=>[{"line"=>6, "column"=>15}], "path"=>["iface", "txn", "fails"] }, ], "data" => { "iface" => { "txn" => nil, "msg" => "THIS SHOULD SHOW UP" }, }, } assert_graphql_equal expected_result, result.to_h end end it "supports extras: [:parent]" do query_str = <<-GRAPHQL { card(name: "Dark Confidant") { parentClassName } expansion(sym: "RAV") { cards { parentClassName } } } GRAPHQL res = InterpreterTest::Schema.execute(query_str, context: { calls: 0 }) assert_equal "NilClass", res["data"]["card"].fetch("parentClassName") assert_equal "InterpreterTest::Query::ExpansionData", res["data"]["expansion"]["cards"].first["parentClassName"] end describe "fragment used twice in different ways" do class FragmentBugSchema < GraphQL::Schema class ProductVariant < GraphQL::Schema::Object field :product, "FragmentBugSchema::Product" end class Product < GraphQL::Schema::Object field :id, ID field :variants, [ProductVariant] def variants [{ product: { id: "1" } }] end end class Query < GraphQL::Schema::Object field :variant, ProductVariant def variant { product: { id: "1" } } end end query(Query) end it "executes successfully" do query_str = <<-GRAPHQL { variant { ...variantFields ... on ProductVariant { product { variants { ...variantFields } } } } } fragment variantFields on ProductVariant { product { id } } GRAPHQL res = FragmentBugSchema.execute(query_str).to_h expected_result = { "variant" => { "product" => { "id" => "1", "variants" => [ { "product" => { "id" => "1" } } ] } } } assert_equal(expected_result, res["data"]) end end describe "multiplex queries" do it "runs multiplex queries" do result = InterpreterTest::Schema.multiplex([ { query: "query Card($name: String!) { card(name: $name) { colors } }", variables: { name: "Dark Confidant" }, operation_name: "Card" }, { query: "query Expansion($expansion: String!) { expansion(sym: $expansion) { cards { name } } }", variables: { expansion: "RAV" }, operation_name: "Expansion" } ]) assert_equal ["BLACK"], result[0]["data"]["card"]["colors"] assert_equal [{"name" => "Dark Confidant"}], result[1]["data"]["expansion"]["cards"] assert_nil Fiber[:__graphql_runtime_info] end end end graphql-ruby-2.5.19/spec/graphql/execution/lazy/000077500000000000000000000000001514115062600216005ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/graphql/execution/lazy/lazy_method_map_spec.rb000066400000000000000000000030751514115062600263200ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Execution::Lazy::LazyMethodMap do def self.test_lazy_method_map it "handles multithreaded access" do a = Class.new b = Class.new(a) c = Class.new(b) lazy_method_map.set(a, :a) threads = 1000.times.map do |i| Thread.new { d = Class.new(c) assert_equal :a, lazy_method_map.get(d.new) } end threads.map(&:join) end it "dups" do a = Class.new b = Class.new(a) c = Class.new(b) lazy_method_map.set(a, :a) lazy_method_map.get(b.new) lazy_method_map.get(c.new) dup_map = lazy_method_map.dup assert_equal 3, dup_map.instance_variable_get(:@storage).size assert_equal :a, dup_map.get(a.new) assert_equal :a, dup_map.get(b.new) assert_equal :a, dup_map.get(c.new) end end describe "with a plain hash" do let(:lazy_method_map) { GraphQL::Execution::Lazy::LazyMethodMap.new(use_concurrent: false) } test_lazy_method_map it "has a Ruby Hash inside" do storage = lazy_method_map .instance_variable_get(:@storage) .instance_variable_get(:@storage) assert_instance_of Hash, storage end end describe "with a Concurrent::Map" do let(:lazy_method_map) { GraphQL::Execution::Lazy::LazyMethodMap.new(use_concurrent: true) } test_lazy_method_map it "has a Concurrent::Map inside" do storage = lazy_method_map.instance_variable_get(:@storage) assert_instance_of Concurrent::Map, storage end end end graphql-ruby-2.5.19/spec/graphql/execution/lazy_spec.rb000066400000000000000000000145411514115062600231440ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Execution::Lazy do include LazyHelpers describe "resolving" do it "calls value handlers" do res = run_query('{ int(value: 2, plus: 1) }') assert_equal 3, res["data"]["int"] end it "Works with Query.new" do query_str = '{ int(value: 2, plus: 1) }' query = GraphQL::Query.new(LazyHelpers::LazySchema, query_str) res = query.result assert_equal 3, res["data"]["int"] end it "can do nested lazy values" do res = run_query %| { a: nestedSum(value: 3) { value nestedSum(value: 7) { value nestedSum(value: 1) { value nestedSum(value: -50) { value } } } } b: nestedSum(value: 2) { value nestedSum(value: 11) { value nestedSum(value: 2) { value nestedSum(value: -50) { value } } } } c: listSum(values: [1,2]) { nestedSum(value: 3) { value } } } | expected_data = { "a"=>{"value"=>14, "nestedSum"=>{ "value"=>46, "nestedSum"=>{ "value"=>95, "nestedSum"=>{"value"=>90} } }}, "b"=>{"value"=>14, "nestedSum"=>{ "value"=>46, "nestedSum"=>{ "value"=>95, "nestedSum"=>{"value"=>90} } }}, "c"=>[ {"nestedSum"=>{"value"=>14}}, {"nestedSum"=>{"value"=>14}} ], } assert_graphql_equal expected_data, res["data"] end [ [1, 2, LazyHelpers::MAGIC_NUMBER_WITH_LAZY_AUTHORIZED_HOOK], [2, LazyHelpers::MAGIC_NUMBER_WITH_LAZY_AUTHORIZED_HOOK, 1], [LazyHelpers::MAGIC_NUMBER_WITH_LAZY_AUTHORIZED_HOOK, 1, 2], ].each do |ordered_values| it "resolves each field at one depth before proceeding to the next depth (using #{ordered_values})" do res = run_query <<-GRAPHQL, variables: { values: ordered_values } query($values: [Int!]!) { listSum(values: $values) { nestedSum(value: 3) { value } } } GRAPHQL # Even though magic number `44`'s `.authorized?` hook returns a lazy value, # these fields should be resolved together and return the same value. assert_equal 56, res["data"]["listSum"][0]["nestedSum"]["value"] assert_equal 56, res["data"]["listSum"][1]["nestedSum"]["value"] assert_equal 56, res["data"]["listSum"][2]["nestedSum"]["value"] end end it "Handles fields that return nil and batches lazy resultion across depths when possible" do values = [ LazyHelpers::MAGIC_NUMBER_THAT_RETURNS_NIL, LazyHelpers::MAGIC_NUMBER_WITH_LAZY_AUTHORIZED_HOOK, 1, 2, ] res = run_query <<-GRAPHQL, variables: { values: values } query($values: [Int!]!) { listSum(values: $values) { nullableNestedSum(value: 3) { value } } } GRAPHQL values = res["data"]["listSum"].map { |s| s && s["nullableNestedSum"]["value"] } assert_equal [nil, 56, 56, 56], values end it "propagates nulls to the root" do res = run_query %| { nestedSum(value: 1) { value nestedSum(value: 2) { nestedSum(value: 13) { value } } } }| assert_nil(res["data"]) assert_equal 1, res["errors"].length end it "propagates partial nulls" do res = run_query %| { nullableNestedSum(value: 1) { value nullableNestedSum(value: 2) { ns: nestedSum(value: 13) { value } } } }| expected_data = { "nullableNestedSum" => { "value" => 1, "nullableNestedSum" => nil, } } assert_equal(expected_data, res["data"]) assert_equal 1, res["errors"].length end it "handles raised errors" do res = run_query %| { a: nullableNestedSum(value: 1) { value } b: nullableNestedSum(value: 13) { value } c: nullableNestedSum(value: 2) { value } }| expected_data = { "a" => { "value" => 3 }, "b" => nil, "c" => { "value" => 3 }, } assert_graphql_equal expected_data, res["data"] expected_errors = [{ "message"=>"13 is unlucky", "locations"=>[{"line"=>4, "column"=>9}], "path"=>["b"], }] assert_equal expected_errors, res["errors"] end it "resolves mutation fields right away" do res = run_query %| { a: nestedSum(value: 2) { value } b: nestedSum(value: 4) { value } c: nestedSum(value: 6) { value } }| assert_equal [12, 12, 12], res["data"].values.map { |d| d["value"] } res = run_query %| mutation { a: nestedSum(value: 2) { value } b: nestedSum(value: 4) { value } c: nestedSum(value: 6) { value } } | assert_equal [2, 4, 6], res["data"].values.map { |d| d["value"] } end end describe "Schema#sync_lazy(object)" do it "Passes objects to that hook at runtime" do res = run_query <<-GRAPHQL { a: nullableNestedSum(value: 1001) { value } b: nullableNestedSum(value: 1013) { value } c: nullableNestedSum(value: 1002) { value } } GRAPHQL # This odd, non-adding behavior is hacked into `#sync_lazy` assert_equal 101, res["data"]["a"]["value"] assert_equal 113, res["data"]["b"]["value"] assert_equal 102, res["data"]["c"]["value"] end end describe "LazyMethodMap" do class SubWrapper < LazyHelpers::Wrapper; end let(:map) { GraphQL::Execution::Lazy::LazyMethodMap.new } it "finds methods for classes and subclasses" do map.set(LazyHelpers::Wrapper, :item) map.set(LazyHelpers::SumAll, :value) b = LazyHelpers::Wrapper.new(1) sub_b = LazyHelpers::Wrapper.new(2) s = LazyHelpers::SumAll.new(3) assert_equal(:item, map.get(b)) assert_equal(:item, map.get(sub_b)) assert_equal(:value, map.get(s)) end end end graphql-ruby-2.5.19/spec/graphql/execution/lookahead_spec.rb000066400000000000000000000657251514115062600241260ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Execution::Lookahead do module LookaheadTest DATA = [ OpenStruct.new(name: "Cardinal", is_waterfowl: false, similar_species_names: ["Scarlet Tanager"], genus: OpenStruct.new(latin_name: "Piranga")), OpenStruct.new(name: "Scarlet Tanager", is_waterfowl: false, similar_species_names: ["Cardinal"], genus: OpenStruct.new(latin_name: "Cardinalis")), OpenStruct.new(name: "Great Egret", is_waterfowl: false, similar_species_names: ["Great Blue Heron"], genus: OpenStruct.new(latin_name: "Ardea")), OpenStruct.new(name: "Great Blue Heron", is_waterfowl: true, similar_species_names: ["Great Egret"], genus: OpenStruct.new(latin_name: "Ardea")), ] def DATA.find_by_name(name) DATA.find { |b| b.name == name } end module Node include GraphQL::Schema::Interface field :id, ID, null: false end class BirdGenus < GraphQL::Schema::Object implements Node field :name, String, null: false field :latin_name, String, null: false field :id, ID, null: false, method: :latin_name end class BirdSpecies < GraphQL::Schema::Object implements Node field :name, String, null: false field :id, ID, null: false, method: :name field :is_waterfowl, Boolean, null: false field :similar_species, [BirdSpecies], null: false def similar_species object.similar_species_names.map { |n| DATA.find_by_name(n) } end field :genus, BirdGenus, null: false, extras: [:lookahead] def genus(lookahead:) if lookahead.selects?(:latin_name) context[:lookahead_latin_name] += 1 end object.genus end end class PlantSpecies < GraphQL::Schema::Object implements Node field :name, String, null: false field :id, ID, null: false, method: :name field :is_edible, Boolean, null: false end class Species < GraphQL::Schema::Union possible_types BirdSpecies, PlantSpecies end class Query < GraphQL::Schema::Object field :find_bird_species, BirdSpecies do argument :by_name, String end def find_bird_species(by_name:) DATA.find_by_name(by_name) end field :node, Node do argument :id, ID end def node(id:) if (node = DATA.find_by_name(id)) node else DATA.map { |d| d.genus }.select { |g| g.name == id } end end field :species, Species do argument :id, ID end def species(id:) DATA.find_by_name(id) end end module LookaheadInstrumenter def execute_query(query:) query.context[:root_lookahead_selections] = query.lookahead.selections super end end class Schema < GraphQL::Schema query(Query) trace_with LookaheadInstrumenter end class AlwaysVisibleSchema < Schema use GraphQL::Schema::AlwaysVisible end end describe "looking ahead" do let(:document) { GraphQL.parse <<-GRAPHQL query($name: String!){ findBirdSpecies(byName: $name) { name similarSpecies { likesWater: isWaterfowl } } t: __typename } GRAPHQL } let(:schema) { LookaheadTest::Schema } let(:query) { GraphQL::Query.new(schema, document: document, variables: { name: "Cardinal" }) } it "has a good test setup" do res = query.result assert_equal [false], res["data"]["findBirdSpecies"]["similarSpecies"].map { |s| s["likesWater"] } end it "can detect fields on objects with symbol or string" do lookahead = query.lookahead.selection("findBirdSpecies") assert_equal true, lookahead.selects?("similarSpecies") assert_equal true, lookahead.selects?(:similar_species) assert_equal false, lookahead.selects?("isWaterfowl") assert_equal false, lookahead.selects?(:is_waterfowl) end it "detects by name, not by alias" do assert_equal true, query.lookahead.selects?("__typename") end it "uses null lookahead when no operation is selected" do query = GraphQL::Query.new(schema, document: document, variables: { name: "Cardinal" }, operation_name: "Invalid") assert_selection_is_null query.lookahead end describe "with a NullWarden" do let(:schema) { LookaheadTest::AlwaysVisibleSchema } it "works" do lookahead = query.lookahead.selection("findBirdSpecies") assert_equal true, lookahead.selects?("similarSpecies") assert_equal true, lookahead.selects?(:similar_species) assert_equal false, lookahead.selects?("isWaterfowl") assert_equal false, lookahead.selects?(:is_waterfowl) end end describe "on unions" do let(:document) { GraphQL.parse <<-GRAPHQL { species(id: "Cardinal") { ... on BirdSpecies { name isWaterfowl } ... on PlantSpecies { name isEdible } } } GRAPHQL } it "works" do lookahead = query.lookahead.selection(:species) assert lookahead.selects?(:name) assert_equal [:name, :is_waterfowl, :name, :is_edible], lookahead.selections.map(&:name) end it "works with different selected types" do lookahead = query.lookahead.selection(:species) # Both have `name` assert lookahead.selects?(:name, selected_type: LookaheadTest::BirdSpecies) assert lookahead.selects?(:name, selected_type: LookaheadTest::PlantSpecies) # Only birds have `isWaterfowl` assert lookahead.selects?(:is_waterfowl, selected_type: LookaheadTest::BirdSpecies) refute lookahead.selects?(:is_waterfowl, selected_type: LookaheadTest::PlantSpecies) # Only plants have `isEdible` refute lookahead.selects?(:is_edible, selected_type: LookaheadTest::BirdSpecies) assert lookahead.selects?(:is_edible, selected_type: LookaheadTest::PlantSpecies) end end describe "fields on interfaces" do let(:document) { GraphQL.parse <<-GRAPHQL query { node(id: "Cardinal") { id ... on BirdSpecies { name } ...Other } } fragment Other on BirdGenus { latinName } GRAPHQL } it "finds fields on object types and interface types" do node_lookahead = query.lookahead.selection("node") assert_equal [:id, :name, :latin_name], node_lookahead.selections.map(&:name) end end describe "inspect" do it "works for root lookaheads" do assert_includes query.lookahead.inspect, "# "Cardinal" }) end it "is true when no constraints are given" do assert_equal true, lookahead.selects?(:find_bird_species, arguments: {}) assert_equal true, lookahead.selects?("__typename", arguments: {}) end it "is false when some given constraints aren't satisfied" do assert_equal false, lookahead.selects?(:find_bird_species, arguments: { by_name: "Chickadee" }) assert_equal false, lookahead.selects?(:find_bird_species, arguments: { by_name: "Cardinal", other: "Nonsense" }) end describe "with literal values" do let(:document) { GraphQL.parse <<-GRAPHQL { findBirdSpecies(byName: "Great Blue Heron") { isWaterfowl } } GRAPHQL } it "works" do assert_equal true, lookahead.selects?(:find_bird_species, arguments: { by_name: "Great Blue Heron" }) end end end it "can do a chained lookahead" do next_lookahead = query.lookahead.selection(:find_bird_species, arguments: { by_name: "Cardinal" }) assert_equal true, next_lookahead.selected? nested_selection = next_lookahead.selection(:similar_species).selection(:is_waterfowl, arguments: {}) assert_equal true, nested_selection.selected? assert_equal false, next_lookahead.selection(:similar_species).selection(:name).selected? end it "can detect fields on lists with symbol or string" do assert_equal true, query.lookahead.selection(:find_bird_species).selection(:similar_species).selection(:is_waterfowl).selected? assert_equal true, query.lookahead.selection("findBirdSpecies").selection("similarSpecies").selection("isWaterfowl").selected? end describe "merging branches and fragments" do let(:document) { GraphQL.parse <<-GRAPHQL { findBirdSpecies(name: "Cardinal") { similarSpecies { __typename } } ...F ... { findBirdSpecies(name: "Cardinal") { similarSpecies { isWaterfowl } } } } fragment F on Query { findBirdSpecies(name: "Cardinal") { similarSpecies { name } } } GRAPHQL } it "finds selections using merging" do merged_lookahead = query.lookahead.selection(:find_bird_species).selection(:similar_species) assert merged_lookahead.selects?(:__typename) assert merged_lookahead.selects?(:is_waterfowl) assert merged_lookahead.selects?(:name) end end end describe "in queries" do it "can be an extra" do query_str = <<-GRAPHQL { cardinal: findBirdSpecies(byName: "Cardinal") { genus { __typename } } scarletTanager: findBirdSpecies(byName: "Scarlet Tanager") { genus { latinName } } greatBlueHeron: findBirdSpecies(byName: "Great Blue Heron") { genus { latinName } } } GRAPHQL context = {lookahead_latin_name: 0} res = LookaheadTest::Schema.execute(query_str, context: context) refute res.key?("errors") assert_equal 2, context[:lookahead_latin_name] assert_equal [:find_bird_species], context[:root_lookahead_selections].map(&:name).uniq assert_equal( [{ by_name: "Cardinal" }, { by_name: "Scarlet Tanager" }, { by_name: "Great Blue Heron" }], context[:root_lookahead_selections].map(&:arguments) ) end it "works for invalid queries" do context = {lookahead_latin_name: 0} res = LookaheadTest::Schema.execute("{ doesNotExist }", context: context) assert res.key?("errors") assert_equal 0, context[:lookahead_latin_name] end describe "When there is an argument error" do class NestedArgumentErrorSchema < GraphQL::Schema class Data < GraphQL::Schema::Object field :echo, String do argument :input, String end def echo(input:) input end end class Query < GraphQL::Schema::Object field :data, Data, extras: [:lookahead] def data(lookahead:) context[:args_class] = lookahead.selection(:echo).arguments.class {} end end query(Query) end it "uses empty arguments" do query_str = "query getEcho($input: String = null) { data { echo(input: $input) } }" res = NestedArgumentErrorSchema.execute(query_str, variables: {}) assert_equal ["`null` is not a valid input for `String!`, please provide a value for this argument."], res["errors"].map { |err| err["message"] } assert_equal Hash, res.context[:args_class] good_res = NestedArgumentErrorSchema.execute("{ data { echo(input: \"Hello\") } }") assert_equal "Hello", good_res["data"]["data"]["echo"] assert_equal Hash, good_res.context[:args_class] end end end describe '#selections' do let(:document) { GraphQL.parse <<-GRAPHQL query { findBirdSpecies(byName: "Laughing Gull") { name similarSpecies { likesWater: isWaterfowl } } } GRAPHQL } def query(doc = document) GraphQL::Query.new(LookaheadTest::Schema, document: doc) end it "provides a list of all selections" do ast_node = document.definitions.first.selections.first field = LookaheadTest::Query.fields["findBirdSpecies"] lookahead = GraphQL::Execution::Lookahead.new(query: query, ast_nodes: [ast_node], field: field) assert_equal [:name, :similar_species], lookahead.selections.map(&:name) end it "filters outs selections which do not match arguments" do ast_node = document.definitions.first lookahead = GraphQL::Execution::Lookahead.new(query: query, ast_nodes: [ast_node], root_type: LookaheadTest::Query) arguments = { by_name: "Cardinal" } assert_equal lookahead.selections(arguments: arguments).map(&:name), [] end it "includes selections which match arguments" do ast_node = document.definitions.first lookahead = GraphQL::Execution::Lookahead.new(query: query, ast_nodes: [ast_node], root_type: LookaheadTest::Query) arguments = { by_name: "Laughing Gull" } assert_equal lookahead.selections(arguments: arguments).map(&:name), [:find_bird_species] end it 'handles duplicate selections across fragments' do doc = GraphQL.parse <<-GRAPHQL query { ... on Query { ...MoreFields } } fragment MoreFields on Query { findBirdSpecies(byName: "Laughing Gull") { name } findBirdSpecies(byName: "Laughing Gull") { ...EvenMoreFields } } fragment EvenMoreFields on BirdSpecies { similarSpecies { likesWater: isWaterfowl } } GRAPHQL lookahead = query(doc).lookahead root_selections = lookahead.selections assert_equal [:find_bird_species], root_selections.map(&:name), "Selections are merged" assert_equal 2, root_selections.first.ast_nodes.size, "It represents both nodes" assert_equal [:name, :similar_species], root_selections.first.selections.map(&:name), "Subselections are merged" end it "avoids merging selections for same field name on distinct types" do query = GraphQL::Query.new(LookaheadTest::Schema, <<-GRAPHQL) query { node(id: "Cardinal") { ... on BirdSpecies { name } ... on BirdGenus { name } id } } GRAPHQL node_lookahead = query.lookahead.selection("node") assert_equal( [[LookaheadTest::Node, :id], [LookaheadTest::BirdSpecies, :name], [LookaheadTest::BirdGenus, :name]], node_lookahead.selections.map { |s| [s.owner_type, s.name] } ) end it "works for missing selections" do ast_node = document.definitions.first.selections.first field = LookaheadTest::Query.fields["findBirdSpecies"] lookahead = GraphQL::Execution::Lookahead.new(query: query, ast_nodes: [ast_node], field: field) null_lookahead = lookahead.selection(:genus) # This is an implementation detail, but I want to make sure the test is set up right assert_instance_of GraphQL::Execution::Lookahead::NullLookahead, null_lookahead assert_equal [], null_lookahead.selections end it "excludes fields skipped by directives" do document = GraphQL.parse <<-GRAPHQL query($skipName: Boolean!, $includeGenus: Boolean!){ findBirdSpecies(byName: "Cardinal") { id name @skip(if: $skipName) genus @include(if: $includeGenus) } } GRAPHQL query = GraphQL::Query.new(LookaheadTest::Schema, document: document, variables: { skipName: false, includeGenus: true }) lookahead = query.lookahead.selection("findBirdSpecies") assert_equal [:id, :name, :genus], lookahead.selections.map(&:name) assert_equal true, lookahead.selects?(:name) query = GraphQL::Query.new(LookaheadTest::Schema, document: document, variables: { skipName: true, includeGenus: false }) lookahead = query.lookahead.selection("findBirdSpecies") assert_equal [:id], lookahead.selections.map(&:name) assert_equal false, lookahead.selects?(:name) end end def assert_selection_exists(selection) assert GraphQL::Execution::Lookahead::NULL_LOOKAHEAD != selection end def assert_selection_is_null(selection) assert_equal GraphQL::Execution::Lookahead::NULL_LOOKAHEAD, selection end describe "#selection" do let(:document) { GraphQL.parse <<-GRAPHQL query { findBirdSpecies(byName: "Laughing Gull") { name similarSpecies { likesWater: isWaterfowl } } } GRAPHQL } def query(doc = document) GraphQL::Query.new(LookaheadTest::Schema, document: doc) end it "returns selection by field name" do ast_node = document.definitions.first.selections.first field = LookaheadTest::Query.fields["findBirdSpecies"] lookahead = GraphQL::Execution::Lookahead.new(query: query, ast_nodes: [ast_node], field: field) assert_selection_exists lookahead.selection("similarSpecies") end describe "when same field is selected twice" do let(:document) { GraphQL.parse <<-GRAPHQL query { gull: findBirdSpecies(byName: "Laughing Gull") { name } tanager: findBirdSpecies(byName: "Scarlet Tanager") { name } } GRAPHQL } let(:graphql_query) do GraphQL::Query.new(LookaheadTest::Schema, document: document) end it "returns lookahead with two ast_nodes" do assert_equal 2, graphql_query.lookahead.selection("findBirdSpecies").ast_nodes.length end end describe "when query has alias" do let(:document) { GraphQL.parse <<-GRAPHQL query { findBirdSpecies(byName: "Laughing Gull") { name similar: similarSpecies { likesWater: isWaterfowl } } } GRAPHQL } let(:graphql_query) do GraphQL::Query.new(LookaheadTest::Schema, document: document) end let(:species_lookahead) do graphql_query.lookahead.selection("findBirdSpecies") end it "returns selection when field name is passed" do assert_selection_exists species_lookahead.selection("similarSpecies") end it "returns null when alias name is passed" do assert_selection_is_null species_lookahead.selection("similar") end describe "when alias has arguments" do let(:document) { GraphQL.parse <<-GRAPHQL query { gull: findBirdSpecies(byName: "Laughing Gull") { name } } GRAPHQL } it "returns selection when field name is passed" do assert_selection_exists graphql_query.lookahead.selection("findBirdSpecies") end it "returns null when alias name is passed" do assert_selection_is_null graphql_query.lookahead.selection("gull") end describe "when same field is selected twice" do let(:document) { GraphQL.parse <<-GRAPHQL query { gull: findBirdSpecies(byName: "Laughing Gull") { name } tanager: findBirdSpecies(byName: "Scarlet Tanager") { name } } GRAPHQL } it "returns null when alias name is passed" do assert_selection_is_null graphql_query.lookahead.selection("gull") assert_selection_is_null graphql_query.lookahead.selection("tanager") end end end end end describe "#alias_selection" do let(:document) { GraphQL.parse <<-GRAPHQL query { findBirdSpecies(byName: "Laughing Gull") { name similar: similarSpecies { likesWater: isWaterfowl } } } GRAPHQL } def query(doc = document) GraphQL::Query.new(LookaheadTest::Schema, document: doc) end let(:graphql_query) do GraphQL::Query.new(LookaheadTest::Schema, document: document) end let(:species_lookahead) do graphql_query.lookahead.selection("findBirdSpecies") end describe "when alias name is passed" do it "returns selection" do assert_selection_exists species_lookahead.alias_selection("similar") end it "returns true from selects_alias?" do assert true, species_lookahead.selects_alias?("similar") end describe "when the aliased field is deeply nested" do it "not finds the deeply-nested alias" do assert_equal [:name, :similar_species], species_lookahead.selections.map(&:name) assert_equal false, species_lookahead.selects_alias?("likesWater") end end end describe "when the same field is executed with the same arguments but different aliases" do let(:document) { GraphQL.parse <<-GRAPHQL query { egret: findBirdSpecies(byName: "Great Egret") { isWaterfowl } otherEgret: findBirdSpecies(byName: "Great Egret") { name } findBirdSpecies(byName: "Great Egret") { __typename } } GRAPHQL } it "distinguishes between the aliased fields" do lookahead = query.lookahead assert_equal [:is_waterfowl], lookahead.alias_selection("egret").selections.map(&:name) assert_equal [:name], lookahead.alias_selection("otherEgret").selections.map(&:name) assert_equal [], lookahead.alias_selection("findBirdSpecies").selections.map(&:name) end it "filters aliased fields by arguments" do lookahead = query.lookahead # No `arguments:` performs no filtering assert_equal [:is_waterfowl], lookahead.alias_selection("egret").selections.map(&:name) # Matching arguments filters to the expected field: assert_equal [:is_waterfowl], lookahead.alias_selection("egret", arguments: {by_name: "Great Egret"}).selections.map(&:name) # Empty `arguments:` matches nothing: assert_equal [], lookahead.alias_selection("egret", arguments: {}).selections.map(&:name) # Mismatching `arguments:` filters to nothing: assert_equal [], lookahead.alias_selection("egret", arguments: {by_name: "Macaw"}).selections.map(&:name) end end describe "when field name is passed" do it "returns null_lookahead" do assert_selection_is_null species_lookahead.alias_selection("similarSpecies") end it "returns false from selects_alias?" do assert_equal false, species_lookahead.selects_alias?("similarSpecies") end end describe "when alias is inside fragment" do let(:document) { GraphQL.parse <<-GRAPHQL fragment BirdSpeciesFragment on BirdSpecies { name similar: similarSpecies { likesWater: isWaterfowl } } query { findBirdSpecies(byName: "Laughing Gull") { ...BirdSpeciesFragment } } GRAPHQL } it "returns selection" do assert_selection_exists species_lookahead.alias_selection("similar") end it "returns true from selects_alias?" do assert true, species_lookahead.selects_alias?("similar") end describe "when fragment name is wrong" do let(:document) { GraphQL.parse <<-GRAPHQL query { findBirdSpecies(byName: "Laughing Gull") { ...WrongFragment } } GRAPHQL } it "raises error" do assert_raises(RuntimeError) { species_lookahead.selects_alias?("similar") } end end end describe "when alias is inside inline fragment" do let(:document) { GraphQL.parse <<-GRAPHQL query { findBirdSpecies(byName: "Laughing Gull") { ...on BirdSpecies { name similar: similarSpecies { likesWater: isWaterfowl } } } } GRAPHQL } it "returns selection" do assert_selection_exists species_lookahead.alias_selection("similar") end it "returns true from selects_alias?" do assert true, species_lookahead.selects_alias?("similar") end end describe "when alias has arguments" do let(:document) { GraphQL.parse <<-GRAPHQL query { gull: findBirdSpecies(byName: "Laughing Gull") { name } } GRAPHQL } it "returns selection" do assert_selection_exists graphql_query.lookahead.alias_selection("gull") end it "returns true from selects_alias?" do assert true, graphql_query.lookahead.selects_alias?("gull") end describe "when same field is selected twice" do let(:document) { GraphQL.parse <<-GRAPHQL query { gull: findBirdSpecies(byName: "Laughing Gull") { name } tanager: findBirdSpecies(byName: "Scarlet Tanager") { name } } GRAPHQL } it "returns selection when alias name is passed" do graphql_query.lookahead.alias_selection("gull", arguments: { by_name: "Laughing Gull" }).tap do |selection| assert_selection_exists selection assert_equal({ by_name: "Laughing Gull" }, selection.arguments) assert_equal 1, selection.ast_nodes.length end graphql_query.lookahead.alias_selection("tanager", arguments: { by_name: "Scarlet Tanager" }).tap do |selection| assert_selection_exists selection assert_equal({ by_name: "Scarlet Tanager" }, selection.arguments) assert_equal 1, selection.ast_nodes.length end end end end end end graphql-ruby-2.5.19/spec/graphql/execution/multiplex_spec.rb000066400000000000000000000176731514115062600242210ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Execution::Multiplex do def multiplex(*a, **kw) LazyHelpers::LazySchema.multiplex(*a, **kw) end let(:q1) { <<-GRAPHQL query Q1 { nestedSum(value: 3) { value nestedSum(value: 7) { value } } } GRAPHQL } let(:q2) { <<-GRAPHQL query Q2 { nestedSum(value: 2) { value nestedSum(value: 11) { value } } } GRAPHQL } let(:q3) { <<-GRAPHQL query Q3 { listSum(values: [1,2]) { nestedSum(value: 3) { value } } } GRAPHQL } let(:queries) { [{query: q1}, {query: q2}, {query: q3}] } describe "multiple queries in the same lazy context" do it "runs multiple queries in the same lazy context" do expected_data = [ {"data"=>{"nestedSum"=>{"value"=>14, "nestedSum"=>{"value"=>46}}}}, {"data"=>{"nestedSum"=>{"value"=>14, "nestedSum"=>{"value"=>46}}}}, {"data"=>{"listSum"=>[{"nestedSum"=>{"value"=>14}}, {"nestedSum"=>{"value"=>14}}]}}, ] res = multiplex(queries) assert_graphql_equal expected_data, res end it "returns responses in the same order as their respective requests" do queries = 2000.times.map do |index| case index % 3 when 0 {query: q1} when 1 {query: q2} when 2 {query: q3} end end responses = multiplex(queries) responses.each.with_index do |response, index| case index % 3 when 0 assert_equal "Q1", response.query.operation_name when 1 assert_equal "Q2", response.query.operation_name when 2 assert_equal "Q3", response.query.operation_name end end end end describe "when some have validation errors or runtime errors" do let(:q1) { " { success: nullableNestedSum(value: 1) { value } }" } let(:q2) { " { runtimeError: nullableNestedSum(value: 13) { value } }" } let(:q3) { "{ invalidNestedNull: nullableNestedSum(value: 1) { value nullableNestedSum(value: 2) { nestedSum(value: 13) { value } # This field will never get executed ns2: nestedSum(value: 13) { value } } } }" } let(:q4) { " { validationError: nullableNestedSum(value: true) }"} it "returns a mix of errors and values" do expected_res = [ { "data"=>{"success"=>{"value"=>2}} }, { "errors"=>[{ "message"=>"13 is unlucky", "locations"=>[{"line"=>1, "column"=>4}], "path"=>["runtimeError"] }], "data"=>{"runtimeError"=>nil}, }, { "errors"=>[{ "message"=>"Cannot return null for non-nullable field LazySum.nestedSum", "path"=>["invalidNestedNull", "nullableNestedSum", "nestedSum"], "locations"=>[{"line"=>5, "column"=>11}], }], "data"=>{"invalidNestedNull"=>{"value" => 2,"nullableNestedSum" => nil}}, }, { "errors" => [{ "message"=>"Field must have selections (field 'nullableNestedSum' returns LazySum but has no selections. Did you mean 'nullableNestedSum { ... }'?)", "locations"=>[{"line"=>1, "column"=>4}], "path"=>["query", "validationError"], "extensions"=>{"code"=>"selectionMismatch", "nodeName"=>"field 'nullableNestedSum'", "typeName"=>"LazySum"} }] }, ] res = multiplex([ {query: q1}, {query: q2}, {query: q3}, {query: q4}, ]) assert_graphql_equal expected_res, res.map(&:to_h) end end describe "context shared by a multiplex run" do it "is provided as context:" do checks = [] multiplex(queries, context: { instrumentation_checks: checks }) assert_equal ["before multiplex 1", "before multiplex 2", "after multiplex 2", "after multiplex 1"], checks end end describe "instrumenting a multiplex run" do it "runs query instrumentation for each query and multiplex-level instrumentation" do checks = [] queries_with_context = queries.map { |q| q.merge(context: { instrumentation_checks: checks }) } multiplex(queries_with_context, context: { instrumentation_checks: checks }) assert_equal [ "before multiplex 1", "before multiplex 2", "before Q1", "before Q2", "before Q3", "after Q3", "after Q2", "after Q1", "after multiplex 2", "after multiplex 1", ], checks end end describe "max_complexity" do it "can successfully calculate complexity" do message = "Query has complexity of 11, which exceeds max complexity of 10" results = multiplex(queries, max_complexity: 10) results.each do |res| assert_equal message, res["errors"][0]["message"] end end end describe "execute_query when errors are raised" do module InspectQueryInstrumentation def execute_multiplex(multiplex:) super ensure InspectQueryInstrumentation.last_json = multiplex.queries.first.result.to_json end class << self attr_accessor :last_json end end class InspectSchema < GraphQL::Schema class Query < GraphQL::Schema::Object field :raise_execution_error, String def raise_execution_error raise GraphQL::ExecutionError, "Whoops" end field :raise_error, String def raise_error raise GraphQL::Error, "Crash" end field :raise_syntax_error, String def raise_syntax_error raise SyntaxError end field :raise_exception, String def raise_exception raise Exception end end query(Query) trace_with(InspectQueryInstrumentation) end unhandled_err_json = '{}' it "can access the query results" do InspectSchema.execute("{ raiseExecutionError }") handled_err_json = '{"errors":[{"message":"Whoops","locations":[{"line":1,"column":3}],"path":["raiseExecutionError"]}],"data":{"raiseExecutionError":null}}' assert_equal handled_err_json, InspectQueryInstrumentation.last_json assert_raises(GraphQL::Error) do InspectSchema.execute("{ raiseError }") end assert_equal unhandled_err_json, InspectQueryInstrumentation.last_json end it "can access the query results when the error is not a StandardError" do assert_raises(SyntaxError) do InspectSchema.execute("{ raiseSyntaxError }") end assert_equal unhandled_err_json, InspectQueryInstrumentation.last_json assert_raises(Exception) do InspectSchema.execute("{ raiseException }") end assert_equal unhandled_err_json, InspectQueryInstrumentation.last_json end end describe "context[:trace]" do class MultiplexTraceSchema < GraphQL::Schema class Query < GraphQL::Schema::Object field :int, Integer def int; 1; end end class Trace < GraphQL::Tracing::Trace def execute_multiplex(multiplex:) @execute_multiplex_count ||= 0 @execute_multiplex_count += 1 super end def execute_query(query:) @execute_query_count ||= 0 @execute_query_count += 1 super end attr_reader :execute_multiplex_count, :execute_query_count end query(Query) end it "uses it instead of making a new trace" do query_str = "{ int }" trace_instance = MultiplexTraceSchema::Trace.new res = MultiplexTraceSchema.multiplex([{query: query_str}, {query: query_str}], context: { trace: trace_instance }) assert_equal [1, 1], res.map { |r| r["data"]["int"]} assert_equal 1, trace_instance.execute_multiplex_count assert_equal 2, trace_instance.execute_query_count end end end graphql-ruby-2.5.19/spec/graphql/execution_error_spec.rb000066400000000000000000000411741514115062600234000ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::ExecutionError do let(:result) { Dummy::Schema.execute(query_string) } describe "when returned from a field" do let(:query_string) {%| { cheese(id: 1) { id error1: similarCheese(source: [YAK]) { ... similarCheeseFields } error2: similarCheese(source: [YAK]) { ... similarCheeseFields } nonError: similarCheese(source: [SHEEP]) { ... similarCheeseFields } flavor } allDairy { ... on Cheese { flavor } ... on Milk { source executionError } } dairyErrors: allDairy(executionErrorAtIndex: 1) { __typename } dairy { milks { source executionError allDairy { __typename ... on Milk { origin executionError } } } } executionError valueWithExecutionError } fragment similarCheeseFields on Cheese { id, flavor } |} it "the error is inserted into the errors key and the rest of the query is fulfilled" do expected_result = { "data"=>{ "dairy" => { "milks" => [ { "source" => "COW", "executionError" => nil, "allDairy" => [ { "__typename" => "Cheese" }, { "__typename" => "Cheese" }, { "__typename" => "Cheese" }, { "__typename" => "Milk", "origin" => "Antiquity", "executionError" => nil } ] } ] }, "executionError" => nil, "valueWithExecutionError" => 0, "cheese"=>{ "id" => 1, "flavor" => "Brie", "error1"=> nil, "error2"=> nil, "nonError"=> { "id" => 3, "flavor" => "Manchego", }, }, "allDairy" => [ { "flavor" => "Brie" }, { "flavor" => "Gouda" }, { "flavor" => "Manchego" }, { "source" => "COW", "executionError" => nil } ], "dairyErrors" => [ { "__typename" => "Cheese" }, nil, { "__typename" => "Cheese" }, { "__typename" => "Milk" } ], }, "errors"=>[ { "message"=>"There was an execution error", "locations"=>[{"line"=>41, "column"=>5}], "path"=>["executionError"] }, { "message"=>"Could not fetch latest value", "locations"=>[{"line"=>42, "column"=>5}], "path"=>["valueWithExecutionError"] }, { "message"=>"missing dairy", "locations"=>[{"line"=>25, "column"=>5}], "path"=>["dairyErrors", 1] }, { "message"=>"There was an execution error", "locations"=>[{"line"=>31, "column"=>9}], "path"=>["dairy", "milks", 0, "executionError"] }, { "message"=>"No cheeses are made from Yak milk!", "locations"=>[{"line"=>5, "column"=>7}], "path"=>["cheese", "error1"] }, { "message"=>"No cheeses are made from Yak milk!", "locations"=>[{"line"=>8, "column"=>7}], "path"=>["cheese", "error2"] }, { "message"=>"There was an execution error", "locations"=>[{"line"=>22, "column"=>9}], "path"=>["allDairy", 3, "executionError"] }, { "message"=>"There was an execution error", "locations"=>[{"line"=>36, "column"=>13}], "path"=>["dairy", "milks", 0, "allDairy", 3, "executionError"] }, ] } assert_equal(expected_result, result.to_h) end end describe "named query when returned from a field" do let(:query_string) {%| query MilkQuery { dairy { milks { source executionError allDairy { __typename ... on Milk { origin executionError } } } } } |} it "the error is inserted into the errors key and the rest of the query is fulfilled" do expected_result = { "data"=>{ "dairy" => { "milks" => [ { "source" => "COW", "executionError" => nil, "allDairy" => [ { "__typename" => "Cheese" }, { "__typename" => "Cheese" }, { "__typename" => "Cheese" }, { "__typename" => "Milk", "origin" => "Antiquity", "executionError" => nil } ] } ] } }, "errors"=>[ { "message"=>"There was an execution error", "locations"=>[{"line"=>6, "column"=>11}], "path"=>["dairy", "milks", 0, "executionError"] }, { "message"=>"There was an execution error", "locations"=>[{"line"=>11, "column"=>15}], "path"=>["dairy", "milks", 0, "allDairy", 3, "executionError"] } ] } assert_equal(expected_result, result) end end describe "minimal lazy non-error case" do let(:query_string) {%| { cheese(id: 1) { nonError: similarCheese(source: [SHEEP]) { id } } } |} it "does lazy non-errors right" do # This is extracted from the test above -- it kept breaking # when working on dataloader, so I isolated it to keep an eye # on the minimal reproduction # # It's `def self.authorized?` is lazy, and it requires # _both_ a lazy resolution and a dataloader run # in order to resolve properly. expected_result = { "data"=>{ "cheese"=>{ "nonError"=> { "id" => 3, }, }, } } assert_equal(expected_result, result.to_h) end end describe "fragment query when returned from a field" do let(:query_string) {%| query MilkQuery { dairy { ...Dairy } } fragment Dairy on Dairy { milks { source executionError allDairy { __typename ...Milk } } } fragment Milk on Milk { origin executionError } |} it "the error is inserted into the errors key and the rest of the query is fulfilled" do expected_result = { "data"=>{ "dairy" => { "milks" => [ { "source" => "COW", "executionError" => nil, "allDairy" => [ { "__typename" => "Cheese" }, { "__typename" => "Cheese" }, { "__typename" => "Cheese" }, { "__typename" => "Milk", "origin" => "Antiquity", "executionError" => nil } ] } ] } }, "errors"=>[ { "message"=>"There was an execution error", "locations"=>[{"line"=>11, "column"=>9}], "path"=>["dairy", "milks", 0, "executionError"] }, { "message"=>"There was an execution error", "locations"=>[{"line"=>21, "column"=>7}], "path"=>["dairy", "milks", 0, "allDairy", 3, "executionError"] } ] } assert_equal(expected_result, result) end end describe "options in ExecutionError" do let(:query_string) {%| { executionErrorWithOptions } |} it "the error is inserted into the errors key and the rest of the query is fulfilled" do expected_result = { "data"=>{"executionErrorWithOptions"=>nil}, "errors"=> [{"message"=>"Permission Denied!", "locations"=>[{"line"=>3, "column"=>7}], "path"=>["executionErrorWithOptions"], "code"=>"permission_denied"}] } assert_equal(expected_result, result) end end describe "extensions in ExecutionError" do let(:query_string) {%| { executionErrorWithExtensions } |} it "the error is inserted into the errors key with custom data set in `extensions`" do expected_result = { "data"=>{"executionErrorWithExtensions"=>nil}, "errors"=> [{"message"=>"Permission Denied!", "locations"=>[{"line"=>3, "column"=>7}], "path"=>["executionErrorWithExtensions"], "extensions"=>{"code"=>"permission_denied"}}] } assert_equal(expected_result, result) end end describe "more than one ExecutionError" do let(:query_string) { %|{ multipleErrorsOnNonNullableField} |} it "the errors are inserted into the errors key and the data is nil even for a NonNullable field" do expected_result = { "data"=>nil, "errors"=> [{"message"=>"This is an error message for some error.", "locations"=>[{"line"=>1, "column"=>3}], "path"=>["multipleErrorsOnNonNullableField"]}, {"message"=>"This is another error message for a different error.", "locations"=>[{"line"=>1, "column"=>3}], "path"=>["multipleErrorsOnNonNullableField"]}], } assert_equal(expected_result, result) end describe "more than one ExecutionError on a field defined to return a list" do let(:query_string) { %|{ multipleErrorsOnNonNullableListField} |} it "the errors are inserted into the errors key and the data is nil even for a NonNullable field" do expected_result = { "data"=>{"multipleErrorsOnNonNullableListField"=>[nil, nil]}, "errors"=> [{"message"=>"The first error message for a field defined to return a list of strings.", "locations"=>[{"line"=>1, "column"=>3}], "path"=>["multipleErrorsOnNonNullableListField", 0]}, {"message"=>"The second error message for a field defined to return a list of strings.", "locations"=>[{"line"=>1, "column"=>3}], "path"=>["multipleErrorsOnNonNullableListField", 1]}], } assert_equal(expected_result, result) end end end it "supports arrays containing only execution errors for list fields" do schema = GraphQL::Schema.from_definition <<-GRAPHQL type Query { testArray: [String]! } GRAPHQL root_value = OpenStruct.new(testArray: [GraphQL::ExecutionError.new("boom!"), GraphQL::ExecutionError.new("bang!"), "OK"]) result = schema.execute("{ testArray }", root_value: root_value) assert_equal({ "testArray" => [nil, nil, "OK"]}, result["data"]) expected_errors = [ { "message"=>"boom!", "locations"=>[{"line"=>1, "column"=>3}], "path"=>["testArray", 0] }, { "message"=>"bang!", "locations"=>[{"line"=>1, "column"=>3}], "path"=>["testArray", 1] } ] assert_equal(expected_errors, result["errors"]) root_value_errors_only = OpenStruct.new(testArray: [GraphQL::ExecutionError.new("zing!"), GraphQL::ExecutionError.new("fizz!")]) result = schema.execute("{ testArray }", root_value: root_value_errors_only) assert_equal({ "testArray" => [nil, nil] }, result["data"]) expected_errors = [ { "message"=>"zing!", "locations"=>[{"line"=>1, "column"=>3}], "path"=>["testArray", 0] }, { "message"=>"fizz!", "locations"=>[{"line"=>1, "column"=>3}], "path"=>["testArray", 1] } ] assert_equal(expected_errors, result["errors"]) end describe "when ExecutionError is raised in resolve_type" do let(:schema) do test_type = Class.new(GraphQL::Schema::Object) do graphql_name "Test" field :dummy, GraphQL::Types::Boolean end test_union = Class.new(GraphQL::Schema::Union) do graphql_name "TestUnion" possible_types test_type end query_type = Class.new(GraphQL::Schema::Object) do graphql_name "Query" field :test, test_union define_method(:test) do 1 end end Class.new(GraphQL::Schema) do query query_type define_singleton_method(:resolve_type) do |abstract_type, obj, ctx| raise GraphQL::ExecutionError.new("resolve_type") end end end it "return execution error with location and path" do query = "{ test { ...on Test { dummy } } }" result = schema.execute(query) expected_result = { "errors"=>[ { "message"=>"resolve_type", "locations"=>[{"line"=>1, "column"=>3}], "path"=>["test"] } ], "data"=>{"test"=>nil} } assert_equal(expected_result, result.to_h) end describe "when using DataLoaders" do let(:schema) do test_type = Class.new(GraphQL::Schema::Object) do graphql_name "Test" field :dummy, GraphQL::Types::Boolean end test_union = Class.new(GraphQL::Schema::Union) do graphql_name "TestUnion" possible_types test_type end query_type = Class.new(GraphQL::Schema::Object) do graphql_name "Query" field :test, test_union define_method(:test) do 1 end end Class.new(GraphQL::Schema) do query query_type use GraphQL::Dataloader define_singleton_method(:resolve_type) do |abstract_type, obj, ctx| raise GraphQL::ExecutionError.new("resolve_type") end end end it "return execution error with location and path" do query = "{ test { ...on Test { dummy } } }" result = schema.execute(query) expected_result = { "errors"=>[ { "message"=>"resolve_type", "locations"=>[{"line"=>1, "column"=>3}], "path"=>["test"] } ], "data"=>{"test"=>nil} } assert_equal(expected_result, result.to_h) end end end describe "when using DataLoaders" do let(:schema) do item_error_loader = Class.new(GraphQL::Dataloader::Source) do def fetch(keys) keys.map { |key| GraphQL::ExecutionError.new("Error for #{key}") } end end query_type = Class.new(GraphQL::Schema::Object) do graphql_name "Query" field :item, String do argument :key, String end define_method(:item) do |key:| dataloader.with(item_error_loader).load(key) end end Class.new(GraphQL::Schema) do query query_type use GraphQL::Dataloader end end let(:result) { schema.execute(query_string) } describe "when querying for unique items" do let(:query_string) { <<-GRAPHQL query { query0: item(key: "a") query1: item(key: "b") } GRAPHQL } it "returns unique execution errors locations and paths" do expected_result = { "data" => { "query0" => nil, "query1" => nil }, "errors" => [ { "message" => "Error for a", "locations" => [{"line" => 2, "column" => 13}], "path" => ["query0"] }, { "message" => "Error for b", "locations" => [{"line" => 3, "column" => 13}], "path" => ["query1"] } ] } assert_equal(expected_result, result.to_h) end end describe "when querying for duplicate items" do let(:query_string) { <<-GRAPHQL query { query0: item(key: "a") query1: item(key: "a") } GRAPHQL } it "returns execution errors for duplicate items" do expected_result = { "data" => { "query0" => nil, "query1" => nil }, "errors" => [ { "message" => "Error for a", "locations" => [{"line" => 2, "column" => 13}], "path" => ["query0"] }, { "message" => "Error for a", "locations" => [{"line" => 3, "column" => 13}], "path" => ["query1"] } ] } assert_equal(expected_result, result.to_h) end end end end graphql-ruby-2.5.19/spec/graphql/introspection/000077500000000000000000000000001514115062600215165ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/graphql/introspection/directive_type_spec.rb000066400000000000000000000114141514115062600260750ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Introspection::DirectiveType do let(:query_string) {%| query getDirectives { __schema { directives { name, args { name, type { kind, name, ofType { name } } }, locations isRepeatable # Deprecated fields: onField onFragment onOperation } } } |} let(:directive_with_deprecated_arg) do Class.new(GraphQL::Schema::Directive) do graphql_name "customTransform" locations GraphQL::Schema::Directive::FIELD argument :old_way, String, required: false, deprecation_reason: "Use the newWay" argument :new_way, String, required: false end end let(:schema) { Class.new(Dummy::Schema) { directive(Class.new(GraphQL::Schema::Directive) { graphql_name("doStuff"); repeatable(true) })}} let(:result) { schema.execute(query_string) } before do schema.max_depth(100) end it "shows directive info " do expected = { "data" => { "__schema" => { "directives" => [ { "name" => "deprecated", "args" => [ {"name"=>"reason", "type"=>{"kind"=>"SCALAR", "name"=>"String", "ofType"=>nil}} ], "locations"=>["FIELD_DEFINITION", "ENUM_VALUE", "ARGUMENT_DEFINITION", "INPUT_FIELD_DEFINITION"], "isRepeatable" => false, "onField" => false, "onFragment" => false, "onOperation" => false, }, { "name"=>"directiveForVariableDefinition", "args"=>[], "locations"=>["VARIABLE_DEFINITION"], "isRepeatable"=>false, "onField"=>false, "onFragment"=>false, "onOperation"=>false, }, { "name"=>"doStuff", "args"=>[], "locations"=>[], "isRepeatable"=>true, "onField"=>false, "onFragment"=>false, "onOperation"=>false, }, { "name" => "include", "args" => [ {"name"=>"if", "type"=>{"kind"=>"NON_NULL", "name"=>nil, "ofType"=>{"name"=>"Boolean"}}} ], "locations"=>["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], "isRepeatable" => false, "onField" => true, "onFragment" => true, "onOperation" => false, }, { "name" => "oneOf", "args" => [], "locations"=>["INPUT_OBJECT"], "isRepeatable" => false, "onField" => false, "onFragment" => false, "onOperation" => false, }, { "name" => "skip", "args" => [ {"name"=>"if", "type"=>{"kind"=>"NON_NULL", "name"=>nil, "ofType"=>{"name"=>"Boolean"}}} ], "locations"=>["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], "isRepeatable" => false, "onField" => true, "onFragment" => true, "onOperation" => false, }, { "name" => "specifiedBy", "args" => [ {"name"=>"url", "type"=>{"kind"=>"NON_NULL", "name"=>nil, "ofType"=>{"name"=>"String"}}} ], "locations"=>["SCALAR"], "isRepeatable" => false, "onField" => false, "onFragment" => false, "onOperation" => false, }, ] } }} assert_equal(expected, result.to_h) end it "hides deprecated arguments by default" do schema.directive(directive_with_deprecated_arg) result = schema.execute <<-GRAPHQL { __schema { directives { name args { name } } } } GRAPHQL directive_result = result["data"]["__schema"]["directives"].find { |d| d["name"] == "customTransform" } expected = [ {"name" => "newWay"} ] assert_equal(expected, directive_result["args"]) end it "can expose deprecated arguments" do schema.directive(directive_with_deprecated_arg) result = schema.execute <<-GRAPHQL { __schema { directives { name args(includeDeprecated: true) { name isDeprecated deprecationReason } } } } GRAPHQL directive_result = result["data"]["__schema"]["directives"].find { |d| d["name"] == "customTransform" } expected = [ {"name" => "oldWay", "isDeprecated" => true, "deprecationReason" => "Use the newWay"}, {"name" => "newWay", "isDeprecated" => false, "deprecationReason" => nil} ] assert_equal(expected, directive_result["args"]) end end graphql-ruby-2.5.19/spec/graphql/introspection/entry_points_spec.rb000066400000000000000000000031741514115062600256170ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Introspection::EntryPoints do describe "#__type" do let(:schema) do nested_invisible_type = Class.new(GraphQL::Schema::Object) do graphql_name 'NestedInvisible' field :foo, String, null: false end invisible_type = Class.new(GraphQL::Schema::Object) do graphql_name 'Invisible' field :foo, String, null: false field :nested_invisible, nested_invisible_type, null: false def self.visible?(context) false end end visible_type = Class.new(GraphQL::Schema::Object) do graphql_name 'Visible' field :foo, String, null: false end query_type = Class.new(GraphQL::Schema::Object) do graphql_name 'Query' field :foo, String, null: false field :invisible, invisible_type, null: false field :visible, visible_type, null: false end Class.new(GraphQL::Schema) do query query_type use GraphQL::Schema::Warden if ADD_WARDEN end end let(:query_string) {%| query getType($name: String!) { __type(name: $name) { name } } |} it "returns reachable types" do result = schema.execute(query_string, variables: { name: 'Visible' }) type_name = result['data']['__type']['name'] assert_equal('Visible', type_name) end it "returns nil for unreachable types" do result = schema.execute(query_string, variables: { name: 'NestedInvisible' }) type_name = result['data']['__type'] assert_nil(type_name) end end end graphql-ruby-2.5.19/spec/graphql/introspection/input_value_type_spec.rb000066400000000000000000000066701514115062600264620ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Introspection::InputValueType do let(:query_string) {%| { __type(name: "DairyProductInput") { name description kind inputFields { name type { kind, name } defaultValue description } } } |} let(:result) { Dummy::Schema.execute(query_string) } it "exposes metadata about input objects, giving extra quotes for strings" do expected = { "data" => { "__type" => { "name"=>"DairyProductInput", "description"=>"Properties for finding a dairy product", "kind"=>"INPUT_OBJECT", "inputFields"=>[ {"name"=>"source", "type"=>{"kind"=>"NON_NULL", "name" => nil}, "defaultValue"=>nil, "description" => "Where it came from"}, {"name"=>"originDairy", "type"=>{"kind"=>"SCALAR", "name" => "String"}, "defaultValue"=>"\"Sugar Hollow Dairy\"", "description" => "Dairy which produced it"}, {"name"=>"fatContent", "type"=>{"kind"=>"SCALAR", "name" => "Float"}, "defaultValue"=>"0.3", "description" => "How much fat it has"}, {"name"=>"organic", "type"=>{"kind"=>"SCALAR", "name" => "Boolean"}, "defaultValue"=>"false", "description" => nil}, {"name"=>"order_by", "type"=>{"kind"=>"INPUT_OBJECT", "name"=>"ResourceOrderType"}, "defaultValue"=>"{direction: \"ASC\"}", "description" => nil}, ] } }} assert_equal(expected, result.to_h) end let(:cheese_type) { Dummy::Schema.execute(%| { __type(name: "Cheese") { fields { name args { name defaultValue } } } } |) } it "converts default values to GraphQL values" do field = cheese_type['data']['__type']['fields'].detect { |f| f['name'] == 'similarCheese' } arg = field['args'].detect { |a| a['name'] == 'nullableSource' } assert_equal('[COW]', arg['defaultValue']) end it "supports list of enum default values" do schema = GraphQL::Schema.from_definition(%| type Query { hello(enums: [MyEnum] = [A, B]): String } enum MyEnum { A B } |) result = schema.execute(%| { __type(name: "Query") { fields { args { defaultValue } } } } |) expected = { "data" => { "__type" => { "fields" => [{ "args" => [{ "defaultValue" => "[A, B]" }] }] } } } assert_equal expected, result end it "supports null default values" do schema = GraphQL::Schema.from_definition(%| type Query { hello(person: Person): String } input Person { firstName: String! lastName: String = null } |) result = schema.execute(%| { __type(name: "Person") { inputFields { name defaultValue } } } |) expected = { "data" => { "__type" => { "inputFields" => [ { "name" => "firstName", "defaultValue" => nil}, { "name" => "lastName", "defaultValue" => "null"} ] } } } assert_equal expected, result end end graphql-ruby-2.5.19/spec/graphql/introspection/introspection_query_spec.rb000066400000000000000000000030501514115062600272000ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe "GraphQL::Introspection::INTROSPECTION_QUERY" do let(:schema) { Class.new(Dummy::Schema) do max_depth(15) end } let(:query_string) { GraphQL::Introspection::INTROSPECTION_QUERY } let(:result) { schema.execute(query_string) } it "runs" do assert(result["data"]) end it "is limited to the max query depth" do query_type = Class.new(GraphQL::Schema::Object) do graphql_name "DeepQuery" field :foo, [[[Float]]], null: false end deep_schema = Class.new(GraphQL::Schema) do query query_type end result = deep_schema.execute(query_string) assert(GraphQL::Schema::Loader.load(result)) end it "doesn't handle too deeply nested (< 8) schemas" do query_type = Class.new(GraphQL::Schema::Object) do graphql_name "DeepQuery" field :foo, [[[[Float]]]], null: false end deep_schema = Class.new(GraphQL::Schema) do query query_type end result = deep_schema.execute(query_string) assert_raises(KeyError) { GraphQL::Schema::Loader.load(result) } end it "doesn't contain blank lines" do int_query = GraphQL::Introspection.query refute_includes int_query, "\n\n" int_query_with_options = GraphQL::Introspection.query( include_deprecated_args: true, include_schema_description: true, include_is_repeatable: true, include_specified_by_url: true, include_is_one_of: true ) refute_includes int_query_with_options, "\n\n" end end graphql-ruby-2.5.19/spec/graphql/introspection/schema_type_spec.rb000066400000000000000000000134511514115062600253620ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Introspection::SchemaType do let(:schema) { Class.new(Dummy::Schema) { description("Cool schema") }} let(:query_string) {%| query getSchema { __schema { description types { name } queryType { fields { name }} mutationType { fields { name }} } } |} let(:result) { schema.execute(query_string) } it "exposes the schema" do expected = { "data" => { "__schema" => { "description" => "Cool schema", "types" => schema.types.values.sort_by(&:graphql_name).map { |t| t.graphql_name.nil? ? (p t; raise("no name for #{t}")) : {"name" => t.graphql_name} }, "queryType"=>{ "fields"=>[ {"name"=>"allAnimal"}, {"name"=>"allAnimalAsCow"}, {"name"=>"allDairy"}, {"name"=>"allEdible"}, {"name"=>"allEdibleAsMilk"}, {"name"=>"cheese"}, {"name"=>"cow"}, {"name"=>"dairy"}, {"name"=>"deepNonNull"}, {"name"=>"error"}, {"name"=>"exampleBeverage"}, {"name"=>"executionError"}, {"name"=>"executionErrorWithExtensions"}, {"name"=>"executionErrorWithOptions"}, {"name"=>"favoriteEdible"}, {"name"=>"fromSource"}, {"name"=>"hugeInteger"}, {"name"=>"maybeNull"}, {"name"=>"milk"}, {"name"=>"multipleErrorsOnNonNullableField"}, {"name"=>"multipleErrorsOnNonNullableListField"}, {"name"=>"root"}, {"name"=>"searchDairy"}, {"name"=>"tracingScalar"}, {"name"=>"valueWithExecutionError"}, ] }, "mutationType"=> { "fields"=>[ {"name"=>"pushValue"}, {"name"=>"replaceValues"}, ] }, } }} assert_equal(expected, result.to_h) end describe "when the schema has types that are only reachable through hidden types" do let(:schema) do nested_invisible_type = Class.new(GraphQL::Schema::Object) do graphql_name 'NestedInvisible' field :foo, String, null: false end invisible_type = Class.new(GraphQL::Schema::Object) do graphql_name 'Invisible' field :foo, String, null: false field :nested_invisible, nested_invisible_type, null: false def self.visible?(context) false end end invisible_input_type = Class.new(GraphQL::Schema::InputObject) do graphql_name 'InvisibleInput' argument :foo, String, required: false def self.visible?(context) false end end visible_input_type = Class.new(GraphQL::Schema::InputObject) do graphql_name 'VisibleInput' argument :foo, String, required: false end visible_type = Class.new(GraphQL::Schema::Object) do graphql_name 'Visible' field :foo, String, null: false end invisible_orphan_type = Class.new(GraphQL::Schema::Object) do graphql_name 'InvisibleOrphan' field :foo, String, null: false def self.visible?(context) false end end query_type = Class.new(GraphQL::Schema::Object) do graphql_name 'Query' field :foo, String, null: false field :invisible, invisible_type, null: false field :visible, visible_type, null: false field :with_invisible_args, String, null: false do argument :invisible, invisible_input_type, required: false argument :visible, visible_input_type end end Class.new(GraphQL::Schema) do query query_type orphan_types invisible_orphan_type use GraphQL::Schema::Warden if ADD_WARDEN end end let(:query_string) {%| query getSchema { __schema { types { name } } } |} it "only returns reachable types" do expected_types = [ 'Boolean', 'Query', 'String', 'Visible', 'VisibleInput', '__Directive', '__DirectiveLocation', '__EnumValue', '__Field', '__InputValue', '__Schema', '__Type', '__TypeKind' ] types = result['data']['__schema']['types'].map { |type| type.fetch('name') } assert_equal(expected_types, types) end end describe "when the schema has hidden directives" do let(:schema) do invisible_directive = Class.new(GraphQL::Schema::Directive) do graphql_name 'invisibleDirective' locations(GraphQL::Schema::Directive::QUERY) argument(:val, Integer, "Initial integer value.", required: false) def self.visible?(context) false end end visible_directive = Class.new(GraphQL::Schema::Directive) do graphql_name 'visibleDirective' locations(GraphQL::Schema::Directive::QUERY) argument(:val, Integer, "Initial integer value.", required: false) def self.visible?(context) true end end query_type = Class.new(GraphQL::Schema::Object) do graphql_name 'Query' field :foo, String, null: false end Class.new(GraphQL::Schema) do use GraphQL::Schema::Visibility query query_type directives invisible_directive, visible_directive end end let(:query_string) {%| query getSchema { __schema { directives { name } } } |} it "only returns visible directives" do expected_dirs = ['deprecated', 'include', 'skip', 'oneOf', 'specifiedBy', 'visibleDirective'] directives = result['data']['__schema']['directives'].map { |dir| dir.fetch('name') } assert_equal(expected_dirs.sort, directives.sort) end end end graphql-ruby-2.5.19/spec/graphql/introspection/type_type_spec.rb000066400000000000000000000224471514115062600251100ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Introspection::TypeType do let(:query_string) {%| query introspectionQuery { cheeseType: __type(name: "Cheese") { name, kind, fields { name, isDeprecated, type { kind, name, ofType { name } } } } milkType: __type(name: "Milk") { interfaces { name }, fields { type { kind, name, ofType { name } } } } dairyAnimal: __type(name: "DairyAnimal") { name, kind, enumValues(includeDeprecated: false) { name, isDeprecated } } dairyProduct: __type(name: "DairyProduct") { name, kind, possibleTypes { name } } animalProduct: __type(name: "AnimalProduct") { name, kind, specifiedByURL, possibleTypes { name }, fields { name } } missingType: __type(name: "NotAType") { name } timeType: __type(name: "Time") { specifiedByURL } } |} let(:result) { Dummy::Schema.execute(query_string, context: {}, variables: {"cheeseId" => 2}) } let(:cheese_fields) {[ {"name"=>"dairyProduct", "isDeprecated" => false, "type"=>{"kind"=>"UNION", "name"=>"DairyProduct", "ofType"=>nil}}, {"name"=>"deeplyNullableCheese", "isDeprecated" => false, "type"=>{ "kind" => "OBJECT", "name" => "Cheese", "ofType" => nil}}, {"name"=>"flavor", "isDeprecated" => false, "type" => { "kind" => "NON_NULL", "name" => nil, "ofType" => { "name" => "String"}}}, {"name"=>"id", "isDeprecated" => false, "type" => { "kind" => "NON_NULL", "name" => nil, "ofType" => { "name" => "Int"}}}, {"name"=>"nullableCheese", "isDeprecated"=>false, "type"=>{ "kind" => "OBJECT", "name" => "Cheese", "ofType"=>nil}}, {"name"=>"origin", "isDeprecated" => false, "type" => { "kind" => "NON_NULL", "name" => nil, "ofType" => { "name" => "String"}}}, {"name"=>"selfAsEdible", "isDeprecated"=>false, "type"=>{"kind"=>"INTERFACE", "name"=>"Edible", "ofType"=>nil}}, {"name"=>"similarCheese", "isDeprecated"=>false, "type"=>{ "kind" => "OBJECT", "name"=>"Cheese", "ofType"=>nil}}, {"name"=>"source", "isDeprecated" => false, "type" => { "kind" => "NON_NULL", "name" => nil, "ofType" => { "name" => "DairyAnimal"}}}, ]} let(:dairy_animals) {[ {"name"=>"NONE", "isDeprecated"=> false }, {"name"=>"COW", "isDeprecated"=> false }, {"name"=>"DONKEY", "isDeprecated"=> false }, {"name"=>"GOAT", "isDeprecated"=> false }, {"name"=>"REINDEER", "isDeprecated"=> false }, {"name"=>"SHEEP", "isDeprecated"=> false }, ]} it "exposes metadata about types" do expected = {"data"=> { "cheeseType" => { "name"=> "Cheese", "kind" => "OBJECT", "fields"=> cheese_fields }, "milkType"=>{ "interfaces"=>[ {"name"=>"AnimalProduct"}, {"name"=>"Edible"}, {"name"=>"EdibleAsMilk"}, {"name"=>"LocalProduct"}, ], "fields"=>[ {"type"=>{"kind"=>"LIST","name"=>nil, "ofType"=>{"name"=>"DairyProduct"}}}, {"type"=>{"kind"=>"SCALAR","name"=>"String", "ofType"=>nil}}, {"type"=>{"kind"=>"NON_NULL","name"=>nil, "ofType"=>{"name"=>"Float"}}}, {"type"=>{"kind"=>"LIST","name"=>nil, "ofType"=>{"name"=>"String"}}}, {"type"=>{"kind"=>"NON_NULL","name"=>nil, "ofType"=>{"name"=>"ID"}}}, {"type"=>{"kind"=>"NON_NULL","name"=>nil, "ofType"=>{"name"=>"String"}}}, {"type"=>{"kind"=>"INTERFACE", "name"=>"Edible", "ofType"=>nil}}, {"type"=>{"kind"=>"NON_NULL","name"=>nil,"ofType"=>{"name"=>"DairyAnimal"}}}, ] }, "dairyAnimal"=>{ "name"=>"DairyAnimal", "kind"=>"ENUM", "enumValues"=> dairy_animals, }, "dairyProduct"=>{ "name"=>"DairyProduct", "kind"=>"UNION", "possibleTypes"=>[{"name"=>"Cheese"}, {"name"=>"Milk"}], }, "animalProduct" => { "name"=>"AnimalProduct", "kind"=>"INTERFACE", "specifiedByURL" => nil, "possibleTypes"=>[{"name"=>"Cheese"}, {"name"=>"Honey"}, {"name"=>"Milk"}], "fields"=>[ {"name"=>"source"}, ] }, "missingType" => nil, "timeType" => { "specifiedByURL" => "https://time.graphql"} }} assert_equal(expected, result.to_h) end describe "deprecated fields" do let(:query_string) {%| query introspectionQuery { cheeseType: __type(name: "Cheese") { name, kind, fields(includeDeprecated: true) { name, isDeprecated, type { kind, name, ofType { name } } } } dairyAnimal: __type(name: "DairyAnimal") { name, kind, enumValues(includeDeprecated: true) { name, isDeprecated } } } |} let(:deprecated_fields) { {"name"=>"fatContent", "isDeprecated"=>true, "type"=>{"kind"=>"NON_NULL","name"=>nil, "ofType"=>{"name"=>"Float"}}} } it "can expose deprecated fields" do new_cheese_fields = ([deprecated_fields] + cheese_fields).sort_by { |f| f["name"] } expected = { "data" => { "cheeseType" => { "name"=> "Cheese", "kind" => "OBJECT", "fields"=> new_cheese_fields }, "dairyAnimal"=>{ "name"=>"DairyAnimal", "kind"=>"ENUM", "enumValues"=> dairy_animals + [{"name" => "YAK", "isDeprecated" => true}], }, }} assert_equal(expected, result) end it "hides deprecated field arguments by default" do result = Dummy::Schema.execute <<-GRAPHQL { __type(name: "Query") { fields { name args { name } } } } GRAPHQL from_source_field = result['data']['__type']['fields'].find { |f| f['name'] == 'fromSource' } expected = [ {"name" => "source"} ] assert_equal(expected, from_source_field['args']) end it "can expose deprecated field arguments" do result = Dummy::Schema.execute <<-GRAPHQL { __type(name: "Query") { fields { name args(includeDeprecated: true) { name isDeprecated deprecationReason } } } } GRAPHQL from_source_field = result['data']['__type']['fields'].find { |f| f['name'] == 'fromSource' } expected = [ {"name" => "source", "isDeprecated" => false, "deprecationReason" => nil}, {"name" => "oldSource", "isDeprecated" => true, "deprecationReason" => "No longer supported"} ] assert_equal(expected, from_source_field['args']) end end describe "input objects" do let(:query_string) {%| query introspectionQuery { __type(name: "DairyProductInput") { name, description, kind, inputFields { name, type { kind, name }, defaultValue } } } |} it "exposes metadata about input objects" do expected = { "data" => { "__type" => { "name"=>"DairyProductInput", "description"=>"Properties for finding a dairy product", "kind"=>"INPUT_OBJECT", "inputFields"=>[ {"name"=>"source", "type"=>{"kind"=>"NON_NULL","name"=>nil, }, "defaultValue"=>nil}, {"name"=>"originDairy", "type"=>{"kind"=>"SCALAR","name"=>"String"}, "defaultValue"=>"\"Sugar Hollow Dairy\""}, {"name"=>"fatContent", "type"=>{"kind"=>"SCALAR","name" => "Float"}, "defaultValue"=>"0.3"}, {"name"=>"organic", "type"=>{"kind"=>"SCALAR","name" => "Boolean"}, "defaultValue"=>"false"}, {"name"=>"order_by", "type"=>{"kind"=>"INPUT_OBJECT", "name"=>"ResourceOrderType"}, "defaultValue"=>"{direction: \"ASC\"}"}, ] } }} assert_equal(expected, result) end it "can expose deprecated input fields" do result = Dummy::Schema.execute <<-GRAPHQL { __type(name: "DairyProductInput") { inputFields(includeDeprecated: true) { name isDeprecated deprecationReason } } } GRAPHQL expected = { "data" => { "__type" => { "inputFields" => [ {"name" => "source", "isDeprecated" => false, "deprecationReason" => nil}, {"name" => "originDairy", "isDeprecated" => false, "deprecationReason" => nil}, {"name" => "fatContent", "isDeprecated" => false, "deprecationReason" => nil}, {"name" => "organic", "isDeprecated" => false, "deprecationReason" => nil}, {"name" => "order_by", "isDeprecated" => false, "deprecationReason" => nil}, {"name" => "oldSource", "isDeprecated" => true, "deprecationReason" => "No longer supported"}, ] } } } assert_equal(expected, result) end it "includes Relay fields" do res = StarWars::Schema.execute <<-GRAPHQL { __schema { types { name fields { name args { name } } } } } GRAPHQL type_result = res["data"]["__schema"]["types"].find { |t| t["name"] == "Faction" } field_result = type_result["fields"].find { |f| f["name"] == "bases" } all_arg_names = ["after", "before", "first", "last", "nameIncludes", "complexOrder"] returned_arg_names = field_result["args"].map { |a| a["name"] } assert_equal all_arg_names.sort, returned_arg_names.sort end end end graphql-ruby-2.5.19/spec/graphql/invalid_null_error_spec.rb000066400000000000000000000003221514115062600240430ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe "GraphQL::InvalidNullError" do it "can be inspected" do assert_equal "GraphQL::InvalidNullError", GraphQL::InvalidNullError.inspect end end graphql-ruby-2.5.19/spec/graphql/language/000077500000000000000000000000001514115062600204015ustar00rootroot00000000000000graphql-ruby-2.5.19/spec/graphql/language/block_string_spec.rb000066400000000000000000000042751514115062600244300ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Language::BlockString do describe "trimming whitespace" do def trim_whitespace(str) GraphQL::Language::BlockString.trim_whitespace(str) end it "matches the examples in graphql-js" do # these are taken from: # https://github.com/graphql/graphql-js/blob/36ec0e9d34666362ff0e2b2b18edeb98e3c9abee/src/language/__tests__/blockStringValue-test.js#L12 # A set of [before, after] pairs: examples = [ [ # Removes common whitespace: " Hello, World! Yours, GraphQL. ", "Hello,\n World!\n\nYours,\n GraphQL." ], [ # Removes leading and trailing newlines: " Hello, World! Yours, GraphQL. ", "Hello,\n World!\n\nYours,\n GraphQL." ], [ # Removes blank lines (with whitespace _and_ newlines:) "\n \n Hello, World! Yours, GraphQL. \n \n", "Hello,\n World!\n\nYours,\n GraphQL." ], [ # Retains indentation from the first line " Hello,\n World!\n\n Yours,\n GraphQL.", " Hello,\n World!\n\nYours,\n GraphQL.", ], [ # Doesn't alter trailing spaces "\n \n Hello, \n World! \n\n Yours, \n GraphQL. ", "Hello, \n World! \n\nYours, \n GraphQL. ", ], [ # Doesn't crash when the string is only a newline "\n", "" ], [ # Removes long blank lines " \n \n Hello, World! Yours, GraphQL. \n \n", "Hello,\n World!\n\nYours,\n GraphQL." ] ] examples.each_with_index do |(before, after), idx| transformed_str = trim_whitespace(before) assert_equal(after, transformed_str, "Example ##{idx + 1}") end end end end graphql-ruby-2.5.19/spec/graphql/language/clexer_spec.rb000066400000000000000000000046571514115062600232360ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" require_relative "./lexer_examples" if defined?(GraphQL::CParser::Lexer) describe GraphQL::CParser::Lexer do subject { GraphQL::CParser::Lexer } def assert_bad_unicode(string, _message = nil) assert_equal :BAD_UNICODE_ESCAPE, subject.tokenize(string).first[0] end it "makes tokens like the other lexer" do str = "{ f1(type: \"str\") ...F2 }\nfragment F2 on SomeType { f2 }" tokens = GraphQL.scan_with_c(str).map { |t| [*t.first(4), t[3].encoding] } old_tokens = GraphQL.scan_with_ruby(str).map { |t| [*t, t[3].encoding] } assert_equal [ [:LCURLY, 1, 1, "{", Encoding::UTF_8], [:IDENTIFIER, 1, 3, "f1", Encoding::UTF_8], [:LPAREN, 1, 5, "(", Encoding::UTF_8], [:TYPE, 1, 6, "type", Encoding::UTF_8], [:COLON, 1, 10, ":", Encoding::UTF_8], [:STRING, 1, 12, "str", Encoding::UTF_8], [:RPAREN, 1, 17, ")", Encoding::UTF_8], [:ELLIPSIS, 1, 19, "...", Encoding::UTF_8], [:IDENTIFIER, 1, 22, "F2", Encoding::UTF_8], [:RCURLY, 1, 25, "}", Encoding::UTF_8], [:FRAGMENT, 2, 1, "fragment", Encoding::UTF_8], [:IDENTIFIER, 2, 10, "F2", Encoding::UTF_8], [:ON, 2, 13, "on", Encoding::UTF_8], [:IDENTIFIER, 2, 16, "SomeType", Encoding::UTF_8], [:LCURLY, 2, 25, "{", Encoding::UTF_8], [:IDENTIFIER, 2, 27, "f2", Encoding::UTF_8], [:RCURLY, 2, 30, "}", Encoding::UTF_8] ], tokens assert_equal(old_tokens, tokens) end it "makes frozen strings when using SchemaParser" do str = "type Query { f1: Int }" schema_ast = GraphQL::CParser::SchemaParser.new(str, nil, GraphQL::Tracing::NullTrace, nil).result default_ast = GraphQL::CParser::Parser.new(str, nil, GraphQL::Tracing::NullTrace, nil).result # Equivalent ASTs: assert_equal schema_ast, default_ast # But this one is frozen: assert_equal "Query", schema_ast.definitions.first.name assert schema_ast.definitions.first.name.frozen? # And this one isn't: assert_equal "Query", default_ast.definitions.first.name refute default_ast.definitions.first.name.frozen? end it "exposes tokens_count" do str = "type Query { f1: Int }" parser = GraphQL::CParser::Parser.new(str, nil, GraphQL::Tracing::NullTrace, nil) assert_equal 7, parser.tokens_count end include LexerExamples end end graphql-ruby-2.5.19/spec/graphql/language/definition_slice_spec.rb000066400000000000000000000123721514115062600252540ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Language::DefinitionSlice do let(:document) { GraphQL.parse(query_string) } describe "anonymous query with no dependencies" do let(:query_string) {%| { version } |} it "is already the smallest slice" do assert_equal document.to_query_string, document.slice_definition(nil).to_query_string end end describe "anonymous mutation with no dependencies" do let(:query_string) {%| mutation { ping { message } } |} it "is already the smallest slice" do assert_equal document.to_query_string, document.slice_definition(nil).to_query_string end end describe "anonymous fragment with no dependencies" do let(:query_string) {%| fragment on User { name } |} it "is already the smallest slice" do assert_equal document.to_query_string, document.slice_definition(nil).to_query_string end end describe "named query with no dependencies" do let(:query_string) {%| query getVersion { version } |} it "is already the smallest slice" do assert_equal document.to_query_string, document.slice_definition("getVersion").to_query_string end end describe "named fragment with no dependencies" do let(:query_string) {%| fragment profileFields on User { firstName lastName } |} it "is already the smallest slice" do assert_equal document.to_query_string, document.slice_definition("profileFields").to_query_string end end describe "document with multiple queries but no subdependencies" do let(:query_string) {%| query getVersion { version } query getTime { time } |} it "returns just the query definition" do assert_equal GraphQL::Language::Nodes::Document.new(definitions: [document.definitions[0]]).to_query_string, document.slice_definition("getVersion").to_query_string assert_equal GraphQL::Language::Nodes::Document.new(definitions: [document.definitions[1]]).to_query_string, document.slice_definition("getTime").to_query_string end end describe "document with multiple fragments but no subdependencies" do let(:query_string) {%| fragment profileFields on User { firstName lastName } fragment avatarFields on User { avatarURL(size: 80) } |} it "returns just the fragment definition" do assert_equal GraphQL::Language::Nodes::Document.new(definitions: [document.definitions[0]]).to_query_string, document.slice_definition("profileFields").to_query_string assert_equal GraphQL::Language::Nodes::Document.new(definitions: [document.definitions[1]]).to_query_string, document.slice_definition("avatarFields").to_query_string end end describe "query with missing spread" do let(:query_string) {%| query getUser { viewer { ...profileFields } } |} it "is ignored" do assert_equal document.to_query_string, document.slice_definition("getUser").to_query_string end end describe "query and fragment subdependency" do let(:query_string) {%| query getUser { viewer { ...profileFields } } fragment profileFields on User { firstName lastName } |} it "returns query and fragment dependency" do assert_equal document.to_query_string, document.slice_definition("getUser").to_query_string end end describe "query and fragment nested subdependencies" do let(:query_string) {%| query getUser { viewer { ...viewerInfo } } fragment viewerInfo on User { ...profileFields } fragment profileFields on User { firstName lastName ...avatarFields } fragment avatarFields on User { avatarURL(size: 80) } |} it "returns query and all fragment dependencies" do assert_equal document.to_query_string, document.slice_definition("getUser").to_query_string end end describe "fragment subdependency referenced multiple times" do let(:query_string) {%| query getUser { viewer { ...viewerInfo ...moreViewerInfo } } fragment viewerInfo on User { ...profileFields } fragment moreViewerInfo on User { ...profileFields } fragment profileFields on User { firstName lastName } |} it "is only returned once" do assert_equal document.to_query_string, document.slice_definition("getUser").to_query_string end end describe "query and unused fragment" do let(:query_string) {%| query getUser { viewer { id } } fragment profileFields on User { firstName lastName } |} it "returns just the query definition" do assert_equal GraphQL::Language::Nodes::Document.new(definitions: [document.definitions[0]]).to_query_string, document.slice_definition("getUser").to_query_string end end end graphql-ruby-2.5.19/spec/graphql/language/document_from_schema_definition_spec.rb000066400000000000000000000601031514115062600303310ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Language::DocumentFromSchemaDefinition do let(:subject) { GraphQL::Language::DocumentFromSchemaDefinition } describe "#document" do let(:schema_idl) { <<-GRAPHQL type QueryType { foo: Foo u: Union } type Foo implements Bar { one: Type two(argument: InputType!): Site three(argument: InputType, other: String): CustomScalar four(argument: String = "string"): String five(argument: [String] = ["string", "string"]): String six(argument: String): Type } interface Bar { one: Type four(argument: String = "string"): String } type Type { a: String } input InputType { key: String! answer: Int = 42 } type MutationType { a(input: InputType): String } # Scalar description scalar CustomScalar enum Site { DESKTOP MOBILE } union Union = Type | QueryType schema { query: QueryType mutation: MutationType } GRAPHQL } let(:schema) { GraphQL::Schema.from_definition(schema_idl) } let(:expected_document) { GraphQL.parse(expected_idl) } describe "when schemas have enums and directives" do let(:schema_idl) { <<-GRAPHQL directive @locale(lang: LangEnum!) on FIELD directive @secret(top: Boolean = false) on FIELD_DEFINITION enum LangEnum { en ru } type Query { i: Int @secret ssn: String @secret(top: true) } GRAPHQL } class DirectiveSchema < GraphQL::Schema class Secret < GraphQL::Schema::Directive argument :top, Boolean, required: false, default_value: false locations FIELD_DEFINITION end class Query < GraphQL::Schema::Object field :i, Int do directive Secret end field :ssn, String do directive Secret, top: true end end class Locale < GraphQL::Schema::Directive class LangEnum < GraphQL::Schema::Enum value "en" value "ru" end locations GraphQL::Schema::Directive::FIELD argument :lang, LangEnum end query(Query) directive(Locale) end it "dumps them into the string" do assert_equal schema_idl, DirectiveSchema.to_definition end end describe "when it has an enum_value with an adjacent custom directive" do let(:schema_idl) { <<-GRAPHQL directive @customEnumValueDirective(fakeArgument: String!) on ENUM_VALUE enum FakeEnum { VALUE1 VALUE2 @customEnumValueDirective(fakeArgument: "Value1 is better...") } type Query { fakeQueryField: FakeEnum! } GRAPHQL } class EnumValueDirectiveSchema < GraphQL::Schema class CustomEnumValueDirective < GraphQL::Schema::Directive locations GraphQL::Schema::Directive::ENUM_VALUE argument :fake_argument, String end class FakeEnum < GraphQL::Schema::Enum value "VALUE1" value "VALUE2" do directive CustomEnumValueDirective, fake_argument: "Value1 is better..." end end class Query < GraphQL::Schema::Object field :fake_query_field, FakeEnum, null: false end query(Query) end it "dumps the custom directive definition to the IDL" do assert_equal schema_idl, EnumValueDirectiveSchema.to_definition end end describe "when printing and schema respects root name conventions" do let(:schema_idl) { <<-GRAPHQL type Query { foo: Foo u: Union } type Foo implements Bar { one: Type two(argument: InputType!): Site three(argument: InputType, other: String): CustomScalar four(argument: String = "string"): String five(argument: [String] = ["string", "string"]): String six(argument: String): Type } interface Bar { one: Type four(argument: String = "string"): String } type Type { a: String } input InputType { key: String! answer: Int = 42 } type Mutation { a(input: InputType): String } # Scalar description scalar CustomScalar enum Site { DESKTOP MOBILE } union Union = Type | Query schema { query: Query mutation: Mutation } GRAPHQL } let(:expected_idl) { <<-GRAPHQL type QueryType { foo: Foo u: Union } type Foo implements Bar { one: Type two(argument: InputType!): Site three(argument: InputType, other: String): CustomScalar four(argument: String = "string"): String five(argument: [String] = ["string", "string"]): String six(argument: String): Type } interface Bar { one: Type four(argument: String = "string"): String } type Type { a: String } input InputType { key: String! answer: Int = 42 } type MutationType { a(input: InputType): String } # Scalar description scalar CustomScalar enum Site { DESKTOP MOBILE } union Union = Type | QueryType GRAPHQL } let(:document) { subject.new( schema ).document } it "returns the IDL without introspection, built ins and schema root" do assert equivalent_node?(expected_document, document) end end describe "with defaults" do let(:expected_idl) { <<-GRAPHQL type QueryType { foo: Foo u: Union } type Foo implements Bar { one: Type two(argument: InputType!): Site three(argument: InputType, other: String): CustomScalar four(argument: String = "string"): String five(argument: [String] = ["string", "string"]): String six(argument: String): Type } interface Bar { one: Type four(argument: String = "string"): String } type Type { a: String } input InputType { key: String! answer: Int = 42 } type MutationType { a(input: InputType): String } # Scalar description scalar CustomScalar enum Site { DESKTOP MOBILE } union Union = Type | QueryType schema { query: QueryType mutation: MutationType } GRAPHQL } let(:document) { subject.new( schema ).document } it "returns the IDL without introspection, built ins and schema if it doesnt respect name conventions" do assert equivalent_node?(expected_document, document) end end describe "with a visibility check" do let(:expected_idl) { <<-GRAPHQL type QueryType { foo: Foo u: Union } type Foo implements Bar { three(argument: InputType, other: String): CustomScalar four(argument: String = "string"): String five(argument: [String] = ["string", "string"]): Site } interface Bar { one: Type four(argument: String = "string"): String } input InputType { key: String! answer: Int = 42 } type MutationType { a(input: InputType): String } # Scalar description scalar CustomScalar enum Site { DESKTOP MOBILE } schema { query: QueryType mutation: MutationType } GRAPHQL } let(:schema) { Class.new(GraphQL::Schema.from_definition(schema_idl)) do def self.visible?(m, ctx) m.graphql_name != "Type" end end } let(:document) { doc_schema = Class.new(schema) do use GraphQL::Schema::Visibility def self.visible?(m, _ctx) m.respond_to?(:graphql_name) && m.graphql_name != "Type" end end subject.new(doc_schema).document } it "returns the IDL minus the filtered members" do assert equivalent_node?(expected_document, document) end end describe "with an only filter" do let(:expected_idl) { <<-GRAPHQL type QueryType { foo: Foo u: Union } type Foo implements Bar { three(argument: InputType, other: String): CustomScalar four(argument: String = "string"): String five(argument: [String] = ["string", "string"]): Site } interface Bar { one: Type four(argument: String = "string"): String } input InputType { key: String! answer: Int = 42 } type MutationType { a(input: InputType): String } enum Site { DESKTOP MOBILE } schema { query: QueryType mutation: MutationType } GRAPHQL } let(:schema) { Class.new(GraphQL::Schema.from_definition(schema_idl)) do def self.visible?(m, ctx) !(m.respond_to?(:kind) && m.kind.scalar? && m.name == "CustomScalar") end end } let(:document) { doc_schema = Class.new(schema) do def self.visible?(m, _ctx) !(m.respond_to?(:kind) && m.kind.scalar? && m.name == "CustomScalar") end end subject.new(doc_schema).document } it "returns the IDL minus the filtered members" do assert equivalent_node?(expected_document, document) end end describe "when excluding built ins and introspection types" do let(:expected_idl) { <<-GRAPHQL type QueryType { foo: Foo u: Union } type Foo implements Bar { one: Type two(argument: InputType!): Site three(argument: InputType, other: String): CustomScalar four(argument: String = "string"): String five(argument: [String] = ["string", "string"]): String six(argument: String): Type } interface Bar { one: Type four(argument: String = "string"): String } type Type { a: String } input InputType { key: String! answer: Int = 42 } type MutationType { a(input: InputType): String } # Scalar description scalar CustomScalar enum Site { DESKTOP MOBILE } union Union = Type | QueryType schema { query: QueryType mutation: MutationType } GRAPHQL } let(:document) { subject.new( schema, always_include_schema: true ).document } it "returns the schema idl besides introspection types and built ins" do assert equivalent_node?(expected_document, document) end end describe "when printing excluding only introspection types" do let(:expected_idl) { <<-GRAPHQL # Represents `true` or `false` values. scalar Boolean # Represents textual data as UTF-8 character sequences. This type is most often # used by GraphQL to represent free-form human-readable text. scalar String type QueryType { foo: Foo } type Foo implements Bar { one: Type two(argument: InputType!): Type three(argument: InputType, other: String): CustomScalar four(argument: String = "string"): String five(argument: [String] = ["string", "string"]): String six(argument: String): Type } interface Bar { one: Type four(argument: String = "string"): String } type Type { a: String } input InputType { key: String! answer: Int = 42 } # Represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. scalar Int type MutationType { a(input: InputType): String } # Represents signed double-precision fractional values as specified by [IEEE # 754](https://en.wikipedia.org/wiki/IEEE_floating_point). scalar Float # Represents a unique identifier that is Base64 obfuscated. It is often used to # refetch an object or as key for a cache. The ID type appears in a JSON response # as a String; however, it is not intended to be human-readable. When expected as # an input type, any string (such as `"VXNlci0xMA=="`) or integer (such as `4`) # input value will be accepted as an ID. scalar ID # Scalar description scalar CustomScalar enum Site { DESKTOP MOBILE } union Union = Type | QueryType directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT # Marks an element of a GraphQL schema as no longer supported. directive @deprecated(reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE schema { query: QueryType mutation: MutationType } GRAPHQL } let(:document) { subject.new( schema, include_built_in_scalars: true, include_built_in_directives: true, ).document } it "returns the schema IDL including only the built ins and not introspection types" do assert equivalent_node?(expected_document, document) end end describe "when printing the full schema" do let(:expected_idl) { <<-GRAPHQL # Represents `true` or `false` values. scalar Boolean # Represents textual data as UTF-8 character sequences. This type is most often # used by GraphQL to represent free-form human-readable text. scalar String # The fundamental unit of any GraphQL Schema is the type. There are many kinds of # types in GraphQL as represented by the `__TypeKind` enum. # # Depending on the kind of a type, certain fields describe information about that # type. Scalar types provide no information beyond a name and description, while # Enum types provide their values. Object and Interface types provide the fields # they describe. Abstract types, Union and Interface, provide the Object types # possible at runtime. List and NonNull types compose other types. type __Type { kind: __TypeKind! name: String description: String fields(includeDeprecated: Boolean = false): [__Field!] interfaces: [__Type!] possibleTypes: [__Type!] enumValues(includeDeprecated: Boolean = false): [__EnumValue!] inputFields: [__InputValue!] ofType: __Type } # An enum describing what kind of type a given `__Type` is. enum __TypeKind { # Indicates this type is a scalar. SCALAR # Indicates this type is an object. `fields` and `interfaces` are valid fields. OBJECT # Indicates this type is an interface. `fields` and `possibleTypes` are valid fields. INTERFACE # Indicates this type is a union. `possibleTypes` is a valid field. UNION # Indicates this type is an enum. `enumValues` is a valid field. ENUM # Indicates this type is an input object. `inputFields` is a valid field. INPUT_OBJECT # Indicates this type is a list. `ofType` is a valid field. LIST # Indicates this type is a non-null. `ofType` is a valid field. NON_NULL } # Object and Interface types are described by a list of Fields, each of which has # a name, potentially a list of arguments, and a return type. type __Field { name: String! description: String args: [__InputValue!]! type: __Type! isDeprecated: Boolean! deprecationReason: String } # Arguments provided to Fields or Directives and the input fields of an # InputObject are represented as Input Values which describe their type and # optionally a default value. type __InputValue { name: String! description: String type: __Type! # A GraphQL-formatted string representing the default value for this input value. defaultValue: String } # One possible value for a given Enum. Enum values are unique values, not a # placeholder for a string or numeric value. However an Enum value is returned in # a JSON response as a string. type __EnumValue { name: String! description: String isDeprecated: Boolean! deprecationReason: String } type QueryType { foo: Foo } type Foo implements Bar { one: Type two(argument: InputType!): Type three(argument: InputType, other: String): Int four(argument: String = "string"): String five(argument: [String] = ["string", "string"]): String six(argument: String): Type } interface Bar { one: Type four(argument: String = "string"): String } type Type { a: String } input InputType { key: String! answer: Int = 42 } # Represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. scalar Int type MutationType { a(input: InputType): String } # A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all # available types and directives on the server, as well as the entry points for # query, mutation, and subscription operations. type __Schema { # A list of all types supported by this server. types: [__Type!]! # The type that query operations will be rooted at. queryType: __Type! # If this server supports mutation, the type that mutation operations will be rooted at. mutationType: __Type # If this server support subscription, the type that subscription operations will be rooted at. subscriptionType: __Type # A list of all directives supported by this server. directives: [__Directive!]! } # A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document. # # In some cases, you need to provide options to alter GraphQL's execution behavior # in ways field arguments will not suffice, such as conditionally including or # skipping a field. Directives provide this by describing additional information # to the executor. type __Directive { name: String! description: String locations: [__DirectiveLocation!]! args: [__InputValue!]! onOperation: Boolean! onFragment: Boolean! onField: Boolean! } # A Directive can be adjacent to many parts of the GraphQL language, a # __DirectiveLocation describes one such possible adjacencies. enum __DirectiveLocation { # Location adjacent to a query operation. QUERY # Location adjacent to a mutation operation. MUTATION # Location adjacent to a subscription operation. SUBSCRIPTION # Location adjacent to a field. FIELD # Location adjacent to a fragment definition. FRAGMENT_DEFINITION # Location adjacent to a fragment spread. FRAGMENT_SPREAD # Location adjacent to an inline fragment. INLINE_FRAGMENT # Location adjacent to a schema definition. SCHEMA # Location adjacent to a scalar definition. SCALAR # Location adjacent to an object type definition. OBJECT # Location adjacent to a field definition. FIELD_DEFINITION # Location adjacent to an argument definition. ARGUMENT_DEFINITION # Location adjacent to an interface definition. INTERFACE # Location adjacent to a union definition. UNION # Location adjacent to an enum definition. ENUM # Location adjacent to an enum value definition. ENUM_VALUE # Location adjacent to an input object type definition. INPUT_OBJECT # Location adjacent to an input object field definition. INPUT_FIELD_DEFINITION } # Represents signed double-precision fractional values as specified by [IEEE # 754](https://en.wikipedia.org/wiki/IEEE_floating_point). scalar Float # Represents a unique identifier that is Base64 obfuscated. It is often used to # refetch an object or as key for a cache. The ID type appears in a JSON response # as a String; however, it is not intended to be human-readable. When expected as # an input type, any string (such as `"VXNlci0xMA=="`) or integer (such as `4`) # input value will be accepted as an ID. scalar ID # Scalar description scalar CustomScalar enum Site { DESKTOP MOBILE } union Union = Type | QueryType directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT # Marks an element of a GraphQL schema as no longer supported. directive @deprecated(reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE schema { query: QueryType mutation: MutationType } GRAPHQL } let(:document) { subject.new( schema, include_introspection_types: true, include_built_in_directives: true, include_built_in_scalars: true, always_include_schema: true, ).document } it "returns the full document AST from the given schema including built ins and introspection" do assert equivalent_node?(expected_document, document) end end end private def equivalent_node?(expected, node) return false unless expected.is_a?(node.class) if expected.respond_to?(:children) && expected.respond_to?(:scalars) children_equal = expected.children.all? do |expected_child| node.children.find { |child| equivalent_node?(expected_child, child) } end scalars_equal = expected.children.all? do |expected_child| node.children.find { |child| equivalent_node?(expected_child, child) } end children_equal && scalars_equal else expected == node end end describe "custom SDL directives" do class CustomSDLDirectiveSchema < GraphQL::Schema class CustomThing < GraphQL::Schema::Directive locations(FIELD_DEFINITION) argument :stuff, String end directive CustomThing class Query < GraphQL::Schema::Object field :f, Int, directives: { CustomThing => { stuff: "ok" } } end query(Query) end it "prints them out" do expected_str = <<~GRAPHQL directive @customThing(stuff: String!) on FIELD_DEFINITION type Query { f: Int @customThing(stuff: "ok") } GRAPHQL assert_equal expected_str, CustomSDLDirectiveSchema.to_definition end end end graphql-ruby-2.5.19/spec/graphql/language/equality_spec.rb000066400000000000000000000042131514115062600235750ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Language::Nodes::AbstractNode do describe ".eql?" do let(:document1) { GraphQL.parse(query_string1) } let(:document2) { GraphQL.parse(query_string2) } describe "large identical document" do let(:query_string1) {%| query getStuff($someVar: Int = 1, $anotherVar: [String!], $skipNested: Boolean! = false) @skip(if: false) { myField: someField(someArg: $someVar, ok: 1.4) @skip(if: $anotherVar) @thing(or: "Whatever") anotherField(someArg: [1, 2, 3]) { nestedField ...moreNestedFields @skip(if: $skipNested) } ... on OtherType @include(unless: false) { field(arg: [{ key: "value", anotherKey: 0.9, anotherAnotherKey: WHATEVER }]) anotherField } ... { id } } fragment moreNestedFields on NestedType @or(something: "ok") { anotherNestedField } |} let(:query_string2) { query_string1 } it "should be equal" do assert document1 == document2 assert document2 == document1 end end describe "different operations" do let(:query_string1) { "query { field }" } let(:query_string2) { "mutation { setField }" } it "should not be equal" do refute document1 == document2 refute document2 == document1 end end describe "different query fields" do let(:query_string1) { "query { foo }" } let(:query_string2) { "query { bar }" } it "should not be equal" do refute document1 == document2 refute document2 == document1 end end describe "different schemas" do let(:query_string1) {%| schema { query: Query } type Query { field: String! } |} let(:query_string2) {%| schema { query: Query } type Query { field: Int! } |} it "should not be equal" do refute document1.eql?(document2) refute document2.eql?(document1) end end end end graphql-ruby-2.5.19/spec/graphql/language/generation_spec.rb000066400000000000000000000015301514115062600240720ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe GraphQL::Language::Generation do describe "#to_query_tring" do let(:document) { GraphQL.parse('type Query { a: String! }') } let(:custom_printer_class) { Class.new(GraphQL::Language::Printer) { def print_field_definition(print_field_definition) print_string("