Easy GraphQL
We are using standard graphql ruby gem for our GraphQL implementation. It is a Ruby implementation of Facebook's GraphQL. https://graphql-ruby.org/getting_started
Implementation in Easy trying follow best practices and standards. We are using graphql gem for schema definition and graphiql-rails for development and testing.
We are using own schema EasyGraphql for all our GraphQL queries and mutations. This schema is defined in easy_extensions/app/api/easy_graphql/app_schema.rb directory in easy_extensions engines.
Lets introduce some our basics:
Types
https://graphql-ruby.org/guides#type-definitions-guides
Our types are in folder */app/api/easy_graphql/types. All types are inherited from Types::BaseObject. We are using Types::Base for all our types. This class is defined in easy_extensions/app/api/easy_graphql/types/base.rb.
Types are used for defining the structure of the data that can be queried. We are using field method for defining fields of the type. We are using argument method for defining arguments of the type.
[!IMPORTANT] Every new type should inherit from
Base!
ID vs. Int vs. Integer
https://graphql-ruby.org/type_definitions/scalars.html
We are using ID for all our primary keys and relations. We are using Integer for all our integer values.
Do not use Int type, we are prefer Integer type instead for "integer" values.
Enums
https://graphql-ruby.org/type_definitions/enums.html
Enums are used for defining a type with a limited set of possible values. We are using enum method for defining enums. We are using Types::BaseEnum for all our enums. This class is defined in easy_extensions/app/api/easy_graphql/types/base_enum.rb.
Enum represent one field => so use singular name for enum. For example IssueStatus or IssueCategory.
Inputs (arguments)
https://graphql-ruby.org/type_definitions/input_objects.html
Inputs are subset of types. They are used for defining arguments of the type. We are using argument method for defining arguments of the type. In Easy we are using Types::BaseInput for all our inputs. This class is defined in easy_extensions/app/api/easy_graphql/types/base_input.rb. Input represents a hash of entity attributes, primarily for Mutations.
Place inputs into */app/api/easy_graphql/types/inputs directory. Name convention of input is model_name_attributes.rb.
[!WARNING]
_attrubtessuffix is used to distinguish input from type.
Best-practice
- Use
fieldmethod for defining fields of the type. - Use
argumentmethod for defining fields of the type. Do not forget specifyrequiredoption for argument. - Use
enummethod for defining enums. - Use
Types::Basefor all our types. - Use
Types::BaseEnumfor all our enums. - Use
IDfor all our primary keys and relations. - Use
Integerfor all our integer values. Do not useIntshortcut.
Resolvers
https://graphql-ruby.org/fields/resolvers.html
Resolvers are used for defining how to fetch the data. We are using argument method for defining params of the type. We are using resolve method for defining how to fetch the data.
We are using Resolvers::Base for all our resolvers. This class is defined in easy_extensions/app/api/easy_graphql/resolvers/base.rb.
Place resolvers into */app/api/easy_graphql/resolvers directory. Name convention of resolver should describe "what resolve does" OR if its fetch entity, just use entity name in plural. For example Issues or MarkAsRead.
Best-practice
- Use
resolvemethod for fetching data. - Use
typemethod for defining return type. - Use
argumentmethod for defining fields of the type. Do not forget specifyrequiredoption for argument. - If you find specify entity, use
findmethod for fetching entity. It raiseActiveRecord::RecordNotFoundif entity not found = This is correct. - Do not forget to check permissions for visibility.
- Prefer use
argumentsmethod for accessing arguments.
Example
Mutations
https://graphql-ruby.org/mutations/mutation_classes.html
Mutations are used for defining how to change the data. We are using argument method for defining arguments of the type. We are using resolve method for defining how to change the data. We are using field method for defining fields of the type (return data of mutation).
We are using Mutations::Base for all our mutations. This class is defined in easy_extensions/app/api/easy_graphql/mutations/base.rb.
Place mutations into */app/api/easy_graphql/mutations directory. Name convention of mutation should describe "what mutation does" OR if its change entity, just use entity name with action prefix. For example CreateIssue or UpdateIssue or DestroyIssue.
[!CAUTION] NEVER ever do more actions in one mutations.
Its forbidden to combine
createandupdatein one mutation ! Always use separate mutations for each logical action.
Error handling
https://graphql-ruby.org/mutations/mutation_classes.html#error-handling
We are using Types::Errors for all our errors. This class is defined in easy_extensions/app/api/easy_graphql/types/error.rb. This class is used for defining errors of the mutation. Define errors on your mutation as new field:
[!WARNING]
_errorsmust be always array ofTypes::Error!
In mutation there is method error which return Errors object for store validation errors. (see easy_engines/easy_extensions/app/api/easy_graphql/mutations/errors.rb)
Object have public methods:
So when you are updating standard ActiveRecord object in mutation, use
entity.init_journal(User.current, "My changes")
errors.add_entity_errors(entity) unless entity.save
{ entity:, errors: errors }
Best-practice
- Use
prefixin mutation name for action. (likeCreate,Update,Destroy) - Use
resolvemethod for changing data. - Use
fielderrors aside your returns. - Always return
errorsfield. If there is no error, return empty array. - Use
argumentmethod for defining fields of the type. Do not forget specifyrequiredoption for argument. - If you find specify entity, use
findmethod for fetching entity. It raiseActiveRecord::RecordNotFoundif entity not found = This is correct. - Always authorize manipulation with object If actor has not permission raise
Unauthorizedexception. - Do not forget
init_journalif entity is journalized. - Use
safe_attributesfor assign attributes to entity. - If you create new entity and it's not valid, return
errorsandnilfor entity.
Mutation Example
Delete Mutations
Delete mutations are special case of mutations. Their response slightly differs from other mutations. Best practice often suggest that, this kind of mutation should return deleted object, or at least its ID.
In Easy we are using graphql to support our front-end solutions, instead of official API. So we are trying to keep mutations response as simple as possible, to minimize possibility of failure. If front end does not need the object or its ID we just return errors to inform front-end about the result of the operation.
Error handling is also slightly different. While action in other mutations should check validation errors, in ActiveRecord destroy method will success even with invalid object.
Failed destroy will raise ActiveRecord::RecordNotDestroyed exception. This exception must be handled and returned like validation error to front-end.
Additional Best-practice
- Return object or its ID only if necessary.
- To indicate success or failure use
errorsfield. - Destroy method doesn't rely on object validity. Invalid object will be destroyed as well.
- Use
ActiveRecord::RecordNotDestroyedexception for error handling. Return exception message as error message of attribute:base.
Delete Mutation Example
Subscriptions
https://graphql-ruby.org/subscriptions/subscription_classes.html
Subscriptions could be considered as specific type of Resolvers. They are supposed to fetch data resolver-like, but the main difference is in the way of fetching the data.
Resolvers are used to fetch data on demand by client side of application. Subscriptions on the other hand, are just initialized by client side of application as channel that provides information when data change. Data fetching part is triggered on server side, when specific event occurs (trigger is called).
We are using Subscriptions::Base to inherit for all our subscriptions. This class is defined in easy_extensions/app/api/easy_graphql/subscriptions/base.rb.
Place subscriptions into */app/api/easy_graphql/subscriptions directory. Name convention of subscription should describe triggering event and associated object in pattern: object_event, e.g.: IssueUpdated.
Subscription classes do not use own implementation of resolve method, but subscribe and update methods instead. Method resolve is implemented within graphql gem and its role is to decide what method should be called, subscribe or update.
Subscription use arguments to define inputs and these arguments are necessary to determine which subscription(s) should be updated when trigger is called.
Thanks to similarity with resolvers, we can use either type method to define output type, or we can use field as well to define fields of subscriptions.
Subscribe
Method subscribe is executed when client side of application initializes subscription. Basically, it is executed only for the first time we touch the subscription.
By default this method does no response and just establishes the channel. It can be overriden to provide some initial data for client side of application.
Update
Method update is executed when server side of application triggers the subscription. It is executed every time the trigger is called and fires data to the client side.
By default this method returns object passed to trigger method. It can be overriden as well to execute custom logic.
Trigger
https://graphql-ruby.org/subscriptions/triggers
Trigger is the way we notify client side of application about expected event has occurred. Trigger method has three mandatory arguments:
event_name, object and arguments and two optional arguments: scope and context.
Combination of event_name and arguments is used to determine which subscription(s) should be updated.
To determine correct subscription(s) with event_name, we have to pass exactly the same arguments as were used when subscribe was executed. In case of multiple arguments, all of them have to match.
Argument object is passed to update method of subscription and by default it is fired to client side. If we stick to the default implementation of update method, object have to match output type of subscription.
EasyGraphql::AppSchema.subscriptions.trigger(event_name, argumnents, object, scope:, context:)
Best-practice
- Use
typefor definition of subscription response if subscription returns ActiveRecord object - Use
subscribemethod to define initial subscription response. - Try to avoid overriding
updatemethod and pass data throughobjectmethod. - Use
argumentsto define inputs for subscriptions. To determine correct subscription all arguments have to match first call. Do not forget specifyrequiredoption for argument where needed. - Subscription should fetch data, not change them.
- Use
findmethod for fetching entity. It raiseActiveRecord::RecordNotFoundif entity not found = This is correct. - Avoid
fielderrors if possible.