commit 052cbe303808e0f303166efd5ff91659afffd2a5 Author: Kilian Hofmann Date: Tue Jun 1 19:54:59 2021 +0200 Initial Commit diff --git a/Plugin.php b/Plugin.php new file mode 100644 index 0000000..b89840f --- /dev/null +++ b/Plugin.php @@ -0,0 +1,69 @@ +addDirectories('graphql'); + $this->bootPackages(); + App::register(GraphqlServiceProvider::class); + } + + public function bootPackages() + { + // Get the namespace of the current plugin to use in accessing the Config of the plugin + $pluginNamespace = str_replace('\\', '.', strtolower(__NAMESPACE__)); + + // Instantiate the AliasLoader for any aliases that will be loaded + $aliasLoader = AliasLoader::getInstance(); + + // Get the packages to boot + $packages = Config::get($pluginNamespace . '::packages'); + + // Boot each package + foreach ($packages as $name => $options) + { + // Setup the configuration for the package, pulling from this plugin's config + if (!empty($options['config']) && !empty($options['config_namespace'])) + { + Config::set($options['config_namespace'], $options['config']); + } + + // Register any Service Providers for the package + if (!empty($options['providers'])) + { + foreach ($options['providers'] as $provider) + { + App::register($provider); + } + } + + // Register any Aliases for the package + if (!empty($options['aliases'])) + { + foreach ($options['aliases'] as $alias => $path) + { + $aliasLoader->alias($alias, $path); + } + } + } + + return $packages; + } +} + diff --git a/classes/CreatesContext.php b/classes/CreatesContext.php new file mode 100644 index 0000000..a120b40 --- /dev/null +++ b/classes/CreatesContext.php @@ -0,0 +1,24 @@ + 'required' + ]; + + public function getCodeClassParent() : string { + return GraphCode::class; + } + + public function runComponents() + { + foreach ($this->components as $component) { + $component->onRun(); + } + } + +} diff --git a/classes/GraphCode.php b/classes/GraphCode.php new file mode 100644 index 0000000..4fd2934 --- /dev/null +++ b/classes/GraphCode.php @@ -0,0 +1,55 @@ +graph = $graph; + $this->controller = $controller; + + Extendable::__construct(); + } + + 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; + } + + public function __set($name, $value) + { + return $this->graph->{$name} = $value; + } + + public function __isset($name) + { + return isset($this->graph->{$name}); + } + +} diff --git a/classes/GraphController.php b/classes/GraphController.php new file mode 100644 index 0000000..4b33974 --- /dev/null +++ b/classes/GraphController.php @@ -0,0 +1,118 @@ +graph = $graph; + $this->router = new GraphRouter($args); + + self::$instance = $this; + } + + /** + * 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; + } + + public function component($alias) { + if (isset($this->graph->components[$alias])) { + return $this->graph->components[$alias]; + } + + foreach ($this->graph->settings['components'] as $component => $properties) { + list($name, $component_alias) = strpos($component, ' ') + ? explode(' ', $component) + : [$component, $component]; + + 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])); + } + + $this->setComponentPropertiesFromParams($componentObj, $this->router->getParameters()); + $componentObj->init(); + + $componentObj->alias = $alias; + $this->graph->components[$alias] = $componentObj; + + return $componentObj; + } + } + } + + /** + * 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); + } + +} diff --git a/classes/GraphRouter.php b/classes/GraphRouter.php new file mode 100644 index 0000000..4a62870 --- /dev/null +++ b/classes/GraphRouter.php @@ -0,0 +1,44 @@ +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; + } + +} diff --git a/classes/GraphqlServiceProvider.php b/classes/GraphqlServiceProvider.php new file mode 100644 index 0000000..d8394b9 --- /dev/null +++ b/classes/GraphqlServiceProvider.php @@ -0,0 +1,22 @@ +app->bind(ProvidesResolver::class, GraphqlProvidesResolver::class); + $this->app->singleton(CreatesContext::class, GraphqlCreatesContext::class); + $this->app->singleton(SchemaSourceProvider::class, GraphqlSchemaSourceProvider::class); + } + +} diff --git a/classes/ResolverProvider.php b/classes/ResolverProvider.php new file mode 100644 index 0000000..dedf76a --- /dev/null +++ b/classes/ResolverProvider.php @@ -0,0 +1,39 @@ +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); + }; + } + +} diff --git a/classes/Schema.php b/classes/Schema.php new file mode 100644 index 0000000..10807af --- /dev/null +++ b/classes/Schema.php @@ -0,0 +1,44 @@ +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'; + } + +} diff --git a/classes/SchemaContext.php b/classes/SchemaContext.php new file mode 100644 index 0000000..a1fdbc5 --- /dev/null +++ b/classes/SchemaContext.php @@ -0,0 +1,30 @@ +source = App::make(SchemaSourceProvider::class); + } + + +} diff --git a/classes/SchemaSourceProvider.php b/classes/SchemaSourceProvider.php new file mode 100644 index 0000000..9ed8261 --- /dev/null +++ b/classes/SchemaSourceProvider.php @@ -0,0 +1,221 @@ +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; + + return $this; + } + + /** + * Stitch together schema documents and return the result as a string. + * + * @return string + */ + public function getSchemaString(): string + { + // root types + $schema = ' + type Query { + graphql: Boolean + } + + type Mutation { + graphql: Boolean + } + '; + // 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; + } + + $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); + } + + public function getFieldGraphMap() { + if (! is_null($this->fieldGraphMap)) { + return $this->fieldGraphMap; + } + + // restore from cache + + $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; + } + } + + // 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 + ); + } + + 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 + 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); + + $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); + } + +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f0359c3 --- /dev/null +++ b/composer.json @@ -0,0 +1,8 @@ +{ + "name": "germanairlinesva/graphql", + "type": "october-plugin", + "description": "None", + "require": { + "composer/installers": "~1.0" + } +} diff --git a/config/config.php b/config/config.php new file mode 100644 index 0000000..0c704dc --- /dev/null +++ b/config/config.php @@ -0,0 +1,251 @@ + [ + 'nuwave/lighthouse' => [ + 'config_namespace' => 'lighthouse', + 'providers' => [ + 'Nuwave\Lighthouse\LighthouseServiceProvider' + ], + 'aliases' => [ + ], + 'config' => [ + + /* + |-------------------------------------------------------------------------- + | Route Configuration + |-------------------------------------------------------------------------- + | + | Controls the HTTP route that your GraphQL server responds to. + | You may set `route` => false, to disable the default route + | registration and take full control. + | + */ + + 'route' => [ + /* + * The URI the endpoint responds to, e.g. mydomain.com/graphql. + */ + 'uri' => 'graphql', + + /* + * Lighthouse creates a named route for convenient URL generation and redirects. + */ + 'name' => 'graphql', + + /* + * + * Beware that middleware defined here runs before the GraphQL execution phase, + * so you have to take extra care to return spec-compliant error responses. + * To apply middleware on a field level, use the @middleware directive. + */ + 'middleware' => [\Nuwave\Lighthouse\Support\Http\Middleware\AcceptJson::class] + ], + + /* + |-------------------------------------------------------------------------- + | Schema Declaration + |-------------------------------------------------------------------------- + | + | This is a path that points to where your GraphQL schema is located + | relative to the app path. You should define your entire GraphQL + | schema in this file (additional files may be imported). + | + */ + + 'schema' => [ + 'register' => '', + ], + + /* + |-------------------------------------------------------------------------- + | Schema Cache + |-------------------------------------------------------------------------- + | + | A large part of schema generation consists of parsing and AST manipulation. + | This operation is very expensive, so it is highly recommended to enable + | caching of the final schema to optimize performance of large schemas. + | + */ + + 'cache' => [ + 'enable' => false, + 'key' => env('GRAPHQL_CACHE_KEY', 'graphql-schema'), + 'ttl' => env('GRAPHQL_CACHE_TTL', null), + ], + + /* + |-------------------------------------------------------------------------- + | Namespaces + |-------------------------------------------------------------------------- + | + | These are the default namespaces where Lighthouse looks for classes + | that extend functionality of the schema. You may pass either a string + | or an array, they are tried in order and the first match is used. + | + */ + + 'namespaces' => [ + 'models' => 'Graphql\\Models', + 'queries' => 'Graphql\\Queries', + 'mutations' => 'Graphql\\Mutations', + 'subscriptions' => 'Graphql\\Subscriptions', + 'interfaces' => 'Graphql\\Interfaces', + 'unions' => 'Graphql\\Unions', + 'scalars' => 'Graphql\\Scalars', + 'directives' => ['GermanAirlinesVa\\Graphql\\GraphQL\\Directives', 'Graphql\\Directives'], + ], + + /* + |-------------------------------------------------------------------------- + | Security + |-------------------------------------------------------------------------- + | + | Control how Lighthouse handles security related query validation. + | This configures the options from http://webonyx.github.io/graphql-php/security/ + | + */ + + 'security' => [ + 'max_query_complexity' => \GraphQL\Validator\Rules\QueryComplexity::DISABLED, + 'max_query_depth' => \GraphQL\Validator\Rules\QueryDepth::DISABLED, + 'disable_introspection' => \GraphQL\Validator\Rules\DisableIntrospection::DISABLED, + ], + + /* + |-------------------------------------------------------------------------- + | Pagination + |-------------------------------------------------------------------------- + | + | Limits the maximum "count" that users may pass as an argument + | to fields that are paginated with the @paginate directive. + | A setting of "null" means the count is unrestricted. + | + */ + + 'paginate_max_count' => null, + + /* + |-------------------------------------------------------------------------- + | Pagination Amount Argument + |-------------------------------------------------------------------------- + | + | Set the name to use for the generated argument on paginated fields + | that controls how many results are returned. + | This setting will be removed in v5. + | + */ + + 'pagination_amount_argument' => 'first', + + /* + |-------------------------------------------------------------------------- + | Debug + |-------------------------------------------------------------------------- + | + | Control the debug level as described in http://webonyx.github.io/graphql-php/error-handling/ + | Debugging is only applied if the global Laravel debug config is set to true. + | + */ + + 'debug' => \GraphQL\Error\Debug::INCLUDE_DEBUG_MESSAGE | \GraphQL\Error\Debug::INCLUDE_TRACE, + + /* + |-------------------------------------------------------------------------- + | Error Handlers + |-------------------------------------------------------------------------- + | + | Register error handlers that receive the Errors that occur during execution + | and handle them. You may use this to log, filter or format the errors. + | The classes must implement \Nuwave\Lighthouse\Execution\ErrorHandler + | + */ + + 'error_handlers' => [ + \Nuwave\Lighthouse\Execution\ExtensionErrorHandler::class, + ], + + /* + |-------------------------------------------------------------------------- + | Global ID + |-------------------------------------------------------------------------- + | + | The name that is used for the global id field on the Node interface. + | When creating a Relay compliant server, this must be named "id". + | + */ + + 'global_id_field' => 'id', + + /* + |-------------------------------------------------------------------------- + | Batched Queries + |-------------------------------------------------------------------------- + | + | GraphQL query batching means sending multiple queries to the server in one request, + | You may set this flag to either process or deny batched queries. + | + */ + + 'batched_queries' => true, + + /* + |-------------------------------------------------------------------------- + | Transactional Mutations + |-------------------------------------------------------------------------- + | + | Sets default setting for transactional mutations. + | You may set this flag to have @create|@update mutations transactional or not. + | + */ + + 'transactional_mutations' => true, + + /* + |-------------------------------------------------------------------------- + | GraphQL Subscriptions + |-------------------------------------------------------------------------- + | + | Here you can define GraphQL subscription "broadcasters" and "storage" drivers + | as well their required configuration options. + | + */ + + 'subscriptions' => [ + /* + * Determines if broadcasts should be queued by default. + */ + 'queue_broadcasts' => env('LIGHTHOUSE_QUEUE_BROADCASTS', true), + + /* + * Default subscription storage. + * + * Any Laravel supported cache driver options are available here. + */ + 'storage' => env('LIGHTHOUSE_SUBSCRIPTION_STORAGE', 'redis'), + + /* + * Default subscription broadcaster. + */ + 'broadcaster' => env('LIGHTHOUSE_BROADCASTER', 'pusher'), + + /* + * Subscription broadcasting drivers with config options. + */ + 'broadcasters' => [ + 'log' => [ + 'driver' => 'log', + ], + 'pusher' => [ + 'driver' => 'pusher', + 'routes' => \Nuwave\Lighthouse\Subscriptions\SubscriptionRouter::class.'@pusher', + 'connection' => 'pusher', + ], + ], + ], + ] + ] + ] +]; diff --git a/init.php b/init.php new file mode 100644 index 0000000..215cf79 --- /dev/null +++ b/init.php @@ -0,0 +1,16 @@ + [ + 'name' => 'GraphQL', + 'description' => '', + ], + 'menu' => [ + 'main' => 'GraphQL', + ], +]; \ No newline at end of file diff --git a/plugin.yaml b/plugin.yaml new file mode 100644 index 0000000..477b06a --- /dev/null +++ b/plugin.yaml @@ -0,0 +1,6 @@ +plugin: + name: 'germanairlinesva.graphql::lang.plugin.name' + description: 'germanairlinesva.graphql::lang.plugin.description' + author: 'German Airlines VA' + icon: oc-icon-database + homepage: '' diff --git a/updates/version.yaml b/updates/version.yaml new file mode 100644 index 0000000..52dd186 --- /dev/null +++ b/updates/version.yaml @@ -0,0 +1,2 @@ +1.0.1: + - Initialize plugin. \ No newline at end of file