All in GraphQL #34

Merged
timw4mail merged 87 commits from develop into master 2020-12-01 10:07:49 -05:00
17 changed files with 274 additions and 279 deletions
Showing only changes of commit 6a82944473 - Show all commits

View File

@ -175,9 +175,10 @@ $routes = [
] ]
], ],
'person' => [ 'person' => [
'path' => '/people/{id}', 'path' => '/people/{id}{/slug}',
'tokens' => [ 'tokens' => [
'id' => SLUG_PATTERN 'id' => SLUG_PATTERN,
'slug' => SLUG_PATTERN,
] ]
], ],
'default_user_info' => [ 'default_user_info' => [

View File

@ -130,7 +130,7 @@ return static function (array $configArray = []): Container {
return $model; return $model;
}); });
$container->set('anilist-model', static function(ContainerInterface $container): Anilist\Model { $container->set('anilist-model', static function(ContainerInterface $container): Anilist\Model {
$requestBuilder = new AnilistRequestBuilder(); $requestBuilder = new AnilistRequestBuilder($container);
$requestBuilder->setLogger($container->getLogger('anilist-request')); $requestBuilder->setLogger($container->getLogger('anilist-request'));
$listItem = new Anilist\ListItem(); $listItem = new Anilist\ListItem();

View File

@ -174,7 +174,7 @@ use function Aviat\AnimeClient\getLocalImg;
<section class='content media-wrap flex flex-wrap flex-justify-start'> <section class='content media-wrap flex flex-wrap flex-justify-start'>
<?php foreach ($people as $pid => $person): ?> <?php foreach ($people as $pid => $person): ?>
<article class='character small-person'> <article class='character small-person'>
<?php $link = $url->generate('person', ['id' => $person['id']]) ?> <?php $link = $url->generate('person', ['id' => $person['id'], 'slug' => $person['slug']]) ?>
<div class="name"> <div class="name">
<a href="<?= $link ?>"> <a href="<?= $link ?>">
<?= $person['name'] ?> <?= $person['name'] ?>

View File

@ -100,7 +100,7 @@
<section class='content media-wrap flex flex-wrap flex-justify-start'> <section class='content media-wrap flex flex-wrap flex-justify-start'>
<?php foreach ($people as $person): ?> <?php foreach ($people as $person): ?>
<article class='character person'> <article class='character person'>
<?php $link = $url->generate('person', ['id' => $person['id']]) ?> <?php $link = $url->generate('person', ['id' => $person['id'], 'slug' => $person['slug']]) ?>
<div class="name"> <div class="name">
<a href="<?= $link ?>"> <a href="<?= $link ?>">
<?= $person['name'] ?> <?= $person['name'] ?>

View File

@ -16,17 +16,28 @@
namespace Aviat\AnimeClient\API\Anilist; namespace Aviat\AnimeClient\API\Anilist;
use Amp\Http\Client\Request;
use Amp\Http\Client\Response;
use Aviat\AnimeClient\API\Anilist;
use Aviat\Ion\Di\ContainerAware;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Json;
use Aviat\Ion\JsonException;
use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use const Aviat\AnimeClient\USER_AGENT; use const Aviat\AnimeClient\USER_AGENT;
use Aviat\AnimeClient\API\APIRequestBuilder; use Aviat\AnimeClient\API\APIRequestBuilder;
final class AnilistRequestBuilder extends APIRequestBuilder { final class AnilistRequestBuilder extends APIRequestBuilder {
use ContainerAware;
/** /**
* The base url for api requests * The base url for api requests
* @var string $base_url * @var string $base_url
*/ */
protected string $baseUrl = 'https://graphql.anilist.co'; protected string $baseUrl = Anilist::BASE_URL;
/** /**
* Valid HTTP request methods * Valid HTTP request methods
@ -40,8 +51,226 @@ final class AnilistRequestBuilder extends APIRequestBuilder {
* @var array * @var array
*/ */
protected array $defaultHeaders = [ protected array $defaultHeaders = [
'User-Agent' => USER_AGENT,
'Accept' => 'application/json', 'Accept' => 'application/json',
'Content-Type' => 'application/json', // 'Accept-Encoding' => 'gzip',
'Content-type' => 'application/json',
'User-Agent' => USER_AGENT,
]; ];
public function __construct(ContainerInterface $container)
{
$this->setContainer($container);
}
/**
* Create a request object
* @param string $url
* @param array $options
* @return Request
* @throws Throwable
*/
public function setUpRequest(string $url, array $options = []): Request
{
$config = $this->getContainer()->get('config');
$anilistConfig = $config->get('anilist');
$request = $this->newRequest('POST', $url);
// You can only authenticate the request if you
// actually have an access_token saved
if ($config->has(['anilist', 'access_token']))
{
$request = $request->setAuth('bearer', $anilistConfig['access_token']);
}
if (array_key_exists('form_params', $options))
{
$request = $request->setFormFields($options['form_params']);
}
if (array_key_exists('query', $options))
{
$request = $request->setQuery($options['query']);
}
if (array_key_exists('body', $options))
{
$request = $request->setJsonBody($options['body']);
}
if (array_key_exists('headers', $options))
{
$request = $request->setHeaders($options['headers']);
}
return $request->getFullRequest();
}
/**
* Run a GraphQL API query
*
* @param string $name
* @param array $variables
* @return array
*/
public function runQuery(string $name, array $variables = []): array
{
$file = realpath(__DIR__ . "/Queries/{$name}.graphql");
if ( ! file_exists($file))
{
throw new LogicException('GraphQL query file does not exist.');
}
$query = file_get_contents($file);
$body = [
'query' => $query
];
if ( ! empty($variables))
{
$body['variables'] = [];
foreach($variables as $key => $val)
{
$body['variables'][$key] = $val;
}
}
return $this->postRequest([
'body' => $body
]);
}
/**
* @param string $name
* @param array $variables
* @return Request
* @throws Throwable
*/
public function mutateRequest (string $name, array $variables = []): Request
{
$file = realpath(__DIR__ . "/Mutations/{$name}.graphql");
if (!file_exists($file))
{
throw new LogicException('GraphQL mutation file does not exist.');
}
$query = file_get_contents($file);
$body = [
'query' => $query
];
if (!empty($variables)) {
$body['variables'] = [];
foreach ($variables as $key => $val)
{
$body['variables'][$key] = $val;
}
}
return $this->setUpRequest(Anilist::BASE_URL, [
'body' => $body,
]);
}
/**
* @param string $name
* @param array $variables
* @return array
* @throws Throwable
*/
public function mutate (string $name, array $variables = []): array
{
$request = $this->mutateRequest($name, $variables);
$response = $this->getResponseFromRequest($request);
return Json::decode(wait($response->getBody()->buffer()));
}
/**
* Make a request
*
* @param string $url
* @param array $options
* @return Response
* @throws Throwable
*/
private function getResponse(string $url, array $options = []): Response
{
$logger = $this->container->getLogger('anilist-request');
$request = $this->setUpRequest($url, $options);
$response = getResponse($request);
$logger->debug('Anilist response', [
'status' => $response->getStatus(),
'reason' => $response->getReason(),
'body' => $response->getBody(),
'headers' => $response->getHeaders(),
'requestHeaders' => $request->getHeaders(),
]);
return $response;
}
/**
* @param Request $request
* @return Response
* @throws Throwable
*/
private function getResponseFromRequest(Request $request): Response
{
$logger = $this->container->getLogger('anilist-request');
$response = getResponse($request);
$logger->debug('Anilist response', [
'status' => $response->getStatus(),
'reason' => $response->getReason(),
'body' => $response->getBody(),
'headers' => $response->getHeaders(),
'requestHeaders' => $request->getHeaders(),
]);
return $response;
}
/**
* Remove some boilerplate for post requests
*
* @param array $options
* @return array
* @throws Throwable
*/
protected function postRequest(array $options = []): array
{
$response = $this->getResponse(Anilist::BASE_URL, $options);
$validResponseCodes = [200, 201];
$logger = $this->container->getLogger('anilist-request');
$logger->debug('Anilist response', [
'status' => $response->getStatus(),
'reason' => $response->getReason(),
'body' => $response->getBody(),
'headers' => $response->getHeaders(),
//'requestHeaders' => $request->getHeaders(),
]);
if ( ! \in_array($response->getStatus(), $validResponseCodes, TRUE))
{
$logger->warning('Non 200 response for POST api call', (array)$response->getBody());
}
$rawBody = wait($response->getBody()->buffer());
try
{
return Json::decode($rawBody);
}
catch (JsonException $e)
{
dump($e);
dump($rawBody);
die();
}
}
} }

View File

@ -16,21 +16,8 @@
namespace Aviat\AnimeClient\API\Anilist; namespace Aviat\AnimeClient\API\Anilist;
use const Aviat\AnimeClient\USER_AGENT;
use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use Amp\Http\Client\Request;
use Amp\Http\Client\Response;
use Aviat\AnimeClient\API\Anilist;
use Aviat\Ion\Json;
use Aviat\Ion\Di\ContainerAware; use Aviat\Ion\Di\ContainerAware;
use LogicException;
use Throwable;
trait AnilistTrait { trait AnilistTrait {
use ContainerAware; use ContainerAware;
@ -40,24 +27,6 @@ trait AnilistTrait {
*/ */
protected AnilistRequestBuilder $requestBuilder; protected AnilistRequestBuilder $requestBuilder;
/**
* The base url for api requests
* @var string $base_url
*/
protected string $baseUrl = Anilist::BASE_URL;
/**
* HTTP headers to send with every request
*
* @var array
*/
protected array $defaultHeaders = [
'Accept' => 'application/json',
'Accept-Encoding' => 'gzip',
'Content-type' => 'application/json',
'User-Agent' => USER_AGENT,
];
/** /**
* Set the request builder object * Set the request builder object
* *
@ -69,223 +38,4 @@ trait AnilistTrait {
$this->requestBuilder = $requestBuilder; $this->requestBuilder = $requestBuilder;
return $this; return $this;
} }
/**
* Create a request object
* @param string $url
* @param array $options
* @return Request
* @throws Throwable
*/
public function setUpRequest(string $url, array $options = []): Request
{
$config = $this->getContainer()->get('config');
$anilistConfig = $config->get('anilist');
$request = $this->requestBuilder->newRequest('POST', $url);
// You can only authenticate the request if you
// actually have an access_token saved
if ($config->has(['anilist', 'access_token']))
{
$request = $request->setAuth('bearer', $anilistConfig['access_token']);
}
if (array_key_exists('form_params', $options))
{
$request = $request->setFormFields($options['form_params']);
}
if (array_key_exists('query', $options))
{
$request = $request->setQuery($options['query']);
}
if (array_key_exists('body', $options))
{
$request = $request->setJsonBody($options['body']);
}
if (array_key_exists('headers', $options))
{
$request = $request->setHeaders($options['headers']);
}
return $request->getFullRequest();
}
/**
* Run a GraphQL API query
*
* @param string $name
* @param array $variables
* @return array
*/
public function runQuery(string $name, array $variables = []): array
{
$file = realpath(__DIR__ . "/Queries/{$name}.graphql");
if ( ! file_exists($file))
{
throw new LogicException('GraphQL query file does not exist.');
}
$query = file_get_contents($file);
$body = [
'query' => $query
];
if ( ! empty($variables))
{
$body['variables'] = [];
foreach($variables as $key => $val)
{
$body['variables'][$key] = $val;
}
}
return $this->postRequest([
'body' => $body
]);
}
/**
* @param string $name
* @param array $variables
* @return Request
* @throws Throwable
*/
public function mutateRequest (string $name, array $variables = []): Request
{
$file = realpath(__DIR__ . "/Mutations/{$name}.graphql");
if (!file_exists($file))
{
throw new LogicException('GraphQL mutation file does not exist.');
}
$query = file_get_contents($file);
$body = [
'query' => $query
];
if (!empty($variables)) {
$body['variables'] = [];
foreach ($variables as $key => $val)
{
$body['variables'][$key] = $val;
}
}
return $this->setUpRequest(Anilist::BASE_URL, [
'body' => $body,
]);
}
/**
* @param string $name
* @param array $variables
* @return array
* @throws Throwable
*/
public function mutate (string $name, array $variables = []): array
{
$request = $this->mutateRequest($name, $variables);
$response = $this->getResponseFromRequest($request);
return Json::decode(wait($response->getBody()->buffer()));
}
/**
* Make a request
*
* @param string $url
* @param array $options
* @return Response
* @throws Throwable
*/
private function getResponse(string $url, array $options = []): Response
{
$logger = NULL;
if ($this->getContainer())
{
$logger = $this->container->getLogger('anilist-request');
}
$request = $this->setUpRequest($url, $options);
$response = getResponse($request);
$logger->debug('Anilist response', [
'status' => $response->getStatus(),
'reason' => $response->getReason(),
'body' => $response->getBody(),
'headers' => $response->getHeaders(),
'requestHeaders' => $request->getHeaders(),
]);
return $response;
}
/**
* @param Request $request
* @return Response
* @throws Throwable
*/
private function getResponseFromRequest(Request $request): Response
{
$logger = NULL;
if ($this->getContainer())
{
$logger = $this->container->getLogger('anilist-request');
}
$response = getResponse($request);
$logger->debug('Anilist response', [
'status' => $response->getStatus(),
'reason' => $response->getReason(),
'body' => $response->getBody(),
'headers' => $response->getHeaders(),
'requestHeaders' => $request->getHeaders(),
]);
return $response;
}
/**
* Remove some boilerplate for post requests
*
* @param array $options
* @return array
* @throws Throwable
*/
protected function postRequest(array $options = []): array
{
$response = $this->getResponse(Anilist::BASE_URL, $options);
$validResponseCodes = [200, 201];
$logger = NULL;
if ($this->getContainer())
{
$logger = $this->container->getLogger('anilist-request');
$logger->debug('Anilist response', [
'status' => $response->getStatus(),
'reason' => $response->getReason(),
'body' => $response->getBody(),
'headers' => $response->getHeaders(),
//'requestHeaders' => $request->getHeaders(),
]);
}
if ( ! \in_array($response->getStatus(), $validResponseCodes, TRUE))
{
if ($logger !== NULL)
{
$logger->warning('Non 200 response for POST api call', (array)$response->getBody());
}
}
// dump(wait($response->getBody()->buffer()));
return Json::decode(wait($response->getBody()->buffer()));
}
} }

View File

@ -38,7 +38,7 @@ final class ListItem extends AbstractListItem {
public function create(array $data): Request public function create(array $data): Request
{ {
$checkedData = Types\MediaListEntry::check($data); $checkedData = Types\MediaListEntry::check($data);
return $this->mutateRequest('CreateMediaListEntry', $checkedData); return $this->requestBuilder->mutateRequest('CreateMediaListEntry', $checkedData);
} }
/** /**
@ -50,7 +50,7 @@ final class ListItem extends AbstractListItem {
public function createFull(array $data): Request public function createFull(array $data): Request
{ {
$checkedData = Types\MediaListEntry::check($data); $checkedData = Types\MediaListEntry::check($data);
return $this->mutateRequest('CreateFullMediaListEntry', $checkedData); return $this->requestBuilder->mutateRequest('CreateFullMediaListEntry', $checkedData);
} }
/** /**
@ -62,7 +62,7 @@ final class ListItem extends AbstractListItem {
*/ */
public function delete(string $id, string $type = 'anime'): Request public function delete(string $id, string $type = 'anime'): Request
{ {
return $this->mutateRequest('DeleteMediaListEntry', ['id' => $id]); return $this->requestBuilder->mutateRequest('DeleteMediaListEntry', ['id' => $id]);
} }
/** /**
@ -73,7 +73,7 @@ final class ListItem extends AbstractListItem {
*/ */
public function get(string $id): array public function get(string $id): array
{ {
return $this->runQuery('MediaListItem', ['id' => $id]); return $this->requestBuilder->runQuery('MediaListItem', ['id' => $id]);
} }
/** /**
@ -90,7 +90,7 @@ final class ListItem extends AbstractListItem {
'progress' => $data->progress, 'progress' => $data->progress,
]); ]);
return $this->mutateRequest('IncrementMediaListEntry', $checkedData); return $this->requestBuilder->mutateRequest('IncrementMediaListEntry', $checkedData);
} }
/** /**
@ -120,6 +120,6 @@ final class ListItem extends AbstractListItem {
'notes' => $notes, 'notes' => $notes,
]); ]);
return $this->mutateRequest('UpdateMediaListEntry', $updateData); return $this->requestBuilder->mutateRequest('UpdateMediaListEntry', $updateData);
} }
} }

View File

@ -89,7 +89,7 @@ final class Model
*/ */
public function checkAuth(): array public function checkAuth(): array
{ {
return $this->runQuery('CheckLogin'); return $this->requestBuilder->runQuery('CheckLogin');
} }
/** /**
@ -110,7 +110,7 @@ final class Model
throw new InvalidArgumentException('Anilist username is not defined in config'); throw new InvalidArgumentException('Anilist username is not defined in config');
} }
return $this->runQuery('SyncUserList', [ return $this->requestBuilder->runQuery('SyncUserList', [
'name' => $anilistUser, 'name' => $anilistUser,
'type' => $type, 'type' => $type,
]); ]);
@ -275,17 +275,25 @@ final class Model
* Get the Anilist list item id from the media id from its MAL id * Get the Anilist list item id from the media id from its MAL id
* this way is more accurate than getting the list item id * this way is more accurate than getting the list item id
* directly from the MAL id * directly from the MAL id
*
* @param string $mediaId
* @return string|null
*/ */
private function getListIdFromMediaId(string $mediaId): string private function getListIdFromMediaId(string $mediaId): ?string
{ {
$config = $this->container->get('config'); $config = $this->container->get('config');
$anilistUser = $config->get(['anilist', 'username']); $anilistUser = $config->get(['anilist', 'username']);
$info = $this->runQuery('ListItemIdByMediaId', [ $info = $this->requestBuilder->runQuery('ListItemIdByMediaId', [
'id' => $mediaId, 'id' => $mediaId,
'userName' => $anilistUser, 'userName' => $anilistUser,
]); ]);
if ( ! empty($info['errors']))
{
return NULL;
}
return (string)$info['data']['MediaList']['id']; return (string)$info['data']['MediaList']['id'];
} }
@ -303,7 +311,7 @@ final class Model
return NULL; return NULL;
} }
$info = $this->runQuery('MediaIdByMalId', [ $info = $this->requestBuilder->runQuery('MediaIdByMalId', [
'id' => $malId, 'id' => $malId,
'type' => mb_strtoupper($type), 'type' => mb_strtoupper($type),
]); ]);

View File

@ -388,6 +388,8 @@ final class KitsuRequestBuilder extends APIRequestBuilder {
Event::emit(EventType::UNAUTHORIZED); Event::emit(EventType::UNAUTHORIZED);
} }
$rawBody = wait($response->getBody()->buffer());
// Any other type of failed request // Any other type of failed request
if ($statusCode > 299 || $statusCode < 200) if ($statusCode > 299 || $statusCode < 200)
{ {

View File

@ -82,7 +82,7 @@ query ($slug: String!) {
canonical canonical
localized localized
} }
#slug slug
} }
role role
} }

View File

@ -82,7 +82,7 @@ query ($id: ID!) {
canonical canonical
localized localized
} }
#slug slug
} }
role role
} }

View File

@ -93,7 +93,7 @@ query ($slug: String!) {
canonical canonical
localized localized
} }
#slug slug
} }
role role
} }

View File

@ -93,7 +93,7 @@ query ($id: ID!) {
canonical canonical
localized localized
} }
#slug slug
} }
role role
} }

View File

@ -94,6 +94,7 @@ final class AnimeTransformer extends AbstractTransformer {
'image' => [ 'image' => [
'original' => $person['image']['original']['url'], 'original' => $person['image']['original']['url'],
], ],
'slug' => $person['slug'],
]; ];
usort($staff[$role], fn ($a, $b) => $a['name'] <=> $b['name']); usort($staff[$role], fn ($a, $b) => $a['name'] <=> $b['name']);

View File

@ -87,6 +87,7 @@ final class MangaTransformer extends AbstractTransformer {
$staff[$role][$person['id']] = [ $staff[$role][$person['id']] = [
'id' => $person['id'], 'id' => $person['id'],
'slug' => $person['slug'],
'name' => $name, 'name' => $name,
'image' => [ 'image' => [
'original' => $person['image']['original']['url'], 'original' => $person['image']['original']['url'],

View File

@ -147,13 +147,13 @@ abstract class BaseCommand extends Command {
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
$app_logger = new Logger('animeclient'); $app_logger = new Logger('animeclient');
$app_logger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/app-cli.log', Logger::NOTICE)); $app_logger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/app-cli.log', Logger::WARNING));
$kitsu_request_logger = new Logger('kitsu-request'); $kitsu_request_logger = new Logger('kitsu-request');
$kitsu_request_logger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/kitsu_request-cli.log', Logger::NOTICE)); $kitsu_request_logger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/kitsu_request-cli.log', Logger::WARNING));
$anilistRequestLogger = new Logger('anilist-request'); $anilistRequestLogger = new Logger('anilist-request');
$anilistRequestLogger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/anilist_request-cli.log', Logger::NOTICE)); $anilistRequestLogger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/anilist_request-cli.log', Logger::WARNING));
$container->setLogger($app_logger); $container->setLogger($app_logger);
$container->setLogger($anilistRequestLogger, 'anilist-request'); $container->setLogger($anilistRequestLogger, 'anilist-request');
@ -203,7 +203,7 @@ abstract class BaseCommand extends Command {
return $model; return $model;
}); });
$container->set('anilist-model', static function ($container): Anilist\Model { $container->set('anilist-model', static function ($container): Anilist\Model {
$requestBuilder = new Anilist\AnilistRequestBuilder(); $requestBuilder = new Anilist\AnilistRequestBuilder($container);
$requestBuilder->setLogger($container->getLogger('anilist-request')); $requestBuilder->setLogger($container->getLogger('anilist-request'));
$listItem = new Anilist\ListItem(); $listItem = new Anilist\ListItem();

View File

@ -51,11 +51,14 @@ final class People extends BaseController {
* Show information about a person * Show information about a person
* *
* @param string $id * @param string $id
* @param string|null $slug
* @return void * @return void
* @throws ContainerException
* @throws NotFoundException
*/ */
public function index(string $id): void public function index(string $id, ?string $slug = NULL): void
{ {
$rawData = $this->model->getPerson($id); $rawData = $this->model->getPerson($id, $slug);
$data = (new PersonTransformer())->transform($rawData)->toArray(); $data = (new PersonTransformer())->transform($rawData)->toArray();
if (( ! array_key_exists('data', $rawData)) || empty($rawData['data'])) if (( ! array_key_exists('data', $rawData)) || empty($rawData['data']))