README.md in graphql-relay-0.6.0 vs README.md in graphql-relay-0.6.1

- old
+ new

@@ -17,63 +17,287 @@ bundle install ``` ## Usage +`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 provide refetching & global identification for Relay. +Global ids (or UUIDs) provide refetching & global identification for Relay. -You should create `GraphQL::Relay::GlobalNodeIdentification` helper by defining `object_from_id(global_id)` & `type_from_object(object)`. The resulting object provides ID resultion methods, a find-by-global-id field and a node interface. [Example](https://github.com/rmosolgo/graphql-relay-ruby/blob/master/spec/support/star_wars_schema.rb#L9-L18) +#### UUID Lookup -ObjectTypes should implement that interface with the `global_id_field` helper: [Example](https://github.com/rmosolgo/graphql-relay-ruby/blob/master/spec/support/star_wars_schema.rb#L30-L31) +Use `GraphQL::Relay::GlobalNodeIdentification` helper by defining `object_from_id(global_id, ctx)` & `type_from_object(object)`. The resulting `NodeIdentification` object is in your schema _and_ internally by `GraphQL::Relay`. -You should attach the field to your query type: [Example](https://github.com/rmosolgo/graphql-relay-ruby/blob/master/spec/support/star_wars_schema.rb#L121) +```ruby +NodeIdentification = GraphQL::Relay::GlobalNodeIdentification.define do + # Given a UUID & the query context, + # return the corresponding application object + object_from_id -> (id, ctx) do + type_name, id = NodeIdentification.from_global_id(id) + # "Post" -> Post.find(id) + Object.const_get(type_name).find(id) + end + + # Given an application object, + # return a GraphQL ObjectType to expose that object + type_from_object -> (object) do + if object.is_a?(Post) + PostType + else + CommentType + end + end +end +``` + +#### UUID fields + +ObjectTypes in your schema should implement `NodeIdentification.interface` with the `global_id_field` helper, for example: + +```ruby +PostType = GraphQL::ObjectType.define do + name "Post" + interfaces [NodeIdentification.interface] + # `id` exposes the UUID + global_id_field :id + + # ... +end +``` + +#### `node` field (find-by-UUID) + +You should also add a field to your root query type for Relay to re-fetch objects: + +```ruby +QueryType = GraphQL::ObjectType.define do + name "Query" + # Used by Relay to lookup objects by UUID: + field :node, field: NodeIdentification.field + + # ... +end +``` + ### Connections -Connections will provide arguments, pagination and `pageInfo` for `Array`s or `ActiveRecord::Relation`s. You can use the `connection` definition helper. +Connections provide pagination and `pageInfo` for `Array`s or `ActiveRecord::Relation`s. -Then, implement the field. It's different than a normal field: - - use the `connection` helper to define it, instead of `field` - - Call `#connection_type` on an `ObjectType` for the field's return type (eg, `ShipType.connection_type`) +#### Connection fields -Examples: +To define a connection field, use the `connection` helper. For a return type, get a type's `.connection_type`. For example: -- [Connection with custom arguments](https://github.com/rmosolgo/graphql-relay-ruby/blob/master/spec/support/star_wars_schema.rb#L51-L63) -- [Connection with a different name than the underlying property](https://github.com/rmosolgo/graphql-relay-ruby/blob/master/spec/support/star_wars_schema.rb#L77) +```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 } -You can also add custom fields to connection objects: [Example](https://github.com/rmosolgo/graphql-relay-ruby/blob/master/spec/support/star_wars_schema.rb#L36-L43) + # ... +end +``` -At runtime, `graphql-relay` chooses a connection implementation to expose a collection. `graphql-relay` provides `ArrayConnection` and `RelationConnection`, but you can also provide your own with `BaseConnection.register_connection_implementation`. +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 + # 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 +``` + +#### Connection types + +You can customize a connection type with `.define_connection`: + +```ruby +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, `PostType.connection_type` will include a `totalCount` field. + +#### 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: + +``` +items = ... # your collection objects +args = {} # stub out arguments for this connection object +connection_class = GraphQL::Relay::BaseConnection.connection_for_items(items) +connection_class.new(items, args) +``` + +`.connection_for_items` will return RelationConnection or ArrayConnection depending on `items`, then you can make a new connection + +#### 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` + # (it is used to find next & previous nodes, + # so it should include `order`) + 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. When you define a mutation, you'll be defining: - - A field for your schema's `mutation` root - - A derived `InputObjectType` for input values - - A derived `ObjectType` for return values +Mutations allow Relay to mutate your system. They conform to a strict API which makes them predictable to the client. -You _don't_ define anything having to do with `clientMutationId`. That's automatically created. +### 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.new( + query: QueryType, + mutation: MutationType +) +``` + +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(-> (inputs, ctx))`, the mutation which will actually happen + - `resolve(-> (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. + resolve -> (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 `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 -Examples: - - Definition: [example](https://github.com/rmosolgo/graphql-relay-ruby/blob/master/spec/support/star_wars_schema.rb#L90) - - Mount on mutation type: [example](https://github.com/rmosolgo/graphql-relay-ruby/blob/master/spec/support/star_wars_schema.rb#L127) ## Todo -- Show how to wrap a simple field in a connection field with `ConnectionField.create` - Add a `max_page_size` config for connections? - Refactor some RelationConnection issues: - fix [unbounded count in page info](https://github.com/rmosolgo/graphql-relay-ruby/blob/88b3d94f75a6dd4c8b2604743108db31f66f8dcc/lib/graphql/relay/base_connection.rb#L79-L86), [details](https://github.com/rmosolgo/graphql-relay-ruby/issues/1) ## More Resources