graphql-hive 0.1.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 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: []