From e610d6165ba961cbdb923825d7685879fc99c7a3 Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Mon, 17 Oct 2016 21:49:37 -0400 Subject: [PATCH] doc(guides) write some new guides --- .gitignore | 2 +- .yardopts | 8 +- Rakefile | 30 +- graphql.gemspec | 4 +- guides/_layouts/default.html | 39 ++- guides/_plugins/api_doc.rb | 16 + guides/executing_queries.md | 92 ----- guides/index.md | 109 ++++++ guides/introduction.md | 170 --------- guides/queries/authorization.md | 92 +++++ guides/queries/error_handling.md | 133 +++++++ guides/queries/executing_queries.md | 124 +++++++ guides/queries/optimization.md | 3 + guides/queries/phases_of_execution.md | 14 + guides/{ => queries}/security.md | 2 +- guides/related_projects.md | 23 ++ guides/relay.md | 459 ------------------------- guides/relay/connections.md | 274 +++++++++++++++ guides/relay/mutations.md | 132 +++++++ guides/relay/object_identification.md | 91 +++++ guides/{ => schema}/code_reuse.md | 13 +- guides/schema/configuration_options.md | 190 ++++++++++ guides/{ => schema}/testing.md | 14 +- guides/schema/types_and_fields.md | 121 +++++++ readme.md | 76 +--- spec/spec_helper.rb | 2 - 26 files changed, 1379 insertions(+), 854 deletions(-) create mode 100644 guides/_plugins/api_doc.rb delete mode 100644 guides/executing_queries.md create mode 100644 guides/index.md delete mode 100644 guides/introduction.md create mode 100644 guides/queries/authorization.md create mode 100644 guides/queries/error_handling.md create mode 100644 guides/queries/executing_queries.md create mode 100644 guides/queries/optimization.md create mode 100644 guides/queries/phases_of_execution.md rename guides/{ => queries}/security.md (99%) create mode 100644 guides/related_projects.md delete mode 100644 guides/relay.md create mode 100644 guides/relay/connections.md create mode 100644 guides/relay/mutations.md create mode 100644 guides/relay/object_identification.md rename guides/{ => schema}/code_reuse.md (93%) create mode 100644 guides/schema/configuration_options.md rename guides/{ => schema}/testing.md (92%) create mode 100644 guides/schema/types_and_fields.md diff --git a/.gitignore b/.gitignore index 68f42a4873..44023af8e7 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,4 @@ _site .sass-cache .jekyll-metadata gh-pages/ -guides/index.md +__* diff --git a/.yardopts b/.yardopts index 75cb275d05..962de13a9a 100644 --- a/.yardopts +++ b/.yardopts @@ -1,5 +1,5 @@ --no-private ---markup markdown ---readme readme.md ---title 'GraphQL Ruby API Documentation' -'lib/**/*.rb' - '**/*.md' +--markup=markdown +--readme=readme.md +--title='GraphQL Ruby API Documentation' +'lib/**/*.rb' - '*.md' diff --git a/Rakefile b/Rakefile index e8571d071f..ab7d44add6 100644 --- a/Rakefile +++ b/Rakefile @@ -18,21 +18,6 @@ end task(default: [:test, :rubocop]) -def load_gem_and_dummy - $:.push File.expand_path("../lib", __FILE__) - $:.push File.expand_path("../spec", __FILE__) - require "graphql" - require "./spec/support/dairy_app" -end - -task :console do - require "irb" - require "irb/completion" - load_gem_and_dummy - ARGV.clear - IRB.start -end - desc "Use Racc & Ragel to regenerate parser.rb & lexer.rb from configuration files" task :build_parser do `rm lib/graphql/language/parser.rb lib/graphql/language/lexer.rb ` @@ -41,17 +26,8 @@ task :build_parser do end namespace :site do - task :generate_readme_index do - File.open("guides/index.md", "w") do |fo| - fo.puts "---\npermalink: \"/\"\n---\n" - File.foreach("readme.md") do |li| - fo.puts li - end - end - end - desc "View the documentation site locally" - task :serve => :generate_readme_index do + task :serve do require "jekyll" # Generate the site in server mode. @@ -67,7 +43,7 @@ namespace :site do end desc "Commit the local site to the gh-pages branch and publish to GitHub Pages" - task :publish => [:generate_readme_index, :html_proofer] do + task :publish => [:html_proofer] do # Ensure the gh-pages dir exists so we can generate into it. puts "Checking for gh-pages dir..." unless File.exist?("./gh-pages") @@ -117,7 +93,7 @@ namespace :site do end desc "Test the generated HTML files" - task :html_proofer => :generate_readme_index do + task :html_proofer do require "html-proofer" Dir.chdir("guides") do diff --git a/graphql.gemspec b/graphql.gemspec index ab736cf272..d1b090f40d 100644 --- a/graphql.gemspec +++ b/graphql.gemspec @@ -14,12 +14,10 @@ Gem::Specification.new do |s| s.license = "MIT" s.required_ruby_version = ">= 2.1.0" # bc optional keyword args - s.files = Dir["{lib}/**/*", "MIT-LICENSE", "readme.md"] + s.files = Dir["{lib}/**/*", "MIT-LICENSE", "readme.md", ".yardopts"] s.test_files = Dir["spec/**/*"] s.add_development_dependency "codeclimate-test-reporter", "~>0.4" - s.add_development_dependency "pry", "~> 0.10" - s.add_development_dependency 'pry-stack_explorer' s.add_development_dependency "guard", "~> 2.12" s.add_development_dependency "guard-bundler", "~> 2.1" s.add_development_dependency "guard-minitest", "~> 2.4" diff --git a/guides/_layouts/default.html b/guides/_layouts/default.html index f50ecfa1b4..5e52ca4335 100644 --- a/guides/_layouts/default.html +++ b/guides/_layouts/default.html @@ -2,7 +2,7 @@ - GraphQL Ruby + GraphQL Ruby - {{ page.title }} @@ -26,16 +26,37 @@
-

Readme

Guides

API Docs

Source Code

diff --git a/guides/_plugins/api_doc.rb b/guides/_plugins/api_doc.rb new file mode 100644 index 0000000000..29d7d3a91d --- /dev/null +++ b/guides/_plugins/api_doc.rb @@ -0,0 +1,16 @@ +module GraphQLSite + module APIDoc + API_DOC_ROOT = "http://www.rubydoc.info/gems/graphql/" + + def api_doc(input) + doc_path = input + .gsub("::", "/") # namespaces + .sub(/#(.+)$/, "#\\1-instance_method") # instance methods + .sub(/\.(.+)$/, "#\\1-class_method") # class methods + + %|#{input}| + end + end +end + +Liquid::Template.register_filter(GraphQLSite::APIDoc) diff --git a/guides/executing_queries.md b/guides/executing_queries.md deleted file mode 100644 index e5ca6481fd..0000000000 --- a/guides/executing_queries.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Executing Queries with `graphql-ruby` - ---- - -After you define your schema, you can evaluate queries with `GraphQL::Schema#execute`. - -At the simplest, you can evaluate a query from string against a schema: - -```ruby -# let's pretend it's a Rails controller! -query_string = params[:query] -result = MySchema.execute(query_string) -render(json: result) -``` - -## Variables - -If your query contains variables, you can provide their values with the `variables:` keyword. - -```ruby -query_string = "query getPost($postId: Int!){ post(id: $postId) { title } }" -result = MySchema.execute(query_string, variables: {"postId" => 2}) -``` - -`variables` keys should be strings, whose names match the variables, without `$`. - -## Context - -You can pass an arbitrary hash of information into the query with the `context:` keyword. - -```ruby -result = MySchema.execute(query_string, context: {current_user: current_user}) -``` - -These values will be accessible by key inside `resolve` functions. For example, this field only returns a value if the current user has high enough permissions: - -```ruby -SecretStringField = GraphQL::Field.new do |f| - f.type !GraphQL::STRING_TYPE - f.description "A string that's only visible to authorized users" - f.resolve ->(obj, args, ctx) { ctx[:current_user].authorized? ? obj.secret_string : nil } -end -``` - -Note that `ctx` is not the _same_ hash that's passed to `GraphQL::Schema#execute`. `ctx` is an instance of `GraphQL::Query::Context`, which exposes the provided hash and may _also_ contain other information about the query. - -## Operation name - -If your query contains multiple operations, you _must_ pass the operation name with the `operation_name:` keyword: - -```ruby -result = MySchema.execute(query_string, operation_name: "getPersonInfo") -``` - -If you don't, you'll get an error. - -## Validation - -By default, `graphql-ruby` performs validation on incoming query strings. If you want to disable this, pass `validate: false`. No guarantees it won't blow up :) - -```ruby -result = MySchema.execute(query_string, validate: false) -``` - -## Custom Execution Strategies - -`graphql` includes a serial execution strategy, but you can also create custom strategies to support advanced behavior. See `GraphQL::SerialExecution#execute` the required behavior. - -Then, set your schema to use your custom execution strategy with `GraphQL::Schema#{query|mutation|subscription}_execution_strategy` - -For example: - -```ruby -class CustomQueryExecutionStrategy - def initialize - # ... - end - - def execute(operation_name, root_type, query) - # ... - end -end - -# ... define your types ... - -MySchema = GraphQL::Schema.define do - query MyQueryType - mutation MyMutationType - # Use your custom strategy: - query_execution_strategy = CustomQueryExecutionStrategy -``` diff --git a/guides/index.md b/guides/index.md new file mode 100644 index 0000000000..2909a386d0 --- /dev/null +++ b/guides/index.md @@ -0,0 +1,109 @@ +--- +title: Welcome +--- + +## Installation + +You can install `graphql` from RubyGems by adding to your application's `Gemfile`: + +```ruby +# Gemfile +gem "graphql" +``` + +Then, running `bundle install`: + +```sh +$ bundle install +``` + +## Getting Started + +Building a GraphQL server goes like this: + +- Define some types +- Connect them to a schema +- Execute queries with your schema + +### Declare types + +Types describe objects in your application and form the basis for [GraphQL's type system](http://graphql.org/learn/schema/#type-system). + +```ruby +PostType = GraphQL::ObjectType.define do + name "Post" + description "A blog post" + # `!` marks a field as "required" or "non-null" + field :id, !types.ID + field :title, !types.String + field :body, !types.String + field :comments, types[!CommentType] +end + +CommentType = GraphQL::ObjectType.define do + name "Comment" + field :id, !types.ID + field :body, !types.String + field :created_at, !types.String +end +``` + +### Build a Schema + +Before building a schema, you have to define an [entry point to your system, the "query root"](http://graphql.org/learn/schema/#the-query-and-mutation-types): + +```ruby +QueryType = GraphQL::ObjectType.define do + name "Query" + description "The query root of this schema" + + field :post do + type PostType + argument :id, !types.ID + description "Find a Post by ID" + resolve -> (obj, args, ctx) { Post.find(args["id"]) } + end +end +``` + +Then, build a schema with `QueryType` as the query entry point: + +```ruby +Schema = GraphQL::Schema.define do + query QueryType +end +``` + +This schema is ready to serve GraphQL queries! See ["Configuration Options"]({{ site.baseurl }}/schema/configuration_options) for all the schema options. + +### Execute queries + +You can execute queries from a query string: + +```ruby +query_string = " +{ + post(id: 1) { + id + title + } +}" +result_hash = Schema.execute(query_string) +# { +# "data" => { +# "post" => { +# "id" => 1, +# "title" => "GraphQL is nice" +# } +# } +# } +``` + +See ["Executing Queries"]({{ site.baseurl }}/queries/executing_queries) for more information about running queries on your schema. + +## Use with Relay + +If you're building a backend for [Relay](http://facebook.github.io/relay/), you'll need: + +- A JSON dump of the schema, which you can get by sending [`GraphQL::Introspection::INTROSPECTION_QUERY`](https://github.com/rmosolgo/graphql-ruby/blob/master/lib/graphql/introspection/introspection_query.rb) +- Relay-specific helpers for GraphQL, see the `GraphQL::Relay` guides. diff --git a/guides/introduction.md b/guides/introduction.md deleted file mode 100644 index 6bd231bc6e..0000000000 --- a/guides/introduction.md +++ /dev/null @@ -1,170 +0,0 @@ ---- -title: Introduction to `graphql-ruby` ---- - -A GraphQL system exposes your application to the world according to its _schema_. A schema includes: - -- _Types_ which expose objects in your application -- _Fields_ which expose properties of those objects (and may connect types to other types) -- _Query root_, which is a special type used for executing read queries -- _Mutation root_, which is a special type used for executing mutations - -Once you've defined your query root and mutation root, you can make a schema: - -```ruby -ApplicationSchema = GraphQL::Schema.define do - query QueryRoot - mutation MutationRoot -end -``` - -At that time, `graphql-ruby` will validate all types and fields. - -## Getting Started with Types - -Create types to wrap objects & expose data through fields. - -Let's imagine we have a blog with only a `Post` model (no comments yet :) : - -```ruby -class Post - attr_accessor :id, :title, :body - def self.find(id) - # get a Post from the database - end -end -``` - -Let's make type for `Post`: - -```ruby -# `types` is a helper for declaring GraphQL types -PostType = GraphQL::ObjectType.define do - name "Post" - description "A blog entry" - - field :id, !types.ID, "The unique ID for this post" - field :title, !types.String, "The title of this post" - field :body, !types.String, "The body of this post" -end -``` - -Now, posts will expose `id`, `title` and `body`. The `field` helper is good for fields that call methods with the same name. Using `!` defines fields as non-null. - -However, the post type isn't accessible yet because it's not attached to a query root. - -## Making a query root - -A query root is "just" a type. Its fields don't call methods on some object, though -- instead, they retrieve objects which will be read. - -```ruby -QueryRoot = GraphQL::ObjectType.define do - name "Query" - description "The query root for this schema" - - field :post do - type PostType - description "Find a Post by id" - argument :id, !types.ID - resolve ->(object, arguments, context) { - Post.find(arguments["id"]) - } - end -end -``` - -The query root has one field, `post`, which finds a `Post` by ID. The `resolve` proc will be called with: - -- `object`: The "parent" of this field (in the above case, it's the query root, not very useful) -- `arguments`: A hash with arguments passed to the field (keys will be strings) -- `context`: An arbitrary object defined when running the query (see [Executing queries](http://www.rubydoc.info/github/rmosolgo/graphql-ruby/file/guides/executing_queries.md)) - - -## Creating a Schema - -Lastly, create the schema: - -```ruby -Schema = GraphQL::Schema.define do - query QueryRoot -end -``` - -This schema could handle queries like: - -``` -{ - firstPost: post(id: 1) { title, body } - nextPost: post(id: 2) { title, body } -} -``` - -## Complex Types - -Given a `Comment` model like this one, let's add it to our schema: - -```ruby -class Comment - attr_accessor :id, :body, :post_id - def post - # find this comment's post - end -end -``` - -First, a `CommentType` to expose comments: - -```ruby -CommentType = GraphQL::ObjectType.define do - name "Comment" - description "A reply to a post" - - field :id, !types.ID, "The unique ID of this comment" - field :body, !types.String, "The content of this comment" - field :post, !PostType, "The post this comment replies to" -end -``` - -Notice that the `post` field has `type: !PostType`. This means it returns a non-null `PostType` (which we defined above). - -We should also add a `comments` field to `PostType`: - -```ruby -PostType = GraphQL::ObjectType.new do |t, types, field| - # ... existing code ... - field :comments, !types[!CommentType], "Responses to this post" -end -``` - -`types[SomeType]` means that this field returns a _list_ of `SomeType`. - -## Executing a Query - -After defining your schema, you can evaluate queries with `GraphQL::Query`. For example: - -```ruby -Schema = GraphQL::Schema.define do - query QueryRoot # QueryRoot defined above -end - -query_string = "query getPost { post(id: 1) { id, title, comments { body } } }" - -result_hash = Schema.execute(query_string) -# { -# "post" : { -# "id" : 1, -# "title" : "GraphQL is cool", -# "comments" : [ -# { "body" : "Yep, sure is."}, -# { "body" : "Still gotta figure out that reducing executor though"} -# ] -# } -# } -``` - -## More Info - -Read more in some other guides: - -- [Defining Your Schema](https://rmosolgo.github.io/graphql-ruby/defining_your_schema) -- [Executing Queries](https://rmosolgo.github.io/graphql-ruby/executing_queries) diff --git a/guides/queries/authorization.md b/guides/queries/authorization.md new file mode 100644 index 0000000000..296e5a23f0 --- /dev/null +++ b/guides/queries/authorization.md @@ -0,0 +1,92 @@ +--- +title: Queries — Authorization +--- + +GraphQL offers a few ways to ensure that clients access data according to their permissions. + +- __Query analyzers__ can assert that the query is valid before running it. +- __Resolve wrappers__ can assert that returned objects are permitted to a given user. + +## Query Analyzers + + + +## Resolve Wrapper + +Sometimes, you can only check permissions when you have the _actual_ object. Let's say you're exposing documents in your API: + +```ruby +field :documents, types[DocumentType] do + resolve -> (obj, args, ctx) { + documents = obj.documents + # sort, filter, etc + # return the documents: + documents + } +end +``` + +You can "wrap" this resolve function to assert that the documents are ok for the current user: + +```ruby +# Take a resolve function and call it. +# Then, check that the result includes permitted records _only_. +# @return [Proc] a new resolve function that checks the return values +def assert_allowed_documents(resolve_func) + -> (obj, args, ctx) { + documents = resolve_func.call(obj, args, ctx) + current_user = ctx[:current_user] + + if documents.all? { |d| current_user.can_view?(d) } + documents + else + nil + end + } +end + +# ... + +field :documents, types[DocumentType] do + # wrap the resolve function with your assertion + resolve assert_allowed_documents(-> (obj, args, ctx) { + # ... + }) +end +``` + +This way, you can "catch" the returned value before giving it to a client. + +This approach can be further parameterized by implementing it as a class, for example: + +```ruby +# Assert that the current user has `permission` on the return value of `block` +class PermissionAssertion + # Get a permission level and the "inner" resolve function + def initialize(permission, resolve_func) + @permission = permission + @resolve_func = resolve_func + end + + # GraphQL will call this, so delegate to the "inner" resolve function + # and check the return value + def call(obj, args, ctx) + value = @resolve_func.call(obj, args, ctx) + current_user = ctx[:current_user] + if current_user.can?(@permission, value) + value + else + nil + end + end +end + +# ... + +# Apply this class to the resolve function: +field :documents, types[DocumentType] do + resolve PermissionAssertion.new(:view, -> (obj, args, ctx) { + # ... + }) +end +``` diff --git a/guides/queries/error_handling.md b/guides/queries/error_handling.md new file mode 100644 index 0000000000..8eae3e3102 --- /dev/null +++ b/guides/queries/error_handling.md @@ -0,0 +1,133 @@ +--- +title: Queries — Error Handling +--- + +Sometimes errors happen! There are a few ways to express them in GraphQL: + +- Expose errors _in_ the schema itself +- Add errors to the response's `"errors"` key + +Additionally, you can provide opt-in error support by wrapping resolvers (described below). + +## Exposing Errors + +You can expose errors as part of your schema. In a sense, this makes errors part of "business as usual", instead of exceptional cases. + +For example, you could add `errors` to `PostType`: + +```ruby +PostType = GraphQL::ObjectType.define do + name "Post" + field :title, types.String + # ... + field :errors, types[types.String], "Reasons the object couldn't be created or updated" do + resolve -> (obj, args, ctx) { obj.errors.full_messages } + end +end +``` + +Then, when creating a post, return the `Post`, even if the save failed: + +```ruby +resolve -> (obj, args, ctx) { + post = Post.new(args["post"].to_h) + # Maybe this fails, no big deal: + post.save + post +} +``` + +Then, when clients create a post, they should check the `errors` field to see if it was successful: + +```graphql +mutation { + createPost(post: {title: "GraphQL is Nice"}) { + id + title + errors # in case the save failed + } +} +``` + +If `errors` is present (and `id` is null), the client knows that the operation was unsuccessful, and they can discover why. + +This technique could be extended by creating dedicated error types, too. + +## The "errors" Key + +A GraphQL response may have an `"errors"` key, for example: + +```ruby +Schema.execute(query_string) +# { +# "errors" => [ +# { "message" => "Something went wrong" }, +# ], +# } +``` + +You can add an error to the `"errors"` key by returning a {{ "GraphQL::ExecutionError" | api_doc }} from a `resolve` function. For example: + +```ruby +resolve -> (obj, args, ctx) { + post_params = args["post"].to_h + if obj.posts.create(post_params) + # on success, return the post: + post + else + # on error, return an error: + GraphQL::ExecutionError.new("Invalid input for Post: #{post.errors.full_messages.join(", ")}") + end +} +``` + +If some part of your `resolve` function would raise an error, you can rescue it and return a {{ "GraphQL::ExecutionError" | api_doc }} instead: + +```ruby +resolve -> (obj, args, ctx) { + post_params = args["post"].to_h + begin + post = obj.posts.create!(post_params) + # on success, return the post: + post + rescue ActiveRecord::RecordInvalid => err + # on error, return an error: + GraphQL::ExecutionError.new("Invalid input for Post: #{post.errors.full_messages.join(", ")}") + end +} +``` + +## Error Handling with Wrappers + +If you don't want to `begin ... rescue ... end` in every field, you can wrap `resolve` functions in error handling. For example, you could make an object that wraps another resolver: + +```ruby +# Wrap field resolver `resolve_func` with a handler for `error_superclass`. +# `RescueFrom` instances are valid field resolvers too. +class RescueFrom + def initialize(error_superclass, resolve_func) + @error_superclass = error_superclass + @resolve_func = resolve_func + end + + def call(obj, args, ctx) + @resolve_func.call(obj, args, ctx) + rescue @error_superclass => err + # Your error handling logic here: + # - return an instance of `GraphQL::ExecutionError` + # - or, return nil: + nil + end +end +``` + +Then, apply it to fields on an opt-in basis: + +```ruby +field :create_post, PostType do + # Wrap the resolve function with `RescueFrom.new(err_class, ...)` + resolve RescueFrom.new(ActiveRecord::RecordInvalid, -> (obj, args, ctx) { ... }) +end +``` + +This way, you get error handling with proper Ruby semantics and no overhead! diff --git a/guides/queries/executing_queries.md b/guides/queries/executing_queries.md new file mode 100644 index 0000000000..a05cb6bb38 --- /dev/null +++ b/guides/queries/executing_queries.md @@ -0,0 +1,124 @@ +--- +title: Queries — Executing Queries +--- + + +You can execute queries with your {{ "GraphQL::Schema" | api_doc }} at get a Ruby Hash as a result. For example, to execute a query from a string: + +```ruby +query_string = "{ ... }" +MySchema.execute(query_string) +# { +# "data" => { ... } +# } +``` + +There are also several options you can use: + +- `variables:` provides values for `$`-named [query variables](http://graphql.org/learn/queries/#variables) +- `context:` accepts application-specific data to pass to `resolve` functions +- `root_value:` will be provided to root-level `resolve` functions as `obj` +- `operation_name:` picks a [named operation](http://graphql.org/learn/queries/#operation-name) from the incoming string to execute +- `document:` accepts an already-parsed query (instead of a string), see {{ "GraphQL.parse" | api_doc }} +- `validate:` may be `false` to skip static validation for this query +- `max_depth:` and `max_complexity:` may override schema-level values + +Some of these options are described in more detail below, see {{ "GraphQL::Query#initialize" | api_doc }} for more information. + +## Variables + +GraphQL provides [query variables](http://graphql.org/learn/queries/#variables) as a way to parameterize query strings. If your query string contains variables, you can provide values in a hash of `{ String => value }` pairs. The keys should _not_ contain `"$"`. + +For example, to provide variables to a query: + +```ruby +query_string = " + query getPost($postId: ID!) { + post(id: $postId) { + title + } + }" + +variables = { "postId" => "1" } + +MySchema.execute(query_string, variables: variables) +``` + +If the variable is a {{ "GraphQL::InputObjectType" | api_doc }}, you can provide a nested hash, for example: + +```ruby +query_string = " +mutation createPost($postParams: PostInput!, $createdById: ID!){ + createPost(params: $postParams, createdById: $createdById) { + id + title + createdBy { name } + } +} +" + +variables = { + "postParams" => { + "title" => "...", + "body" => "..." + }, + "createdById" => "5", +} + +MySchema.execute(query_string, variables: variables) +``` + +## Context + +You can provide application-specific values to GraphQL as `context:`. This is available in many places: + +- `resolve` functions +- `Schema#resolve_type` hook +- ID generation & fetching + +Common uses for `context:` include the current user or auth token. To provide a `context:` value, pass a hash to `Schema#execute`: + +```ruby +context = { + current_user: session[:current_user], + current_organization: session[:current_organization], +} + +MySchema.execute(query_string, context: context) +``` + +Then, you can access those values during execution: + +```ruby +resolve -> (obj, args, ctx) { + ctx[:current_user] # => # + # ... +} +``` + +Note that `ctx` is _not_ the hash that you passed it. It's an instance of {{ "GraphQL::Query::Context" | api_doc }}, but it delegates `#[](key)` to the hash you provide. + +## Root Value + +You can provide a root `obj` value with `root_value:`. For example, to base the query off of the current organization: + +```ruby +current_org = session[:current_organization] +MySchema.execute(query_string, root_value: current_org) +``` + +That value will be provided to root-level fields, such as mutation fields. For example: + +```ruby +MutationType = GraphQL::ObjectType.define do + name "Mutation" + field :createPost, types.Post do + resolve -> (obj, args, ctx) { + obj # => # + # ... + } + end +end +``` + +{{ "GraphQL::Relay::Mutation" | api_doc }} fields will also receive `root_value:` as `obj` (assuming they're attached directly to your `MutationType`). diff --git a/guides/queries/optimization.md b/guides/queries/optimization.md new file mode 100644 index 0000000000..50ece85a90 --- /dev/null +++ b/guides/queries/optimization.md @@ -0,0 +1,3 @@ +--- +title: Queries — Optimization +--- diff --git a/guides/queries/phases_of_execution.md b/guides/queries/phases_of_execution.md new file mode 100644 index 0000000000..a4d7402a1f --- /dev/null +++ b/guides/queries/phases_of_execution.md @@ -0,0 +1,14 @@ +--- +title: Queries — Phases of Execution +--- + +When GraphQL receives a query string, it goes through these steps: + +- Tokenize: {{ "GraphQL::Language::Lexer" | api_doc }} splits the string into a stream of tokens +- Parse: {{ "GraphQL::Language::Parser" | api_doc }} builds an abstract syntax tree (AST) out of the stream of tokens +- Validate: {{ "GraphQL::StaticValidation::Validator" | api_doc }} validates the incoming AST as a valid query for the schema +- Rewrite: {{ "GraphQL::InternalRepresentation::Rewrite" | api_doc }} builds a tree of {{ + "GraphQL::InternalRepresentation::Node" | api_doc }}s which express the query in a simpler way than the AST +- Analyze: If there are any query analyzers, they are run with {{ "GraphQL::Analysis::AnalyzeQuery" | api_doc }} +- Execute: The query is traversed, {{ "resolve" | api_doc }} functions are called and the response is built +- Respond: The response is returned as a Hash diff --git a/guides/security.md b/guides/queries/security.md similarity index 99% rename from guides/security.md rename to guides/queries/security.md index 4b384adc95..a756d07c73 100644 --- a/guides/security.md +++ b/guides/queries/security.md @@ -1,5 +1,5 @@ --- -title: GraphQL Security +title: Queries — Security --- Since a GraphQL endpoint provides arbitrary access to your application, you should employ safeguards to prevent large queries from swamping your system. diff --git a/guides/related_projects.md b/guides/related_projects.md new file mode 100644 index 0000000000..812ee9e898 --- /dev/null +++ b/guides/related_projects.md @@ -0,0 +1,23 @@ +--- +title: Related Projects +--- + +Want to add something? Please open a pull request [on GitHub](https://github.com/rmosolgo/graphql-ruby)! + +## Code + +- `graphql-ruby` + Rails demo ([src](https://github.com/rmosolgo/graphql-ruby-demo) / [heroku](http://graphql-ruby-demo.herokuapp.com)) +- [`graphql-batch`](https://github.com/shopify/graphql-batch), a batched query execution strategy +- [`graphql-libgraphqlparser`](https://github.com/rmosolgo/graphql-libgraphqlparser-ruby), bindings to [libgraphqlparser](https://github.com/graphql/libgraphqlparser), a C-level parser. +- Rails Helpers: + - [`graphql-activerecord`](https://github.com/goco-inc/graphql-activerecord) + - [`graphql-rails-resolve`](https://github.com/colepatrickturner/graphql-rails-resolver) + +## Blog Posts + +- Building a blog in GraphQL and Relay on Rails [Introduction](https://medium.com/@gauravtiwari/graphql-and-relay-on-rails-getting-started-955a49d251de), [Part 1]( https://medium.com/@gauravtiwari/graphql-and-relay-on-rails-creating-types-and-schema-b3f9b232ccfc), [Part 2](https://medium.com/@gauravtiwari/graphql-and-relay-on-rails-first-relay-powered-react-component-cb3f9ee95eca) +- https://medium.com/@khor/relay-facebook-on-rails-8b4af2057152 +- https://blog.jacobwgillespie.com/from-rest-to-graphql-b4e95e94c26b#.4cjtklrwt +- http://mgiroux.me/2015/getting-started-with-rails-graphql-relay/ +- http://mgiroux.me/2015/uploading-files-using-relay-with-rails/ +- http://mgiroux.me/2016/journey-into-graphql-ruby-query-execution/ diff --git a/guides/relay.md b/guides/relay.md deleted file mode 100644 index a9b29fb721..0000000000 --- a/guides/relay.md +++ /dev/null @@ -1,459 +0,0 @@ ---- -title: GraphQL::Relay ---- - -Since version `0.18.0`, `GraphQL::Relay` provides several helpers for making a Relay-compliant GraphQL endpoint in Ruby: - -- [global ids](#global-ids) support Relay's UUID-based refetching -- [connections](#connections) implement Relay's pagination -- [mutations](#mutations) allow Relay to mutate your system predictably - - -## Global Ids - -Global ids (or UUIDs) support two features in Relay: - -- __Caching__: Unique IDs are used as primary keys in Relay's client-side cache. -- __Refetching__: Relay uses unique IDs to refetch objects when it determines that its cache is stale. (It uses the `Query.node` field to refetch objects.) - -### Defining UUIDs - -You must provide a function for generating UUIDs and fetching objects with them. In your schema, define `id_from_object` and `object_from_id`: - -```ruby -MySchema = GraphQL::Schema.define do - id_from_object ->(object, type_definition, query_ctx) { - # Call your application's UUID method here - # It should return a string - MyApp::GlobalId.encrypt(object.class.name, object.id) - } - - object_from_id ->(id, query_ctx) { - class_name, item_id = MyApp::GlobalId.decrypt(id) - # "Post" => Post.find(id) - Object.const_get(class_name).find(item_id) - } -end -``` - -An unencrypted ID generator is provided in the gem. It uses `Base64` to encode values. You can use it like this: - -```ruby -MySchema = GraphQL::Schema.define do - # Create UUIDs by joining the type name & ID, then base64-encoding it - id_from_object ->(object, type_definition, query_ctx) { - GraphQL::Schema::UniqueWithinType.encode(type_definition.name, object.id) - } - - object_from_id ->(id, query_ctx) { - type_name, item_id = GraphQL::Schema::UniqueWithinType.decode(id) - # Now, based on `type_name` and `id` - # find an object in your application - # .... - } -end -``` - -### UUID fields - -To participate in Relay's caching and refetching, objects must do two things: - -- Implement the `"Node"` interface -- Define an `"id"` field which returns a UUID - -To implement the node interface, include `GraphQL::Relay::Node.interface` in your list of interfaces: - -```ruby -PostType = GraphQL::ObjectType.define do - name "Post" - # Implement the "Node" interface for Relay - interfaces [GraphQL::Relay::Node.interface] - # ... -end -``` - -To add a UUID field named `"id"`, use the `global_id_field` helper: - -```ruby -PostType = GraphQL::ObjectType.define do - name "Post" - # ... - # `id` exposes the UUID - global_id_field :id - # ... -end -``` - -Now, `PostType` can participate in Relay's UUID-based features. - -### `node` field (find-by-UUID) - -You should also provide a root-level `node` field so that Relay can refetch objects from your schema. It is provided as `GraphQL::Relay::Node.field`, so you can attach it like this: - -```ruby -QueryType = GraphQL::ObjectType.define do - name "Query" - # Used by Relay to lookup objects by UUID: - field :node, GraphQL::Relay::Node.field - # ... -end -``` - -## Connections - -Connections provide pagination and `pageInfo` for `Array`s, `ActiveRecord::Relation`s or `Sequel::Dataset`s. - -### Connection fields - -To define a connection field, use the `connection` helper. For a return type, get a type's `.connection_type`. For example: - -```ruby -PostType = GraphQL::ObjectType.define do - # `comments` field returns a CommentsConnection: - connection :comments, CommentType.connection_type - # To avoid circular dependencies, wrap the return type in a proc: - connection :similarPosts, -> { PostType.connection_type } - - # ... -end -``` - -You can also define custom arguments and a custom resolve function for connections, just like other fields: - -```ruby -connection :featured_comments, CommentType.connection_type do - # Use a name to disambiguate this from `CommentType.connection_type` - name "CommentConnectionWithSince" - - # Add an argument: - argument :since, types.String - - # Return an Array or ActiveRecord::Relation - resolve ->(post, args, ctx) { - comments = post.comments.featured - - if args[:since] - comments = comments.where("created_at >= ", since) - end - - comments - } -end -``` - -### Maximum Page Size - -You can limit the number of results with `max_page_size:`: - -```ruby -connection :featured_comments, CommentType.connection_type, max_page_size: 50 -``` - -### Connection types - -You can customize a connection type with `.define_connection`: - -```ruby -PostConnectionWithTotalCountType = PostType.define_connection do - field :totalCount do - type types.Int - # `obj` is the Connection, `obj.object` is the collection of Posts - resolve ->(obj, args, ctx) { obj.object.count } - end -end - -``` - -Now, you can use `PostConnectionWithTotalCountType` to define a connection with the "totalCount" field: - -```ruby -AuthorType = GraphQL::ObjectType.define do - # Use the custom connection type: - connection :posts, PostConnectionWithTotalCountType -end -``` - -### Custom edge types - -If you need custom fields on `edge`s, you can define an edge type and pass it to a connection: - -```ruby -# Person => Membership => Team -MembershipSinceEdgeType = TeamType.define_edge do - name "MembershipSinceEdge" - field :memberSince, types.Int, "The date that this person joined this team" do - resolve ->(obj, args, ctx) { - obj # => GraphQL::Relay::Edge instance - person = obj.parent - team = obj.node - membership = Membership.where(person: person, team: team).first - membership.created_at.to_i - } - end -end -``` - -Then, pass the edge type when defining the connection type: - -```ruby -TeamMembershipsConnectionType = TeamType.define_connection(edge_type: MembershipSinceEdgeType) do - # Use a name so it doesn't conflict with "TeamConnection" - name "TeamMembershipsConnection" -end -``` - -Now, you can query custom fields on the `edge`: - -```graphql -{ - me { - teams { - edge { - memberSince # <= Here's your custom field - node { - teamName: name - } - } - } - } -} -``` - -### Custom Edge classes - -For more robust custom edges, you can define a custom edge class. It will be `obj` in the edge type's resolve function. For example, to define a membership edge: - -```ruby -# Make sure to familiarize yourself with GraphQL::Relay::Edge -- -# you have to avoid naming conflicts here! -class MembershipSinceEdge < GraphQL::Relay::Edge - # Cache `membership` to avoid multiple DB queries - def membership - @membership ||= begin - # "parent" and "node" are passed in from the surrounding Connection, - # See `Edge#initialize` for details - person = self.parent - team = self.node - Membership.where(person: person, team: team).first - end - end - - def member_since - membership.created_at.to_i - end - - def leader? - membership.leader? - end -end -``` - -Then, hook it up with custom edge type and custom connection type: - -```ruby -# Person => Membership => Team -MembershipSinceEdgeType = BaseType.define_edge do - name "MembershipSinceEdge" - field :memberSince, types.Int, "The date that this person joined this team", property: :member_since - field :isPrimary, types.Boolean, "Is this person the team leader?", property: :primary? -end - -TeamMembershipsConnectionType = TeamType.define_connection( - edge_class: MembershipSinceEdge, - edge_type: MembershipSinceEdgeType, - ) do - # Use a name so it doesn't conflict with "TeamConnection" - name "TeamMembershipsConnection" -end -``` - -### Connection objects - -Maybe you need to make a connection object yourself (for example, to return a connection type from a mutation). You can create a connection object like this: - -```ruby -items = [...] # your collection objects -args = {} # stub out arguments for this connection object -connection_class = GraphQL::Relay::BaseConnection.connection_for_nodes(items) -connection_class.new(items, args) -``` - -`.connection_for_nodes` will return RelationConnection or ArrayConnection depending on `items`, then you can make a new connection - -For specifying a connection based on an `ActiveRecord::Relation` or `Sequel::Dataset`: - -```ruby -object = {} # your newly created object -items = [...] # your AR or sequel collection objects -args = {} # stub out arguments for this connection object -items_connection = GraphQL::Relay::RelationConnection.new( - items, - args -) -edge = GraphQL::Relay::Edge.new(object, items_connection) -``` - -Additionally, connections may be provided with the GraphQL::Field that created them. This may be used for custom introspection or instrumentation. For example, - -```ruby - Schema.get_field(TodoListType, "todos") - # => # - context.irep_node.definitions[TodoListType] - # => # - # although this one may not work with fields on interfaces -``` - -### Custom connections - -You can define a custom connection class and add it to `GraphQL::Relay`. - -First, define the custom connection: - -```ruby -class SetConnection < BaseConnection - # derive a cursor from `item` - def cursor_from_node(item) - # ... - end - - private - # apply `#first` & `#last` to limit results - def paged_nodes - # ... - end - - # apply cursor, order, filters, etc - # to get a subset of matching objects - def sliced_nodes - # ... - end -end -``` - -Then, register the new connection with `GraphQL::Relay::BaseConnection`: - -```ruby -# When exposing a `Set`, use `SetConnection`: -GraphQL::Relay::BaseConnection.register_connection_implementation(Set, SetConnection) -``` - -At runtime, `GraphQL::Relay` will use `SetConnection` to expose `Set`s. - -### Creating connection fields by hand - -If you need lower-level access to Connection fields, you can create them programmatically. Given a `GraphQL::Field` which returns a collection of items, you can turn it into a connection field with `ConnectionField.create`. - -For example, to wrap a field with a connection field: - -```ruby -field = GraphQL::Field.new -# ... define the field -connection_field = GraphQL::Relay::ConnectionField.create(field) -``` - -## Mutations - -Mutations allow Relay to mutate your system. They conform to a strict API which makes them predictable to the client. - -## Mutation root - -To add mutations to your GraphQL schema, define a mutation type and pass it to your schema: - -```ruby -# Define the mutation type -MutationType = GraphQL::ObjectType.define do - name "Mutation" - # ... -end - -# and pass it to the schema -MySchema = GraphQL::Schema.define do - query QueryType, - mutation MutationType -end -``` - -Like `QueryType`, `MutationType` is a root of the schema. - -## Mutation fields - -Members of `MutationType` are _mutation fields_. For GraphQL in general, mutation fields are identical to query fields _except_ that they have side-effects (which mutate application state, eg, update the database). - -For Relay-compliant GraphQL, a mutation field must comply to a strict API. `GraphQL::Relay` includes a mutation definition helper (see below) to make it simple. - -After defining a mutation (see below), add it to your mutation type: - -```ruby -MutationType = GraphQL::ObjectType.define do - name "Mutation" - # Add the mutation's derived field to the mutation type - field :addComment, field: AddCommentMutation.field - # ... -end -``` - -## Relay mutations - -To define a mutation, use `GraphQL::Relay::Mutation.define`. Inside the block, you should configure: - - `name`, which will name the mutation field & derived types - - `input_field`s, which will be applied to the derived `InputObjectType` - - `return_field`s, which will be applied to the derived `ObjectType` - - `resolve(->(obj, inputs, ctx) { ... })`, the mutation which will actually happen - - -For example: - -```ruby -AddCommentMutation = GraphQL::Relay::Mutation.define do - # Used to name derived types: - name "AddComment" - - # Accessible from `input` in the resolve function: - input_field :postId, !types.ID - input_field :authorId, !types.ID - input_field :content, !types.String - - # The result has access to these fields, - # resolve must return a hash with these keys - return_field :post, PostType - return_field :comment, CommentType - - # The resolve proc is where you alter the system state. - # `object` is the `root_value:` passed to `Schema.execute`. - resolve ->(object, inputs, ctx) { - post = Post.find(inputs[:postId]) - comment = post.comments.create!(author_id: inputs[:authorId], content: inputs[:content]) - - {comment: comment, post: post} - } -end -``` - -Under the hood, GraphQL creates: - - A field for your schema's `mutation` root - - A derived `InputObjectType` for input values - - A derived `ObjectType` for return values - -The resolve proc: - - Takes `obj`, which is the `root_value:` provided to `Schema.execute` - - Takes `inputs`, which is a hash whose keys are the ones defined by `input_field` - - Takes `ctx`, which is the query context you passed with the `context:` keyword - - Must return a hash with keys matching your defined `return_field`s (unless you provide a `return_type`, see below) - -### Specify a Return Type - -Instead of specifying `return_field`s, you can specify a `return_type` for a mutation. This type will be used to expose the object returned from `resolve`. - -```ruby -CreateUser = GraphQL::Relay::Mutation.define do - return_type UserMutationResultType - # ... - resolve ->(obj, input, ctx) { - user = User.create(input) - # this object will be treated as `UserMutationResultType` - UserMutationResult.new(user, client_mutation_id: input[:clientMutationId]) - } -end -``` - -If you provide your own return type, it's up to you to support `clientMutationId` diff --git a/guides/relay/connections.md b/guides/relay/connections.md new file mode 100644 index 0000000000..c659efb86c --- /dev/null +++ b/guides/relay/connections.md @@ -0,0 +1,274 @@ +--- +title: Relay — Connections +--- + +Relay expresses [one-to-many relationships with _connections_](https://facebook.github.io/relay/docs/graphql-connections.html). Connections support pagination, filtering and metadata in a robust way. + +`graphql-ruby` includes built-in connection support for `Array`, `ActiveRecord::Relation`s, and `Sequel::Dataset`s. You can define custom connection classes to expose other collections with GraphQL. + +### Connection fields + +To define a connection field, use the `connection` helper. For a return type, get a type's `.connection_type`. The `resolve` proc should return a collection (eg, `Array` or `ActiveRecord::Relation`) _without_ pagination. (The connection will paginate the collection). + +For example: + +```ruby +PostType = GraphQL::ObjectType.define do + # `Post#comments` returns an ActiveRecord::Relation + # The GraphQL field returns a Connection + connection :comments, CommentType.connection_type + # `Post#similar_posts` returns an Array + connection :similarPosts, PostType.connection_type, property: :similar_posts + + # ... +end +``` + +You can also define custom arguments and a custom resolve function for connections, just like other fields: + +```ruby +connection :featured_comments, CommentType.connection_type do + # Use a name to disambiguate this from `CommentType.connection_type` + name "CommentConnectionWithSince" + + # Add an argument: + argument :since, types.String + + # Return an Array or ActiveRecord::Relation + resolve -> (post, args, ctx) { + comments = post.comments.featured + + if args[:since] + comments = comments.where("created_at >= ", since) + end + + comments + } +end +``` + +### Maximum Page Size + +You can limit the number of results with `max_page_size:`: + +```ruby +connection :featured_comments, CommentType.connection_type, max_page_size: 50 +``` + +### Connection types + +You can customize a connection type with `.define_connection`: + +```ruby +# Make a customized connection type +PostConnectionWithTotalCountType = PostType.define_connection do + name "PostConnectionWithTotalCount" + field :totalCount do + type types.Int + # - `obj` is the Connection + # - `obj.nodes` is the collection of Posts + resolve -> (obj, args, ctx) { obj.nodes.count } + end +end + +``` + +Now, you can use `PostConnectionWithTotalCountType` to define a connection with the "totalCount" field: + +```ruby +AuthorType = GraphQL::ObjectType.define do + # Use the custom connection type: + connection :posts, PostConnectionWithTotalCountType +end +``` + +This way, you can query your custom fields, for example: + +```graphql +{ + author(id: 1) { + posts { + totalCount # <= Your custom field + } + } +} +``` + +### Custom edge types + +If you need custom fields on `edge`s, you can define an edge type and pass it to a connection: + +```ruby +# Person => Membership => Team +MembershipSinceEdgeType = TeamType.define_edge do + name "MembershipSinceEdge" + field :memberSince, types.Int, "The date that this person joined this team" do + resolve -> (obj, args, ctx) { + obj # => GraphQL::Relay::Edge instance + person = obj.parent + team = obj.node + membership = Membership.where(person: person, team: team).first + membership.created_at.to_i + } + end +end +``` + +Then, pass the edge type when defining the connection type: + +```ruby +TeamMembershipsConnectionType = TeamType.define_connection(edge_type: MembershipSinceEdgeType) do + # Use a name so it doesn't conflict with "TeamConnection" + name "TeamMembershipsConnection" +end +``` + +Now, you can query custom fields on the `edge`: + +```graphql +{ + me { + teams { + edge { + memberSince # <= Here's your custom field + node { + teamName: name + } + } + } + } +} +``` + +### Custom Edge classes + +For more robust custom edges, you can define a custom edge class. It will be `obj` in the edge type's resolve function. For example, to define a membership edge: + +```ruby +# Make sure to familiarize yourself with GraphQL::Relay::Edge -- +# you have to avoid naming conflicts here! +class MembershipSinceEdge < GraphQL::Relay::Edge + # Cache `membership` to avoid multiple DB queries + def membership + @membership ||= begin + # "parent" and "node" are passed in from the surrounding Connection, + # See `Edge#initialize` for details + person = self.parent + team = self.node + Membership.where(person: person, team: team).first + end + end + + def member_since + membership.created_at.to_i + end + + def leader? + membership.leader? + end +end +``` + +Then, hook it up with custom edge type and custom connection type: + +```ruby +# Person => Membership => Team +MembershipSinceEdgeType = BaseType.define_edge do + name "MembershipSinceEdge" + field :memberSince, types.Int, "The date that this person joined this team", property: :member_since + field :isPrimary, types.Boolean, "Is this person the team leader?", property: :primary? +end + +TeamMembershipsConnectionType = TeamType.define_connection( + edge_class: MembershipSinceEdge, + edge_type: MembershipSinceEdgeType, + ) do + # Use a name so it doesn't conflict with "TeamConnection" + name "TeamMembershipsConnection" +end +``` + +### Connection objects + +Maybe you need to make a connection object yourself (for example, to return a connection type from a mutation). You can create a connection object like this: + +```ruby +items = [...] # your collection objects +args = {} # stub out arguments for this connection object +connection_class = GraphQL::Relay::BaseConnection.connection_for_nodes(items) +connection_class.new(items, args) +``` + +`.connection_for_nodes` will return RelationConnection or ArrayConnection depending on `items`, then you can make a new connection + +For specifying a connection based on an `ActiveRecord::Relation` or `Sequel::Dataset`: + +```ruby +object = {} # your newly created object +items = [...] # your AR or Sequel collection +args = {} # stub out arguments for this connection object +items_connection = GraphQL::Relay::RelationConnection.new( + items, + args +) +edge = GraphQL::Relay::Edge.new(object, items_connection) +``` + +Additionally, connections may be provided with the `GraphQL::Field` that created them. This may be used for custom introspection or instrumentation. For example, + +```ruby + Schema.get_field(TodoListType, "todos") + # => # + context.irep_node.definitions[TodoListType] + # => # + # although this one may not work with fields on interfaces +``` + +### Custom connections + +You can define a custom connection class and add it to `GraphQL::Relay`. + +First, define the custom connection: + +```ruby +require "set" # From Ruby's standard library +class SetConnection < BaseConnection + # derive a cursor from `item` + def cursor_from_node(item) + # ... + end + + private + # apply `#first` & `#last` to limit results + def paged_nodes + # ... + end + + # apply cursor, order, filters, etc + # to get a subset of matching objects + def sliced_nodes + # ... + end +end +``` + +Then, register the new connection with `GraphQL::Relay::BaseConnection`: + +```ruby +# When exposing a `Set`, use `SetConnection`: +GraphQL::Relay::BaseConnection.register_connection_implementation(Set, SetConnection) +``` + +At runtime, `GraphQL::Relay` will use `SetConnection` to expose `Set`s. + +### Creating connection fields by hand + +If you need lower-level access to Connection fields, you can create them programmatically. Given a `GraphQL::Field` which returns a collection of items, you can turn it into a connection field with `ConnectionField.create`. + +For example, to wrap a field with a connection field: + +```ruby +field = GraphQL::Field.new +# ... define the field +connection_field = GraphQL::Relay::ConnectionField.create(field) +``` diff --git a/guides/relay/mutations.md b/guides/relay/mutations.md new file mode 100644 index 0000000000..d8d9f6dac8 --- /dev/null +++ b/guides/relay/mutations.md @@ -0,0 +1,132 @@ +--- +title: Relay — Mutations +--- + +Relay uses a [strict mutation API](https://facebook.github.io/relay/docs/graphql-mutations.html#content) for modifying the state of your application. This API makes mutations predictable to the client. + + +## Mutation root + +To add mutations to your GraphQL schema, define a mutation type and pass it to your schema: + +```ruby +# Define the mutation type +MutationType = GraphQL::ObjectType.define do + name "Mutation" + # ... +end + +# and pass it to the schema +MySchema = GraphQL::Schema.define do + query QueryType, + mutation MutationType +end +``` + +Like `QueryType`, `MutationType` is a root of the schema. + +## Mutation fields + +Members of `MutationType` are _mutation fields_. For GraphQL in general, mutation fields are identical to query fields _except_ that they have side-effects (which mutate application state, eg, update the database). + +For Relay-compliant GraphQL, a mutation field must comply to a strict API. `GraphQL::Relay` includes a mutation definition helper (see below) to make it simple. + +After defining a mutation (see below), add it to your mutation type: + +```ruby +MutationType = GraphQL::ObjectType.define do + name "Mutation" + # Add the mutation's derived field to the mutation type + field :addComment, field: AddCommentMutation.field + # ... +end +``` + +## Relay mutations + +To define a mutation, use `GraphQL::Relay::Mutation.define`. Inside the block, you should configure: + - `name`, which will name the mutation field & derived types + - `input_field`s, which will be applied to the derived `InputObjectType` + - `return_field`s, which will be applied to the derived `ObjectType` + - `resolve(-> (object, inputs, ctx) { ... })`, the mutation which will actually happen + +For example: + +```ruby +AddCommentMutation = GraphQL::Relay::Mutation.define do + # Used to name derived types, eg `"AddCommentInput"`: + name "AddComment" + + # Accessible from `input` in the resolve function: + input_field :postId, !types.ID + input_field :authorId, !types.ID + input_field :content, !types.String + + # The result has access to these fields, + # resolve must return a hash with these keys + return_field :post, PostType + return_field :comment, CommentType + + # The resolve proc is where you alter the system state. + resolve -> (inputs, ctx) { + post = Post.find(inputs[:postId]) + comment = post.comments.create!(author_id: inputs[:authorId], content: inputs[:content]) + + # The keys in this hash correspond to `return_field`s above: + { + comment: comment, + post: post, + } + } +end +``` + +## Derived Objects + +`graphql-ruby` uses your mutation to define some members of the schema. Under the hood, GraphQL creates: + +- A field for your schema's `mutation` root, as `AddCommentMutation.field` +- A derived `InputObjectType` for input values, as `AddCommentMutation.input_type` +- A derived `ObjectType` for return values, as `AddCommentMutation.return_type` + +Each of these derived objects maintains a reference to the parent `Mutation` in the `mutation` attribute. So you can access it from the derived object: + +```ruby +# Define a mutation: +AddCommentMutation = GraphQL::Relay::Mutation.define { ... } +# Get the derived input type: +AddCommentMutationInput = AddCommentMutation.input_type +# Reference the parent mutation: +AddCommentMutationInput.mutation +# => # +``` + +## Mutation Resolution + +In the mutation's `resolve` function, it can mutate your application state (eg, writing to the database) and return some results. + +`resolve` is called with: + +- `object`, which is the `root_value:` provided to `Schema.execute` +- `inputs`, which is a hash whose keys are the ones defined by `input_field`. (This value comes from `args[:input]`.) +- `ctx`, which is the query context + +It must return a `hash` whose keys match your defined `return_field`s. (Or, if you specified a `return_type`, you can return an object suitable for that type.) + +### Specify a Return Type + +Instead of specifying `return_field`s, you can specify a `return_type` for a mutation. This type will be used to expose the object returned from `resolve`. + +```ruby +CreateUser = GraphQL::Relay::Mutation.define do + return_type UserMutationResultType + # ... + resolve -> (obj, input, ctx) { + user = User.create(input) + # this object will be treated as `UserMutationResultType` + UserMutationResult.new(user, client_mutation_id: input[:clientMutationId]) + } +end +``` + +If you provide your own return type, it's up to you to support `clientMutationId` diff --git a/guides/relay/object_identification.md b/guides/relay/object_identification.md new file mode 100644 index 0000000000..0f9ff5481d --- /dev/null +++ b/guides/relay/object_identification.md @@ -0,0 +1,91 @@ +--- +title: Relay — Object Identification +--- + +Relay uses [global object identification](https://facebook.github.io/relay/docs/graphql-object-identification.html) support some of its features: + +- __Caching__: Unique IDs are used as primary keys in Relay's client-side cache. +- __Refetching__: Relay uses unique IDs to refetch objects when it determines that its cache is stale. (It uses the `Query.node` field to refetch objects.) + +### Defining UUIDs + +You must provide a function for generating UUIDs and fetching objects with them. In your schema, define `id_from_object` and `object_from_id`: + +```ruby +MySchema = GraphQL::Schema.define do + id_from_object = -> (object, type_definition, query_ctx) { + # Call your application's UUID method here + # It should return a string + MyApp::GlobalId.encrypt(object.class.name, object.id) + } + + object_from_id = -> (id, query_ctx) { + class_name, item_id = MyApp::GlobalId.decrypt(id) + # "Post" => Post.find(id) + Object.const_get(class_name).find(item_id) + } +end +``` + +An unencrypted ID generator is provided in the gem. It uses `Base64` to encode values. You can use it like this: + +```ruby +MySchema = GraphQL::Schema.define do + # Create UUIDs by joining the type name & ID, then base64-encoding it + id_from_object = -> (object, type_definition, query_ctx) { + GraphQL::Schema::UniqueWithinType.encode(type_definition.name, object.id) + } + + object_from_id = -> (id, query_ctx) { + type_name, item_id = GraphQL::Schema::UniqueWithinType.decode(id) + # Now, based on `type_name` and `id` + # find an object in your application + # .... + } +end +``` + +### UUID fields + +To participate in Relay's caching and refetching, objects must do two things: + +- Implement the `"Node"` interface +- Define an `"id"` field which returns a UUID + +To implement the node interface, include `GraphQL::Relay::Node.interface` in your list of interfaces: + +```ruby +PostType = GraphQL::ObjectType.define do + name "Post" + # Implement the "Node" interface for Relay + interfaces [GraphQL::Relay::Node.interface] + # ... +end +``` + +To add a UUID field named `"id"`, use the `global_id_field` helper: + +```ruby +PostType = GraphQL::ObjectType.define do + name "Post" + # ... + # `id` exposes the UUID + global_id_field :id + # ... +end +``` + +Now, `PostType` can participate in Relay's UUID-based features. + +### `node` field (find-by-UUID) + +You should also provide a root-level `node` field so that Relay can refetch objects from your schema. It is provided as `GraphQL::Relay::Node.field`, so you can attach it like this: + +```ruby +QueryType = GraphQL::ObjectType.define do + name "Query" + # Used by Relay to lookup objects by UUID: + field :node, GraphQL::Relay::Node.field + # ... +end +``` diff --git a/guides/code_reuse.md b/guides/schema/code_reuse.md similarity index 93% rename from guides/code_reuse.md rename to guides/schema/code_reuse.md index fa9b8ba70c..e3a1e99752 100644 --- a/guides/code_reuse.md +++ b/guides/schema/code_reuse.md @@ -1,5 +1,5 @@ --- -title: Code Reuse with GraphQL +title: Schema — Code Reuse --- Here are a few techniques for code reuse with graphql-ruby: @@ -34,11 +34,13 @@ module NameResolver end end -# ... - +# use `NameResolver` instead of a proc field :name, types.String do resolve(NameResolver) end +# `resolve: resolver` is equivalent to `do resolve(resolver) end` +# so this is the same: +field :name, types.String, resolve: NameResolver ``` Or, you can pass an instance of a class: @@ -55,7 +57,6 @@ class MethodCallResolver end # ... - field :name, types.String do resolve(MethodCallResolver.new(:name)) end @@ -87,6 +88,7 @@ PostType = GraphQL::ObjectType.define do ... end However, you can call `.define` anytime and store the result anywhere. For example, you can define a method which creates types: ```ruby +# @return [GraphQL::ObjectType] a type derived from `model_class` def create_type(model_class) GraphQL::ObjectType.define do name(model_class.name) @@ -98,8 +100,9 @@ def create_type(model_class) end end +# @return [GraphQL::BaseType] a GraphQL type for `database_type` def convert_type(database_type) - # return a GraphQL type for `database_type` + # ... end ``` diff --git a/guides/schema/configuration_options.md b/guides/schema/configuration_options.md new file mode 100644 index 0000000000..80b8d34acb --- /dev/null +++ b/guides/schema/configuration_options.md @@ -0,0 +1,190 @@ +--- +title: Schema — Configuration Options +--- + +Many things can be added to a GraphQL schema. They fall into a few categories: + +- Data entry points: `query`, `mutation`, `subscription` +- Manually adding types: `orphan_types` +- Execution functions: `resolve_type`, `id_from_object`, `object_from_id` +- Security options: `max_depth`, `max_complexity` +- Middleware: `middleware` +- Query analyzers: `query_analyzer` +- Execution strategies: `query_execution_strategy`, `mutation_execution_strategy`, `subscription_execution_strategy` + +## Data Entry Points + +`query`, `mutation` and `subscriptions` are [root-level](http://graphql.org/learn/schema/#the-query-and-mutation-types) `GraphQL::ObjectType`s. + +```ruby +QueryType = GraphQL::ObjectType.define { ... } +MutationType = GraphQL::ObjectType.define { ... } +SubscriptionType = GraphQL::ObjectType.define { ... } + +GraphQL::Schema.define do + # required + query QueryType + # optional + mutation MutationType + subscription SubscriptionType +end +``` + + +## Orphan Types + +The schema builds its type system by traversing its data entry points. In some cases, types should be present in the schema but aren't available via traversal, so you have to add them yourself. + +The clearest case of this is when a type implements an interface, but isn't a return type of any other field. Since it's not the return type of a field, it might not be found by traversal, so you can add it in `orphan_types`: + +```ruby +GraphQL::Schema.define do + # ... + # Make sure these types are present in the schema: + orphan_types [AudioType, VideoType, ImageType] +end +``` + +It's OK to add a type to `orphan_types` even if it's already in the schema. + +## Execution Functions + +During execution, a GraphQL schema may need help from you, which you can provide in these hooks: + +- `resolve_type(obj, ctx)`: When we have a member of an interface or union, which object type should we use? +- `id_from_object(object, type, ctx)` (Relay only): Generate a unique ID for `object` +- `object_from_id(id, ctx)` (Relay only): Given a unique ID `id`, return the object which it identifies + +These hooks are provided as objects that respond to `#call`, for example, a `Proc` literal: + +```ruby +GraphQL::Schema.define do + # Hooks for query execution: + resolve_type -> (obj, ctx) { ... } + id_from_object -> (obj, type, ctx) { ... } + object_from_id -> (id, ctx) { ... } +end +``` + +See ["Object Identification"]({{ site.baseurl }}/relay/object_identification) for more information about Relay IDs. + +## Security Options + +These options can prevent execution of malicious queries. + +```ruby +GraphQL::Schema.define do + # Prevent excessively deep or complex queries + max_depth 8 + max_complexity 120 +end +``` + +See ["Security"]({{ site.baseurl }}/queries/security) for more information. + +## Middleware + +You can use _middleware_ to affect the evaluation of fields in your schema. They function like `before_action`s and `after_action`s in Rails controllers. + +A middleware is any object that responds to `#call(*args, next_middleware)`. Inside that method, it should either: + +- send `call` to the next middleware to continue the evaluation; or +- return a value to end the evaluation early. + +Middlewares' `#call` is invoked with several arguments: + +- `parent_type` is the type whose field is being accessed +- `parent_object` is the object being exposed by that type +- `field_definition` is the definition for the field being accessed +- `field_args` is the hash of arguments passed to the field +- `query_context` is the context object passed throughout the query +- `next_middleware` represents the execution chain. Call `#call` to continue evalution. + +Add a middleware to a schema by adding to the `#middleware` array. + +### Example: Authorization + +This middleware only continues evaluation if the `current_user` is permitted to read the target object: + +```ruby +class AuthorizationMiddleware + def call(parent_type, parent_object, field_definition, field_args, query_context, next_middleware) + current_user = query_context[:current_user] # passed in when creating the query + if current_user && current_user.can_read?(parent_object) + # This user is authorized, so continue execution + next_middleware.call + else + # Silently halt execution + nil + end + end +end +``` + +Then, add the middleware to your schema: + +```ruby +GraphQL::Schema.define do + middleware AuthorizationMiddleware.new +end +``` + +Now, all field access will be wrapped by that authorization routine. + +## Query Analyzers + +Query analyzers are like middleware for the validation phase. They're called at each node of the query's internal representation (see `GraphQL::InternalRepresentation::Node`). If they return a `GraphQL::AnalysisError` (or an array of those errors), the query won't be run and the error will be added to the response's `errors` key. + +The minimal API is `.call(memo, visit_type, internal_representation_node)`. For example: + +```ruby +ast_node_logger = -> (memo, visit_type, internal_representation_node) { + if visit_type == :enter + puts "Visiting #{internal_representation_node.name}!" + end +} +MySchema.query_analyzers << ast_node_logger +``` + +Whatever `.call(...)` returns will be passed as `memo` for the next visit. + +The analyzer can implement a few __other methods__. If they're present, they'll be called: + +- `.initial_value(query)` will be called to generate an initial value for `memo` +- `.final_value(memo)` will be called _after_ visiting the the query + +If the last value of `memo` (or the return of `.final_value`) is a `GraphQL::AnalysisError`, the query won't be executed and the error will be added to the `errors` key of the response. + +`graphql-ruby` includes a few query analyzers: + +- `GraphQL::Analysis::QueryDepth` and `GraphQL::Analysis::QueryComplexity` for inspecting query depth and complexity +- `GraphQL::Analysis::MaxQueryDepth` and `GraphQL::Analysis::MaxQueryComplexity` are used internally to implement `max_depth:` and `max_complexity:` options + +## Execution Strategies + +`graphql` includes a serial execution strategy, but you can also create custom strategies to support advanced behavior. See `GraphQL::SerialExecution#execute` the required behavior. + +Then, set your schema to use your custom execution strategy with `GraphQL::Schema#{query|mutation|subscription}_execution_strategy` + +For example: + +```ruby +class CustomQueryExecutionStrategy + def initialize + # ... + end + + def execute(operation_name, root_type, query) + # ... + end +end + +# ... define your types ... + +MySchema = GraphQL::Schema.define do + query MyQueryType + mutation MyMutationType + # Use your custom strategy: + query_execution_strategy CustomQueryExecutionStrategy +end +``` diff --git a/guides/testing.md b/guides/schema/testing.md similarity index 92% rename from guides/testing.md rename to guides/schema/testing.md index 15e8aefeb5..5999c947e3 100644 --- a/guides/testing.md +++ b/guides/schema/testing.md @@ -1,5 +1,5 @@ --- -title: Testing a GraphQL Schema +title: Schema — Testing --- There are a few ways to test the behavior of your GraphQL schema: @@ -8,7 +8,6 @@ There are a few ways to test the behavior of your GraphQL schema: - Test schema elements (types, fields) in isolation - Execute GraphQL queries and test the result - ## Don't test the schema The easiest way to test behavior of a GraphQL schema is to extract behavior into separate objects and test those objects in isolation. For Rails, you don't test your models by running controller tests, right? Similarly, you can test "lower-level" parts of the system on their own without running end-to-end tests. @@ -100,7 +99,7 @@ post.fields # => {"id" => , ... } post.fields.keys # => ["id", "title", "body", "author", "comments"] ``` -The returned value is an instance of the type class you used to `.define` it (eg, `GraphQL::ObjectType`, `GraphQL::EnumType`, `GraphQL::InputObjectType`). +The returned value of `Schema#types[type_name]` is an instance of the type class you used to `.define` it (eg, `GraphQL::ObjectType`, `GraphQL::EnumType`, `GraphQL::InputObjectType`). #### Fields @@ -130,7 +129,7 @@ Similarly, you can access: - `GraphQL::Field#type`, the field's return type - `GraphQL::InputObjectType#arguments`, which are `String` => `GraphQL::Argument` pairs - `GraphQL::EnumType#values`, which are `String` => `GraphQL::EnumType::EnumValue` pairs -- `GraphQL::InterfaceType#possible_types` and `GraphQL::UnionType#possible_types`, which are lists of types. +- `GraphQL::Schema#possible_types(type_defn)`, which returns the possible types for union or interface types in a given schema `GraphQL::BaseType#unwrap` may also be helpful. It returns the "inner-most" type. For example: @@ -183,10 +182,9 @@ describe MySchema do context "when there's a current user" do # override `context` - let(:context) {{ - current_user: User.new(name: "ABC") - }} - + let(:context) { + { current_user: User.new(name: "ABC") } + } it "shows the user's name" do user_name = result["data"]["viewer"]["name"] expect(user_name).to eq("ABC") diff --git a/guides/schema/types_and_fields.md b/guides/schema/types_and_fields.md new file mode 100644 index 0000000000..f3416ec4f8 --- /dev/null +++ b/guides/schema/types_and_fields.md @@ -0,0 +1,121 @@ +--- +title: Schema — Types and Fields +--- + +Types, fields and arguments make up a schema's type system. + +## Types + +Types describe objects and values in a system. The API documentation for each type contains a detailed description with examples. + +Objects are described with {{ "GraphQL::ObjectType" | api_doc }}s. + +Scalar values are described with built-in scalars (string, int, float, boolean, ID) or custom {{ "GraphQL::EnumType" | api_doc }}s. You can define custom {{ "GraphQL::ScalarType" | api_doc }}s, too. + +Scalars and enums can be sent to GraphQL as inputs. For complex inputs (key-value pairs), use {{ "GraphQL::InputObjectType" | api_doc }}. + +There are two abstract types, too: + +- {{ "GraphQL::InterfaceType" | api_doc }} describes a collection of object types which implement some of the same fields. +- {{ "GraphQL::UnionType" | api_doc }} describes a collection of object types which may appear in the same place in the schema (ie, may be returned by the same field.) + + +{{ "GraphQL::ListType" | api_doc }} and {{ "GraphQL::NonNullType" | api_doc }} modify other types, describing them as "list of _T_" or "required _T_". + +## Fields + +{{ "GraphQL::ObjectType" | api_doc }}s and {{ "GraphQL::InterfaceType" | api_doc }}s may expose their values with _fields_. A field definition looks like this: + +```ruby +PostType = GraphQL::ObjectType.define do + # ... + # name , type , description (optional) + field :title, types.String, "The title of the Post" +end +``` + +By default, fields are resolved by sending the name to the underlying object (eg `post.title` in the example above). + +You can define a different resolution by providing a `resolve` function: + +```ruby +PostType = GraphQL::ObjectType.define do + # ... + # name , type , description (optional) + field :teaser, types.String, "The teaser of the Post" do + # how to get the value? + resolve -> (obj, args, ctx) { + # first 40 chars of the body + obj.body[0, 40] + } + end +end +``` + +The resolve function receives inputs: + +- `object`: The underlying object for this type (above, a `Post` instance) +- `arguments`: The arguments for this field (see below, a {{ "GraphQL::Query::Arguments" | api_doc }} instance) +- `context`: The context for this query (see ["Executing Queries"]({{ site.baseurl }}/queries/executing_queries), a {{ "GraphQL::Query::Context" | api_doc }} instance) + +In fact, the `field do ... end` block is passed to {{ "GraphQL::Field" | api_doc }}'s `.define` method, so you can define many things there: + +```ruby +field do + name "teaser" + type types.String + description "..." + resolve -> (obj, args, ctx) { ... } + deprecation_reason "Too long, use .title instead" + complexity 2 +end +``` + +## Arguments + +Fields can take __arguments__ as input. These can be used to determine the return value (eg, filtering search results) or to modify the application state (eg, updating the database in `MutationType`). + +Arguments are defined with the `argument` helper: + +```ruby +field :search_posts, types[PostType] do + argument :category, types.String + resolve -> (obj, args, ctx) { + args[:category] + # => maybe a string, eg "Programming" + if args[:category] + Post.where(category: category).limit(10) + else + Post.all.limit(10) + end + } +end +``` + +Use `!` to mark an argument as _required_: + +```ruby +# This argument is a required string: +argument :category, !types.String +``` + +Only certain types are valid for arguments: + +- {{ "GraphQL::ScalarType" | api_doc }}, including built-in scalars (string, int, float, boolean, ID) +- {{ "GraphQL::EnumType" | api_doc }} +- {{ "GraphQL::InputObjectType" | api_doc }}, which allows key-value pairs as input +- {{ "GraphQL::ListType" | api_doc }}s of a valid input type +- {{ "GraphQL::NonNullType" | api_doc }}s of a valid input type + + +The `args` parameter of a `resolve` function will always be a `{{ "GraphQL::Query::Arguments" | api_doc }}`. You can access specific arguments with `["arg_name"]` or `[:arg_name]`. You recursively turn it into a Ruby Hash with `to_h`. Inside `args`, scalars will be parsed into Ruby values and enums will be converted to their `value:` (if one was provided). + +```ruby +resolve -> (obj, args, ctx) { + args["category"] == args[:category] + # => true + args.to_h + # => { "category" => "Programming" } + # ... +} +``` diff --git a/readme.md b/readme.md index 2e633ce796..ea4be54160 100644 --- a/readme.md +++ b/readme.md @@ -24,63 +24,9 @@ gem 'graphql' $ bundle install ``` -## Overview +## Getting Started -#### Declare types & build a schema - -```ruby -# Declare a type... -PostType = GraphQL::ObjectType.define do - name "Post" - description "A blog post" - - field :id, !types.ID - field :title, !types.String - field :body, !types.String - field :comments, types[!CommentType] -end - -# ...and a query root -QueryType = GraphQL::ObjectType.define do - name "Query" - description "The query root of this schema" - - field :post do - type PostType - argument :id, !types.ID - resolve ->(obj, args, ctx) { Post.find(args["id"]) } - end -end - -# Then create your schema -Schema = GraphQL::Schema.define do - query QueryType - max_depth 8 -end -``` - -#### Execute queries - -Execute GraphQL queries on a given schema, from a query string. - -```ruby -result_hash = Schema.execute(query_string) -# { -# "data" => { -# "post" => { -# "id" => 1, -# "title" => "GraphQL is nice" -# } -# } -# } -``` - -#### Use with Relay - -If you're building a backend for [Relay](http://facebook.github.io/relay/), you'll need: - -- A JSON dump of the schema, which you can get by sending [`GraphQL::Introspection::INTROSPECTION_QUERY`](https://github.com/rmosolgo/graphql-ruby/blob/master/lib/graphql/introspection/introspection_query.rb) -- Relay-specific helpers for GraphQL, see [`GraphQL::Relay`](http://www.rubydoc.info/github/rmosolgo/graphql-ruby/file/guides/relay.md) +See "Getting Started" on the [website]((https://rmosolgo.github.io/graphql-ruby/) or on [GitHub](https://github.com/rmosolgo/graphql-ruby/blob/master/guides/index.md) ## Goals @@ -94,23 +40,7 @@ If you're building a backend for [Relay](http://facebook.github.io/relay/), you' - __Report bugs__ by posting a description, full stack trace, and all relevant code in a [GitHub issue](https://github.com/rmosolgo/graphql-ruby/issues). - __Features & patches__ are welcome! Consider discussing it in an [issue](https://github.com/rmosolgo/graphql-ruby/issues) or in the [#ruby channel on Slack](https://graphql-slack.herokuapp.com/) to make sure we're on the same page. - __Run the tests__ with `rake test` or start up guard with `bundle exec guard`. -- __Build the site__ with `rake site:serve`, then visit `localhost:4000`. - -## Related Projects - -### Code - -- `graphql-ruby` + Rails demo ([src](https://github.com/rmosolgo/graphql-ruby-demo) / [heroku](http://graphql-ruby-demo.herokuapp.com)) -- [`graphql-batch`](https://github.com/shopify/graphql-batch), a batched query execution strategy -- [`graphql-libgraphqlparser`](https://github.com/rmosolgo/graphql-libgraphqlparser-ruby), bindings to [libgraphqlparser](https://github.com/graphql/libgraphqlparser), a C-level parser. - -### Blog Posts - -- Building a blog in GraphQL and Relay on Rails [Introduction](https://medium.com/@gauravtiwari/graphql-and-relay-on-rails-getting-started-955a49d251de), [Part 1]( https://medium.com/@gauravtiwari/graphql-and-relay-on-rails-creating-types-and-schema-b3f9b232ccfc), [Part 2](https://medium.com/@gauravtiwari/graphql-and-relay-on-rails-first-relay-powered-react-component-cb3f9ee95eca) -- https://medium.com/@khor/relay-facebook-on-rails-8b4af2057152 -- https://blog.jacobwgillespie.com/from-rest-to-graphql-b4e95e94c26b#.4cjtklrwt -- http://mgiroux.me/2015/getting-started-with-rails-graphql-relay/ -- http://mgiroux.me/2015/uploading-files-using-relay-with-rails/ +- __Build the site__ with `rake site:serve`, then visit `http://localhost:4000/graphql-ruby/`. ## To Do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e52d06a9d6..e188e8c38f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -8,8 +8,6 @@ require "minitest/autorun" require "minitest/focus" require "minitest/reporters" -require "pry" -require 'pry-stack_explorer' Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new Minitest::Spec.make_my_diffs_pretty!