GraphiQL in Backend

This commit is contained in:
Gogs
2021-06-02 15:46:20 +02:00
parent e7d7d18f77
commit a65593b4f9
24 changed files with 61895 additions and 617 deletions
+11 -13
View File
@@ -6,19 +6,17 @@ use Illuminate\Http\Request;
use Nuwave\Lighthouse\Support\Contracts\CreatesContext as LighthouseCreatesContext;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class CreatesContext implements LighthouseCreatesContext
{
/**
* Generate GraphQL context.
*
* @param Request $request
*
* @return GraphQLContext
*/
public function generate(Request $request): GraphQLContext
{
return new SchemaContext($request);
}
/**
* Generate GraphQL context.
*
* @param Request $request
*
* @return GraphQLContext
*/
public function generate(Request $request): GraphQLContext
{
return new SchemaContext($request);
}
}
+23 -31
View File
@@ -5,42 +5,34 @@ namespace GermanAirlinesVa\Graphql\Classes;
use Lang;
use Cms\Classes\Page;
class Graph extends Page
{
/**
* @var string The container name associated with the model.
*/
protected $dirName = 'graphs';
/**
* @var string The container name associated with the model.
*/
protected $dirName = 'graphs';
/**
* @var array The attributes that are mass assignable.
*/
protected $fillable = ['title', 'description', 'markup', 'settings', 'code'];
/**
* @var array The attributes that are mass assignable.
*/
protected $fillable = [
'title',
'description',
'markup',
'settings',
'code'
];
/**
* @var array The rules to be applied to the data.
*/
public $rules = [
'title' => 'required',
];
/**
* @var array The rules to be applied to the data.
*/
public $rules = [
'title' => 'required'
];
public function getCodeClassParent(): string
{
return GraphCode::class;
}
public function getCodeClassParent() : string {
return GraphCode::class;
public function runComponents()
{
foreach ($this->components as $component) {
$component->onRun();
}
public function runComponents()
{
foreach ($this->components as $component) {
$component->onRun();
}
}
}
}
+32 -34
View File
@@ -8,48 +8,46 @@ use October\Rain\Extension\Extendable;
*/
class GraphCode extends CodeBase
{
public $graph;
public $graph;
/**
* Creates the object instance.
* @param \GermanAirlinesVa\Graphql\Classes\Graph $graph Specifies the Headstart graph.
* @param null
* @param \GermanAirlinesVa\Graphql\Classes\GraphController $controller Specifies the Graph controller.
*/
public function __construct($graph, $layout, $controller)
{
$this->graph = $graph;
$this->controller = $controller;
/**
* Creates the object instance.
* @param \GermanAirlinesVa\Graphql\Classes\Graph $graph Specifies the Headstart graph.
* @param null
* @param \GermanAirlinesVa\Graphql\Classes\GraphController $controller Specifies the Graph controller.
*/
public function __construct($graph, $layout, $controller)
{
$this->graph = $graph;
$this->controller = $controller;
Extendable::__construct();
}
Extendable::__construct();
public function __get($name)
{
if (isset($this->graph->components[$name]) || isset($this->layout->components[$name])) {
return $this[$name];
}
public function __get($name)
{
if (isset($this->graph->components[$name]) || isset($this->layout->components[$name])) {
return $this[$name];
}
if (($value = $this->graph->{$name}) !== null) {
return $value;
}
if (array_key_exists($name, $this->controller->vars)) {
return $this[$name];
}
return null;
if (($value = $this->graph->{$name}) !== null) {
return $value;
}
public function __set($name, $value)
{
return $this->graph->{$name} = $value;
if (array_key_exists($name, $this->controller->vars)) {
return $this[$name];
}
public function __isset($name)
{
return isset($this->graph->{$name});
}
return null;
}
public function __set($name, $value)
{
return $this->graph->{$name} = $value;
}
public function __isset($name)
{
return isset($this->graph->{$name});
}
}
+86 -88
View File
@@ -8,111 +8,109 @@ use Cms\Classes\Controller;
use Cms\Classes\CmsException;
use Cms\Classes\ComponentManager;
class GraphController// extends Controller
class GraphController // extends Controller
{
protected $graph;
protected $graph;
/**
* @var self Cache of self
*/
protected static $instance;
/**
* @var self Cache of self
*/
protected static $instance;
/**
* Creates the controller.
*/
public function __construct($graph, $args)
{
$this->graph = $graph;
$this->router = new GraphRouter($args);
self::$instance = $this;
}
/**
* Creates the controller.
*/
public function __construct($graph, $args)
{
$this->graph = $graph;
$this->router = new GraphRouter($args);
/**
* Returns an existing instance of the controller.
* If the controller doesn't exists, returns null.
* @return mixed Returns the controller object or null.
*/
public static function getController()
{
return self::$instance;
}
self::$instance = $this;
public function component($alias)
{
if (isset($this->graph->components[$alias])) {
return $this->graph->components[$alias];
}
/**
* Returns an existing instance of the controller.
* If the controller doesn't exists, returns null.
* @return mixed Returns the controller object or null.
*/
public static function getController()
{
return self::$instance;
}
foreach ($this->graph->settings['components'] as $component => $properties) {
[$name, $component_alias] = strpos($component, ' ') ? explode(' ', $component) : [$component, $component];
public function component($alias) {
if (isset($this->graph->components[$alias])) {
return $this->graph->components[$alias];
if ($component_alias == $alias) {
// make component
$manager = ComponentManager::instance();
if (!($componentObj = $manager->makeComponent($name, null, $properties))) {
throw new CmsException(
Lang::get('cms::lang.component.not_found', [
'name' => $name,
])
);
}
foreach ($this->graph->settings['components'] as $component => $properties) {
list($name, $component_alias) = strpos($component, ' ')
? explode(' ', $component)
: [$component, $component];
$this->setComponentPropertiesFromParams($componentObj, $this->router->getParameters());
$componentObj->init();
if ($component_alias == $alias) {
// make component
$manager = ComponentManager::instance();
$componentObj->alias = $alias;
$this->graph->components[$alias] = $componentObj;
if (!$componentObj = $manager->makeComponent($name, null, $properties)) {
throw new CmsException(Lang::get('cms::lang.component.not_found', ['name' => $name]));
}
return $componentObj;
}
}
}
$this->setComponentPropertiesFromParams($componentObj, $this->router->getParameters());
$componentObj->init();
/**
* Sets component property values from partial parameters.
* The property values should be defined as {{ param }}.
* @param ComponentBase $component The component object.
* @param array $parameters Specifies the partial parameters.
*/
protected function setComponentPropertiesFromParams($component, $parameters = [])
{
$properties = $component->getProperties();
$routerParameters = $this->router->getParameters();
$componentObj->alias = $alias;
$this->graph->components[$alias] = $componentObj;
foreach ($properties as $propertyName => $propertyValue) {
if (is_array($propertyValue)) {
continue;
}
return $componentObj;
}
$matches = [];
if (preg_match('/^\{\{([^\}]+)\}\}$/', $propertyValue, $matches)) {
$paramName = trim($matches[1]);
if (substr($paramName, 0, 1) == ':') {
$routeParamName = substr($paramName, 1);
$newPropertyValue = $routerParameters[$routeParamName] ?? null;
} else {
$newPropertyValue = $parameters[$paramName] ?? null;
}
$component->setProperty($propertyName, $newPropertyValue);
$component->setExternalPropertyName($propertyName, $paramName);
}
}
}
/**
* Sets component property values from partial parameters.
* The property values should be defined as {{ param }}.
* @param ComponentBase $component The component object.
* @param array $parameters Specifies the partial parameters.
*/
protected function setComponentPropertiesFromParams($component, $parameters = [])
{
$properties = $component->getProperties();
$routerParameters = $this->router->getParameters();
foreach ($properties as $propertyName => $propertyValue) {
if (is_array($propertyValue)) {
continue;
}
$matches = [];
if (preg_match('/^\{\{([^\}]+)\}\}$/', $propertyValue, $matches)) {
$paramName = trim($matches[1]);
if (substr($paramName, 0, 1) == ':') {
$routeParamName = substr($paramName, 1);
$newPropertyValue = $routerParameters[$routeParamName] ?? null;
}
else {
$newPropertyValue = $parameters[$paramName] ?? null;
}
$component->setProperty($propertyName, $newPropertyValue);
$component->setExternalPropertyName($propertyName, $paramName);
}
}
}
/**
* Returns a routing parameter.
* @param string $name Routing parameter name.
* @param string $default Default to use if none is found.
* @return string
*/
public function param($name, $default = null)
{
return $this->router->getParameter($name, $default);
}
/**
* Returns a routing parameter.
* @param string $name Routing parameter name.
* @param string $default Default to use if none is found.
* @return string
*/
public function param($name, $default = null)
{
return $this->router->getParameter($name, $default);
}
}
+27 -30
View File
@@ -7,38 +7,35 @@ use Cms\Classes\Router;
class GraphRouter //extends Router
{
/**
* @var array A list of parameters names and values extracted from the URL pattern and URL string.
*/
protected $parameters = [];
/**
* @var array A list of parameters names and values extracted from the URL pattern and URL string.
*/
protected $parameters = [];
public function __construct($parameters)
{
$this->parameters = $parameters;
}
public function __construct($parameters)
{
$this->parameters = $parameters;
}
/**
* Returns the current routing parameters.
* @return array
*/
public function getParameters()
{
return $this->parameters;
}
/**
* Returns a routing parameter.
* @return array
*/
public function getParameter($name, $default = null)
{
if (isset($this->parameters[$name]) && !empty($this->parameters[$name])) {
return $this->parameters[$name];
}
return $default;
/**
* Returns the current routing parameters.
* @return array
*/
public function getParameters()
{
return $this->parameters;
}
/**
* Returns a routing parameter.
* @return array
*/
public function getParameter($name, $default = null)
{
if (isset($this->parameters[$name]) && !empty($this->parameters[$name])) {
return $this->parameters[$name];
}
return $default;
}
}
+8 -10
View File
@@ -9,14 +9,12 @@ use GermanAirlinesVa\Graphql\Classes\CreatesContext as GraphqlCreatesContext;
use October\Rain\Support\ServiceProvider;
class GraphqlServiceProvider extends ServiceProvider {
public function register()
{
$this->app->bind(ProvidesResolver::class, GraphqlProvidesResolver::class);
$this->app->singleton(CreatesContext::class, GraphqlCreatesContext::class);
$this->app->singleton(SchemaSourceProvider::class, GraphqlSchemaSourceProvider::class);
}
class GraphqlServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(ProvidesResolver::class, GraphqlProvidesResolver::class);
$this->app->singleton(CreatesContext::class, GraphqlCreatesContext::class);
$this->app->singleton(SchemaSourceProvider::class, GraphqlSchemaSourceProvider::class);
}
}
+26 -28
View File
@@ -6,34 +6,32 @@ use GraphQL\Type\Definition\ResolveInfo;
use Cms\Classes\CodeParser;
use Nuwave\Lighthouse\Schema\ResolverProvider as LighthouseResolverProvider;
class ResolverProvider extends LighthouseResolverProvider
{
/**
* Provide a field resolver in case no resolver directive is defined for a field.
*
* @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* @return \Closure
*/
public function provideResolver(FieldValue $fieldValue): Closure
{
return function ($root, array $args, $context, ResolveInfo $resolveInfo) use ($fieldValue) {
$fieldName = $fieldValue->getFieldName();
// use local graph resolver in code section if existent
if ($graphObj = $context->source->findGraph($fieldName)) {
/* @var $graphObj \GermanAirlinesVa\Graphql\Classes\Graph */
$parser = new CodeParser($graphObj);
$codeObj = $parser->source($graphObj, null, new GraphController($graphObj, $args));
$resolveMethod = 'resolve' . studly_case($fieldName);
class ResolverProvider extends LighthouseResolverProvider {
/**
* Provide a field resolver in case no resolver directive is defined for a field.
*
* @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* @return \Closure
*/
public function provideResolver(FieldValue $fieldValue): Closure
{
return function ($root, array $args, $context, ResolveInfo $resolveInfo) use ($fieldValue) {
$fieldName = $fieldValue->getFieldName();
// use local graph resolver in code section if existent
if ($graphObj = $context->source->findGraph($fieldName)) {
/* @var $graphObj \GermanAirlinesVa\Graphql\Classes\Graph */
$parser = new CodeParser($graphObj);
$codeObj = $parser->source($graphObj, null, new GraphController($graphObj, $args));
$resolveMethod = 'resolve' . studly_case($fieldName);
if (method_exists($codeObj, $resolveMethod)) {
return $codeObj->$resolveMethod($root, $args, $context, $resolveInfo);
}
}
// default resolver
return parent::provideResolver($fieldValue)($root, $args, $context, $resolveInfo);
};
}
if (method_exists($codeObj, $resolveMethod)) {
return $codeObj->$resolveMethod($root, $args, $context, $resolveInfo);
}
}
// default resolver
return parent::provideResolver($fieldValue)($root, $args, $context, $resolveInfo);
};
}
}
+30 -32
View File
@@ -6,39 +6,37 @@ use Cms\Classes\Theme;
class Schema extends Theme
{
/**
* Returns the absolute theme path.
* @param string $dirName Optional theme directory. Defaults to $this->getDirName()
* @return string
*/
public function getPath(?string $dirName = null): string
{
if (!$dirName) {
$dirName = $this->getDirName();
}
return base_path() . '/' . $dirName;
/**
* Returns the absolute theme path.
* @param string $dirName Optional theme directory. Defaults to $this->getDirName()
* @return string
*/
public function getPath(?string $dirName = null): string
{
if (!$dirName) {
$dirName = $this->getDirName();
}
return base_path() . '/' . $dirName;
}
/**
* Returns a list of graphs in the template.
* @param boolean $skipCache Indicates if the pages should be reloaded from the disk bypassing the cache.
* @return array Returns an array of Nocio\Headstart\Classes\Graph objects.
*/
public function listGraphs($skipCache = false)
{
return Graph::listInTheme($this, $skipCache);
}
/**
* Returns the active theme code.
*
* @return string
* If the theme doesn't exist, returns null.
*/
public static function getActiveThemeCode(): ?string
{
return 'graphql';
}
/**
* Returns a list of graphs in the template.
* @param boolean $skipCache Indicates if the pages should be reloaded from the disk bypassing the cache.
* @return array Returns an array of Nocio\Headstart\Classes\Graph objects.
*/
public function listGraphs($skipCache = false)
{
return Graph::listInTheme($this, $skipCache);
}
/**
* Returns the active theme code.
*
* @return string
* If the theme doesn't exist, returns null.
*/
public static function getActiveThemeCode(): ?string
{
return 'graphql';
}
}
+12 -16
View File
@@ -7,24 +7,20 @@ use Nuwave\Lighthouse\Schema\Context;
use Illuminate\Http\Request;
use Nuwave\Lighthouse\Schema\Source\SchemaSourceProvider;
class SchemaContext extends Context
{
public $source;
public $source;
/**
* Create new context.
*
* @param Request $request
* @param $source
*/
public function __construct(Request $request)
{
parent::__construct($request);
$this->source = App::make(SchemaSourceProvider::class);
}
/**
* Create new context.
*
* @param Request $request
* @param $source
*/
public function __construct(Request $request)
{
parent::__construct($request);
$this->source = App::make(SchemaSourceProvider::class);
}
}
+164 -168
View File
@@ -11,66 +11,64 @@ use Cms\Classes\ComponentPartial;
class SchemaSourceProvider implements LighthouseSchemaSourceProvider
{
protected $fieldGraphMapCacheKey = 'graphql-schema-field-graph-map';
protected $fieldGraphMapCacheKey = 'graphql-schema-field-graph-map';
/**
* @var Schema
*/
public $template;
/**
* @var Schema
*/
public $template;
/**
* @var string
*/
protected $rootSchemaPath;
/**
* @var string
*/
protected $rootSchemaPath;
/**
* @var array
* Collects graphs and their parsed schema
*/
protected $graphMap;
/**
* @var array
* Collects graphs and their parsed schema
*/
protected $graphMap;
/**
* @var array
* Maps field names to graphs
*/
protected $fieldGraphMap;
/**
* @var array
* Maps field names to graphs
*/
protected $fieldGraphMap;
/**
* SchemaSource constructor.
*
* @param string|null $template
*/
public function __construct($template = null)
{
$this->template = Schema::load(is_string($template) ? $template : 'graphql');
$this->rootSchemaPath = $this->template->getPath();
}
/**
* Set schema root path.
*
* @param string $path
*
* @return SchemaSourceProvider
*/
public function setRootPath(string $path): self
{
$this->rootSchemaPath = $path;
/**
* SchemaSource constructor.
*
* @param string|null $template
*/
public function __construct($template = null)
{
$this->template = Schema::load(is_string($template) ? $template : 'graphql');
$this->rootSchemaPath = $this->template->getPath();
}
return $this;
}
/**
* Set schema root path.
*
* @param string $path
*
* @return SchemaSourceProvider
*/
public function setRootPath(string $path): self
{
$this->rootSchemaPath = $path;
return $this;
}
/**
* Stitch together schema documents and return the result as a string.
*
* @return string
*/
public function getSchemaString(): string
{
// root types
$schema = '
/**
* Stitch together schema documents and return the result as a string.
*
* @return string
*/
public function getSchemaString(): string
{
// root types
$schema = '
type Query {
dummy: Boolean
}
@@ -79,143 +77,141 @@ class SchemaSourceProvider implements LighthouseSchemaSourceProvider
dummy: Boolean
}
';
// schema
$schema .= collect($this->getGraphMap())->implode('schema', '');
return $schema;
// schema
$schema .= collect($this->getGraphMap())->implode('schema', '');
return $schema;
}
public function findGraph($fieldName, $findOrFail = false)
{
if (!isset($this->getFieldGraphMap()[$fieldName])) {
if ($findOrFail) {
throw new Error("Could not find graph of field '{$fieldName}'.");
}
return null;
}
public function findGraph($fieldName, $findOrFail=false) {
if (!isset($this->getFieldGraphMap()[$fieldName])) {
if ($findOrFail) {
throw new Error("Could not find graph of field '{$fieldName}'.");
}
$name = $this->getFieldGraphMap()[$fieldName];
return null;
}
$name = $this->getFieldGraphMap()[$fieldName];
if (isset($this->graphMap[$name])) {
// if available, use already instantiated graph objects in memory
return $this->graphMap[$name]['graph'];
}
return Graph::loadCached($this->template, $name);
if (isset($this->graphMap[$name])) {
// if available, use already instantiated graph objects in memory
return $this->graphMap[$name]['graph'];
}
public function getFieldGraphMap() {
if (! is_null($this->fieldGraphMap)) {
return $this->fieldGraphMap;
}
return Graph::loadCached($this->template, $name);
}
// restore from cache
public function getFieldGraphMap()
{
if (!is_null($this->fieldGraphMap)) {
return $this->fieldGraphMap;
}
$cacheable = true;
// restore from cache
if ($cacheable) {
$map = Cache::get($this->fieldGraphMapCacheKey, false);
if (
$map &&
($map = @unserialize(@base64_decode($map))) &&
is_array($map)
) {
$this->fieldGraphMap = $map;
return $this->fieldGraphMap;
}
}
// rebuild mapping
$this->fieldGraphMap = [];
foreach ($this->getGraphMap() as $key => $element) {
if (empty(trim($element['schema']))) {
continue;
}
// parse the AST to collect fields mapping
$ast = Parser::parse($element['schema'], ['noLocation' => true]);
$fields = collect($ast->definitions)
->filter(function ($node) {
// we only consider the root types since their fields are unique
return $node->name->value == 'Query' || $node->name->value == 'Mutation';
})
->flatMap(function ($node) {
// collect field names
return collect($node->fields)->map(function($field) {
return $field->name->value;
});
});
// map as [$field_name => $graph_filename]
$this->fieldGraphMap = array_merge($this->fieldGraphMap, array_fill_keys($fields->toArray(), $key));
}
// cache mapping
if ($cacheable) {
Cache::put(
$this->fieldGraphMapCacheKey,
base64_encode(serialize($this->fieldGraphMap)),
10
);
}
$cacheable = true;
if ($cacheable) {
$map = Cache::get($this->fieldGraphMapCacheKey, false);
if ($map && ($map = @unserialize(@base64_decode($map))) && is_array($map)) {
$this->fieldGraphMap = $map;
return $this->fieldGraphMap;
}
}
public function getGraphMap() {
if (!is_null($this->graphMap)) {
return $this->graphMap;
}
// rebuild mapping
$this->graphMap = [];
$component_re = '/{%\s*component\s+["|\'](.*)["|\']\s*%}/m';
$component_str = [$this, 'getComponentSchemaString'];
foreach ($this->template->listGraphs() as $graph) {
/* @var $graph \GermanAirlinesVa\Graphql\Classes\Graph */
$markup = $graph->markup;
$this->fieldGraphMap = [];
$schema = preg_replace_callback($component_re, function(array $matches) use ($graph, $component_str) {
$matched_alias = $matches[1];
foreach ($graph->settings['components'] as $component => $properties) {
// find component by alias
list($name, $alias) = strpos($component, ' ')
? explode(' ', $component)
: [$component, $component];
if ($alias == $matched_alias) {
// resolve component schema
return $component_str($name, $alias);
}
}
// if not found, remove
return '';
}, $markup);
foreach ($this->getGraphMap() as $key => $element) {
if (empty(trim($element['schema']))) {
continue;
}
$this->graphMap[$graph->getFileName()] = [
'graph' => $graph,
'schema' => $schema
];
}
// parse the AST to collect fields mapping
$ast = Parser::parse($element['schema'], ['noLocation' => true]);
$fields = collect($ast->definitions)
->filter(function ($node) {
// we only consider the root types since their fields are unique
return $node->name->value == 'Query' || $node->name->value == 'Mutation';
})
->flatMap(function ($node) {
// collect field names
return collect($node->fields)->map(function ($field) {
return $field->name->value;
});
});
return $this->graphMap;
// map as [$field_name => $graph_filename]
$this->fieldGraphMap = array_merge($this->fieldGraphMap, array_fill_keys($fields->toArray(), $key));
}
public function getComponentSchemaString($componentName, $alias) {
$manager = ComponentManager::instance();
$componentObj = $manager->makeComponent($componentName);
if ($partial = ComponentPartial::load($componentObj, 'schema.graphqls')) {
$content = $partial->getContent();
$content = str_replace('__SELF__', $alias, $content);
// cache mapping
return $content;
} else {
return "# {$componentName} does not provide a default schema definition";
}
if ($cacheable) {
Cache::put($this->fieldGraphMapCacheKey, base64_encode(serialize($this->fieldGraphMap)), 10);
}
public function clearCache() {
Cache::forget($this->fieldGraphMapCacheKey);
return $this->fieldGraphMap;
}
public function getGraphMap()
{
if (!is_null($this->graphMap)) {
return $this->graphMap;
}
$this->graphMap = [];
$component_re = '/{%\s*component\s+["|\'](.*)["|\']\s*%}/m';
$component_str = [$this, 'getComponentSchemaString'];
foreach ($this->template->listGraphs() as $graph) {
/* @var $graph \GermanAirlinesVa\Graphql\Classes\Graph */
$markup = $graph->markup;
$schema = preg_replace_callback(
$component_re,
function (array $matches) use ($graph, $component_str) {
$matched_alias = $matches[1];
foreach ($graph->settings['components'] as $component => $properties) {
// find component by alias
[$name, $alias] = strpos($component, ' ') ? explode(' ', $component) : [$component, $component];
if ($alias == $matched_alias) {
// resolve component schema
return $component_str($name, $alias);
}
}
// if not found, remove
return '';
},
$markup
);
$this->graphMap[$graph->getFileName()] = [
'graph' => $graph,
'schema' => $schema,
];
}
return $this->graphMap;
}
public function getComponentSchemaString($componentName, $alias)
{
$manager = ComponentManager::instance();
$componentObj = $manager->makeComponent($componentName);
if ($partial = ComponentPartial::load($componentObj, 'schema.graphqls')) {
$content = $partial->getContent();
$content = str_replace('__SELF__', $alias, $content);
return $content;
} else {
return "# {$componentName} does not provide a default schema definition";
}
}
public function clearCache()
{
Cache::forget($this->fieldGraphMapCacheKey);
}
}