graphql-hive 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 63bc735d6516da78a9f7cab670defd21e68a8295132dba4f871d0b4fa6f8b944
4
+ data.tar.gz: df3580ad1ebabd31c5ec3960811c1d4defc1702c020c1152ee029ac2879574cc
5
+ SHA512:
6
+ metadata.gz: 0624adc8d88ef8ef468ec08b4fa9a0c3f375bd35a2a700dac3a4bca22631c1f88aec175405d598210b48d330c145a878af11188dc396c45f52dd1706bb99b784
7
+ data.tar.gz: 7c2cc7f8327471969eb9512e5ab4550f9f9093042a930fe1c497adf2958c01f868f468ed6aac3ec2def5d750b5352e7543dc9c1168e0d049a89134939460476f
@@ -0,0 +1,27 @@
1
+ name: CI
2
+ on:
3
+ - pull_request
4
+
5
+ jobs:
6
+ rubocop:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v3
10
+ - uses: ruby/setup-ruby@359bebbc29cbe6c87da6bc9ea3bc930432750108
11
+ with:
12
+ ruby-version: 2.6
13
+ bundler-cache: true
14
+ - run: bundle exec rake rubocop
15
+ test:
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ ruby-version: ['3.1', '3.0', '2.7', '2.6']
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v3
23
+ - uses: ruby/setup-ruby@359bebbc29cbe6c87da6bc9ea3bc930432750108
24
+ with:
25
+ ruby-version: ${{ matrix.ruby-version }}
26
+ bundler-cache: true
27
+ - run: bundle exec rake spec
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+ graphql-hive-0.1.0.gem
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,37 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.6
3
+ NewCops: enable
4
+ Exclude:
5
+ - 'examples/**/*'
6
+ - 'gemfiles/**/*'
7
+ - 'tmp/**/*'
8
+ - 'vendor/**/*'
9
+
10
+ Metrics/BlockLength:
11
+ Exclude:
12
+ - 'spec/**/*'
13
+
14
+ Layout/LineLength:
15
+ Exclude:
16
+ - 'spec/**/*'
17
+
18
+ Naming/FileName:
19
+ Enabled: false
20
+
21
+ Style/ClassVars:
22
+ Enabled: false
23
+
24
+ Metrics/AbcSize:
25
+ Enabled: false
26
+
27
+ Metrics/PerceivedComplexity:
28
+ Enabled: false
29
+
30
+ Metrics/CyclomaticComplexity:
31
+ Enabled: false
32
+
33
+ Metrics/MethodLength:
34
+ Enabled: false
35
+
36
+ Metrics/ClassLength:
37
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in graphql-hive.gemspec
8
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,58 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ graphql-hive (0.1.0)
5
+ graphql (~> 2.0.9)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ ast (2.4.2)
11
+ diff-lcs (1.5.0)
12
+ graphql (2.0.9)
13
+ parallel (1.22.1)
14
+ parser (3.1.2.0)
15
+ ast (~> 2.4.1)
16
+ rainbow (3.1.1)
17
+ rake (10.5.0)
18
+ regexp_parser (2.5.0)
19
+ rexml (3.2.5)
20
+ rspec (3.11.0)
21
+ rspec-core (~> 3.11.0)
22
+ rspec-expectations (~> 3.11.0)
23
+ rspec-mocks (~> 3.11.0)
24
+ rspec-core (3.11.0)
25
+ rspec-support (~> 3.11.0)
26
+ rspec-expectations (3.11.0)
27
+ diff-lcs (>= 1.2.0, < 2.0)
28
+ rspec-support (~> 3.11.0)
29
+ rspec-mocks (3.11.1)
30
+ diff-lcs (>= 1.2.0, < 2.0)
31
+ rspec-support (~> 3.11.0)
32
+ rspec-support (3.11.0)
33
+ rubocop (1.30.1)
34
+ parallel (~> 1.10)
35
+ parser (>= 3.1.0.0)
36
+ rainbow (>= 2.2.2, < 4.0)
37
+ regexp_parser (>= 1.8, < 3.0)
38
+ rexml (>= 3.2.5, < 4.0)
39
+ rubocop-ast (>= 1.18.0, < 2.0)
40
+ ruby-progressbar (~> 1.7)
41
+ unicode-display_width (>= 1.4.0, < 3.0)
42
+ rubocop-ast (1.18.0)
43
+ parser (>= 3.1.1.0)
44
+ ruby-progressbar (1.11.0)
45
+ unicode-display_width (2.1.0)
46
+
47
+ PLATFORMS
48
+ ruby
49
+
50
+ DEPENDENCIES
51
+ bundler (~> 1.17)
52
+ graphql-hive!
53
+ rake (~> 10.0)
54
+ rspec (~> 3.0)
55
+ rubocop (~> 1.30)
56
+
57
+ BUNDLED WITH
58
+ 1.17.3
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Charly POLY
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Charly POLY
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # GraphQL Hive: `graphql-ruby` integration
2
+
3
+ <p align="center">
4
+ <img src="cover.png" width="500" alt="GraphQL Hive" />
5
+ </p>
6
+
7
+
8
+
9
+ [GraphQL Hive](https://graphql-hive.com/) provides all the tools to get visibility of your GraphQL architecture at all stages, from standalone APIs to composed schemas (Federation, Stitching):
10
+ - **Schema Registry** with custom breaking changes detection
11
+ - **Monitoring** of RPM, latency, error rate and more
12
+ - **Integrations** with your favorite tools (Slack, Github Actions and more)
13
+
14
+
15
+ <br/>
16
+
17
+ ----
18
+
19
+ <br/>
20
+
21
+
22
+ # Getting started
23
+
24
+
25
+ ## 0. Get your Hive token
26
+
27
+ If you are using Hive as a service, please refer to our documentation: https://docs.graphql-hive.com/features/tokens.
28
+
29
+ ## 1. Install the `graphql-hive` gem
30
+
31
+ ```
32
+ gem install graphql-hive
33
+ ```
34
+
35
+ <br/>
36
+
37
+ ## 2. Configure `GraphQL::Hive` in your Schema
38
+
39
+ Add `GraphQL::Hive` **at the end** of your schema definition:
40
+
41
+ ```ruby
42
+ class Schema < GraphQL::Schema
43
+ query QueryType
44
+
45
+ use(
46
+ GraphQL::Hive,
47
+ {
48
+ token: '<YOUR_TOKEN>',
49
+ reporting: {
50
+ author: ENV['GITHUB_USER'],
51
+ commit: ENV['GITHUB_COMMIT']
52
+ },
53
+ }
54
+ )
55
+ end
56
+
57
+ ```
58
+
59
+ The `reporting` configuration is required to push your GraphQL Schema to the Hive registry.
60
+ Doing so will help better detect breaking changes and more upcoming features.
61
+ If you only want to use the operations monitoring, replace the `monitoring` option with the following `report_schema: false`.
62
+
63
+ <br/>
64
+
65
+
66
+ **You are all set! 🚀**
67
+
68
+ When deploying or starting up your GraphQL API, `graphql-hive` will immediately:
69
+ - publish the schema to the Hive registry
70
+ - forward the operations metrics to Hive
71
+
72
+
73
+ <br/>
74
+
75
+ ## 3. See how your GraphQL API is operating
76
+
77
+ You should now see operations information (RPM, error rate, queries performed) on your [GraphQL Hive dashboard](https://app.graphql-hive.com/):
78
+
79
+ <p align="center">
80
+ <img src="operations-dashboard.png" width="500" alt="GraphQL Hive" />
81
+ </p>
82
+
83
+
84
+ <br/>
85
+
86
+
87
+ ## 4. Going further: use the Hive Github app
88
+
89
+ Stay on top of your GraphQL Schema changes by installing the Hive Github Application and enabling Slack notifications about breaking changes:
90
+
91
+ https://docs.graphql-hive.com/features/integrations#github
92
+
93
+ <br/>
94
+
95
+ ----
96
+
97
+ <br/>
98
+
99
+
100
+ # Configuration
101
+
102
+ You will find below the complete list of options of `GraphQL::Hive`:
103
+
104
+ ```ruby
105
+ class MySchema < GraphQL::Schema
106
+ use(
107
+ GraphQL::Hive,
108
+ {
109
+ token: 'YOUR-TOKEN',
110
+ collect_usage: true, # optional
111
+ report_schema: true, # optional
112
+ enabled: true, # Enable/Disable Hive Client (optional)
113
+ debug: false, # verbose logs
114
+ logger: MyLogger.new, # optional
115
+ endpoint: 'app.graphql-hive.com', # optional
116
+ port: 80, # optional
117
+ buffer_size: 50, # forward the operations data to Hive every 50 requests
118
+ reporting: { # mandatory if `report_schema: true`
119
+ # mandatory member of `reporting`
120
+ author: 'Author of the latest change',
121
+ # mandatory member of `reporting`
122
+ commit: 'git sha or any identifier',
123
+ service_name: '', # optional
124
+ service_url: '', # optional
125
+ },
126
+ # you can pass an optional proc that will help identify the client (ex: Apollo web app) that performed the query
127
+ client_info: Proc.new { |context| { name: context.client_name, version: context.client_version } }
128
+ }
129
+ )
130
+
131
+ # ...
132
+
133
+ end
134
+ ```
135
+
136
+ <br/>
137
+
138
+ **A note on `buffer_size` and performances**
139
+
140
+ The `graphql-hive` usage reporter, responsible for sending the operations data to Hive, is running in a separate `Thread` to avoid any major impact on your GraphQL API performances.
141
+
142
+ If your GraphQL API has a high RPM, we encourage you to increase the `buffer_size` value.
143
+
144
+ However, please note that a higher `buffer_size` value will introduce some peak of increase of memory comsumption.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+ RuboCop::RakeTask.new
10
+
11
+ task(default: %i[spec rubocop])
data/cover.png ADDED
Binary file
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'graphql'
4
+ gem 'graphql-hive', path: '../../'
5
+ gem 'puma'
6
+ gem 'rack-contrib'
7
+ gem 'sinatra'
8
+ gem 'sinatra-contrib'
@@ -0,0 +1,48 @@
1
+ PATH
2
+ remote: ../..
3
+ specs:
4
+ graphql-hive (0.1.0)
5
+ graphql (~> 2.0.9)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ graphql (2.0.9)
11
+ multi_json (1.15.0)
12
+ mustermann (1.1.1)
13
+ ruby2_keywords (~> 0.0.1)
14
+ nio4r (2.5.8)
15
+ puma (5.6.4)
16
+ nio4r (~> 2.0)
17
+ rack (2.2.3.1)
18
+ rack-contrib (2.3.0)
19
+ rack (~> 2.0)
20
+ rack-protection (2.2.0)
21
+ rack
22
+ ruby2_keywords (0.0.5)
23
+ sinatra (2.2.0)
24
+ mustermann (~> 1.0)
25
+ rack (~> 2.2)
26
+ rack-protection (= 2.2.0)
27
+ tilt (~> 2.0)
28
+ sinatra-contrib (2.2.0)
29
+ multi_json
30
+ mustermann (~> 1.0)
31
+ rack-protection (= 2.2.0)
32
+ sinatra (= 2.2.0)
33
+ tilt (~> 2.0)
34
+ tilt (2.0.10)
35
+
36
+ PLATFORMS
37
+ ruby
38
+
39
+ DEPENDENCIES
40
+ graphql
41
+ graphql-hive!
42
+ puma
43
+ rack-contrib
44
+ sinatra
45
+ sinatra-contrib
46
+
47
+ BUNDLED WITH
48
+ 1.17.3
@@ -0,0 +1,31 @@
1
+ require 'sinatra'
2
+ require 'sinatra/json'
3
+ require 'rack/contrib'
4
+
5
+ require_relative 'schema'
6
+
7
+ # Test query:
8
+ #
9
+ # query GetPost($input: [PostInput!]!) {
10
+ # post(input: $input, test: TEST1) {
11
+ # title
12
+ # myId: id
13
+ # }
14
+ # }
15
+
16
+ class DemoApp < Sinatra::Base
17
+ use Rack::JSONBodyParser
18
+
19
+ post '/graphql' do
20
+ result = Schema.execute(
21
+ params['query'],
22
+ variables: params[:variables],
23
+ operation_name: params[:operationName],
24
+ context: {
25
+ client_name: 'GraphQL Client',
26
+ client_version: '1.0'
27
+ }
28
+ )
29
+ json result
30
+ end
31
+ end
@@ -0,0 +1,2 @@
1
+ require './app'
2
+ run DemoApp
@@ -0,0 +1,47 @@
1
+ require 'graphql'
2
+ require 'graphql-hive'
3
+
4
+ module Types
5
+ class PostType < GraphQL::Schema::Object
6
+ description 'A blog post'
7
+ field :id, ID, null: false
8
+ field :title, String, null: false
9
+ # fields should be queried in camel-case (this will be `truncatedPreview`)
10
+ field :truncated_preview, String, null: false
11
+ end
12
+ end
13
+
14
+ class Types::PostInput < GraphQL::Schema::InputObject
15
+ description 'Query Post arguments'
16
+ argument :id, ID, required: true
17
+ end
18
+
19
+ class Types::TestEnum < GraphQL::Schema::Enum
20
+ value 'TEST1'
21
+ value 'TEST2'
22
+ value 'TEST3'
23
+ end
24
+
25
+ class QueryType < GraphQL::Schema::Object
26
+ description 'The query root of this schema'
27
+
28
+ # First describe the field signature:
29
+ field :post, Types::PostType, 'Find a post by ID' do
30
+ argument :input, [Types::PostInput]
31
+ argument :test, Types::TestEnum
32
+ end
33
+
34
+ # Then provide an implementation:
35
+ def post(input:, test:)
36
+ { id: 1, title: 'GraphQL Hive with `graphql-ruby`',
37
+ truncated_preview: 'Monitor operations, inspect your queries and publish your GraphQL schema with GraphQL Hive' }
38
+ end
39
+ end
40
+
41
+ class Schema < GraphQL::Schema
42
+ query QueryType
43
+
44
+ use(GraphQL::Hive, { buffer_size: 2, token: 'c3715c1afab97070c5c5b9bce8d02c7b', debug: true, reporting: { author: 'Charly Poly', commit: '109bb1e748bae21bdfe663c0ffc7e830' }, client_info: proc { |context|
45
+ { name: context[:client_name], version: context[:client_version] }
46
+ } })
47
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'graphql-hive/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'graphql-hive'
9
+ spec.version = Graphql::Hive::VERSION
10
+ spec.authors = ['Charly POLY']
11
+ spec.email = ['cpoly55@gmail.com']
12
+
13
+ spec.summary = '"GraphQL Hive integration for `graphql-ruby`"'
14
+ spec.description = '"Monitor operations, inspect your queries and publish your GraphQL schema with GraphQL Hive"'
15
+ spec.homepage = 'https://docs.graphql-hive.com/specs/integrations'
16
+ spec.license = 'MIT'
17
+
18
+ spec.metadata = { 'rubygems_mfa_required' => 'true' }
19
+
20
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.6.0')
21
+
22
+ spec.require_paths = ['lib']
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
25
+ end
26
+
27
+ spec.add_dependency 'graphql', '~> 2.0.9'
28
+
29
+ spec.add_development_dependency 'bundler', '~> 1.17'
30
+ spec.add_development_dependency 'rake', '~> 10.0'
31
+ spec.add_development_dependency 'rspec', '~> 3.0'
32
+ spec.add_development_dependency 'rubocop', '~> 1.30'
33
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ class Hive < GraphQL::Tracing::PlatformTracing
5
+ # Fetch all users fields, input objects and enums
6
+ class Analyzer < GraphQL::Analysis::AST::Analyzer
7
+ def initialize(query_or_multiplex)
8
+ puts query_or_multiplex.inspect
9
+ super
10
+ @used_fields = Set.new
11
+ end
12
+
13
+ def on_leave_field(node, _parent, visitor)
14
+ @used_fields.add(visitor.parent_type_definition.graphql_name)
15
+ @used_fields.add([visitor.parent_type_definition.graphql_name, node.name].join('.'))
16
+ end
17
+
18
+ def on_leave_argument(node, parent, visitor)
19
+ @used_fields.add([visitor.parent_type_definition.graphql_name, parent.name, node.name].join('.'))
20
+
21
+ arg_type = visitor.argument_definition.type.unwrap
22
+ arg_type_kind = visitor.argument_definition.type.unwrap.kind
23
+ if arg_type_kind.input_object?
24
+ @used_fields.add(arg_type.graphql_name)
25
+ arg_type.arguments.each do |arg|
26
+ @used_fields.add([arg_type.graphql_name, arg[0]].join('.'))
27
+ end
28
+ elsif arg_type_kind.enum?
29
+ @used_fields.add([arg_type.graphql_name, node.value.name].join('.'))
30
+ end
31
+ end
32
+
33
+ attr_reader :used_fields
34
+
35
+ def result
36
+ @used_fields
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+
6
+ module GraphQL
7
+ class Hive < GraphQL::Tracing::PlatformTracing
8
+ # API client
9
+ class Client
10
+ def initialize(options)
11
+ @options = options
12
+ end
13
+
14
+ def send(path, body, _log_type)
15
+ uri =
16
+ URI::HTTP.build(
17
+ scheme: @options[:port].to_s == '443' ? 'https' : 'http',
18
+ host: @options[:endpoint] || 'app.graphql-hive.com',
19
+ port: @options[:port] || '443',
20
+ path: path
21
+ )
22
+
23
+ http = ::Net::HTTP.new(uri.host, uri.port)
24
+ http.use_ssl = true
25
+ http.read_timeout = 2
26
+ request = Net::HTTP::Post.new(uri.request_uri)
27
+ request['content-type'] = 'application/json'
28
+ request['x-api-token'] = @options[:token]
29
+ request['User-Agent'] = "Hive@#{Graphql::Hive::VERSION}"
30
+ request['graphql-client-name'] = 'Hive Ruby Client'
31
+ request['graphql-client-version'] = Graphql::Hive::VERSION
32
+ request.body = JSON.generate(body)
33
+ response = http.request(request)
34
+
35
+ @options[:logger].debug(response.inspect)
36
+ @options[:logger].debug(response.body.inspect)
37
+ rescue StandardError => e
38
+ @options[:logger].fatal("Failed to send data: #{e}")
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ class Hive < GraphQL::Tracing::PlatformTracing
5
+ # - removes literals
6
+ # - removes aliases
7
+ # - sort nodes and directives (files, arguments, variables)
8
+ class Printer < GraphQL::Language::Printer
9
+ def print_node(node, indent: '')
10
+ case node
11
+ when Float, Integer
12
+ '0'
13
+ when String
14
+ ''
15
+ else
16
+ super(node, indent: indent)
17
+ end
18
+ end
19
+
20
+ # rubocop:disable Style/RedundantInterpolation
21
+ def print_field(field, indent: '')
22
+ out = "#{indent}".dup
23
+ out << "#{field.name}"
24
+ out << "(#{field.arguments.sort_by(&:name).map { |a| print_argument(a) }.join(', ')})" if field.arguments.any?
25
+ out << print_directives(field.directives)
26
+ out << print_selections(field.selections, indent: indent)
27
+ out
28
+ end
29
+ # rubocop:enable Style/RedundantInterpolation
30
+
31
+ def print_directives(directives)
32
+ super(directives.sort_by(&:name))
33
+ end
34
+
35
+ def print_selections(selections, indent: '')
36
+ super(selections.sort_by(&:name), indent: indent)
37
+ end
38
+
39
+ def print_directive(directive)
40
+ out = "@#{directive.name}".dup
41
+
42
+ if directive.arguments.any?
43
+ out << "(#{directive.arguments.sort_by(&:name).map { |a| print_argument(a) }.join(', ')})"
44
+ end
45
+
46
+ out
47
+ end
48
+
49
+ def print_operation_definition(operation_definition, indent: '')
50
+ out = "#{indent}#{operation_definition.operation_type}".dup
51
+ out << " #{operation_definition.name}" if operation_definition.name
52
+
53
+ # rubocop:disable Layout/LineLength
54
+ if operation_definition.variables.any?
55
+ out << "(#{operation_definition.variables.sort_by(&:name).map { |v| print_variable_definition(v) }.join(', ')})"
56
+ end
57
+ # rubocop:enable Layout/LineLength
58
+
59
+ out << print_directives(operation_definition.directives)
60
+ out << print_selections(operation_definition.selections, indent: indent)
61
+ out
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'graphql-hive/analyzer'
5
+ require 'graphql-hive/printer'
6
+
7
+ module GraphQL
8
+ class Hive < GraphQL::Tracing::PlatformTracing
9
+ # Report usage to Hive API without impacting application performances
10
+ class UsageReporter
11
+ @@instance = nil
12
+
13
+ @queue = nil
14
+ @thread = nil
15
+ @operations_buffer = nil
16
+ @client = nil
17
+ @logger = nil
18
+
19
+ def self.instance
20
+ @@instance
21
+ end
22
+
23
+ def initialize(options, client)
24
+ @@instance = self
25
+
26
+ @buffer_size = options[:buffer_size]
27
+ @logger = options[:logger]
28
+ @client = client
29
+ @operations_buffer = []
30
+
31
+ @queue = Queue.new
32
+
33
+ @thread = Thread.new do
34
+ while !@queue.empty? || !@queue.closed?
35
+ operations = @queue.pop(false)
36
+ process_operations operations
37
+ end
38
+ end
39
+ end
40
+
41
+ def add_operation(operation)
42
+ @logger.debug("add operation to buffer: #{operation}")
43
+
44
+ @operations_buffer << operation
45
+
46
+ return unless @operations_buffer.size >= @buffer_size
47
+
48
+ @logger.debug('buffer is full, sending!')
49
+ @queue.push @operations_buffer
50
+ @operations_buffer = []
51
+ end
52
+
53
+ def on_exit
54
+ @queue.push @operations_buffer unless @operations_buffer.empty?
55
+ @queue.close
56
+ @thread.join
57
+ end
58
+
59
+ private
60
+
61
+ def process_operations(operations)
62
+ report = {
63
+ size: 0,
64
+ map: {},
65
+ operations: []
66
+ }
67
+
68
+ operations.each do |operation|
69
+ add_operation_to_report(report, operation)
70
+ end
71
+
72
+ @logger.debug("sending report: #{report}")
73
+
74
+ @client.send('/usage', report, :usage)
75
+ end
76
+
77
+ def add_operation_to_report(report, operation)
78
+ timestamp, queries, results, duration = operation
79
+
80
+ errors = errors_from_results(results)
81
+
82
+ operation_name = queries.map(&:operations).map(&:keys).flatten.compact.join(', ')
83
+ operation = ''
84
+ fields = Set.new
85
+
86
+ queries.each do |query|
87
+ analyzer = GraphQL::Hive::Analyzer.new(query)
88
+ visitor = GraphQL::Analysis::AST::Visitor.new(
89
+ query: query,
90
+ analyzers: [analyzer]
91
+ )
92
+
93
+ visitor.visit
94
+
95
+ fields.merge(analyzer.result)
96
+
97
+ operation += "\n" unless operation.empty?
98
+ operation += GraphQL::Hive::Printer.new.print(visitor.result)
99
+ end
100
+
101
+ md5 = Digest::MD5.new
102
+ md5.update operation
103
+ operation_map_key = md5.hexdigest
104
+
105
+ operation_record = {
106
+ operationMapKey: operation_map_key,
107
+ timestamp: timestamp.to_i,
108
+ execution: {
109
+ ok: errors[:errorsTotal].zero?,
110
+ duration: duration,
111
+ errorsTotal: errors[:errorsTotal],
112
+ errors: errors[:errors]
113
+ }
114
+ }
115
+
116
+ # context = results[0].query.context
117
+ # TBD
118
+ # operation_record[:metadata] = { client: @options[:client_info].call(context) } if @options[:client_info]
119
+
120
+ report[:map][operation_map_key] = {
121
+ fields: fields.to_a,
122
+ operationName: operation_name,
123
+ operation: operation
124
+ }
125
+ report[:operations] << operation_record
126
+ report[:size] += 1
127
+ end
128
+
129
+ def errors_from_results(results)
130
+ acc = { errorsTotal: 0, errors: [] }
131
+ results.each do |result|
132
+ errors = result.to_h.fetch('errors', [])
133
+ errors.each do |error|
134
+ acc[:errorsTotal] += 1
135
+ acc[:errors] << { message: error['message'], path: error['path'].join('.') }
136
+ end
137
+ end
138
+ acc
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Graphql
4
+ module Hive
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ require 'graphql-hive/version'
6
+ require 'graphql-hive/usage_reporter'
7
+ require 'graphql-hive/client'
8
+
9
+ # class MySchema < GraphQL::Schema
10
+ # use(
11
+ # GraphQL::Hive,
12
+ # {
13
+ # token: 'YOUR-TOKEN',
14
+ # collect_usage: true,
15
+ # report_schema: true,
16
+ # enabled: true, // Enable/Disable Hive Client
17
+ # debug: true, // Debugging mode
18
+ # logger: MyLogger.new,
19
+ # endpoint: 'app.graphql-hive.com',
20
+ # port: 80,
21
+ # reporting: {
22
+ # author: 'Author of the latest change',
23
+ # commit: 'git sha or any identifier',
24
+ # service_name: '',
25
+ # service_url: '',
26
+ # },
27
+ # client_info: Proc.new { |context| { name: context.client_name, version: context.client_version } }
28
+ # }
29
+ # )
30
+ #
31
+ # # ...
32
+ #
33
+ # end
34
+
35
+ module GraphQL
36
+ # GraphQL Hive usage collector and schema reporter
37
+ class Hive < GraphQL::Tracing::PlatformTracing
38
+ @@schema = nil
39
+ @@instance = nil
40
+
41
+ @usage_reporter = nil
42
+ @client = nil
43
+
44
+ REPORT_SCHEMA_MUTATION = <<~MUTATION
45
+ mutation schemaPublish($input: SchemaPublishInput!) {
46
+ schemaPublish(input: $input) {
47
+ __typename
48
+ }
49
+ }
50
+ MUTATION
51
+
52
+ DEFAULT_OPTIONS = {
53
+ enabled: true,
54
+ collect_usage: true,
55
+ read_operations: true,
56
+ report_schema: true,
57
+ buffer_size: 50,
58
+ logger: nil
59
+ }.freeze
60
+
61
+ self.platform_keys = {
62
+ 'lex' => 'lex',
63
+ 'parse' => 'parse',
64
+ 'validate' => 'validate',
65
+ 'analyze_query' => 'analyze_query',
66
+ 'analyze_multiplex' => 'analyze_multiplex',
67
+ 'execute_multiplex' => 'execute_multiplex',
68
+ 'execute_query' => 'execute_query',
69
+ 'execute_query_lazy' => 'execute_query_lazy'
70
+ }
71
+
72
+ def initialize(options = {})
73
+ opts = DEFAULT_OPTIONS.merge(options)
74
+ validate_options!(opts)
75
+ super(opts)
76
+
77
+ @@instance = self
78
+
79
+ @client = GraphQL::Hive::Client.new(opts)
80
+ @usage_reporter = GraphQL::Hive::UsageReporter.new(opts, @client)
81
+
82
+ # buffer
83
+ @report = {
84
+ size: 0,
85
+ map: {},
86
+ operations: []
87
+ }
88
+
89
+ send_report_schema(@@schema) if @@schema && opts[:report_schema] && @options[:enabled]
90
+ end
91
+
92
+ def self.instance
93
+ @@instance
94
+ end
95
+
96
+ def self.use(schema, **kwargs)
97
+ @@schema = schema
98
+ super
99
+ end
100
+
101
+ # called on trace events
102
+ def platform_trace(platform_key, _key, data)
103
+ return yield unless @options[:enabled] && @options[:collect_usage]
104
+
105
+ if platform_key == 'execute_multiplex'
106
+ if data[:multiplex]
107
+ queries = data[:multiplex].queries
108
+ timestamp = (Time.now.utc.to_f * 1000).to_i
109
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
110
+ results = yield
111
+ ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
112
+ elapsed = ending - starting
113
+ duration = (elapsed.to_f * (10**9)).to_i
114
+
115
+ report_usage(timestamp, queries, results, duration) unless queries.empty?
116
+
117
+ results
118
+ else
119
+ yield
120
+ end
121
+ else
122
+ yield
123
+ end
124
+ end
125
+
126
+ # compat
127
+ def platform_authorized_key(type)
128
+ "#{type.graphql_name}.authorized.graphql"
129
+ end
130
+
131
+ # compat
132
+ def platform_resolve_type_key(type)
133
+ "#{type.graphql_name}.resolve_type.graphql"
134
+ end
135
+
136
+ # compat
137
+ def platform_field_key(type, field)
138
+ "graphql.#{type.name}.#{field.name}"
139
+ end
140
+
141
+ def on_exit
142
+ @usage_reporter.on_exit
143
+ end
144
+
145
+ private
146
+
147
+ def validate_options!(options)
148
+ if options[:logger].nil?
149
+ options[:logger] = Logger.new($stdout)
150
+ original_formatter = Logger::Formatter.new
151
+ options[:logger].formatter = proc { |severity, datetime, progname, msg|
152
+ original_formatter.call(severity, datetime, progname, "[hive] #{msg.dump}")
153
+ }
154
+ end
155
+ if !options.include?(:token) && (!options.include?(:enabled) || options.enabled)
156
+ options[:logger].warn('`token` options is missing')
157
+ options[:enabled] = false
158
+ false
159
+ elsif options[:report_schema] &&
160
+ (
161
+ !options.include?(:reporting) ||
162
+ (
163
+ options.include?(:reporting) && (
164
+ !options[:reporting].include?(:author) || !options[:reporting].include?(:commit)
165
+ )
166
+ )
167
+ )
168
+
169
+ options[:logger].warn('`reporting.author` and `reporting.commit` options are required')
170
+ false
171
+ end
172
+ true
173
+ end
174
+
175
+ def report_usage(timestamp, queries, results, duration)
176
+ @usage_reporter.add_operation([timestamp, queries, results, duration])
177
+ end
178
+
179
+ def send_report_schema(schema)
180
+ sdl = GraphQL::Schema::Printer.new(schema).print_schema
181
+
182
+ body = {
183
+ query: REPORT_SCHEMA_MUTATION,
184
+ operationName: 'schemaPublish',
185
+ variables: {
186
+ input: {
187
+ sdl: sdl,
188
+ author: @options[:reporting][:author],
189
+ commit: @options[:reporting][:commit],
190
+ service: @options[:reporting][:service_name],
191
+ url: @options[:reporting][:service_url],
192
+ force: true
193
+ }
194
+ }
195
+ }
196
+
197
+ puts(JSON.generate(body))
198
+
199
+ @client.send('/registry', body, :'report-schema')
200
+ end
201
+ end
202
+ end
203
+
204
+ at_exit do
205
+ GraphQL::Hive.instance&.on_exit
206
+ end
Binary file
metadata ADDED
@@ -0,0 +1,139 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: graphql-hive
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Charly POLY
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-06-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: graphql
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 2.0.9
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 2.0.9
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.17'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.17'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.30'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.30'
83
+ description: '"Monitor operations, inspect your queries and publish your GraphQL schema
84
+ with GraphQL Hive"'
85
+ email:
86
+ - cpoly55@gmail.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".github/workflows/ci.yml"
92
+ - ".gitignore"
93
+ - ".rspec"
94
+ - ".rubocop.yml"
95
+ - Gemfile
96
+ - Gemfile.lock
97
+ - LICENSE
98
+ - LICENSE.txt
99
+ - README.md
100
+ - Rakefile
101
+ - cover.png
102
+ - examples/simple-api/Gemfile
103
+ - examples/simple-api/Gemfile.lock
104
+ - examples/simple-api/app.rb
105
+ - examples/simple-api/config.ru
106
+ - examples/simple-api/schema.rb
107
+ - graphql-hive.gemspec
108
+ - lib/graphql-hive.rb
109
+ - lib/graphql-hive/analyzer.rb
110
+ - lib/graphql-hive/client.rb
111
+ - lib/graphql-hive/printer.rb
112
+ - lib/graphql-hive/usage_reporter.rb
113
+ - lib/graphql-hive/version.rb
114
+ - operations-dashboard.png
115
+ homepage: https://docs.graphql-hive.com/specs/integrations
116
+ licenses:
117
+ - MIT
118
+ metadata:
119
+ rubygems_mfa_required: 'true'
120
+ post_install_message:
121
+ rdoc_options: []
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: 2.6.0
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ requirements: []
135
+ rubygems_version: 3.0.6
136
+ signing_key:
137
+ specification_version: 4
138
+ summary: '"GraphQL Hive integration for `graphql-ruby`"'
139
+ test_files: []