Upgrade to Pro — share decks privately, control downloads, hide ads and more …

GraphQL & Ruby

GraphQL & Ruby

A gentle introduction to GraphQL, and creating a GraphQL service in Ruby. Given at the August 2017 meeting of the London Ruby Users Group.

Darren Oakley

August 14, 2017
Tweet

More Decks by Darren Oakley

Other Decks in Technology

Transcript

  1. What is GraphQL? It’s a spec… • A query language

    for APIs • A server-side runtime for executing queries on your data ◦ In a TYPED schema YOU define • Database / Storage Engine agnostic • Implemented in many different languages
  2. What is GraphQL? A GraphQL service is created by: •

    defining types • fields on those types • providing functions for each field on each type type Company { id: ID! name: String! catchPhrase: String employees: [Employee] } type Employee { id: ID! firstName: String! lastName: String! age: Int email: String company: Company }
  3. Queries query { company(id: 123) { id name } }

    { "data": { "company": { "id": 123, "name": "Hirthe-Ritchie" } } }
  4. Queries query { company(id: 123) { id name catchPhrase }

    } { "data": { "company": { "id": 123, "name": "Hirthe-Ritchie" "catchPhrase": "Business-focused coherent parallelism" } } }
  5. Queries { employee(id: 456) { firstName lastName email age company

    { name catchPhrase } } } { "data": { "employee": { "firstName": "Arlo", "lastName": "Stehr", "email": "[email protected]", "age": 22, "company": { "name": "Nolan-Jacobs", "catchPhrase": "Integrated clear-thinking monitoring" } } } }
  6. mutation { createCompany(input: { name: "Rockhopper Brewing", catchPhrase: "We make

    beer" }) { success company { id name catchPhrase } } } Mutations { "data": { "createCompany": { "success": true, "company": { "id": "5", "name": "Rockhopper Brewing", "catchPhrase": "We make beer" } } } }
  7. mutation { updateCompany(input: { id: 5, catchPhrase: "We make GOOD

    beer" }) { success company { id name catchPhrase } } } Mutations { "data": { "updateCompany": { "success": true, "company": { "id": "5", "name": "Rockhopper Brewing", "catchPhrase": "We make GOOD beer" } } } }
  8. Why Choose GraphQL? Pros • The Schema - documentation is

    built right in • Fetch all data for a page in one request • Flexibility for multiple different clients • No need for API versioning as you evolve the schema over time
  9. Cons • HTTP semantics be damned - 200 OK for

    all responses… • Caching can be an issue - needs to be keyed on the (POST) request body • Relay is clever, too clever, and not well documented - we couldn’t figure out how to use it effectively and went in favour of Apollo. Why Choose GraphQL?
  10. GraphQL & Ruby http://graphql-ruby.org • Should work with any web

    framework, but pulls in Rails • Supports ActiveRecord and Sequel::Model http://graphql.pro • Authorization strategies built in • Monitoring • Persisted queries • and more...
  11. Defining Your Schema GraphqlSchema = GraphQL::Schema.define do query Types::QueryType mutation

    Types::MutationType resolve_type ->(object, _ctx) { "Types::#{object.class.name}Type".constantize } end
  12. Types Types::EmployeeType = GraphQL::ObjectType.define do name 'Employee' field :id, !types.ID

    field :firstName, !types.String { resolve ->(obj, _arg, _ctx) { obj.first_name } } field :lastName, !types.String { resolve ->(obj, _arg, _ctx) { obj.last_name } } field :age, types.Int field :email, types.String field :company, Types::CompanyType end
  13. Types Types::CompanyType = GraphQL::ObjectType.define do name 'Company' field :id, !types.ID

    field :name, !types.String field :catchPhrase, types.String { resolve ->(obj, _arg, _ctx) { obj.catch_phrase } } field :employees, types[Types::EmployeeType] do argument :limit, types.Int resolve ->(obj, args, _ctx) { employees = obj.employees employees = employees.limit(args[:limit]) if args[:limit] employees } end end
  14. Exposing a Query Types::QueryType = GraphQL::ObjectType.define do name 'Query' field

    :company, function: Functions::FindRecord.new(model: Company, type: Types::CompanyType) field :employee, function: Functions::FindRecord.new(model: Employee, type: Types::EmployeeType) end
  15. Functions class Functions::FindRecord < GraphQL::Function attr_reader :type def initialize(model:, type:)

    @model = model @type = type end argument :id, GraphQL::ID_TYPE def call(_obj, args, _ctx) @model.find(args[:id]) end end
  16. Adding a Mutation Mutations::CreateCompanyMutation = GraphQL::Relay::Mutation.define do name 'CreateCompany' input_field

    :name, !types.String input_field :catchPhrase, types.String return_field :company, Types::CompanyType return_interfaces [Interfaces::MutationResultInterface] resolve ->(_obj, inputs, _ctx) { comp = Company.create(name: inputs[:name], catch_phrase: inputs[:catchPhrase]) { company: comp.errors.none? ? comp : nil, success: comp.errors.none?, errors: comp.errors.details.to_a } } end
  17. Adding a Mutation - ResultInterface Interfaces::MutationResultInterface = GraphQL::InterfaceType.define do name

    'MutationResultInterface' field :success, !types.Boolean field :errors, types[Types::ValidationErrorType] end
  18. Adding a Mutation - Surfacing Errors Types::ValidationErrorType = GraphQL::ObjectType.define do

    name 'ValidationError' field :attribute, !types.String do resolve ->(obj, _arg, _ctx) { obj.first.to_s.camelize(:lower) } end field :message, !types.String do resolve ->(obj, _arg, _ctx) { obj.second.map { |elm| elm[:error] }.join(', ') } end end
  19. Adding a Mutation Types::MutationType = GraphQL::ObjectType.define do name 'Mutation' field

    :createCompany, field: Mutations::CreateCompanyMutation.field field :updateCompany, field: Mutations::UpdateCompanyMutation.field end
  20. Testing RSpec.describe 'Types::EmployeeType' do subject { GraphqlSchema.execute(query, context: {}, variables:

    {}) } let(:employee) { create(:employee) } describe 'querying for a employee and their company' do let(:query) { %`{ employee(id: #{employee.id}) { email company { name } } }` } it 'returns the employee and their company' do expect(subject.dig('data', 'employee', 'email')).to eq(employee.email) expect(subject.dig('data', 'employee', 'company', 'name')).to eq(employee.company.name) end end end
  21. • The Ruby GraphQL DSL didn’t seem very ruby-ish to

    us... ◦ Lots of definition functions ◦ Lambdas passed around everywhere • Documentation wasn’t great • We wanted to experiment with Elixir ◦ Functional programming ◦ Pattern Matching ◦ Fault Tolerance ◦ etc etc… ◦ Basically, most of the things listed here: http://bit.ly/2ux5PZZ Why We Switched?
  22. Don’t write off the Ruby GraphQL gem In heavy use

    at Github and Shopify - it’s not going to go away! It helped us build a PoC GraphQL service very quickly to evaluate the approach, but we then decided to rebuild and extend with Elixir.