graphql-extras 0.2.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e0eea726ab643425fcc8b63f518a757a132549b2f4d7bc15fa9be79ac2cf830
4
- data.tar.gz: '08847c52f2c42c505e8201147c3826adebf21f33919385d4985cb6f334300493'
3
+ metadata.gz: 1ad1e3740a8a7ef06a6dad31a459153a121e254824225bf1a12c2718a59a699f
4
+ data.tar.gz: 36ce81c3f1f0888924668ccef5bde169958e45a97837cc669d94a416edc573de
5
5
  SHA512:
6
- metadata.gz: 41487444c67cba8707b0fc8bea877bcb9ad659f6d3732e2a28a79823d451ba20bf7ccb32a9d3c3e739dd8ba6237423c661f70fe3bf0a7a543bdae870e3ef1091
7
- data.tar.gz: 3c2557eefb6bcd90f68a09ac019fa967743c0cd89a23f0bdce1708363daf26877d53a975aa11057bde08d9b0b3f4bc8e3d18989f61083648cd6bef90448cb256
6
+ metadata.gz: ba8b4d40badcfba328280f397f8fb7ab7f42ce5257478c0482e48c4f0abf533f4b170d51f507caf6378ed6239d51460b4fe0f4623a6cfb93e0b565bbbf81a80d
7
+ data.tar.gz: de3c6a7f726b0b9ca1311906c1c3d05c0896fe8ba1b64d9d059cc0b7c5d82c227ae7ec17611b999b666201b97818daea30d2f996fda5f9d9fefa849d00ec9fc2
@@ -0,0 +1,25 @@
1
+ name: Build
2
+ on: [push]
3
+ jobs:
4
+ build:
5
+ runs-on: ubuntu-latest
6
+ steps:
7
+ - name: Checkout
8
+ uses: actions/checkout@v2
9
+
10
+ - name: Setup Ruby
11
+ uses: actions/setup-ruby@v1
12
+ with:
13
+ ruby-version: 2.6.x
14
+
15
+ - name: Install packages
16
+ run: sudo apt-get install libsqlite3-dev
17
+
18
+ - name: Install bundler
19
+ run: gem install bundler
20
+
21
+ - name: Install dependencies
22
+ run: bundle install
23
+
24
+ - name: Test
25
+ run: bundle exec rspec
@@ -0,0 +1,41 @@
1
+ name: Publish
2
+ on:
3
+ release:
4
+ types: [published]
5
+ jobs:
6
+ publish:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - name: Checkout
10
+ uses: actions/checkout@v2
11
+
12
+ - name: Setup Ruby
13
+ uses: actions/setup-ruby@v1
14
+ with:
15
+ ruby-version: 2.6.x
16
+
17
+ - name: Install packages
18
+ run: sudo apt-get install libsqlite3-dev
19
+
20
+ - name: Install bundler
21
+ run: gem install bundler
22
+
23
+ - name: Install dependencies
24
+ run: bundle install
25
+
26
+ - name: Test
27
+ run: bundle exec rspec
28
+
29
+ - name: Set version
30
+ run: perl -pi -e "s/0\.0\.0/${GITHUB_REF:11}/" lib/graphql/extras/version.rb
31
+
32
+ - name: Publish
33
+ run: |
34
+ mkdir -p $HOME/.gem
35
+ touch $HOME/.gem/credentials
36
+ chmod 0600 $HOME/.gem/credentials
37
+ printf -- "---\n:rubygems_api_key: ${RUBYGEMS_TOKEN}\n" > $HOME/.gem/credentials
38
+ gem build *.gemspec
39
+ gem push *.gem
40
+ env:
41
+ RUBYGEMS_TOKEN: ${{ secrets.RUBYGEMS_TOKEN }}
data/README.md CHANGED
@@ -1,7 +1,30 @@
1
- # GraphQL::Extras [![Build Status](https://travis-ci.org/rzane/graphql-extras.svg?branch=master)](https://travis-ci.org/rzane/graphql-extras)
1
+ <h1 align="center">GraphQL::Extras</h1>
2
+
3
+ <div align="center">
4
+
5
+ ![Build](https://github.com/rzane/graphql-extras/workflows/Build/badge.svg)
6
+ ![Version](https://img.shields.io/gem/v/graphql-extras)
7
+
8
+ </div>
2
9
 
3
10
  A collection of utilities for building GraphQL APIs.
4
11
 
12
+ **Table of Contents**
13
+
14
+ - [Installation](#installation)
15
+ - [Usage](#usage)
16
+ - [GraphQL::Extras::Controller](#graphqlextrascontroller)
17
+ - [GraphQL::Extras::Preload](#graphqlextraspreload)
18
+ - [GraphQL::Extras::PreloadSource](#graphqlextraspreloadsource)
19
+ - [GraphQL::Extras::Types](#graphqlextrastypes)
20
+ - [Date](#date)
21
+ - [DateTime](#datetime)
22
+ - [Decimal](#decimal)
23
+ - [Upload](#upload)
24
+ - [GraphQL::Extras::Test](#graphqlextrastest)
25
+ - [Development](#development)
26
+ - [Contributing](#contributing)
27
+
5
28
  ## Installation
6
29
 
7
30
  Add this line to your application's Gemfile:
@@ -30,34 +53,44 @@ class GraphqlController < ApplicationController
30
53
  end
31
54
  ```
32
55
 
33
- ### GraphQL::Extras::Batch::AssociationLoader
56
+ ### GraphQL::Extras::Preload
34
57
 
35
- This is a subclass of [`GraphQL::Batch::Loader`](https://github.com/Shopify/graphql-batch) that performs eager loading of Active Record associations.
58
+ This allows you to preload associations before resolving fields.
36
59
 
37
60
  ```ruby
38
- loader = GraphQL::Extras::Batch::AssociationLoader.for(:blog)
39
- loader.load(Post.first)
40
- loader.load_many(Post.all)
41
- ```
42
-
43
- ### GraphQL::Extras::Batch::Resolvers
61
+ class Schema < GraphQL::Schema
62
+ use GraphQL::Dataloader
63
+ end
44
64
 
45
- This includes a set of convenience methods for query batching.
65
+ class BaseField < GraphQL::Schema::Field
66
+ prepend GraphQL::Extras::Preload
67
+ end
46
68
 
47
- ```ruby
48
- class Post < GraphQL::Schema::Object
49
- include GraphQL::Extras::Batch::Resolver
69
+ class BaseObject < GraphQL::Schema::Object
70
+ field_class BaseField
71
+ end
50
72
 
51
- field :blog, BlogType, resolve: association(:blog), null: false
52
- field :comments, [CommentType], resolve: association(:comments, preload: { comments: :user }), null: false
53
- field :blog_title, String, null: false
73
+ class PostType < BaseObject
74
+ field :author, AuthorType, preload: :author, null: false
75
+ field :author_posts, [PostType], preload: {author: :posts}, null: false
76
+ field :depends_on_author, Integer, preload: :author, null: false
54
77
 
55
- def blog_title
56
- association(object, :blog).then(&:title)
78
+ def author_posts
79
+ object.author.posts
57
80
  end
58
81
  end
59
82
  ```
60
83
 
84
+ ### GraphQL::Extras::PreloadSource
85
+
86
+ This is a subclass of [`GraphQL::Dataloader::Source`](https://graphql-ruby.org/dataloader/overview.html) that performs eager loading of Active Record associations.
87
+
88
+ ```ruby
89
+ loader = dataloader.with(GraphQL::Extras::PreloadSource, :blog)
90
+ loader.load(Post.first)
91
+ loader.load_many(Post.all)
92
+ ```
93
+
61
94
  ### GraphQL::Extras::Types
62
95
 
63
96
  In your base classes, you should include the `GraphQL::Extras::Types`.
@@ -88,7 +121,7 @@ This scalar takes a `DateTime` and transmits it as a string, using ISO 8601 form
88
121
  field :created_at, DateTime, required: true
89
122
  ```
90
123
 
91
- *Note: This is just an alias for the `ISO8601DateTime` type that is included in the `graphql` gem.*
124
+ _Note: This is just an alias for the `ISO8601DateTime` type that is included in the `graphql` gem._
92
125
 
93
126
  #### Decimal
94
127
 
@@ -119,36 +152,44 @@ Take note of the correspondence between the value `"image"` and the additional H
119
152
 
120
153
  See [apollo-link-upload](https://github.com/rzane/apollo-link-upload) for the client-side implementation.
121
154
 
122
- ### RSpec integration
155
+ ### GraphQL::Extras::Test
156
+
157
+ This module makes it really easy to test your schema.
123
158
 
124
- Add the following to your `rails_helper.rb` (or `spec_helper.rb`).
159
+ First, create a test schema:
125
160
 
126
161
  ```ruby
127
- require "graphql/extras/rspec"
162
+ # spec/support/test_schema.rb
163
+ require "graphql/extras/test"
164
+
165
+ class TestSchema < GraphQL::Extras::Test::Schema
166
+ configure schema: Schema, queries: "spec/**/*.graphql"
167
+ end
128
168
  ```
129
169
 
130
170
  Now, you can run tests like so:
131
171
 
132
172
  ```ruby
173
+ require "support/test_schema"
174
+
133
175
  RSpec.describe "hello" do
134
176
  let(:context) { { name: "Ray" } }
135
- let(:schema) { use_schema(Schema, context: context) }
136
- let(:queries) { graphql_fixture("hello.graphql") }
177
+ let(:schema) { TestSchema.new(context) }
137
178
 
138
179
  it "allows easily executing queries" do
139
- result = schema.execute(queries.hello)
180
+ query = schema.hello
140
181
 
141
- expect(result).to be_successful_query
142
- expect(result['data']['hello']).to eq("world")
182
+ expect(query).to be_successful
183
+ expect(query.data["hello"]).to eq("world")
143
184
  end
144
- end
145
- ```
146
185
 
147
- The `graphql_fixture` method assumes that your queries live in `spec/fixtures/graphql`. You can change this assumption with the following configuration:
186
+ it "parses errors" do
187
+ query = schema.kaboom
148
188
 
149
- ```ruby
150
- RSpec.configure do |config|
151
- config.graphql_fixture_path = '/path/to/queries'
189
+ expect(query).not_to be_successful
190
+ expect(query.errors[0].message).to eq("Invalid")
191
+ expect(query.errors[0].code).to eq("VALIDATION_ERROR")
192
+ end
152
193
  end
153
194
  ```
154
195
 
@@ -10,10 +10,10 @@ Gem::Specification.new do |spec|
10
10
 
11
11
  spec.summary = %q{Utiltities for building GraphQL APIs.}
12
12
  spec.description = %q{A set of modules and types for buildign GraphQL APIs.}
13
- spec.homepage = "https://github.com/promptworks/graphql-extras"
13
+ spec.homepage = "https://github.com/rzane/graphql-extras"
14
14
 
15
15
  spec.metadata["homepage_uri"] = spec.homepage
16
- spec.metadata["source_code_uri"] = "https://github.com/promptworks/graphql-extras"
16
+ spec.metadata["source_code_uri"] = "https://github.com/rzane/graphql-extras"
17
17
 
18
18
  # Specify which files should be added to the gem when it is released.
19
19
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -25,13 +25,12 @@ Gem::Specification.new do |spec|
25
25
  spec.require_paths = ["lib"]
26
26
 
27
27
  spec.add_dependency "activesupport", ">= 5.2"
28
- spec.add_dependency "graphql", "~> 1.9"
29
- spec.add_dependency "graphql-batch", "~> 0.4"
28
+ spec.add_dependency "graphql", "~> 1.12"
30
29
 
31
30
  spec.add_development_dependency "bundler", "~> 2.0"
32
- spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency "rake", "~> 13.0"
33
32
  spec.add_development_dependency "rspec", "~> 3.0"
34
- spec.add_development_dependency "rspec-rails", "~> 3.8"
33
+ spec.add_development_dependency "rspec-rails", "~> 4.0"
35
34
  spec.add_development_dependency "actionpack", ">= 5.2"
36
35
  spec.add_development_dependency "activerecord", ">= 5.2"
37
36
  spec.add_development_dependency "sqlite3", "~> 1.4"
@@ -1,7 +1,7 @@
1
1
  require "graphql/extras/version"
2
2
  require "graphql/extras/types"
3
3
  require "graphql/extras/controller"
4
- require "graphql/extras/batch"
4
+ require "graphql/extras/preload"
5
5
 
6
6
  module GraphQL
7
7
  module Extras
@@ -0,0 +1,33 @@
1
+ module GraphQL
2
+ module Extras
3
+ class PreloadSource < GraphQL::Dataloader::Source
4
+ def initialize(preload)
5
+ @preload = preload
6
+ end
7
+
8
+ def fetch(records)
9
+ preloader = ActiveRecord::Associations::Preloader.new
10
+ preloader.preload(records, @preload)
11
+ records
12
+ end
13
+ end
14
+
15
+ module Preload
16
+ # @override
17
+ def initialize(*args, preload: nil, **opts, &block)
18
+ @preload = preload
19
+ super(*args, **opts, &block)
20
+ end
21
+
22
+ # @override
23
+ def resolve(object, args, context)
24
+ if @preload
25
+ loader = context.dataloader.with(PreloadSource, @preload)
26
+ loader.load(object.object)
27
+ end
28
+
29
+ super
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1 @@
1
+ require "graphql/extras/test/schema"
@@ -0,0 +1,59 @@
1
+ module GraphQL
2
+ module Extras
3
+ module Test
4
+ class Loader
5
+ FragmentNotFoundError = Class.new(StandardError)
6
+
7
+ attr_reader :fragments
8
+ attr_reader :operations
9
+
10
+ def initialize
11
+ @fragments = {}
12
+ @operations = {}
13
+ end
14
+
15
+ def load(path)
16
+ document = ::GraphQL.parse_file(path)
17
+ document.definitions.each do |node|
18
+ case node
19
+ when Nodes::FragmentDefinition
20
+ fragments[node.name] = node
21
+ when Nodes::OperationDefinition
22
+ operations[node.name] = node
23
+ end
24
+ end
25
+ end
26
+
27
+ def print(operation)
28
+ printer = ::GraphQL::Language::Printer.new
29
+ nodes = [operation, *resolve_fragments(operation)]
30
+ nodes.map { |node| printer.print(node) }.join("\n")
31
+ end
32
+
33
+ private
34
+
35
+ Nodes = ::GraphQL::Language::Nodes
36
+
37
+ # Recursively iterate through the node's fields and find
38
+ # resolve all of the fragment definitions that are needed.
39
+ def resolve_fragments(node)
40
+ node.selections.flat_map do |field|
41
+ case field
42
+ when Nodes::FragmentSpread
43
+ fragment = fetch_fragment!(field.name)
44
+ [fragment, *resolve_fragments(fragment)]
45
+ else
46
+ resolve_fragments(field)
47
+ end
48
+ end
49
+ end
50
+
51
+ def fetch_fragment!(name)
52
+ fragments.fetch(name) do
53
+ raise FragmentNotFoundError, "Fragment `#{name}` is not defined"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,37 @@
1
+ module GraphQL
2
+ module Extras
3
+ module Test
4
+ class Response
5
+ attr_reader :data
6
+ attr_reader :errors
7
+
8
+ def initialize(payload)
9
+ @data = payload["data"]
10
+ @errors = payload.fetch("errors", []).map do |error|
11
+ Error.new(error)
12
+ end
13
+ end
14
+
15
+ def successful?
16
+ errors.empty?
17
+ end
18
+
19
+ class Error
20
+ attr_reader :message
21
+ attr_reader :extensions
22
+ attr_reader :code
23
+ attr_reader :path
24
+ attr_reader :locations
25
+
26
+ def initialize(payload)
27
+ @message = payload["message"]
28
+ @path = payload["path"]
29
+ @locations = payload["locations"]
30
+ @extensions = payload["extensions"]
31
+ @code = payload.dig("extensions", "code")
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,57 @@
1
+ require "securerandom"
2
+ require "active_support/inflector"
3
+ require "active_support/core_ext/hash"
4
+ require "graphql/extras/test/loader"
5
+ require "graphql/extras/test/response"
6
+
7
+ module GraphQL
8
+ module Extras
9
+ module Test
10
+ class Schema
11
+ def self.configure(schema:, queries:)
12
+ loader = Loader.new
13
+
14
+ Dir.glob(queries) do |path|
15
+ loader.load(path)
16
+ end
17
+
18
+ loader.operations.each do |name, operation|
19
+ query = loader.print(operation)
20
+
21
+ define_method(name.underscore) do |variables = {}|
22
+ __execute(schema, query, variables)
23
+ end
24
+ end
25
+ end
26
+
27
+ def initialize(context = {})
28
+ @context = context
29
+ end
30
+
31
+ private
32
+
33
+ def __execute(schema, query, variables)
34
+ uploads = {}
35
+
36
+ variables = variables.deep_transform_keys do |key|
37
+ key.to_s.camelize(:lower)
38
+ end
39
+
40
+ variables = variables.deep_transform_values do |value|
41
+ if value.respond_to? :tempfile
42
+ id = SecureRandom.uuid
43
+ uploads[id] = value
44
+ id
45
+ else
46
+ value
47
+ end
48
+ end
49
+
50
+ context = @context.merge(uploads: uploads)
51
+ result = schema.execute(query, variables: variables, context: context)
52
+ Response.new(result.to_h)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -1,5 +1,5 @@
1
1
  module GraphQL
2
2
  module Extras
3
- VERSION = "0.2.5"
3
+ VERSION = "0.4.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql-extras
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ray Zane
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-04-16 00:00:00.000000000 Z
11
+ date: 2021-03-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -30,28 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.9'
33
+ version: '1.12'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.9'
41
- - !ruby/object:Gem::Dependency
42
- name: graphql-batch
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '0.4'
48
- type: :runtime
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '0.4'
40
+ version: '1.12'
55
41
  - !ruby/object:Gem::Dependency
56
42
  name: bundler
57
43
  requirement: !ruby/object:Gem::Requirement
@@ -72,14 +58,14 @@ dependencies:
72
58
  requirements:
73
59
  - - "~>"
74
60
  - !ruby/object:Gem::Version
75
- version: '10.0'
61
+ version: '13.0'
76
62
  type: :development
77
63
  prerelease: false
78
64
  version_requirements: !ruby/object:Gem::Requirement
79
65
  requirements:
80
66
  - - "~>"
81
67
  - !ruby/object:Gem::Version
82
- version: '10.0'
68
+ version: '13.0'
83
69
  - !ruby/object:Gem::Dependency
84
70
  name: rspec
85
71
  requirement: !ruby/object:Gem::Requirement
@@ -100,14 +86,14 @@ dependencies:
100
86
  requirements:
101
87
  - - "~>"
102
88
  - !ruby/object:Gem::Version
103
- version: '3.8'
89
+ version: '4.0'
104
90
  type: :development
105
91
  prerelease: false
106
92
  version_requirements: !ruby/object:Gem::Requirement
107
93
  requirements:
108
94
  - - "~>"
109
95
  - !ruby/object:Gem::Version
110
- version: '3.8'
96
+ version: '4.0'
111
97
  - !ruby/object:Gem::Dependency
112
98
  name: actionpack
113
99
  requirement: !ruby/object:Gem::Requirement
@@ -157,9 +143,10 @@ executables: []
157
143
  extensions: []
158
144
  extra_rdoc_files: []
159
145
  files:
146
+ - ".github/workflows/build.yml"
147
+ - ".github/workflows/publish.yml"
160
148
  - ".gitignore"
161
149
  - ".rspec"
162
- - ".travis.yml"
163
150
  - Gemfile
164
151
  - README.md
165
152
  - Rakefile
@@ -167,16 +154,19 @@ files:
167
154
  - bin/setup
168
155
  - graphql-extras.gemspec
169
156
  - lib/graphql/extras.rb
170
- - lib/graphql/extras/batch.rb
171
157
  - lib/graphql/extras/controller.rb
172
- - lib/graphql/extras/rspec.rb
158
+ - lib/graphql/extras/preload.rb
159
+ - lib/graphql/extras/test.rb
160
+ - lib/graphql/extras/test/loader.rb
161
+ - lib/graphql/extras/test/response.rb
162
+ - lib/graphql/extras/test/schema.rb
173
163
  - lib/graphql/extras/types.rb
174
164
  - lib/graphql/extras/version.rb
175
- homepage: https://github.com/promptworks/graphql-extras
165
+ homepage: https://github.com/rzane/graphql-extras
176
166
  licenses: []
177
167
  metadata:
178
- homepage_uri: https://github.com/promptworks/graphql-extras
179
- source_code_uri: https://github.com/promptworks/graphql-extras
168
+ homepage_uri: https://github.com/rzane/graphql-extras
169
+ source_code_uri: https://github.com/rzane/graphql-extras
180
170
  post_install_message:
181
171
  rdoc_options: []
182
172
  require_paths:
@@ -192,7 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
192
182
  - !ruby/object:Gem::Version
193
183
  version: '0'
194
184
  requirements: []
195
- rubygems_version: 3.1.2
185
+ rubygems_version: 3.0.3
196
186
  signing_key:
197
187
  specification_version: 4
198
188
  summary: Utiltities for building GraphQL APIs.
data/.travis.yml DELETED
@@ -1,7 +0,0 @@
1
- ---
2
- sudo: false
3
- language: ruby
4
- cache: bundler
5
- rvm:
6
- - 2.6.3
7
- before_install: gem install bundler -v 2.0.2
@@ -1,47 +0,0 @@
1
- require "graphql/batch"
2
-
3
- module GraphQL
4
- module Extras
5
- module Batch
6
- class AssociationLoader < GraphQL::Batch::Loader
7
- def initialize(name, preload: name)
8
- @name = name
9
- @preload = preload
10
- end
11
-
12
- def cache_key(record)
13
- record.object_id
14
- end
15
-
16
- def perform(records)
17
- preloader = ActiveRecord::Associations::Preloader.new
18
- preloader.preload(records, @preload)
19
-
20
- records.each do |record|
21
- fulfill(record, record.public_send(@name))
22
- end
23
- end
24
- end
25
-
26
- module Resolvers
27
- def self.included(base)
28
- base.extend ClassMethods
29
- end
30
-
31
- module ClassMethods
32
- def association(name)
33
- lambda do |record, _args, _ctx|
34
- loader = AssociationLoader.for(name)
35
- loader.load(record)
36
- end
37
- end
38
- end
39
-
40
- def association(record, name)
41
- loader = AssociationLoader.for(name)
42
- loader.load(record)
43
- end
44
- end
45
- end
46
- end
47
- end
@@ -1,133 +0,0 @@
1
- require "yaml"
2
- require "active_support/inflector"
3
- require "active_support/core_ext/hash"
4
-
5
- module GraphQL
6
- module Extras
7
- module RSpec
8
- class Queries
9
- def add(key, value)
10
- define_singleton_method(key) { value }
11
- end
12
- end
13
-
14
- class Schema
15
- def initialize(schema, context: {})
16
- @schema = schema
17
- @context = context
18
- end
19
-
20
- def execute(query, variables = {})
21
- variables = deep_camelize_keys(variables)
22
- variables, uploads = extract_uploads(variables)
23
- context = @context.merge(uploads: uploads)
24
-
25
- result = @schema.execute(query, variables: variables, context: context)
26
- result.to_h
27
- end
28
-
29
- private
30
-
31
- def extract_uploads(variables)
32
- uploads = {}
33
- variables = deep_transform_values(variables) { |value|
34
- if upload?(value)
35
- SecureRandom.hex.tap { |key| uploads.merge!(key => value) }
36
- else
37
- value
38
- end
39
- }
40
-
41
- [variables, uploads]
42
- end
43
-
44
- def deep_camelize_keys(variables)
45
- variables.deep_transform_keys { |key| key.to_s.camelize(:lower) }
46
- end
47
-
48
- def deep_transform_values(data, &block)
49
- case data
50
- when Array
51
- data.map { |v| deep_transform_values(v, &block) }
52
- when Hash
53
- data.transform_values { |v| deep_transform_values(v, &block) }
54
- else
55
- yield data
56
- end
57
- end
58
-
59
- def upload?(value)
60
- value.respond_to?(:tempfile) && value.respond_to?(:original_filename)
61
- end
62
- end
63
-
64
- class Parser
65
- include ::GraphQL::Language
66
-
67
- def initialize(document)
68
- @operations = document.definitions
69
- .grep(Nodes::OperationDefinition)
70
-
71
- @fragments = document.definitions
72
- .grep(Nodes::FragmentDefinition)
73
- .reduce({}) { |acc, f| acc.merge(f.name => f) }
74
- end
75
-
76
- def parse
77
- queries = Queries.new
78
- printer = Printer.new
79
-
80
- @operations.each do |op|
81
- nodes = [op, *find_fragments(op)]
82
- nodes = nodes.map { |node| printer.print(node) }
83
- queries.add op.name.underscore, nodes.join
84
- end
85
-
86
- queries
87
- end
88
-
89
- private
90
-
91
- def find_fragments(node)
92
- node.selections.flat_map do |field|
93
- if field.is_a? Nodes::FragmentSpread
94
- fragment = @fragments.fetch(field.name)
95
- [fragment, *find_fragments(fragment)]
96
- else
97
- find_fragments(field)
98
- end
99
- end
100
- end
101
- end
102
-
103
- def graphql_fixture(filename)
104
- root = ::RSpec.configuration.graphql_fixture_path
105
- file = File.join(root, filename)
106
- document = ::GraphQL.parse_file(file)
107
- parser = Parser.new(document)
108
- parser.parse
109
- end
110
-
111
- def use_schema(*args)
112
- Schema.new(*args)
113
- end
114
- end
115
- end
116
- end
117
-
118
- RSpec::Matchers.define :be_successful_query do
119
- match do |result|
120
- result['errors'].nil?
121
- end
122
-
123
- failure_message do |result|
124
- errors = result['errors'].map(&:deep_stringify_keys)
125
- message = "expected query to be successful, but encountered errors:\n"
126
- message + errors.to_yaml.lines.drop(1).join.indent(2)
127
- end
128
- end
129
-
130
- RSpec.configure do |config|
131
- config.add_setting :graphql_fixture_path, default: "spec/fixtures/graphql"
132
- config.include GraphQL::Extras::RSpec, type: :graphql
133
- end