Skip to content

Commit

Permalink
Merge pull request #2038 from rmosolgo/1.9-dev
Browse files Browse the repository at this point in the history
Put 1.9-dev on master
  • Loading branch information
Robert Mosolgo authored Jan 10, 2019
2 parents 48327f2 + 4d8ffc2 commit 7eb62c2
Show file tree
Hide file tree
Showing 232 changed files with 11,755 additions and 2,519 deletions.
5 changes: 5 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ matrix:
include:
- env:
- DISPLAY=':99.0'
- TESTING_INTERPRETER=yes
rvm: 2.4.3
addons:
apt:
Expand All @@ -36,6 +37,10 @@ matrix:
gemfile: spec/dummy/Gemfile
script:
- cd spec/dummy && bundle exec rails test:system
- env:
- TESTING_INTERPRETER=yes
rvm: 2.4.3
gemfile: gemfiles/rails_5.2.gemfile
- rvm: 2.3.8
gemfile: gemfiles/rails_3.2.gemfile
- rvm: 2.3.8
Expand Down
6 changes: 6 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ namespace :bench 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
end

namespace :test do
Expand Down
105 changes: 96 additions & 9 deletions benchmark/run.rb
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
# frozen_string_literal: true
require "dummy/schema"
TESTING_INTERPRETER = true
require "graphql"
require "jazz"
require "benchmark/ips"
require 'ruby-prof'
require 'memory_profiler'
require "ruby-prof"
require "memory_profiler"

module GraphQLBenchmark
QUERY_STRING = GraphQL::Introspection::INTROSPECTION_QUERY
DOCUMENT = GraphQL.parse(QUERY_STRING)
SCHEMA = Dummy::Schema
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 = GraphQL.parse(File.read(File.join(BENCHMARK_PATH, "abstract_fragments_2.graphql")))


BIG_SCHEMA = GraphQL::Schema.from_definition(File.join(BENCHMARK_PATH, "big_schema.graphql"))
BIG_QUERY = GraphQL.parse(File.read(File.join(BENCHMARK_PATH, "big_query.graphql")))

module_function
def self.run(task)

Benchmark.ips do |x|
case task
when "query"
Expand All @@ -40,16 +40,103 @@ def self.profile
# Warm up any caches:
SCHEMA.execute(document: DOCUMENT)
# CARD_SCHEMA.validate(ABSTRACT_FRAGMENTS)

res = nil
result = RubyProf.profile do
# CARD_SCHEMA.validate(ABSTRACT_FRAGMENTS)
SCHEMA.execute(document: DOCUMENT)
res = SCHEMA.execute(document: DOCUMENT)
end
# printer = RubyProf::FlatPrinter.new(result)
# printer = RubyProf::GraphHtmlPrinter.new(result)
printer = RubyProf::FlatPrinterWithLineNumbers.new(result)

printer.print(STDOUT, {})
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.report("Querying for #{ProfileLargeResult::DATA.size} objects") {
schema.execute(document: document)
}
end

result = RubyProf.profile do
schema.execute(document: document)
end
printer = RubyProf::FlatPrinter.new(result)
# printer = RubyProf::GraphHtmlPrinter.new(result)
# printer = RubyProf::FlatPrinterWithLineNumbers.new(result)

printer.print(STDOUT, {})

report = MemoryProfiler.report do
schema.execute(document: document)
end

report.pretty_print
end

module ProfileLargeResult
DATA = 1000.times.map {
{
id: SecureRandom.uuid,
int1: SecureRandom.random_number(100000),
int2: SecureRandom.random_number(100000),
string1: SecureRandom.base64,
string2: SecureRandom.base64,
boolean1: SecureRandom.random_number(1) == 0,
boolean2: SecureRandom.random_number(1) == 0,
int_array: 10.times.map { SecureRandom.random_number(100000) },
string_array: 10.times.map { SecureRandom.base64 },
boolean_array: 10.times.map { SecureRandom.random_number(1) == 0 },
}
}


class FooType < GraphQL::Schema::Object
field :id, ID, null: false
field :int1, Integer, null: false
field :int2, Integer, null: false
field :string1, String, null: false
field :string2, String, null: false
field :boolean1, Boolean, null: false
field :boolean2, Boolean, null: false
field :string_array, [String], null: false
field :int_array, [Integer], null: false
field :boolean_array, [Boolean], null: false
end

class QueryType < GraphQL::Schema::Object
description "Query root of the system"
field :foos, [FooType], null: false, description: "Return a list of Foo objects"
def foos
DATA
end
end

class Schema < GraphQL::Schema
query QueryType
if TESTING_INTERPRETER
use GraphQL::Execution::Interpreter
end
end

ALL_FIELDS = GraphQL.parse <<-GRAPHQL
{
foos {
id
int1
int2
string1
string2
boolean1
boolean2
stringArray
intArray
booleanArray
}
}
GRAPHQL
end
end
13 changes: 13 additions & 0 deletions guides/authorization/accessibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,23 @@ desc: Reject queries from unauthorized users if they access certain parts of the
index: 2
---

__NOTE:__ This kind of authorization is deprecated and isn't supported by the {% internal_link "forthcoming GraphQL runtime", "/queries/interpreter" %} because it's too slow.

With GraphQL-Ruby, you can inspect an incoming query, and return a custom error if that query accesses some unauthorized parts of the schema.

This is different from {% internal_link "visibility", "/authorization/visibility" %}, where unauthorized parts of the schema are treated as non-existent. It's also different from {% internal_link "authorization", "/authorization/authorization" %}, which makes checks _while running_, instead of _before running_.

## Opt-In

To use this kind of authorization, you must add a query analyzer:

```ruby
class MySchema < GraphQL::Schema
# Set up ahead-of-time `accessible?` authorization
query_analyzer GraphQL::Authorization::Analyzer
end
```

## Preventing Access

You can override some `.accessible?(context)` methods to prevent access to certain members of the schema:
Expand Down
35 changes: 35 additions & 0 deletions guides/fields/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,23 @@ field :players, [User], null: false,
hash_key: "allPlayers"
```

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
Expand Down Expand Up @@ -137,6 +154,24 @@ def current_winning_streak(include_ties:)
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:
Expand Down
1 change: 1 addition & 0 deletions guides/guides.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- name: GraphQL Pro
- name: GraphQL Pro - OperationStore
- name: JavaScript Client
- name: Language Tools
- name: Other
---

Expand Down
143 changes: 143 additions & 0 deletions guides/language_tools/visitor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
---
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 } }")
# => #<GraphQL::Language::Nodes::Document ...>
```

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.
Loading

0 comments on commit 7eb62c2

Please sign in to comment.