Skip to content

Commit

Permalink
Add apollo-upload-server supports
Browse files Browse the repository at this point in the history
  • Loading branch information
mcg-web committed Feb 1, 2018
1 parent 4b752f1 commit 78c88ca
Show file tree
Hide file tree
Showing 27 changed files with 858 additions and 39 deletions.
7 changes: 4 additions & 3 deletions Config/CustomScalarTypeDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ public function getDefinition()
->children()
->append($this->nameSection())
->append($this->descriptionSection())
->variableNode('serialize')->isRequired()->end()
->variableNode('parseValue')->isRequired()->end()
->variableNode('parseLiteral')->isRequired()->end()
->variableNode('scalarType')->end()
->variableNode('serialize')->end()
->variableNode('parseValue')->end()
->variableNode('parseLiteral')->end()
->end();

return $node;
Expand Down
88 changes: 88 additions & 0 deletions Definition/Type/CustomScalarType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

namespace Overblog\GraphQLBundle\Definition\Type;

use GraphQL\Type\Definition\CustomScalarType as BaseCustomScalarType;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Utils\Utils;

class CustomScalarType extends BaseCustomScalarType
{
public function __construct(array $config = [])
{
$config['name'] = isset($config['name']) ? $config['name'] : uniqid('CustomScalar');
parent::__construct($config);

$this->config['scalarType'] = isset($this->config['scalarType']) ? $this->config['scalarType'] : null;
$this->initOptionalFunctions();
}

/**
* {@inheritdoc}
*/
public function serialize($value)
{
return $this->call('serialize', $value);
}

/**
* {@inheritdoc}
*/
public function parseValue($value)
{
return $this->call('parseValue', $value);
}

/**
* {@inheritdoc}
*/
public function parseLiteral(/* GraphQL\Language\AST\ValueNode */ $valueNode)
{
return $this->call('parseLiteral', $valueNode);
}

private function call($type, $value)
{
if (isset($this->config['scalarType'])) {
$scalarType = $this->config['scalarType'];
$scalarType = is_callable($scalarType) ? $scalarType() : $scalarType;

return call_user_func([$scalarType, $type], $value);
} else {
return parent::$type($value);
}
}

private function initOptionalFunctions()
{
foreach (['parseLiteral', 'parseValue'] as $field) {
if (!isset($this->config[$field])) {
$this->config[$field] = static function () {
return null;
};
}
}
}

public function assertValid()
{
if (isset($this->config['scalarType'])) {
$scalarType = $this->config['scalarType'];
if (is_callable($scalarType)) {
$scalarType = $scalarType();
}

Utils::invariant(
$scalarType instanceof ScalarType,
sprintf(
'%s must provide a valid "scalarType" instance of %s but got: %s',
$this->name,
ScalarType::class,
Utils::printSafe($scalarType)
)
);
} else {
parent::assertValid();
}
}
}
8 changes: 6 additions & 2 deletions Definition/Type/SchemaDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace Overblog\GraphQLBundle\Definition\Type;

use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType;
Expand Down Expand Up @@ -84,7 +83,12 @@ private function decorateInterfaceOrUnionType($type, ResolverMapInterface $resol

private function decorateCustomScalarType(CustomScalarType $type, ResolverMapInterface $resolverMap)
{
static $allowedFields = [ResolverMapInterface::SERIALIZE, ResolverMapInterface::PARSE_VALUE, ResolverMapInterface::PARSE_LITERAL];
static $allowedFields = [
ResolverMapInterface::SCALAR_TYPE,
ResolverMapInterface::SERIALIZE,
ResolverMapInterface::PARSE_VALUE,
ResolverMapInterface::PARSE_LITERAL,
];

foreach ($allowedFields as $fieldName) {
$this->configTypeMapping($type, $resolverMap, $fieldName);
Expand Down
20 changes: 20 additions & 0 deletions Generator/TypeGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Composer\Autoload\ClassLoader;
use Overblog\GraphQLBundle\Config\Processor;
use Overblog\GraphQLBundle\Definition\Argument;
use Overblog\GraphQLBundle\Definition\Type\CustomScalarType;
use Overblog\GraphQLGenerator\Generator\TypeGenerator as BaseTypeGenerator;
use Symfony\Component\Filesystem\Filesystem;

Expand Down Expand Up @@ -141,6 +142,25 @@ function ($childrenComplexity, $args = []) <closureUseStatements>{
return $code;
}

/**
* @param array $value
*
* @return string
*/
protected function generateScalarType(array $value)
{
return $this->callableCallbackFromArrayValue($value, 'scalarType');
}

protected function generateParentClassName(array $config)
{
if ('custom-scalar' === $config['type']) {
return $this->shortenClassName(CustomScalarType::class);
} else {
return parent::generateParentClassName($config);
}
}

public function compile($mode)
{
$cacheDir = $this->getCacheDir();
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ OverblogGraphQLBundle

This Symfony bundle provides integration of [GraphQL](https://facebook.github.io/graphql/) using [webonyx/graphql-php](https://github.com/webonyx/graphql-php)
and [GraphQL Relay](https://facebook.github.io/relay/docs/graphql-relay-specification.html).
It also supports batching using libs like [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer) or [Apollo GraphQL](http://dev.apollodata.com/core/network.html#query-batching).
It also supports:
* batching with [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer)
* batching with [Apollo GraphQL](http://dev.apollodata.com/core/network.html#query-batching).
* upload and batching upload with [apollo-upload-client](https://github.com/jaydenseric/apollo-upload-client)

[![Build Status](https://travis-ci.org/overblog/GraphQLBundle.svg?branch=master)](https://travis-ci.org/overblog/GraphQLBundle)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/overblog/GraphQLBundle/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/overblog/GraphQLBundle/?branch=master)
Expand Down
28 changes: 21 additions & 7 deletions Request/BatchParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

class BatchParser implements ParserInterface
{
use UploadParserTrait;

const PARAM_ID = 'id';

private static $queriesDefaultValue = [
Expand Down Expand Up @@ -49,17 +51,29 @@ public function parse(Request $request)
*/
private function getParsedBody(Request $request)
{
$type = explode(';', $request->headers->get('content-type'))[0];
$contentType = explode(';', $request->headers->get('content-type'))[0];

// JSON object
if ($type !== static::CONTENT_TYPE_JSON) {
throw new BadRequestHttpException(sprintf('Only request with content type "%s" is accepted.', static::CONTENT_TYPE_JSON));
}
switch ($contentType) {
case static::CONTENT_TYPE_JSON:
$parsedBody = json_decode($request->getContent(), true);

if (JSON_ERROR_NONE !== json_last_error()) {
throw new BadRequestHttpException('POST body sent invalid JSON');
}
break;

$parsedBody = json_decode($request->getContent(), true);
case static::CONTENT_TYPE_FORM_DATA:
$parsedBody = $this->treatUploadFiles($request->request->all(), $request->files->all());
break;

if (JSON_ERROR_NONE !== json_last_error()) {
throw new BadRequestHttpException('POST body sent invalid JSON');
default:
throw new BadRequestHttpException(sprintf(
'Batching parser only accepts "%s" or "%s" content-type but got %s.',
static::CONTENT_TYPE_JSON,
static::CONTENT_TYPE_FORM_DATA,
json_encode($contentType)
));
}

return $parsedBody;
Expand Down
4 changes: 3 additions & 1 deletion Request/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

class Parser implements ParserInterface
{
use UploadParserTrait;

/**
* @param Request $request
*
Expand Down Expand Up @@ -55,7 +57,7 @@ private function getParsedBody(Request $request)
// URL-encoded query-string
case static::CONTENT_TYPE_FORM:
case static::CONTENT_TYPE_FORM_DATA:
$parsedBody = $request->request->all();
$parsedBody = $this->treatUploadFiles($request->request->all(), $request->files->all());
break;

default:
Expand Down
64 changes: 64 additions & 0 deletions Request/UploadParserTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

namespace Overblog\GraphQLBundle\Request;

use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\PropertyAccess\PropertyAccess;

trait UploadParserTrait
{
/**
* @param array $operations
* @param array $map
* @param array $files
*
* @return array
*/
protected function mappingUploadFiles(array $operations, array $map, array $files)
{
$accessor = PropertyAccess::createPropertyAccessorBuilder()
->enableExceptionOnInvalidIndex()
->getPropertyAccessor();

foreach ($map as $fileName => $locations) {
foreach ($locations as $location) {
$fileKey = sprintf('[%s]', $fileName);
if (!$accessor->isReadable($files, $fileKey)) {
throw new BadRequestHttpException(sprintf('File %s is missing in the request.', json_encode($fileName)));
}
$file = $accessor->getValue($files, $fileKey);
$locationKey = $this->locationToPropertyAccessPath($location);
if (!$accessor->isReadable($operations, $locationKey)) {
throw new BadRequestHttpException(sprintf('Map entry %s could not be localized in operations.', json_encode($location)));
}
$accessor->setValue($operations, $locationKey, $file);
}
}

return $operations;
}

protected function locationToPropertyAccessPath($location)
{
return array_reduce(
explode('.', $location),
function ($carry, $item) {
return sprintf('%s[%s]', $carry, $item);
}
);
}

protected function isUploadPayload(array $payload)
{
return isset($payload['operations']) && isset($payload['map']) && is_array($payload['operations']) && is_array($payload['map']);
}

protected function treatUploadFiles(array $parsedBody, array $files)
{
if ($this->isUploadPayload($parsedBody)) {
return $this->mappingUploadFiles($parsedBody['operations'], $parsedBody['map'], $files);
} else {
return $parsedBody;
}
}
}
1 change: 1 addition & 0 deletions Resolver/ResolverMapInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface ResolverMapInterface
const RESOLVE_FIELD = '__resolveField';
const IS_TYPEOF = '__isTypeOf';
// custom scalar
const SCALAR_TYPE = '__scalarType';
const SERIALIZE = '__serialize';
const PARSE_VALUE = '__parseValue';
const PARSE_LITERAL = '__parseLiteral';
Expand Down
11 changes: 8 additions & 3 deletions Resources/doc/definitions/resolver-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@ and override `map` method and return an `array` or any `ArrayAccess` and `Traver
- `Overblog\GraphQLBundle\Resolver\ResolverMapInterface::RESOLVE_FIELD` equivalent to `resolveField`.
- `Overblog\GraphQLBundle\Resolver\ResolverMapInterface::IS_TYPE_OF` equivalent to `isTypeOf`.
* [Custom scalar](type-system/scalars.md#custom-scalar) type
- `Overblog\GraphQLBundle\Resolver\ResolverMapInterface::SERIALIZE` equivalent to `serialize`
- `Overblog\GraphQLBundle\Resolver\ResolverMapInterface::PARSE_VALUE` equivalent to `parseValue`
- `Overblog\GraphQLBundle\Resolver\ResolverMapInterface::PARSE_LITERAL` equivalent to `parseLiteral`
- Direct usage:
- `Overblog\GraphQLBundle\Resolver\ResolverMapInterface::SERIALIZE` equivalent to `serialize`
- `Overblog\GraphQLBundle\Resolver\ResolverMapInterface::PARSE_VALUE` equivalent to `parseValue`
- `Overblog\GraphQLBundle\Resolver\ResolverMapInterface::PARSE_LITERAL` equivalent to `parseLiteral`
- Reusing an existing scalar type
- `Overblog\GraphQLBundle\Resolver\ResolverMapInterface::SCALAR_TYPE` equivalent to `scalarType`

Usage
-----
Expand Down Expand Up @@ -109,6 +112,8 @@ class MyResolverMap extends ResolverMap
return str_replace(' Formatted Baz', '', $valueNode->value);
},
],
// or reuse an existing scalar (note: description and name will be override by decorator)
//'Baz' => [self::SCALAR_TYPE => function () { return new FooScalarType(); }],
];
}
}
Expand Down
9 changes: 9 additions & 0 deletions Resources/doc/definitions/type-system/scalars.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,12 @@ class DateTimeType
}
}
```

If you prefer reusing a scalar type

```yaml
MyEmail:
type: custom-scalar
config:
scalarType: '@=newObject("App\\Type\\EmailType")'
```
Loading

0 comments on commit 78c88ca

Please sign in to comment.