From 74def3956e190825e668725c9e4e065e03fda6a1 Mon Sep 17 00:00:00 2001 From: "K.I.T.T" Date: Fri, 27 May 2022 10:48:41 +0000 Subject: [PATCH] chore: release version 1.20.0 --- CHANGELOG.md | 16 ++ README.md | 21 ++- composer.json | 2 +- drush.services.yml | 1 + package.json | 4 +- silverback_gatsby.links.task.yml | 5 + silverback_gatsby.module | 23 +++ silverback_gatsby.permissions.yml | 2 + silverback_gatsby.routing.yml | 9 + silverback_gatsby.services.yml | 5 +- src/Commands/SilverbackGatsbyCommands.php | 24 ++- src/GatsbyUpdateHandler.php | 7 +- src/GatsbyUpdateTracker.php | 6 +- src/GatsbyUpdateTrigger.php | 70 ++++++++ src/GatsbyUpdateTriggerInterface.php | 16 ++ src/GraphQL/Build.php | 169 +++++++++++++++++++ src/GraphQL/ComposableSchema.php | 35 +++- tests/src/Kernel/GatsbyUpdateTrackerTest.php | 7 + 18 files changed, 410 insertions(+), 12 deletions(-) create mode 100644 silverback_gatsby.links.task.yml create mode 100644 silverback_gatsby.permissions.yml create mode 100644 silverback_gatsby.routing.yml create mode 100644 src/GraphQL/Build.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 15e3388..1e913eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.20.0](https://github.com/AmazeeLabs/silverback-mono/compare/@-amazeelabs/silverback_gatsby@1.19.5...@-amazeelabs/silverback_gatsby@1.20.0) (2022-05-27) + + +### Bug Fixes + +* better naming for trigger ([9a78e52](https://github.com/AmazeeLabs/silverback-mono/commit/9a78e52a36687d737552b24fdda99c9dc255ecd8)) + + +### Features + +* periodic build ([3038e08](https://github.com/AmazeeLabs/silverback-mono/commit/3038e08bb1d3ed08a0c4d9886df870f30dbc7afb)) + + + + + ## [1.19.5](https://github.com/AmazeeLabs/silverback-mono/compare/@-amazeelabs/silverback_gatsby@1.19.4...@-amazeelabs/silverback_gatsby@1.19.5) (2022-05-12) diff --git a/README.md b/README.md index 0a9d712..07142fd 100644 --- a/README.md +++ b/README.md @@ -221,7 +221,7 @@ query MainMenu { The `@menu` directive also takes an optional `max_level` argument. It can be used to restrict the number of levels a type will include, which in turn can -optimize caching and Gatsby build times. +optimize caching and Gatsby build times. In many cases, the main page layout only displays the first level of menu items. When a new page is created and attached to the third level, Gatsby will still re-render all pages, because the menu that is used in the header changed. By @@ -295,3 +295,22 @@ access control and use it for the "Preview" environment on Gatsby cloud, so unpublished content can be previewed. Another sensible case would be to create a "build" user that has access to published content and block anonymous access to Drupal entirely. + +## Trigger a build + +There are multiple ways to trigger a Gatsby build: +- on entity save +- via the Drupal UI or Drush. + +### On entity save + +On the _Build_ tab of the schema configuration, check the _Trigger a build on entity save_ checkbox. + +### Drupal UI + +On the same _Build_ tab, click the _Gatsby Build_ button. + +### Drush + +This command can be configured in the system cron. +`drush silverback-gatsby:build [server_id]` diff --git a/composer.json b/composer.json index 4cc46f0..b64f7c7 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "amazeelabs/silverback_gatsby", "type": "drupal-module", - "version": "1.19.5", + "version": "1.20.0", "description": "Bridge module between Gatsby and Drupal.", "homepage": "https://silverback.netlify.app", "license": "GPL-2.0+", diff --git a/drush.services.yml b/drush.services.yml index 5753b4a..e851600 100644 --- a/drush.services.yml +++ b/drush.services.yml @@ -4,5 +4,6 @@ services: arguments: - '@entity_type.manager' - '@plugin.manager.graphql.schema' + - '@silverback_gatsby.update_trigger' tags: - { name: drush.command } diff --git a/package.json b/package.json index 36a6540..208f2e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@-amazeelabs/silverback_gatsby", - "version": "1.19.5", + "version": "1.20.0", "main": "index.js", "scripts": { "version": "sync-composer-version", @@ -16,5 +16,5 @@ "repository": "git@github.com:AmazeeLabs/silverback_gatsby.git", "branch": "main" }, - "gitHead": "c45047d9f948ad5c47976d307d088abb2d378951" + "gitHead": "78cebd6b4f85a05d86c2c924e6363bee4e603d06" } diff --git a/silverback_gatsby.links.task.yml b/silverback_gatsby.links.task.yml new file mode 100644 index 0000000..0422420 --- /dev/null +++ b/silverback_gatsby.links.task.yml @@ -0,0 +1,5 @@ +entity.graphql_server.build_form: + route_name: entity.graphql_server.build_form + base_route: entity.graphql_server.edit_form + title: Build + weight: -1 diff --git a/silverback_gatsby.module b/silverback_gatsby.module index 8ff7300..01c1e13 100644 --- a/silverback_gatsby.module +++ b/silverback_gatsby.module @@ -31,3 +31,26 @@ function silverback_gatsby_entity_update(EntityInterface $entity) { function silverback_gatsby_entity_delete(EntityInterface $entity) { _silverback_gatsby_entity_event($entity); } + +/** + * Implements hook_entity_type_alter(). + */ +function silverback_gatsby_entity_type_alter(array &$entity_types) { + /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */ + foreach ($entity_types as $entity_type) { + if ($entity_type->id() === 'graphql_server') { + if (!$entity_type->hasHandlerClass('build')) { + $entity_type->setHandlerClass( + 'build', + Drupal\silverback_gatsby\GraphQL\Build::class + ); + } + if (!$entity_type->getFormClass('build')) { + $entity_type->setFormClass( + 'build', + Drupal\silverback_gatsby\GraphQL\Build::class + ); + } + } + } +} diff --git a/silverback_gatsby.permissions.yml b/silverback_gatsby.permissions.yml new file mode 100644 index 0000000..2f1a721 --- /dev/null +++ b/silverback_gatsby.permissions.yml @@ -0,0 +1,2 @@ +trigger a gatsby build: + title: 'Trigger a Gatsby Build' diff --git a/silverback_gatsby.routing.yml b/silverback_gatsby.routing.yml new file mode 100644 index 0000000..00be7f7 --- /dev/null +++ b/silverback_gatsby.routing.yml @@ -0,0 +1,9 @@ +entity.graphql_server.build_form: + path: '/admin/config/graphql/servers/build/{graphql_server}' + defaults: + _entity_form: 'graphql_server.build' + _title: 'Build' + requirements: + _permission: 'administer graphql configuration+trigger a gatsby build' + options: + _admin_route: TRUE diff --git a/silverback_gatsby.services.yml b/silverback_gatsby.services.yml index 2412bb3..6a0e2a3 100644 --- a/silverback_gatsby.services.yml +++ b/silverback_gatsby.services.yml @@ -10,7 +10,10 @@ services: silverback_gatsby.update_trigger: class: Drupal\silverback_gatsby\GatsbyUpdateTrigger - arguments: ['@http_client', '@messenger', '@entity_type.manager'] + arguments: + - '@http_client' + - '@messenger' + - '@entity_type.manager' silverback_gatsby.update_tracker: class: Drupal\silverback_gatsby\GatsbyUpdateTracker diff --git a/src/Commands/SilverbackGatsbyCommands.php b/src/Commands/SilverbackGatsbyCommands.php index 4947e85..c8bbf42 100644 --- a/src/Commands/SilverbackGatsbyCommands.php +++ b/src/Commands/SilverbackGatsbyCommands.php @@ -4,6 +4,7 @@ use Drupal\Component\Plugin\PluginManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\silverback_gatsby\GatsbyUpdateTriggerInterface; use Drupal\silverback_gatsby\GraphQL\ComposableSchema; use Drush\Commands\DrushCommands; @@ -22,11 +23,17 @@ class SilverbackGatsbyCommands extends DrushCommands { protected EntityTypeManagerInterface $entityTypeManager; protected PluginManagerInterface $schemaPluginManager; + protected GatsbyUpdateTriggerInterface $updateTrigger; - public function __construct(EntityTypeManagerInterface $entityTypeManager, PluginManagerInterface $schemaPluginManager) { + public function __construct( + EntityTypeManagerInterface $entityTypeManager, + PluginManagerInterface $schemaPluginManager, + GatsbyUpdateTriggerInterface $updateTrigger + ) { parent::__construct(); $this->entityTypeManager = $entityTypeManager; $this->schemaPluginManager = $schemaPluginManager; + $this->updateTrigger = $updateTrigger; } /** @@ -64,4 +71,19 @@ public function schemaExport($folder = '../generated') { file_put_contents($path, implode("\n", $definition)); } } + + /** + * Trigger a Gatsby build for a given GraphQL server. + * + * @param string $server + * The server id. + * + * @command silverback-gatsby:build + * @aliases sgb + * @usage silverback-gatsby:build [server_id] + */ + public function triggerBuild($server) { + $this->updateTrigger->triggerLatestBuild($server); + } + } diff --git a/src/GatsbyUpdateHandler.php b/src/GatsbyUpdateHandler.php index 2285052..68ef30d 100644 --- a/src/GatsbyUpdateHandler.php +++ b/src/GatsbyUpdateHandler.php @@ -81,6 +81,11 @@ public function handle(string $feedClassName, $context) { } } + // If the configuration is not set, assume TRUE. + $trigger = TRUE; + if (array_key_exists('build_trigger_on_save', $config[$schema_id])) { + $trigger = $config[$schema_id]['build_trigger_on_save'] === 1; + } foreach ($schema->getExtensions() as $extension) { if ($extension instanceof SilverbackGatsbySchemaExtension) { foreach ($extension->getFeeds() as $feed) { @@ -89,7 +94,7 @@ public function handle(string $feedClassName, $context) { && $updates = $feed->investigateUpdate($context, $account) ) { foreach ($updates as $update) { - $this->gatsbyUpdateTracker->track($server->id(), $update->type, $update->id); + $this->gatsbyUpdateTracker->track($server->id(), $update->type, $update->id, $trigger); } } } diff --git a/src/GatsbyUpdateTracker.php b/src/GatsbyUpdateTracker.php index 4dd8b16..cbb62fc 100644 --- a/src/GatsbyUpdateTracker.php +++ b/src/GatsbyUpdateTracker.php @@ -39,7 +39,7 @@ public function clear() : void { /** * {@inheritDoc} */ - public function track(string $server, string $type, string $id) : int { + public function track(string $server, string $type, string $id, bool $trigger = TRUE) : int { if (isset($this->tracked[$server]) && isset($this->tracked[$server][$type]) && in_array($id,$this->tracked[$server][$type])) { return $this->latestBuild($server); } @@ -51,7 +51,9 @@ public function track(string $server, string $type, string $id) : int { 'uid' => $this->currentUser->id(), 'timestamp' => time(), ])->execute(); - $this->trigger->trigger($server, $buildId); + if ($trigger) { + $this->trigger->trigger($server, $buildId); + } return $buildId; } diff --git a/src/GatsbyUpdateTrigger.php b/src/GatsbyUpdateTrigger.php index aa6267c..41d04fe 100644 --- a/src/GatsbyUpdateTrigger.php +++ b/src/GatsbyUpdateTrigger.php @@ -4,11 +4,16 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Messenger\MessengerInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\graphql\Entity\ServerInterface; use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; class GatsbyUpdateTrigger implements GatsbyUpdateTriggerInterface { + use StringTranslationTrait; + protected array $buildIds = []; protected Client $httpClient; protected MessengerInterface $messenger; @@ -44,6 +49,71 @@ public function trigger(string $server, int $id) : void { } } + /** + * {@inheritDoc} + */ + public function triggerLatestBuild(string $server) : TranslatableMarkup { + $servers = $this->entityTypeManager + ->getStorage('graphql_server') + ->loadByProperties(['name' => $server]); + + if (empty($servers)) { + $message = $this->t('No server found with id @server_id.', [ + '@server_id' => $server, + ]); + $this->messenger->addError($message); + return $message; + } + + $serverEntity = reset($servers); + if ($serverEntity instanceof ServerInterface) { + // No dependency injection, prevent circular reference. + /** @var \Drupal\silverback_gatsby\GatsbyUpdateTrackerInterface $updateTracker */ + $updateTracker = \Drupal::service('silverback_gatsby.update_tracker'); + $latestBuildId = $updateTracker->latestBuild($serverEntity->id()); + if (!$this->isFrontendLatestBuild($latestBuildId, $serverEntity)) { + $message = $this->t('Triggering a build for server @server_id.', [ + '@server_id' => $server, + ]); + $this->messenger->addStatus($message); + $this->trigger($serverEntity->id(), $latestBuildId); + } + else { + $message = $this->t('Build is already up-to-date for server @server_id.', [ + '@server_id' => $server, + ]); + $this->messenger->addStatus($message); + } + } + + return $message; + } + + /** + * Check on the frontend if the latest build already occurred. + * + * If the build url is not configured, presume false so the build + * will still happen. + * + * @param string $latestBuildId + * @param \Drupal\graphql\Entity\ServerInterface $serverEntity + * + * @return bool + */ + protected function isFrontendLatestBuild(int $latestBuildId, ServerInterface $serverEntity) { + $result = FALSE; + $configuration = $serverEntity->get('schema_configuration')[$serverEntity->get('schema')]; + if (!empty($configuration['build_url'])) { + $response = $this->httpClient->get($configuration['build_url'] . '/build.json'); + if ($response->getStatusCode() === 200) { + $content = json_decode($response->getBody()->getContents(), TRUE); + $buildId = array_key_exists('drupalBuildId', $content) ? (int) $content['drupalBuildId'] : 0; + $result = $latestBuildId === $buildId; + } + } + return $result; + } + protected function getWebhook($server_id) { $server = $this->entityTypeManager ->getStorage('graphql_server') diff --git a/src/GatsbyUpdateTriggerInterface.php b/src/GatsbyUpdateTriggerInterface.php index eac4af5..2a522e4 100644 --- a/src/GatsbyUpdateTriggerInterface.php +++ b/src/GatsbyUpdateTriggerInterface.php @@ -2,6 +2,8 @@ namespace Drupal\silverback_gatsby; +use Drupal\Core\StringTranslation\TranslatableMarkup; + interface GatsbyUpdateTriggerInterface { /** @@ -14,6 +16,20 @@ interface GatsbyUpdateTriggerInterface { */ public function trigger(string $server, int $id) : void; + /** + * Trigger a build for a given server based on latest build id. + * + * Compares the latest build id with the one on the frontend to not + * trigger unnecessary builds. + * + * @param string $server + * The server id. + * + * @return TranslatableMarkup + * The resut message. + */ + public function triggerLatestBuild(string $server) : TranslatableMarkup; + /** * Send out notifications about potential updates to all Gatsby servers. */ diff --git a/src/GraphQL/Build.php b/src/GraphQL/Build.php new file mode 100644 index 0000000..d14252d --- /dev/null +++ b/src/GraphQL/Build.php @@ -0,0 +1,169 @@ +schemaManager = $schemaManager; + $this->updateTrigger = $updateTrigger; + $this->currentUser = $currentUser; + } + + /** + * {@inheritdoc} + * + * @codeCoverageIgnore + */ + public static function create(ContainerInterface $container): self { + return new static( + $container->get('plugin.manager.graphql.schema'), + $container->get('silverback_gatsby.update_trigger'), + $container->get('current_user') + ); + } + + /** + * {@inheritdoc} + */ + public function buildForm( + array $form, + FormStateInterface $form_state + ) { + $form = parent::buildForm($form, $form_state); + + $form['trigger_build'] = [ + '#type' => 'button', + '#value' => $this->t('Gatsby Build'), + '#required' => FALSE, + '#ajax' => [ + 'callback' => [$this, 'gatsbyBuild'], + ], + '#suffix' => '', + ]; + + /** @var \Drupal\graphql\Entity\ServerInterface $server */ + $server = $this->entity; + $schema = $server->get('schema'); + $form['actions']['#access'] = $this->currentUser->hasPermission('administer graphql configuration'); + $form['schema_configuration'] = [ + '#type' => 'container', + '#access' => $this->currentUser->hasPermission('administer graphql configuration'), + '#prefix' => '
', + '#suffix' => '
', + '#tree' => TRUE, + ]; + + /** @var \Drupal\graphql\Plugin\SchemaPluginInterface $instance */ + $instance = $schema ? $this->schemaManager->createInstance($schema) : NULL; + if ($instance instanceof PluginFormInterface && $instance instanceof ConfigurableInterface) { + $instance->setConfiguration($server->get('schema_configuration')[$schema] ?? []); + $form['schema_configuration'][$schema] = [ + '#type' => 'fieldset', + '#title' => $this->t('Build configuration'), + '#tree' => TRUE, + ]; + $form['schema_configuration'][$schema] += $instance->buildConfigurationForm([], $form_state); + } + return $form; + } + + /** + * Gatsby build. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return \Drupal\Core\Ajax\AjaxResponse + * AjaxResponse. + */ + public function gatsbyBuild(array $form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + $buildMessage = $this->updateTrigger->triggerLatestBuild($this->entity->id()); + $response->addCommand(new HtmlCommand('.gatsby-build-message', $buildMessage)); + return $response; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + parent::submitForm($form, $form_state); + $server = $this->entity; + $schema = $server->get('schema'); + /** @var \Drupal\graphql\Plugin\SchemaPluginInterface $instance */ + $instance = $this->schemaManager->createInstance($schema); + if ($instance instanceof PluginFormInterface && $instance instanceof ConfigurableInterface) { + $state = SubformState::createForSubform($form['schema_configuration'][$schema], $form, $form_state); + $instance->submitConfigurationForm($form['schema_configuration'][$schema], $state); + } + } + + /** + * {@inheritdoc} + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + public function save(array $form, FormStateInterface $form_state) { + $save_result = parent::save($form, $form_state); + $this->messenger()->addMessage($this->t('Saved the %label server.', [ + '%label' => $this->entity->label(), + ])); + + $form_state->setRedirect('entity.graphql_server.collection'); + return $save_result; + } + +} diff --git a/src/GraphQL/ComposableSchema.php b/src/GraphQL/ComposableSchema.php index fef2101..49678bf 100644 --- a/src/GraphQL/ComposableSchema.php +++ b/src/GraphQL/ComposableSchema.php @@ -6,6 +6,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Routing\RouteMatchInterface; use Drupal\graphql\Plugin\GraphQL\Schema\ComposableSchema as OriginalComposableSchema; use Drupal\graphql\Plugin\SchemaExtensionPluginManager; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -25,6 +26,7 @@ class ComposableSchema extends OriginalComposableSchema { protected EntityTypeManagerInterface $entityTypeManager; + protected RouteMatchInterface $routeMatch; public function __construct( array $configuration, @@ -34,7 +36,8 @@ public function __construct( ModuleHandlerInterface $moduleHandler, SchemaExtensionPluginManager $extensionManager, array $config, - EntityTypeManagerInterface $entityTypeManager + EntityTypeManagerInterface $entityTypeManager, + RouteMatchInterface $routeMatch ) { parent::__construct( $configuration, @@ -43,9 +46,11 @@ public function __construct( $astCache, $moduleHandler, $extensionManager, - $config + $config, + $routeMatch ); $this->entityTypeManager = $entityTypeManager; + $this->routeMatch = $routeMatch; } public static function create( @@ -62,7 +67,8 @@ public static function create( $container->get('module_handler'), $container->get('plugin.manager.graphql.schema_extension'), $container->getParameter('graphql.config'), - $container->get('entity_type.manager') + $container->get('entity_type.manager'), + $container->get('current_route_match') ); } @@ -96,13 +102,34 @@ public function buildConfigurationForm( $form_state ); + // Build configuration has to be defined on this form, + // access is set to false if current route is not the build form one. + $isBuildForm = $this->routeMatch->getRouteName() === 'entity.graphql_server.build_form'; + $form['extensions']['#disabled'] = $isBuildForm; + $form['build_trigger_on_save'] = [ + '#type' => 'checkbox', + '#access' => $isBuildForm, + '#required' => FALSE, + '#title' => $this->t('Trigger a build on entity save.'), + '#description' => $this->t('If not checked, make sure to have an alternate build method (cron, manual).'), + '#default_value' => $this->configuration['build_trigger_on_save'] ?? '', + ]; $form['build_webhook'] = [ '#type' => 'textfield', + '#access' => $isBuildForm, '#required' => FALSE, '#title' => $this->t('Build webhook'), '#description' => $this->t('A webhook that will be notified when content changes relevant to this server happen.'), '#default_value' => $this->configuration['build_webhook'] ?? '', ]; + $form['build_url'] = [ + '#type' => 'url', + '#access' => $isBuildForm, + '#required' => FALSE, + '#title' => $this->t('Build url'), + '#description' => $this->t('The frontend url that is the result of the build. With the scheme and without a trailing slash (https://www.example.com).'), + '#default_value' => $this->configuration['build_url'] ?? '', + ]; /** @var \Drupal\graphql\Form\ServerForm $formObject */ $formObject = $form_state->getFormObject(); @@ -111,6 +138,7 @@ public function buildConfigurationForm( $form['user'] = [ '#type' => 'select', + '#access' => $isBuildForm, '#options' => ['' => $this->t('- None -')], '#title' => $this->t('Notification user'), '#description' => $this->t('Only changes visible to this user will trigger build updates.'), @@ -131,6 +159,7 @@ public function buildConfigurationForm( $form['role'] = [ '#type' => 'select', + '#access' => $isBuildForm, '#required' => FALSE, '#options' => ['' => $this->t('- None -')], '#title' => $this->t('Notification role'), diff --git a/tests/src/Kernel/GatsbyUpdateTrackerTest.php b/tests/src/Kernel/GatsbyUpdateTrackerTest.php index 3709a64..6f67d3e 100644 --- a/tests/src/Kernel/GatsbyUpdateTrackerTest.php +++ b/tests/src/Kernel/GatsbyUpdateTrackerTest.php @@ -88,6 +88,13 @@ public function testMultipleBuilds() { ); } + public function testDeferredBuild() { + $this->tracker->track('foo', 'Page', '1', FALSE); + $this->assertEquals(1, $this->tracker->latestBuild('foo')); + _drupal_shutdown_function(); + $this->checkTotalNotifications(0); + } + public function testInvalidDiff() { $this->tracker->track('foo', 'Page', '1'); $this->tracker->track('foo', 'Page', '2');