GraphQL types, queries and mutations¶
Easy8 uses the graphql-ruby gem. All queries, mutations and subscriptions belong to one schema, EasyGraphql::AppSchema (app/api/easy_graphql/app_schema.rb). graphiql-rails is mounted at /graphiql for trying queries.
Where the code goes¶
Core classes live in app/api/easy_graphql/. A plugin keeps its own classes in <plugin>/app/api/easy_graphql/ using the same sub-folders (types, resolvers, mutations, subscriptions). The legacy <plugin>/api/ folder is deprecated.
A plugin adds its fields to the core root types with EasyGraphql.register_patch. Require the file from a plugin initialiser, for example config/initializers/easy_api.rb:
| lib/my_plugin/easy_graphql.rb | |
|---|---|
The same call extends an existing type, for example EasyGraphql.register_patch("EasyGraphql::Types::Issue") with an extra field. EasyGraphql.patch still works but is deprecated.
After you change the schema, regenerate the frontend schema and types: see GraphQL services and definitions.
Types¶
https://graphql-ruby.org/guides#type-definitions-guides
Types live in app/api/easy_graphql/types and inherit from EasyGraphql::Types::Base (types/base.rb). Declare data with field and filter arguments with argument.
ID vs. Int vs. Integer¶
https://graphql-ruby.org/type_definitions/scalars.html
Use ID for primary keys and relations. Use Integer for other integer values.
Enums¶
https://graphql-ruby.org/type_definitions/enums.html
Enums inherit from GraphQL::Schema::Enum and live in types/enums, for example EasyGraphql::Types::Enums::SortOrder. Name an enum in the singular, for example IssueStatus.
Inputs (arguments)¶
https://graphql-ruby.org/type_definitions/input_objects.html
An input is a hash of entity attributes, used mostly by mutations. Inputs inherit from EasyGraphql::Types::BaseInput (types/base_input.rb) and live in types/inputs. Name the file model_name_attributes.rb; the _attributes suffix distinguishes an input from a type.
Best-practice¶
- Inherit every type from
Types::Base. - Set the
requiredoption on everyargument. - Use
IDfor primary keys and relations,Integerfor other integers.
Resolvers¶
https://graphql-ruby.org/fields/resolvers.html
A resolver fetches data. Inherit from EasyGraphql::Resolvers::Base (resolvers/base.rb), declare parameters with argument, the return type with type, and fetch the data in resolve.
Place resolvers in app/api/easy_graphql/resolvers. Name a resolver after what it does, or after the entity in plural when it fetches entities, for example Issues or MarkAsRead.
Best-practice¶
- Set the
requiredoption on everyargumentand read values througharguments. - Fetch a single entity with
find.ActiveRecord::RecordNotFoundfor a missing record is the correct outcome. - Fetch only records the current user may see, for example through the
visiblescope.
Example¶
Mutations¶
https://graphql-ruby.org/mutations/mutation_classes.html
A mutation changes data. Inherit from EasyGraphql::Mutations::Base (mutations/base.rb), declare inputs with argument, the returned data with field, and do the change in resolve.
Place mutations in app/api/easy_graphql/mutations. Name a mutation after what it does, or after the entity with an action prefix, for example CreateIssue, UpdateIssue or DeleteIssue.
[!CAUTION] One mutation does one action. Never combine
createandupdatein one mutation.
Error handling¶
https://graphql-ruby.org/mutations/mutation_classes.html#error-handling
Errors use EasyGraphql::Types::Error (types/error.rb). Define errors on your mutation as a field:
errors must always be an array of Types::Error.
Inside a mutation, errors returns an EasyGraphql::Mutations::Errors object (mutations/errors.rb) with two methods:
To save an ActiveRecord object in a mutation:
entity.init_journal(User.current, "My changes")
errors.add_entity_errors(entity) unless entity.save
{ entity:, errors: }
Best-practice¶
- Prefix the mutation name with the action (
Create,Update,Delete). - Always return the
errorsfield. Return an empty array when there is no error. - Set the
requiredoption on everyargument. - Fetch an existing entity with
findon avisiblescope.ActiveRecord::RecordNotFoundfor a missing record is the correct outcome. - Authorise every change. When the user lacks permission, raise
Unauthorized. - Call
init_journalwhen the entity is journalised. - Assign attributes with
safe_attributes=. - When a new entity is invalid, return
errorsandnilfor the entity.
Mutation example¶
Delete mutations¶
A delete mutation returns the least it can. Return the deleted object or its ID only when the frontend needs it; otherwise return errors alone, which tells the frontend whether the operation succeeded.
ActiveRecord#destroy succeeds even for an invalid object. A failed destroy raises ActiveRecord::RecordNotDestroyed: rescue it and return its message as an error on attribute :base.
Delete mutation example¶
Subscriptions¶
https://graphql-ruby.org/subscriptions/subscription_classes.html
A subscription is a channel the client opens once; the server then pushes data whenever a trigger fires. Inherit from EasyGraphql::Subscriptions::Base (subscriptions/base.rb) and place the class in app/api/easy_graphql/subscriptions. Name it after the object and the event, for example IssueUpdated.
Implement subscribe (runs once, when the client subscribes; may return initial data) and, only when needed, update (runs on every trigger; by default returns the triggered object). Do not override resolve.
Setup, registration, triggering, frontend and testing are described in Real-time updates.