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=fullSchema
type 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=
!contentMesh
Normal 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"
}