diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b56254..59b0618 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.15.0](https://github.com/AmazeeLabs/silverback-mono/compare/@-amazeelabs/silverback_gatsby@1.14.2...@-amazeelabs/silverback_gatsby@1.15.0) (2022-01-25) + + +### Features + +* introduce new directives and rename existing ones ([c9255ca](https://github.com/AmazeeLabs/silverback-mono/commit/c9255ca6c3bdd8ad954be00013c6f2f0faae4eaf)) + + + + + ## [1.14.2](https://github.com/AmazeeLabs/silverback-mono/compare/@-amazeelabs/silverback_gatsby@1.14.1...@-amazeelabs/silverback_gatsby@1.14.2) (2022-01-19) diff --git a/README.md b/README.md index cbc6cfc..a5c837b 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ now. ## Automatic creation of Gatsby pages -Available using `@path` and `@template` field directives. See +Available using `@isPath` and `@isTemplate` field directives. See [`@amazeelabs/gatsby-source-silverback`](../../../npm/@amazeelabs/gatsby-source-silverback) plugin README for details. @@ -97,7 +97,7 @@ plugin README for details. There are directives which create GraphQL resolvers automatically. -### @property +### @resolveProperty This field directive is a shortcut for `property_path` data producer. @@ -105,7 +105,7 @@ For example, this schema ```graphql type Page @entity(type: "node", bundle: "page") { - body: String @property(path: "field_body.0.processed") + body: String @resolveProperty(path: "field_body.0.processed") } ``` @@ -119,6 +119,51 @@ $builder->produce('property_path', [ ]) ``` +### @resolveEntityPath + +Resolves the relative path to an entity. A shortcut for `entity_url`+`url_path` +data producers. + +Example: + +```graphql +type Page @entity(type: "node", bundle: "page") { + path: String! @resolveEntityPath +} +``` + +### @resolveEntityReference + +Resolves the references entities. A shortcut for `entity_reference` data +producer. + +Example: + + +```graphql +type Page @entity(type: "node", bundle: "page") { + relatedArticles: [Article]! @resolveEntityReference(field: "field_related_articles", single: false) + parentPage: Page @resolveEntityReference(field: "field_related_articles", single: true) +} +``` + + +### @resolveEntityReferenceRevisions + +Resolves the entity reference revisions fields, e.g. Paragraphs. A shortcut for +`entity_reference_revisions` data producer. + +Example: + + +```graphql +type Page @entity(type: "node", bundle: "page") { + paragraphs: [PageParagraphs!]! @resolveEntityReferenceRevisions(field: "field_paragraphs", single: false) + singleParagraph: ParagraphText @resolveEntityReferenceRevisions(field: "field_single_paragraph", single: true) +} +``` + + ## Menus To expose Drupal menus to Gatsby, one can use the `@menu` directive. It takes a diff --git a/composer.json b/composer.json index ee78ccc..8854dfa 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "amazeelabs/silverback_gatsby", "type": "drupal-module", - "version": "1.14.2", + "version": "1.15.0", "description": "Bridge module between Gatsby and Drupal.", "homepage": "https://silverback.netlify.app", "license": "GPL-2.0+", diff --git a/graphql/.graphqlconfig b/graphql/.graphqlconfig new file mode 100644 index 0000000..230d6e2 --- /dev/null +++ b/graphql/.graphqlconfig @@ -0,0 +1,4 @@ +{ + "name": "Silverback Gatsby Schema Extensions", + "schemaPath": "*.graphqls" +} diff --git a/graphql/silverback_gatsby.base.graphqls b/graphql/silverback_gatsby.base.graphqls index 406be1a..2a98146 100644 --- a/graphql/silverback_gatsby.base.graphqls +++ b/graphql/silverback_gatsby.base.graphqls @@ -8,11 +8,44 @@ type Feed { templateFieldName: String } +################################################################################ # Directives for the automatic Gatsby pages creation. +################################################################################ + +directive @isPath on FIELD_DEFINITION +""" +DEPRECATED, use @isPath +""" directive @path on FIELD_DEFINITION + +directive @isTemplate on FIELD_DEFINITION +""" +DEPRECATED, use @isTemplate +""" directive @template on FIELD_DEFINITION +################################################################################ # Directives for automatic resolvers. +################################################################################ + +directive @resolveEntityPath on FIELD_DEFINITION + +directive @resolveProperty( + path: String! +) on FIELD_DEFINITION +""" +DEPRECATED, use @resolveProperty +""" directive @property( path: String! ) on FIELD_DEFINITION + +directive @resolveEntityReference( + field: String! + single: Boolean! +) on FIELD_DEFINITION + +directive @resolveEntityReferenceRevisions( + field: String! + single: Boolean! +) on FIELD_DEFINITION diff --git a/package.json b/package.json index a74f805..de9366b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@-amazeelabs/silverback_gatsby", - "version": "1.14.2", + "version": "1.15.0", "main": "index.js", "scripts": { "version": "sync-composer-version", @@ -16,5 +16,5 @@ "repository": "git@github.com:AmazeeLabs/silverback_gatsby.git", "branch": "main" }, - "gitHead": "47f18d66214a736ee8d9a191163abd5245ef4bc4" + "gitHead": "d37f09b7d6e69736f853480e9f42738143a69519" } diff --git a/src/Plugin/FeedBase.php b/src/Plugin/FeedBase.php index 81baf06..43b3c7c 100644 --- a/src/Plugin/FeedBase.php +++ b/src/Plugin/FeedBase.php @@ -53,8 +53,8 @@ public function __construct($config, $plugin_id, $plugin_definition) { parent::__construct($config, $plugin_id, $plugin_definition); $this->builder = new ResolverBuilder(); $this->typeName = $config['typeName']; - $this->pathFieldName = $config['createPageFields']['path'] ?? NULL; - $this->templateFieldName = $config['createPageFields']['template'] ?? NULL; + $this->pathFieldName = $config['createPageFields']['isPath'] ?? NULL; + $this->templateFieldName = $config['createPageFields']['isTemplate'] ?? NULL; } /** diff --git a/src/Plugin/GraphQL/SchemaExtension/SilverbackGatsbySchemaExtension.php b/src/Plugin/GraphQL/SchemaExtension/SilverbackGatsbySchemaExtension.php index 3ea1194..389568b 100644 --- a/src/Plugin/GraphQL/SchemaExtension/SilverbackGatsbySchemaExtension.php +++ b/src/Plugin/GraphQL/SchemaExtension/SilverbackGatsbySchemaExtension.php @@ -4,8 +4,10 @@ use Drupal\Component\Plugin\PluginManagerInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\TranslatableInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\graphql\GraphQL\Execution\ResolveContext; +use Drupal\graphql\GraphQL\Resolver\ResolverInterface; use Drupal\graphql\GraphQL\ResolverBuilder; use Drupal\graphql\GraphQL\ResolverRegistry; use Drupal\graphql\GraphQL\ResolverRegistryInterface; @@ -45,13 +47,23 @@ class SilverbackGatsbySchemaExtension extends SdlSchemaExtensionPluginBase protected array $feeds = []; /** - * The list of fields marked with "property" directive. + * The list of fields marked with "resolve*" directives. * * @var array - * Keys are GraphQL paths, values are directive arguments. - * Example: ['Page.title' => ['path' => 'title.value']] + * Keys are GraphQL paths, values are directive names and arguments. + * Example: + * [ + * 'Page.path' => [ + * 'name' => 'resolvePath', + * 'arguments' => [], + * ], + * 'Page.title' => [ + * 'name' => 'resolveProperty', + * 'arguments' => ['path' => 'title.value'], + * ] + * ] */ - protected array $properties = []; + protected array $resolvers; /** * @var \Drupal\Core\Plugin\DefaultPluginManager|object|null @@ -149,17 +161,11 @@ public function getFeeds(): array { foreach ($field->directives as $fieldDirective) { // Directives used for automatic page creation. - if (in_array($fieldDirective->name->value, ['path', 'template'], TRUE)) { - $config['createPageFields'][$fieldDirective->name->value] = $field->name->value; + if (in_array($fieldDirective->name->value, ['isPath', 'path'], TRUE)) { + $config['createPageFields']['isPath'] = $field->name->value; } - - // The @property directive. - if ($fieldDirective->name->value === 'property') { - $graphQlPath = $definition->name->value . '.' . $field->name->value; - foreach ($fieldDirective->arguments->getIterator() as $arg) { - /** @var \GraphQL\Language\AST\ArgumentNode $arg */ - $this->properties[$graphQlPath][$arg->name->value] = $arg->value->value; - } + if (in_array($fieldDirective->name->value, ['isTemplate', 'template'], TRUE)) { + $config['createPageFields']['isTemplate'] = $field->name->value; } } } @@ -172,6 +178,47 @@ public function getFeeds(): array { return $this->feeds; } + /** + * @see SilverbackGatsbySchemaExtension::$resolvers + */ + protected function getResolveDirectives(): array { + if (isset($this->resolvers)) { + return $this->resolvers; + } + $this->resolvers = []; + foreach ($this->parentAst->definitions->getIterator() as $definition) { + if (!($definition instanceof ObjectTypeDefinitionNode)) { + continue; + } + foreach ($definition->fields as $field) { + foreach ($field->directives as $fieldDirective) { + $list = [ + 'resolveEntityPath', + 'resolveProperty', + 'property', + 'resolveEntityReference', + 'resolveEntityReferenceRevisions', + ]; + if (in_array($fieldDirective->name->value, $list, TRUE)) { + $graphQlPath = $definition->name->value . '.' . $field->name->value; + $name = $fieldDirective->name->value === 'property' + ? 'resolveProperty' + : $fieldDirective->name->value; + $this->resolvers[$graphQlPath] = [ + 'name' => $name, + 'arguments' => [], + ]; + foreach ($fieldDirective->arguments->getIterator() as $arg) { + /** @var \GraphQL\Language\AST\ArgumentNode $arg */ + $this->resolvers[$graphQlPath]['arguments'][$arg->name->value] = $arg->value->value; + } + } + } + } + } + return $this->resolvers; + } + /** * Build the automatic schema definition for a given Feed. */ @@ -301,15 +348,69 @@ protected function addFieldResolvers(ResolverRegistry $registry, ResolverBuilder } } - foreach ($this->properties as $path => $args) { - [$typeName, $fieldName] = explode('.', $path); - $registry->addFieldResolver($typeName, $fieldName, $builder->produce('property_path', [ - 'path' => $builder->fromValue($args['path']), - 'value' => $builder->fromParent(), - 'type' => $builder->callback( - fn(EntityInterface $entity) => $entity->getTypedData()->getDataDefinition()->getDataType() - ), - ])); + $addResolver = function(string $path, ResolverInterface $resolver) use ($registry) { + [$type, $field] = explode('.', $path); + $registry->addFieldResolver($type, $field, $resolver); + }; + foreach ($this->getResolveDirectives() as $path => $definition) { + switch ($definition['name']) { + + case 'resolveEntityPath': + $addResolver($path, $builder->compose( + $builder->produce('entity_url')->map('entity', $builder->fromParent()), + $builder->produce('url_path')->map('url', $builder->fromParent()) + )); + break; + + case 'resolveProperty': + $addResolver($path, $builder->produce('property_path', [ + 'path' => $builder->fromValue($definition['arguments']['path']), + 'value' => $builder->fromParent(), + 'type' => $builder->callback( + fn(EntityInterface $entity) => $entity->getTypedData()->getDataDefinition()->getDataType() + ), + ])); + break; + + case 'resolveEntityReference': + $resolverMultiple = $builder->defaultValue( + $builder->produce('entity_reference') + ->map('entity', $builder->fromParent()) + ->map('language', $builder->callback( + fn(TranslatableInterface $value) => $value->language()->getId() + )) + ->map('field', $builder->fromValue($definition['arguments']['field'])), + $builder->fromValue([]) + ); + if ($definition['arguments']['single']) { + $addResolver($path, $builder->compose( + $resolverMultiple, + $builder->callback(fn(array $values) => reset($values) ?: NULL) + )); + } + else { + $addResolver($path, $resolverMultiple); + } + break; + + case 'resolveEntityReferenceRevisions': + $resolverMultiple = $builder->defaultValue( + $builder->produce('entity_reference_revisions') + ->map('entity', $builder->fromParent()) + ->map('field', $builder->fromValue($definition['arguments']['field'])), + $builder->fromValue([]) + ); + if ($definition['arguments']['single']) { + $addResolver($path, $builder->compose( + $resolverMultiple, + $builder->callback(fn(array $values) => reset($values) ?: NULL) + )); + } + else { + $addResolver($path, $resolverMultiple); + } + break; + } } }