new Deps
This commit is contained in:
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
preset: laravel
|
||||
risky: true
|
||||
enabled:
|
||||
- declare_strict_types
|
||||
- unalign_double_arrow
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Add `RequestParser` to convert an incoming HTTP request to one or more `OperationParams`
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019 Benedikt Franke
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
||||
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
# laragraph/utils
|
||||
|
||||
[](https://github.com/laragraph/utils/actions)
|
||||
[](https://codecov.io/gh/laragraph/utils)
|
||||
[](https://github.styleci.io/repos/228471198)
|
||||
|
||||
[](https://packagist.org/packages/laragraph/utils)
|
||||
[](https://packagist.org/packages/laragraph/utils)
|
||||
|
||||
Utilities for using GraphQL with Laravel
|
||||
|
||||
## Installation
|
||||
|
||||
Install through composer
|
||||
|
||||
```bash
|
||||
composer require laragraph/utils
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
This package holds basic utilities that are useful for building a GraphQL server with Laravel.
|
||||
If you want to build an application, we recommend using a full framework that integrates the
|
||||
primitives within this package:
|
||||
|
||||
- SDL-first: [Lighthouse](https://github.com/nuwave/lighthouse)
|
||||
- Code-first: [graphql-laravel](https://github.com/rebing/graphql-laravel)
|
||||
|
||||
## Changelog
|
||||
|
||||
See [`CHANGELOG.md`](CHANGELOG.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
See [`CONTRIBUTING.md`](.github/CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
This package is licensed using the MIT License.
|
||||
Vendored
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "laragraph/utils",
|
||||
"type": "library",
|
||||
"description": "Utilities for using GraphQL with Laravel",
|
||||
"homepage": "https://github.com/laragraph/utils",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Benedikt Franke",
|
||||
"email": "benedikt@franke.tech"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.2 || ^8.0",
|
||||
"illuminate/contracts": "5.6.* || 5.7.* || 5.8.* || ^6 || ^7 || ^8",
|
||||
"illuminate/http": "5.6.* || 5.7.* || 5.8.* || ^6 || ^7 || ^8",
|
||||
"thecodingmachine/safe": "^1.1",
|
||||
"webonyx/graphql-php": "^0.13.2 || ^14"
|
||||
},
|
||||
"require-dev": {
|
||||
"ergebnis/composer-normalize": "^2.11",
|
||||
"infection/infection": "~0.20",
|
||||
"jangregor/phpstan-prophecy": "^0.8.1",
|
||||
"orchestra/testbench": "3.6.* || 3.7.* || 3.8.* || 3.9.* || ^4 || ^5 || ^6",
|
||||
"phpstan/extension-installer": "^1.0",
|
||||
"phpstan/phpstan": "^0.12.57",
|
||||
"phpstan/phpstan-deprecation-rules": "^0.12.5",
|
||||
"phpstan/phpstan-strict-rules": "^0.12.5",
|
||||
"phpunit/phpunit": "^7.5 || ^8.5",
|
||||
"thecodingmachine/phpstan-safe-rule": "^1.0"
|
||||
},
|
||||
"config": {
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laragraph\\Utils\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Laragraph\\Utils\\Tests\\": "tests/"
|
||||
},
|
||||
"files": [
|
||||
"vendor/symfony/var-dumper/Resources/functions/dump.php"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"support": {
|
||||
"issues": "https://github.com/laragraph/utils/issues",
|
||||
"source": "https://github.com/laragraph/utils"
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laragraph\Utils;
|
||||
|
||||
use GraphQL\Server\Helper;
|
||||
use GraphQL\Server\OperationParams;
|
||||
use GraphQL\Server\RequestError;
|
||||
use GraphQL\Utils\Utils;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class RequestParser
|
||||
{
|
||||
/**
|
||||
* @var \GraphQL\Server\Helper
|
||||
*/
|
||||
protected $helper;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->helper = new Helper();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an incoming HTTP request to one or more OperationParams.
|
||||
*
|
||||
* @return \GraphQL\Server\OperationParams|array<int, \GraphQL\Server\OperationParams>
|
||||
*
|
||||
* @throws \GraphQL\Server\RequestError
|
||||
*/
|
||||
public function parseRequest(Request $request)
|
||||
{
|
||||
$method = $request->getMethod();
|
||||
$bodyParams = [];
|
||||
/** @var array<string, mixed> $queryParams */
|
||||
$queryParams = $request->query();
|
||||
|
||||
if ($method === 'POST') {
|
||||
/**
|
||||
* Never null, since Symfony defaults to application/x-www-form-urlencoded.
|
||||
*
|
||||
* @var string $contentType
|
||||
*/
|
||||
$contentType = $request->header('Content-Type');
|
||||
|
||||
if (stripos($contentType, 'application/json') !== false) {
|
||||
/** @var string $content */
|
||||
$content = $request->getContent();
|
||||
$bodyParams = \Safe\json_decode($content, true);
|
||||
|
||||
if (! is_array($bodyParams)) {
|
||||
throw new RequestError(
|
||||
'GraphQL Server expects JSON object or array, but got '.
|
||||
Utils::printSafeJson($bodyParams)
|
||||
);
|
||||
}
|
||||
} elseif (stripos($contentType, 'application/graphql') !== false) {
|
||||
/** @var string $content */
|
||||
$content = $request->getContent();
|
||||
$bodyParams = ['query' => $content];
|
||||
} elseif (stripos($contentType, 'application/x-www-form-urlencoded') !== false) {
|
||||
/** @var array<string, mixed> $bodyParams */
|
||||
$bodyParams = $request->post();
|
||||
} elseif (stripos($contentType, 'multipart/form-data') !== false) {
|
||||
$bodyParams = $this->inlineFiles($request);
|
||||
} else {
|
||||
throw new RequestError('Unexpected content type: '.Utils::printSafeJson($contentType));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->helper->parseRequestParams($method, $bodyParams, $queryParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline file uploads given through a multipart request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array<mixed>
|
||||
*/
|
||||
protected function inlineFiles(Request $request): array
|
||||
{
|
||||
/** @var string|null $mapParam */
|
||||
$mapParam = $request->post('map');
|
||||
if ($mapParam === null) {
|
||||
throw new RequestError(
|
||||
'Could not find a valid map, be sure to conform to GraphQL multipart request specification: https://github.com/jaydenseric/graphql-multipart-request-spec'
|
||||
);
|
||||
}
|
||||
|
||||
/** @var string|null $operationsParam */
|
||||
$operationsParam = $request->post('operations');
|
||||
if ($operationsParam === null) {
|
||||
throw new RequestError(
|
||||
'Could not find valid operations, be sure to conform to GraphQL multipart request specification: https://github.com/jaydenseric/graphql-multipart-request-spec'
|
||||
);
|
||||
}
|
||||
|
||||
/** @var array<string, mixed>|array<int, array<string, mixed>> $operations */
|
||||
$operations = \Safe\json_decode($operationsParam, true);
|
||||
|
||||
/** @var array<int|string, array<int, string>> $map */
|
||||
$map = \Safe\json_decode($mapParam, true);
|
||||
|
||||
foreach ($map as $fileKey => $operationsPaths) {
|
||||
/** @var array<string> $operationsPaths */
|
||||
$file = $request->file((string) $fileKey);
|
||||
|
||||
/** @var string $operationsPath */
|
||||
foreach ($operationsPaths as $operationsPath) {
|
||||
Arr::set($operations, $operationsPath, $file);
|
||||
}
|
||||
}
|
||||
|
||||
return $operations;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laragraph\Utils\Tests\Unit;
|
||||
|
||||
use GraphQL\Server\RequestError;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Laragraph\Utils\RequestParser;
|
||||
use Orchestra\Testbench\TestCase;
|
||||
use Safe\Exceptions\JsonException;
|
||||
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
|
||||
|
||||
class RequestParserTest extends TestCase
|
||||
{
|
||||
public function testGetWithQuery(): void
|
||||
{
|
||||
$query = /** @lang GraphQL */ '{ foo }';
|
||||
$request = $this->makeRequest('GET', ['query' => $query]);
|
||||
|
||||
$parser = new RequestParser();
|
||||
/** @var \GraphQL\Server\OperationParams $params */
|
||||
$params = $parser->parseRequest($request);
|
||||
|
||||
self::assertSame($query, $params->query);
|
||||
}
|
||||
|
||||
public function testPostWithJson(): void
|
||||
{
|
||||
$query = /** @lang GraphQL */ '{ foo }';
|
||||
$request = $this->makeRequest(
|
||||
'POST',
|
||||
[],
|
||||
[],
|
||||
['Content-Type' => 'application/json'],
|
||||
\Safe\json_encode(['query' => $query])
|
||||
);
|
||||
|
||||
$parser = new RequestParser();
|
||||
/** @var \GraphQL\Server\OperationParams $params */
|
||||
$params = $parser->parseRequest($request);
|
||||
|
||||
self::assertSame($query, $params->query);
|
||||
}
|
||||
|
||||
public function testPostWithQueryApplicationGraphQL(): void
|
||||
{
|
||||
$query = /** @lang GraphQL */ '{ foo }';
|
||||
$request = $this->makeRequest(
|
||||
'POST',
|
||||
[],
|
||||
[],
|
||||
['Content-Type' => 'application/graphql'],
|
||||
$query
|
||||
);
|
||||
|
||||
$parser = new RequestParser();
|
||||
/** @var \GraphQL\Server\OperationParams $params */
|
||||
$params = $parser->parseRequest($request);
|
||||
|
||||
self::assertSame($query, $params->query);
|
||||
}
|
||||
|
||||
public function testPostWithRegularForm(): void
|
||||
{
|
||||
$query = /** @lang GraphQL */ '{ foo }';
|
||||
$request = $this->makeRequest(
|
||||
'POST',
|
||||
['query' => $query],
|
||||
[],
|
||||
['Content-Type' => 'application/x-www-form-urlencoded']
|
||||
);
|
||||
|
||||
$parser = new RequestParser();
|
||||
/** @var \GraphQL\Server\OperationParams $params */
|
||||
$params = $parser->parseRequest($request);
|
||||
|
||||
self::assertSame($query, $params->query);
|
||||
}
|
||||
|
||||
public function testPostDefaultsToRegularForm(): void
|
||||
{
|
||||
$query = /** @lang GraphQL */ '{ foo }';
|
||||
$request = $this->makeRequest(
|
||||
'POST',
|
||||
['query' => $query]
|
||||
);
|
||||
|
||||
$parser = new RequestParser();
|
||||
/** @var \GraphQL\Server\OperationParams $params */
|
||||
$params = $parser->parseRequest($request);
|
||||
|
||||
self::assertSame($query, $params->query);
|
||||
}
|
||||
|
||||
public function testNonSensicalContentType(): void
|
||||
{
|
||||
$request = $this->makeRequest(
|
||||
'POST',
|
||||
[],
|
||||
[],
|
||||
['Content-Type' => 'foobar']
|
||||
);
|
||||
|
||||
$parser = new RequestParser();
|
||||
$this->expectException(RequestError::class);
|
||||
$parser->parseRequest($request);
|
||||
}
|
||||
|
||||
public function testNoQuery(): void
|
||||
{
|
||||
$request = $this->makeRequest('GET');
|
||||
|
||||
$parser = new RequestParser();
|
||||
/** @var \GraphQL\Server\OperationParams $params */
|
||||
$params = $parser->parseRequest($request);
|
||||
|
||||
self::assertSame(null, $params->query);
|
||||
}
|
||||
|
||||
public function testInvalidJson(): void
|
||||
{
|
||||
$request = $this->makeRequest(
|
||||
'POST',
|
||||
[],
|
||||
[],
|
||||
['Content-Type' => 'application/json'],
|
||||
'this is not valid json'
|
||||
);
|
||||
|
||||
$parser = new RequestParser();
|
||||
$this->expectException(JsonException::class);
|
||||
$parser->parseRequest($request);
|
||||
}
|
||||
|
||||
public function testNonArrayJson(): void
|
||||
{
|
||||
$request = $this->makeRequest(
|
||||
'POST',
|
||||
[],
|
||||
[],
|
||||
['Content-Type' => 'application/json'],
|
||||
'"this should be a map with query, variables, etc."'
|
||||
);
|
||||
|
||||
$parser = new RequestParser();
|
||||
$this->expectException(RequestError::class);
|
||||
$parser->parseRequest($request);
|
||||
}
|
||||
|
||||
public function testMultipartFormRequest(): void
|
||||
{
|
||||
$file = UploadedFile::fake()->create('image.jpg', 500);
|
||||
|
||||
$request = $this->makeRequest(
|
||||
'POST',
|
||||
[
|
||||
'operations' => /** @lang JSON */ '
|
||||
{
|
||||
"query": "mutation Upload($file: Upload!) { upload(file: $file) }",
|
||||
"variables": {
|
||||
"file": null
|
||||
}
|
||||
}
|
||||
',
|
||||
'map' => /** @lang JSON */ '
|
||||
{
|
||||
"0": ["variables.file"]
|
||||
}
|
||||
',
|
||||
],
|
||||
[
|
||||
'0' => $file,
|
||||
],
|
||||
[
|
||||
'Content-Type' => 'multipart/form-data',
|
||||
]
|
||||
);
|
||||
|
||||
$parser = new RequestParser();
|
||||
/** @var \GraphQL\Server\OperationParams $params */
|
||||
$params = $parser->parseRequest($request);
|
||||
|
||||
self::assertSame('mutation Upload($file: Upload!) { upload(file: $file) }', $params->query);
|
||||
|
||||
$variables = $params->variables;
|
||||
self::assertNotNull($variables);
|
||||
/** @var array<string, mixed> $variables */
|
||||
self::assertSame($file, $variables['file']);
|
||||
}
|
||||
|
||||
public function testMultipartFormWithoutMap(): void
|
||||
{
|
||||
$request = $this->makeRequest(
|
||||
'POST',
|
||||
[],
|
||||
[],
|
||||
[
|
||||
'Content-Type' => 'multipart/form-data',
|
||||
]
|
||||
);
|
||||
|
||||
$parser = new RequestParser();
|
||||
$this->expectException(RequestError::class);
|
||||
$parser->parseRequest($request);
|
||||
}
|
||||
|
||||
public function testMultipartFormWithoutOperations(): void
|
||||
{
|
||||
$request = $this->makeRequest(
|
||||
'POST',
|
||||
[
|
||||
'map' => /** @lang JSON */ '
|
||||
{
|
||||
"0": ["variables.file"]
|
||||
}
|
||||
',
|
||||
],
|
||||
[],
|
||||
[
|
||||
'Content-Type' => 'multipart/form-data',
|
||||
]
|
||||
);
|
||||
|
||||
$parser = new RequestParser();
|
||||
$this->expectException(RequestError::class);
|
||||
$parser->parseRequest($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $method
|
||||
* @param array<mixed> $parameters
|
||||
* @param array<mixed> $files
|
||||
* @param array<mixed> $headers
|
||||
* @param string|resource|null $content
|
||||
* @return \Illuminate\Http\Request
|
||||
*/
|
||||
public function makeRequest(string $method, array $parameters = [], array $files = [], array $headers = [], $content = null): Request
|
||||
{
|
||||
$symfonyRequest = SymfonyRequest::create(
|
||||
'http://foo.bar/graphql',
|
||||
$method,
|
||||
$parameters,
|
||||
[],
|
||||
$files,
|
||||
$this->transformHeadersToServerVars($headers),
|
||||
$content
|
||||
);
|
||||
|
||||
return Request::createFromBase($symfonyRequest);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user