No under/over fetching of data
Single endpoint
Query mirrors structure of components (React, Vue)
Easy for client-side developers!
No HTTP/Server-side caching
Dependant on tooling (eg: GraphiQL)
Federation?
Difficult for back-end developers!
👍
👎
Declarative way to define the data model
Declarative way to duplicate the data model
Extensible: type, enum, interface, extend, directive, tooling
Extensible? single location, union, directive, tooling, bureaucratic
Versioning, documentation, type validation
Verbose, limited, RESTy, reinvents the wheel
Incremental adoption
Non-comprehensive
author
comments
author
post
/?query=query&variable=value&fragment=fragmentQuery/?query=field(args)@alias<directive(args)>/?
query=
  field(
    args
  )@alias<
    directive(
      args
    )
  > /?
query=
  posts(
    limit: 5
  )@posts.
    id|
    date(format: d/m/Y)|
    title<
      skip(if: false)
    >
/?
query=
  posts(
    ids: [1, 1499, 1178],
    order: $order
  )@posts.
    id|
    date(format: d/m/Y)|
    title<
      skip(if: false)
    >|
    --props&
order=title|ASC&
props=
  url|
  author.
    name|
    url
/?query=fullSchematype Human {
  name: String
  appearsIn: [Episode]
  starships: [Starship]
}
enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}
type Starship {
  name: String
}class HumanTypeResolver extends AbstractTypeResolver
{
  public function getTypeName(): string
  {
    return 'Human';
  }
  public function getSchemaTypeDescription(): ?string
  {
    return $this->translationAPI->__('Representation of a human');
  }
  public function getID($resultItem)
  {
    $human = $resultItem;
    return $human->ID;
  }
  public function getTypeDataLoaderClass(): string
  {
    return HumanTypeDataLoader::class;
  }
}
class HumanTypeDataLoader extends AbstractTypeQueryableDataLoader
{
  public function getObjects(array $ids): array
  {
    $humanTypeAPI = HumanTypeAPIFacade::getInstance();
    return $humanTypeAPI->getHumans(['include' => $ids]);
  }
  public function executeQuery($query, array $options = [])
  {
    $options['return-type'] = 'ids';
    $humanTypeAPI = HumanTypeAPIFacade::getInstance();
    return $humanTypeAPI->getHumans($query, $options);
  }
}
class HumanFieldResolver extends AbstractDBDataFieldResolver
{
  public const NEWHOPE = 'newhope';
  public const EMPIRE = 'empire';
  public const JEDI = 'jedi';
  public static function getClassesToAttachTo(): array
  {
    return array(
      HumanTypeResolver::class,
    );
  }
  public static function getFieldNamesToResolve(): array
  {
    return [
      'name',
      'appearsIn',
      'starship',
    ];
  }
  public function getSchemaFieldType(TypeResolverInterface $typeResolver, string $fieldName): ?string
  {
    $types = [
      'name' => SchemaDefinition::TYPE_STRING,
      'appearsIn' => SchemaDefinition::TYPE_ENUM,
      'starship' => SchemaDefinition::TYPE_ID,
    ];
    return $types[$fieldName] ?? parent::getSchemaFieldType($typeResolver, $fieldName);
  }
  public function addSchemaDefinitionForField(array &$schemaDefinition, TypeResolverInterface $typeResolver, string $fieldName): void
  {
    switch ($fieldName) {
      case 'appearsIn':
        $schemaDefinition[SchemaDefinition::ARGNAME_ENUMVALUES] = [
          self::NEWHOPE,
          self::EMPIRE,
          self::JEDI,
        ];
        break;
    }
  }
  public function getSchemaFieldDescription(TypeResolverInterface $typeResolver, string $fieldName): ?string
  {
    $translationAPI = TranslationAPIFacade::getInstance();
    $descriptions = [
      'name' => $translationAPI->__('Name of the human'),
      'appearsIn' => $translationAPI->__('In what film the human appears'),
      'starship' => $translationAPI->__('Human\'s starship'),
    ];
    return $descriptions[$fieldName] ?? parent::getSchemaFieldDescription($typeResolver, $fieldName);
  }
  public function resolveValue(TypeResolverInterface $typeResolver, $resultItem, string $fieldName, array $fieldArgs = [], ?array $variables = null, ?array $expressions = null, array $options = [])
  {
    // It receives a Human object
    $human = $resultItem;
    switch ($fieldName) {
      case 'name':
        return $human->name;
      case 'appearsIn':
        return $human->episode;
      case 'starship':
        // It returns a Starship ID, not an object!
        return $human->starship;
    }
    return parent::resolveValue($typeResolver, $resultItem, $fieldName, $fieldArgs, $variables, $expressions, $options);
  }
  public function resolveFieldTypeResolverClass(TypeResolverInterface $typeResolver, string $fieldName, array $fieldArgs = []): ?string
  {
    switch ($fieldName) {
      case 'starship':
        return StarshipTypeResolver::class;
    }
    return parent::resolveFieldTypeResolverClass($typeResolver, $fieldName, $fieldArgs);
  }
}
/?
postId=1&
query=
  post($postId).
    date(d/m/Y)|
    title<
      skip(false)
    >/?
postId=1&
query=
  post(id:$postId).
    date(format:d/m/Y)|
    title<
      skip(if:false)
    >1. /?query=not(true)
2. /?query=or([1,0])
3. /?query=and([1,0])
4. /?query=if(true, Show this text, Hide this text)
5. /?query=equals(first text, second text)
6. /?query=isNull(),isNull(something)
7. /?query=sprintf(%s API is %s, [PoP, cool])
8. /?query=context/?query=
  post(
    id: arrayItem(
      posts(
        limit: 1,
        order: date|DESC
      ), 
    0)
  )@latestPost.
    id|
    title|
    date/?
format=Y-m-d&
query=
  posts.
    if (
      hasComments(), 
      sprintf(
        "This post has %s comment(s) and title '%s'", [
          commentsCount(),
          title()
        ]
      ), 
      sprintf(
        "This post was created on %s and has no comments", [
          date(format: if(not(empty($format)), $format, d/m/Y))
        ]
      )
    )@postDesc/?query=
  posts.
    title|
    featuredImage<
      skip(if:isNull(featuredImage()))
    >.
      src/?
format=Y-m-d&
query=
  posts.
    sprintf(
      "This post has %s comment(s) and title '%s'", [
        comments-count(),
        title()
      ]
    )@postDesc<include(if:has-comments())>|
    sprintf(
      "This post was created on %s and has no comments", [
        date(format: if(not(empty($format)), $format, d/m/Y))
      ]
    )@postDesc<include(if:not(has-comments()))>/?query=
  posts.
    title|
    featuredImage?.
      src/?query=
  echo([
    [banana, apple],
    [strawberry, grape, melon]
  ])@fruitJoin<
    forEach<
      applyFunction(
        function: arrayJoin,
        addArguments: [
          array: %value%,
          separator: "---"
        ]
      )
    >
  >/?query=
  echo([
    [
      text: Hello my friends,
      translateTo: fr
    ],
    [
      text: How do you like this software so far?,
      translateTo: es
    ],
  ])@translated<
    forEach<
      advancePointerInArray(
        path: text,
        appendExpressions: [
          toLang:extract(%value%,translateTo)
        ]
      )<
        translate(
          from: en,
          to: %toLang%,
          oneLanguagePerField: true,
          override: true
        )
      >
    >
  >//1. Operators have max-age 1 year
/?query=
  echo(Hello world!)
//2. Most fields have max-age 1 hour
/?query=
  echo(Hello world!)|
  posts.
    title
//3. Nested fields also supported
/?query=
  echo(posts())
//4. "time" field has max-age 0
/?query=
  time
//5. To not cache a response:
//a. Add field "time"
/?query=
  time|
  echo(Hello world!)|
  posts.
    title
//b. Add <cacheControl(maxAge:0)>
/?query=
  echo(Hello world!)|
  posts.
    title<cacheControl(maxAge:0)>//1. Standard behaviour
/?query=
  posts.
    excerpt
//2. New feature not yet available
/?query=
  posts.
    excerpt(length:30)
//3. New feature available under 
// experimental branch
/?query=
  posts.
    excerpt(
      length:30,
      branch:experimental
    )/?query=
  me.
    name
/?query=
  posts.
     author.
       posts.
         comments.
           author.
             id|
             name|
             posts.
               id|
               title|
               url|
               tags.
                 id|
                 slug
// The Google Translate API is called once,
// containing 10 pieces of text to translate:
// 2 fields (title and excerpt) for 5 posts
/?query=
  posts(limit:5).
    --props|
    --props@spanish<
      translate(en,es)
    >&
props=
  title|
  excerpt
// Here there are 3 calls to the API, one for
// every language (Spanish, French and German),
// 10 strings each, all calls are concurrent
/?query=
  posts(limit:5).
    --props|
    --props@spanish<
      translate(en,es)
    >|
    --props@french<
      translate(en,fr)
    >|
    --props@german<
      translate(en,de)
    >&
props=
  title|
  excerpt
//1. <translate> calls the Google Translate API
/?query=
  posts(limit:5).
    title|
    title@spanish<
      translate(en,es)
    >
    
//2. Translate to Spanish and back to English
/?query=
  posts(limit:5).
    title|
    title@translateAndBack<
      translate(en,es),
      translate(es,en)
    >
    
//3. Change the provider through arguments
// (link gives error: Azure is not implemented)
/?query=
  posts(limit:5).
    title|
    title@spanish<
      translate(en,es,provider:azure)
    >/?query=
echo([
  usd: [
    bitcoin: extract(
      getJSON("https://api.cryptonator.com/api/ticker/btc-usd"), 
      ticker.price
    ),
    ethereum: extract(
      getJSON("https://api.cryptonator.com/api/ticker/eth-usd"), 
      ticker.price
    )
  ],
  euro: [
    bitcoin: extract(
      getJSON("https://api.cryptonator.com/api/ticker/btc-eur"), 
      ticker.price
    ),
    ethereum: extract(
      getJSON("https://api.cryptonator.com/api/ticker/eth-eur"), 
      ticker.price
    )
  ]
])@cryptoPrices
//1. Get data from a REST endpoint
/?query=
  getJSON("https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions")@userEmailLangList
    
//2. Access and manipulate the data
/?query=
  extract(
    getJSON("https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions"),
    email
  )@userEmailList
  
//3. Convert the data into an input to another system
/?query=
  getJSON(
    sprintf(
      "https://newapi.getpop.org/users/api/rest/?query=name|email%26emails[]=%s",
      [arrayJoin(
        extract(
          getJSON("https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions"),
          email
        ),
        "%26emails[]="
      )]
    )
  )@userNameEmailList
/?query=
  echo([
    github: "https://api.github.com/repos/leoloso/PoP",
    weather: "https://api.weather.gov/zones/forecast/MOZ028/forecast",
    photos: "https://picsum.photos/v2/list"
  ])@meshServices|
  getAsyncJSON(getSelfProp(%self%, meshServices))@meshServiceData|
  echo([
    weatherForecast: extract(
      getSelfProp(%self%, meshServiceData),
      weather.periods
    ),
    photoGalleryURLs: extract(
      getSelfProp(%self%, meshServiceData),
      photos.url
    ),
    githubMeta: echo([
      description: extract(
        getSelfProp(%self%, meshServiceData),
        github.description
      ),
      starCount: extract(
        getSelfProp(%self%, meshServiceData),
        github.stargazers_count
      )
    ])
  ])@contentMesh// 1. Inspect services
/?query=
  meshServices
// 2. Retrieve data
/?query=
  meshServiceData
// 3. Process data
/?query=
  contentMesh
// 4. Customize data
/?query=
  contentMesh(
    githubRepo: "getpop/api-graphql",
    weatherZone: AKZ017,
    photoPage: 3
  )@contentMesh// 1. Save services
/?query=
  --meshServices
// 2. Retrieve data
/?query=
  --meshServiceData
// 3. Process data
/?query=
  --contentMesh
// 4. Customize data
/?
githubRepo=getpop/api-graphql&
weatherZone=AKZ017&
photoPage=3&
query=
  --contentMesh// 1. Access persisted query
/?query=
  !contentMesh
// 2. Customize it with variables
/?
githubRepo=getpop/api-graphql&
weatherZone=AKZ017&
photoPage=3&
query=
  !contentMeshNormal schema
Namespaced schema
// Selecting version for fields
/?query=
  userServiceURLs(versionConstraint:^0.1)|
  userServiceURLs(versionConstraint:">0.1")|
  userServiceURLs(versionConstraint:^0.2)
// Selecting version for directives
/?query=
  post($postId).
    title@titleCase<makeTitle(versionConstraint:^0.1)>|
    title@upperCase<makeTitle(versionConstraint:^0.2)>
&postId=1// Query data for a single resource
{single-post-url}/api/rest/?query=
  id|
  title|
  author.
    id|
    name
// Query data for a set of resources
{post-list-url}/api/rest/?query=
  id|
  title|
  author.
    id|
    name
// Output as XML: Replace /graphql with /xml
/api/xml/?query=
  posts.
    id|
    title|
    author.
      id|
      name
// Output as props: Replace /graphql with /props
/api/props/?query=
  posts.
    id|
    title|
    excerpt
/api/?query=
  posts.
     author.
       posts.
         comments.
           author.
             id|
             name|
             posts.
               id|
               title|
               url
//1. Deprecated fields
/?query=
  posts.
    title|
    published//1. Deprecated fields
/?query=
  posts.
    title|
    published
    
//2. Schema warning
/?query=
  posts(limit:3.5).
    title
    
//3. Database warning
/?query=
  users.
    posts(limit:name()).
      title
      
//4. Query error
/?query=
  posts.
    id[book](key:value)
    
//5. Schema error
/?query=
  posts.
    non-existant-field|
    is-status(
      status:non-existant-value
    )//1. Deprecated fields
/?query=
  posts.
    title|
    published
    
//2. Schema warning
/?query=
  posts(limit:3.5).
    title
    
//3. Database warning
/?query=
  users.
    posts(limit:name()).
      title/?query=
  posts(limit:3.5).
    title/?query=
  post(divide(a,4)).
    title/?query=
  echo([hola,chau])<
    forEach<
      translate(notexisting:prop)
    >
  >/?
actions[]=show-logs&
postId=1&
query=
  post($postId).
    title|
    date(d/m/Y)
/?
postId=1&
query=
  post($postId)@post.
    content|
    date(d/m/Y)@date,
  getJSON("https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions")@userList|
  arrayUnique(
    extract(
      getSelfProp(%self%, userList),
      lang
    )
  )@userLangs|
  extract(
    getSelfProp(%self%, userList),
    email
  )@userEmails|
  arrayFill(
    getJSON(
      sprintf(
        "https://newapi.getpop.org/users/api/rest/?query=name|email%26emails[]=%s",
        [arrayJoin(
          getSelfProp(%self%, userEmails),
          "%26emails[]="
        )]
      )
    ),
    getSelfProp(%self%, userList),
    email
  )@userData,
  self.
    post($postId)@post<
      copyRelationalResults(
        [content, date],
        [postContent, postDate]
      )
    >|
    self.
      getSelfProp(%self%, postContent)@postContent<
        translate(
          from: en,
          to: arrayDiff([
            getSelfProp(%self%, userLangs),
            [en]
          ])
        ),
        renameProperty(postContent-en)
      >|
      getSelfProp(%self%, userData)@userPostData<
        forEach<
          applyFunction(
            function: arrayAddItem(
              array: [],
              value: ""
            ),
            addArguments: [
              key: postContent,
              array: %value%,
              value: getSelfProp(
                %self%,
                sprintf(
                  postContent-%s,
                  [extract(%value%, lang)]
                )
              )
            ]
          ),
          applyFunction(
            function: arrayAddItem(
              array: [],
              value: ""
            ),
            addArguments: [
              key: header,
              array: %value%,
              value: sprintf(
                string: "<p>Hi %s, we published this post on %s, enjoy!</p>",
                values: [
                  extract(%value%, name),
                  getSelfProp(%self%, postDate)
                ]
              )
            ]
          )
        >
      >|
      self.
        getSelfProp(%self%, userPostData)@translatedUserPostProps<
          forEach(
            if: not(
              equals(
                extract(%value%, lang),
                en
              )
            )
          )<
            advancePointerInArray(
              path: header,
              appendExpressions: [
                toLang: extract(%value%, lang)
              ]
            )<
              translate(
                from: en,
                to: %toLang%,
                oneLanguagePerField: true,
                override: true
              )
            >
          >
        >|
        self.
          getSelfProp(%self%,translatedUserPostProps)@emails<
            forEach<
              applyFunction(
                function: arrayAddItem(
                  array: [],
                  value: []
                ),
                addArguments: [
                  key: content,
                  array: %value%,
                  value: concat([
                    extract(%value%, header),
                    extract(%value%, postContent)
                  ])
                ]
              ),
              applyFunction(
                function: arrayAddItem(
                  array: [],
                  value: []
                ),
                addArguments: [
                  key: to,
                  array: %value%,
                  value: extract(%value%, email)
                ]
              ),
              applyFunction(
                function: arrayAddItem(
                  array: [],
                  value: []
                ),
                addArguments: [
                  key: subject,
                  array: %value%,
                  value: "PoP API example :)"
                ]
              ),
              sendByEmail
            >
          >/?query=
  addPost($title, $content).
    addComment($comment1)|
    addComment($comment2).
      author<sendConfirmationByEmail>.
        followers<notifyByEmail, notifyBySlack>"require": {
  "getpop/graphql": "dev-master",
  "getpop/engine-wp": "dev-master",
  "getpop/posts-wp": "dev-master",
  "getpop/users-wp": "dev-master",
  "getpop/comments-wp": "dev-master",
  "getpop/pages-wp": "dev-master",
  "getpop/postmedia-wp": "dev-master",
  "getpop/taxonomies-wp": "dev-master"
}