graphql-perl - graphql-js tutorial translation to graphql-perl and Mojolicious::Plugin::GraphQL

Version 0.16 of GraphQL implements most of using the Schema Definition Language to create schemas, rather than doing so programmatically. So let's now translate all the idioms in the JavaScript GraphQL tutorial into Perl!

EDIT version 0.17 implements the rest, so this tutorial translated has been updated slightly as you don't need to specify a schema at all if you call your query type Query, etc.

The format I'll use is to give each of the JS tutorial pages, with the Perl code to do what's given there:

Getting Started With GraphQL.js

You will need Perl 5.14. Consider using perlbrew to get this.

cpanm Mojolicious::Plugin::GraphQL # also gets Mojolicious and GraphQL

Then the first script:

use 5.014;
use GraphQL::Schema;
use GraphQL::Execution 'execute';
use Data::Dumper;
my $schema = GraphQL::Schema->from_doc(<<'EOF');
type Query { helloWorld: String }
EOF
say Dumper execute($schema, "{ helloWorld }",
  { helloWorld=>"Hello world!" });

Running an Express GraphQL Server

We'll use Mojolicious::Lite. There's a sample applet on https://github.com/graphql-perl/sample-mojolicious.

use Mojolicious::Lite;
use GraphQL::Schema;
my $schema = GraphQL::Schema->from_doc(<<'EOF');
type Query {
  helloWorld: String
}
EOF
plugin GraphQL => {
  schema => $schema,
  root_value => { helloWorld => 'Hello, world!' },
  graphiql => 1,
};
app->start;

Execute with:

./myapp.pl daemon -l http://*:5000

Look at: http://localhost:5000/graphql.

GraphQL Clients

(Note differences are only for ports and field name)

curl -X POST -H "Content-Type: application/json" \
   -d '{"query": "{ helloWorld }"}' \
  http://localhost:5000/graphql

JavaScript:

var xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.open("POST", "/graphql");
xhr.setRequestHeader("Content-Type", "application/json");
xhr.setRequestHeader("Accept", "application/json");
xhr.onload = function () {
  console.log('data returned:', xhr.response);
}
xhr.send(JSON.stringify({query: "{ helloWorld }"}));

Note that (I believe) since the tutorial was written, GraphiQL now has a "variables" pane so you can use it for that. The JS console snippet will also work.

Basic Types

use Mojolicious::Lite;
use GraphQL::Schema;
my $schema = GraphQL::Schema->from_doc(<<'EOF');
type Query {
  quoteOfTheDay: String
  random: Float!
  rollThreeDice: [Int]
}
EOF
my $root_value = {
  quoteOfTheDay => sub {rand() < 0.5 ? 'Take it easy' : 'Salvation lies within'},
  random => sub {rand()},
  rollThreeDice => sub {[map 1+int(rand()*6), (1..3)]},
};
plugin GraphQL => {
  schema => $schema,
  root_value => $root_value,
  graphiql => 1,
};
app->start;

Passing Arguments

use Mojolicious::Lite;
use GraphQL::Schema;
my $schema = GraphQL::Schema->from_doc(<<'EOF');
type Query {
  rollDice(numDice: Int!, numSides: Int): [Int]
}
EOF
my $root_value = {
  rollDice => sub { my $args = shift; [map 1+int(rand()*$args->{numSides}), (1..$args->{numDice})]},
};
plugin GraphQL => {
  schema => $schema,
  root_value => $root_value,
  graphiql => 1,
};
app->start;

Object Types

use Mojolicious::Lite;
use GraphQL::Schema;
my $schema = GraphQL::Schema->from_doc(<<'EOF');
type RandomDie {
  numSides: Int!
  rollOnce: Int!
  roll(numRolls: Int!): [Int]
}
type Query {
  getDie(numSides: Int = 6): RandomDie
}
EOF
{
package RandomDie;
sub new { my ($class, $sides) = @_; return bless {numSides=>$sides}, $class; }
sub numSides { shift->{numSides} }
sub rollOnce { my ($self) = @_; 1 + int(rand() * $self->{numSides}) }
sub roll { my ($self, $args) = @_; [map $self->rollOnce, (1..$args->{numRolls})] }
}
my $root_value = {
  getDie => sub { my $args = shift; RandomDie->new($args->{numSides}) },
};
plugin GraphQL => {
  schema => $schema,
  root_value => $root_value,
  graphiql => 1,
};
app->start;

Mutations and Input Types

use Mojolicious::Lite;
use GraphQL::Schema;
my $schema = GraphQL::Schema->from_doc(<<'EOF');
input MessageInput {
  content: String
  author: String
}
type Message {
  id: ID!
  content: String
  author: String
}
type Query {
  getMessage(id: ID!): Message
}
type Mutation {
  createMessage(input: MessageInput): Message
  updateMessage(id: ID!, input: MessageInput): Message
}
EOF
{
# If Message had any complex fields, we'd put them on this object.
package Message;
sub new { my ($class, $id, $args) = @_; return bless {id => $id, %{$args->{input}}}, $class; }
sub id { shift->{id} }
sub content { shift->{content} }
sub author { shift->{author} }
}
my $fakedb = {};
my $root_value = {
  getMessage => sub {
    my $id = shift->{id};
    die "No message $id\n" if !$fakedb->{$id};
    Message->new($id, $fakedb->{$id})
  },
  createMessage => sub {
    my $input = shift;
    # Create a random id for our "database".
    my $id = int 1000 * rand();
    $fakedb->{$id} = {%$input};
    Message->new($id, $input)
  },
  updateMessage => sub {
    my $args = shift;
    my ($id, $input) = @{$args}{qw(id input)};
    die "No message $id\n" if !$fakedb->{$id};
    # This replaces all old data, but some apps might want partial update.
    $fakedb->{$id} = {%$input};
    Message->new($id, $input)
  },
};
plugin GraphQL => {
  schema => $schema,
  root_value => $root_value,
  graphiql => 1,
};
app->start;

Authentication and Express Middleware

This is a large topic. In order to stay with the non-blocking Mojo idiom, you should use Mojolicious-compatible means of authentication. One method is using under, as discussed here. There is also a Mojolicious plugin to make this easier.

Additionally, the Mojolicious GraphQL plugin will by default pass the request headers on as the "context" (per-request) object to GraphQL::Execution::execute, but with a route-handler you can do anything you like. The plugin's SYNOPSIS gives an example.

Constructing Types

Using Schema language:

use Mojolicious::Lite;
use GraphQL::Schema;
my $schema = GraphQL::Schema->from_doc(<<'EOF');
type User {
  id: String
  name: String
}
type Query {
  user(id: String): User
}
EOF
# Maps id to User hashref
my $fakedb = {
  a => { id => 'a', name => 'alice' },
  b => { id => 'b', name => 'bob' },
};
my $root_value = {
  user => sub {
    my $id = shift->{id};
    die "No user $id\n" if !$fakedb->{$id};
    $fakedb->{$id}
  },
};
plugin GraphQL => {
  schema => $schema,
  root_value => $root_value,
  graphiql => 1,
};
app->start;

Without Schema language, i.e. instead using resolvers on fields:

use strict; use warnings;
use Mojolicious::Lite;
use GraphQL::Schema;
use GraphQL::Type::Object;
use GraphQL::Type::Scalar qw($String);
# Maps id to User hashref
my $fakedb = {
  a => { id => 'a', name => 'alice' },
  b => { id => 'b', name => 'bob' },
};
my $UserType = GraphQL::Type::Object->new(
  name => 'User',
  fields => {
    id => { type => $String },
    name => { type => $String },
  },
);
my $schema = GraphQL::Schema->new(query => GraphQL::Type::Object->new(
  name => 'Query',
  fields => {
    user => {
      type => $UserType,
      args => { id => { type => $String } },
      resolve => sub {
        my ($root_value, $args) = @_;
        my $id = $args->{id};
        die "No user $id\n" if !$fakedb->{$id};
        $fakedb->{$id}
      },
    },
  },
));
plugin GraphQL => {
  schema => $schema,
  graphiql => 1,
};
app->start;

For more about resolvers in graphql-perl: graphql-perl resolver documentation.

express-graphql

For more on this, see the Mojolicious::Plugin::GraphQL documentation.

Acknowledgements

The porting of GraphQL to Perl 5 is sponsored by Perl Careers.

Thanks to the folks on #mojo on irc.perl.org for valuable pointers on improving this posting.

Leave a comment

About Mohawk

user-pic I blog about Perl.