Sofia
Account created on 21 October 2008, over 16 years ago
  • Drupal architect at Liip 
#

Merge Requests

More

Recent comments

🇧🇬Bulgaria pfrenssen Sofia

Here is a Drush script I used to fix the data corruption that occurs if paragraphs have been wrongly translated due to this bug. This is specific to my project in that it only considers that nodes will have paragraphs. Other projects might also have paragraphs on other entity types. It updates the database tables directly since it is not possible to update default translations through the entity API.

This is completely untested and just intended as a starting point. Do not run this in production ;)


use Drupal\node\Entity\Node;

$query = <<<MYSQL
  SELECT pi.id, pifd.langcode, pifd.default_langcode, pifd.parent_id, pifd.parent_type
  FROM paragraphs_item pi
  LEFT JOIN paragraphs_item_field_data pifd on pi.id = pifd.id
  WHERE pifd.parent_type IN ('node', 'paragraph')
MYSQL;

$connection = \Drupal::database();
$paragraphsData = $connection->query($query)->fetchAll();
// The data set is huge, create a lookup table for quick access.
$paragraphsLookup = [];
foreach ($paragraphsData as $i => $paragraphData) {
  $paragraphsLookup[$paragraphData->id] = $i;
}

$query = <<<MYSQL
  SELECT nid, langcode FROM node_field_data WHERE default_langcode = 1
MYSQL;
$nodeLangcodes = $connection->query($query)->fetchAllKeyed();

$foundInvalidParagraphs = [];

foreach ($paragraphsData as $paragraphData) {
  // Skip paragraphs that are not the default translation because they are not
  // affected.
  if (!$paragraphData->default_langcode) {
    continue;
  }
  $hostId = getHostId($paragraphData, $paragraphsData, $paragraphsLookup, $nodeLangcodes);
  if (!$hostId) {
    // Parent paragraph no longer exists, skip.
    continue;
  }
  $hostLangcode = $nodeLangcodes[$hostId] ?? NULL;
  if (!$hostLangcode) {
    // Node no longer exists, skip.
    continue;
  }
  // If the host language is not the same as the paragraph language, we have
  // an invalid translation.
  if ($paragraphData->langcode !== $hostLangcode) {
    $foundInvalidParagraphs[$hostId][] = $paragraphData->id;
  }
}

foreach ($foundInvalidParagraphs as $hostId => $paragraphIds) {
  $node = Node::load($hostId)->getUntranslated();
  $hostLangcode = $node->language()->getId();

  echo "\nProcessing node {$node->id()} - $hostLangcode ({$node->getTitle()})\n";
  foreach ($paragraphIds as $paragraphId) {
    echo "  - Setting default translation for paragraph $paragraphId to $hostLangcode\n";
    $queries = [
      <<<MYSQL
        UPDATE paragraphs_item_revision_field_data
        SET default_langcode = CASE WHEN langcode = :host_langcode THEN 1 ELSE 0 END
        WHERE id = :paragraph_id
      MYSQL,
      <<<MYSQL
        UPDATE paragraphs_item_field_data
        SET default_langcode = CASE WHEN langcode = :host_langcode THEN 1 ELSE 0 END
        WHERE id = :paragraph_id
      MYSQL,
    ];
    foreach ($queries as $query) {
      $connection->query($query, [
        ':host_langcode' => $hostLangcode,
        ':paragraph_id' => $paragraphId,
      ]);
    }
  }
}

function getHostId($paragraphData, &$paragraphsData, &$paragraphsLookup, &$nodeLangcodes): ?int {
  if ($paragraphData->parent_type === 'node') {
    return (int) $paragraphData->parent_id;
  }
  $parentIndex = $paragraphsLookup[$paragraphData->parent_id] ?? NULL;
  if (!$parentIndex) {
    // Parent paragraph no longer exists, return NULL.
    return NULL;
  }
  return getHostId($paragraphsData[$parentIndex], $paragraphsData, $paragraphsLookup, $nodeLangcodes);
}
🇧🇬Bulgaria pfrenssen Sofia

Added tests and ported the fix also to the Stable widget. There is a failure but this is also affecting the main dev branch and unrelated to this issue.

🇧🇬Bulgaria pfrenssen Sofia

pfrenssen made their first commit to this issue’s fork.

🇧🇬Bulgaria pfrenssen Sofia

Thanks for the review! I will address the remarks,

🇧🇬Bulgaria pfrenssen Sofia

And finally we can introduce an event that can be used to alter the collection name.

🇧🇬Bulgaria pfrenssen Sofia

When creating a collection, the name of the collection is part of the schema that is passed to the Typesense server, so we need to make sure the altered collection name is set on the schema.

🇧🇬Bulgaria pfrenssen Sofia

Next up, we can improve and simplify the ::getCollectionName() method. It currently works like this:

  public function getCollectionName(?IndexInterface $index): ?string {
    if ($index == NULL) {
      return NULL;
    }

    try {
      /** @var \Drupal\search_api_typesense\Entity\TypesenseSchemaInterface $typesense_schema */
      $typesense_schema = $this->entityTypeManager
        ->getStorage('typesense_schema')
        ->load($index->id());

      if ($typesense_schema == NULL) {
        return NULL;
      }

      return $typesense_schema->id();
    }
    catch (InvalidPluginDefinitionException | PluginNotFoundException $e) {
      return NULL;
    }
  }

So basically the logic is:

  1. Get the index ID.
  2. Load the TypesenseSchema entity with the same ID.
  3. If it exists, return the ID of the entity we just loaded.

But since both the index and the schema entities have exactly the same ID, this whole logic can be reduced to:

  public function getCollectionName(?IndexInterface $index): ?string {
    return $index?->id();
  }

I also think it doesn't make any sense to allow passing in NULL for $index, and we should also not allow NULL to be returned. So this can become:

  public function getCollectionName(IndexInterface $index): string {
    return $index->id();
  }
🇧🇬Bulgaria pfrenssen Sofia

Many methods in TypesenseClientInterface are taking a collection name as an argument, for example:

  /**
   * Removes a Typesense index.
   *
   * @param string|null $collection_name
   *   The name of the index to remove.
   */
  public function dropCollection(?string $collection_name): void;

In the current code base, since the collection name was always equal to the index ID, we were often passing in the index ID in lieu of the collection name, here is an example from SearchApiTypesenseBackend::removeIndex()

$this->getTypesenseClient()->dropCollection($index->id());

We should change this everywhere to use the collection name instead, so we properly decouple the collections from indexes. It will become:

$collection_name = $this->getCollectionName($index);
$this->getTypesenseClient()->dropCollection($collection_name);

In order to facilitate this, we should make SearchApiTypesenseBackend::getCollectionName() public so we can easily get the collection name for a given index.

🇧🇬Bulgaria pfrenssen Sofia

In the current implementation the Search API index name is used when the Typesense collection is created. This has caused some confusion in the code base for interacting with the schema:

Using the index ID
Sometimes we are retrieving the schema using the index ID, example:

src/Controller/TypesenseIndexController.php
62:    $schema = $backend->getSchema($search_api_index->id())?->getSchema();

Using the collection name
Sometimes we are retrieving the schema using the collection name, example:

src/Plugin/search_api/backend/SearchApiTypesenseBackend.php
407:    $schema = $this->getSchema($collection);

Since in 100% of the cases, the collection name is derived from the index, let's standardize this on the index.

🇧🇬Bulgaria pfrenssen Sofia

Thanks for bumping this :) We are already D11 compliant for a year now, this can be closed.

🇧🇬Bulgaria pfrenssen Sofia

Thanks for the review! I created a followup for the coding standards violations since these are out of scope, the violations are in files untouched by this MR. 📌 Fix coding standards violations Active

🇧🇬Bulgaria pfrenssen Sofia

This is fixed for 2.x, but can still potentially be backported to the 1.x branch.

🇧🇬Bulgaria pfrenssen Sofia

Thanks for working on this!

Instead of adding additional code to the form validate handler, let's instead mark the field as required in Registrant::baseFieldDefinitions() as was proposed in the issue summary.

By doing this we will inform Drupal that the field is required. Drupal will then take care of the form validation for us. Also it will do some other helpful things like marking the field as required, support entity validation etc.

We do not need to make any changes to the form, it will all be handled automatically :)

🇧🇬Bulgaria pfrenssen Sofia

Thanks for the quick response. The attached MR fixes it for me. I am not 100% sure if the patch is omitting certain fields that are necessary for the CSV export since I am not using that feature. Please let me know if anything needs to be done.

🇧🇬Bulgaria pfrenssen Sofia

Maybe a better solution would be to remove Drupal 9.5 from the supported versions and require PHP 8. Both Drupal 9.5 and PHP 7.4 are unsupported so the maintainers' efforts are better spent elsewhere.

🇧🇬Bulgaria pfrenssen Sofia

I made issues for our 4 plugin types:

Would it be OK to include these in the roadmap for the 5.x branch?

🇧🇬Bulgaria pfrenssen Sofia

Now that we are planning the next major, would it be a good idea to get attribute based plugin definitions in? [#SchemaExtension()] rather than @SchemaExtension.

🇧🇬Bulgaria pfrenssen Sofia

pfrenssen changed the visibility of the branch 11.x to hidden.

🇧🇬Bulgaria pfrenssen Sofia

pfrenssen changed the visibility of the branch 1255092-url-should-return to hidden.

🇧🇬Bulgaria pfrenssen Sofia

The automatic merge wasn't triggered.

🇧🇬Bulgaria pfrenssen Sofia

Fixed PHPStan error. Maybe we can set PHPStan to not allow failures?

🇧🇬Bulgaria pfrenssen Sofia

Just updating the version strings is not sufficient. I did a check and the following issues also need to be addressed:

  1. Class Drupal\mailgun\Commands\MailgunSanitizeCommands implements deprecated interface Drush\Drupal\Commands\sql\SanitizePluginInterface: use \Drush\Commands\sql\sanitize\SanitizePluginInterface instead.
  2. MailgunMail.php, line 111: Call to deprecated method renderPlain(). Deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. Use Drupal\Core\Render\RendererInterface::renderInIsolation() instead.
🇧🇬Bulgaria pfrenssen Sofia

@owenbush do you want to weigh in?

🇧🇬Bulgaria pfrenssen Sofia

Thanks! Looks good to me. There are some test failures but this is because the module is not yet Drupal 11 compatible, that is unrelated to this issue.

🇧🇬Bulgaria pfrenssen Sofia

Setting this to major since this is causing a test failure that is affecting all open issues for 3.0.x.

🇧🇬Bulgaria pfrenssen Sofia

Thanks very much for the offer.

We could really need some help with reviewing the open issues. I was onboarded as a co-maintainer and worked on a number of issues in the second half of 2024. I went through many tickets, reviewed and merged the ones that were ready and started the 3.x branch. Unfortunately the momentum has slowed down a lot because many tickets that are in "needs review" state are the ones that I worked on myself and I couldn't move them forwards.

🇧🇬Bulgaria pfrenssen Sofia

Moving this to 3.x since this is D11 related.

Thanks for the report, this looks like exactly the kind of issue that the D11 OOP hook system is designed to solve.

It would be good to add #[LegacyHook] to the old hook implementation so it is not executed twice. There is some more information about this in Support for object oriented hook implementations using autowired services .

🇧🇬Bulgaria pfrenssen Sofia

pfrenssen made their first commit to this issue’s fork.

🇧🇬Bulgaria pfrenssen Sofia

I was too quick in deciding to expose the metadata type directly as children of container / composite elements. It is actually better to expose WebformElement because there can be fields on them that are essential for rendering the forms in the frontend.

A good example is the "maxlength" for text fields. This is exposed on WebformElementTextBase which is an interface for WebformElement.

So I reverted Expose the element metadata directly in nested elements Active again in this MR.

🇧🇬Bulgaria pfrenssen Sofia

This is looking great, thanks a lot! We can now configure PHPStan to not allow any failures and this is good to go.

🇧🇬Bulgaria pfrenssen Sofia

I agree, Drupal 10.2 is no longer supported so we can drop it and set Drupal 10.3 as the minimum version.

🇧🇬Bulgaria pfrenssen Sofia

I have done the first part of the work: I added support for generic composite elements. It looks like there are a few gotchas with composite elements:

The webform module expects the values for the child elements to be passed as parent_element_key[0][child_element_key] but then the validation messages are returned as parent_element_key[items][0][child_element_key]. This is going to be confusing for the GraphQL consumer that there is an unexpected [items] part appearing in the element key.

Another thing I noticed is that when a child element is required, and the composite element is configured to show N number of empty rows, that the validation errors returned by the Webform module are duplicated N times. I am guessing this is intended to make it easier to highlight the validation error in the Drupal rendered webform for the N empty rows. But for the GraphQL consumer this might be confusing since they might get more validation errors back than values they are submitting.

A third thing is that, since we don't know in advance how many child elements are going to be rendered for a composite field that allows multivalue child elements, the Webform module returns element keys like parent_element_key__child_element_key. This will need to be converted into parent_element_key[N][child_element_key] by the frontend implementation. We need to document this somehow.

I think in scope of this issue I am not going to try to transform the internally used logic for handling validation of these elements and just return what we get. Now we have some special cases when handling validation of composite elements, but there will probably be more edge cases in other elements. If this becomes problematic in the future we can implement a plugin system to handle these kinds of edge cases for each Webform element type.

🇧🇬Bulgaria pfrenssen Sofia

Thanks so much for coming up with this smart workaround!

🇧🇬Bulgaria pfrenssen Sofia

This is a great suggestion. The help text configuration in the Webform module consists of 3 configuration options which can be exposed as metadata fields:

metadata {
  help: String,
  helpTitle: String,
  helpDisplay: WebformHelpDisplayOption
}

enum WebformHelpDisplayOption {
  ELEMENT_AFTER
  ELEMENT_BEFORE
  TITLE_AFTER
  TITLE_BEFORE
}
🇧🇬Bulgaria pfrenssen Sofia

I split off the removal of the nesting level to Expose the element metadata directly in nested elements Active . I will implement that first because it will simplify this issue.

🇧🇬Bulgaria pfrenssen Sofia

I started on this. It looks like we can also support the fallback element named WebformElementWebformAddress which works out of the box (without needing the Address module).

The address elements are so-called "composite" elements which share a lot of similarities to other elements like "Flexbox" and "Fieldset". We can unify these under a shared WebformElementContainerBase interface.

Now that Move properties on the WebformElement interface Active is in we can remove 1 level of nesting by directly exposing WebformElementMetadata on the child elements rather than exposing WebformElement which has become just a wrapper intended to slim down the schema.

🇧🇬Bulgaria pfrenssen Sofia

This is looking great, thanks very much!

🇧🇬Bulgaria pfrenssen Sofia

Thanks for the feedback @dulnan! I will implement this.

🇧🇬Bulgaria pfrenssen Sofia

Unassigning, let's first figure out the best way to approach this.

🇧🇬Bulgaria pfrenssen Sofia

The GraphQL specification actually required fields defined on an interface to be duplicated across all the types that implement the interface.

I mistakenly thought we could generate a schema like this:

interface WebformElement {
  key: String!
  title: String
  description: String
  multiple: Int
  required: WebformRequiredElement
  type: String!
}

interface WebformElementMultipleValuesBase {
  multipleValues: WebformElementMultipleValues
}

type WebformElementAddress implements WebformElement & WebformElementMultipleValuesBase {
}

type WebformElementCaptcha implements WebformElement {
}

But the GraphQL interface specification actually _requires_ the fields to be duplicated on the implementors, with the motivation that this makes it easier to declare covariant types.

So it quickly becomes bloated like this:

interface WebformElement {
  key: String!
  title: String
  description: String
  multiple: Int
  required: WebformRequiredElement
  type: String!
}

interface WebformElementMultipleValuesBase {
  multipleValues: WebformElementMultipleValues
}

type WebformElementAddress implements WebformElement & WebformElementMultipleValuesBase {
  key: String!
  title: String
  description: String
  multiple: Int
  required: WebformRequiredElement
  type: String!
  multipleValues: WebformElementMultipleValues
}

type WebformElementCaptcha implements WebformElement {
  key: String!
  title: String
  description: String
  multiple: Int
  required: WebformRequiredElement
  type: String!
}

Since we are still building out the schema it is easy to see how this can bloat the schema exponentially.

A good solution to limit this could be to define only a single element on an interface, for example:

interface WebformElement {
  metadata: WebformElementMetadata!
}

interface WebformElementMultipleValuesBase {
  multipleValues: WebformElementMultipleValues
}

type WebformElementMetadata {
  key: String!
  title: String
  description: String
  multiple: Int
  required: WebformRequiredElement
  type: String!
}

type WebformElementAddress implements WebformElement & WebformElementMultipleValuesBase {
  metadata: WebformElementMetadata!
  multipleValues: WebformElementMultipleValues
}

type WebformElementCaptcha implements WebformElement {
  metadata: WebformElementMetadata!
  type: String!
}

Which is incidentally how it was in version 2.x-alpha1, but this got flattened out as part of the change request Remove Metadata subfields Fixed .

It is probably a good idea to revisit this, and to reconsider adding back the metadata field to limit schema bloat.

We should also take other measures to limit the size of the schema. For example we should not expose all field types by default.

🇧🇬Bulgaria pfrenssen Sofia

Thanks @agunjan085 for picking this up! Just a heads up, this is an issue intended for the 2.x-dev branch, but the merge requests are still made for the old 8.x-1.x branch. Please make sure you are working on the right branch!

Unfortunately I do not have the right permissions to fix the default branch in Gitlab.

🇧🇬Bulgaria pfrenssen Sofia

Thanks for the updates @VladimirAus, I am really happy with how this is shaping up. Not only is the PHP error solved in a clean way, but we also have a helpful exception that can be caught by calling code and gives useful information about the underlying problem.

I would like to remove the second test because I think it is unnecessary and would make future maintenance of the class harder. And we have a PHPStan warning. I left comments on the MR.

Production build 0.71.5 2024