Make more complex queries possible

Created on 17 August 2023, over 1 year ago
Updated 18 August 2023, over 1 year ago

Problem/Motivation

The code that converts the YAML query definition into an actual Graph QL query has limitations. Arguments for example can only be defined on the top level field. It would be nice if we can define more complex queries. An example of a structure that we need that does not work right now is

  query:
    search:
      listings:
        arguments:
          locale: nl_BE
          metadata:
            size: 400
          publication:
            changedDateFrom: '2023-08-12T00:00:00.000Z'
        fields:
          - metadata:
            - currentPage
            - totalPages
          - listings:
            - id
            - details:
              - vehicle:
                - classification:
                  - make:
                    - formatted
                  - model:
                    - formatted
                  - modelVersionInput
                - numberOfDoors

Steps to reproduce

Might not be that easy as you need an GraphQL API that has a complex structure. :-)

Proposed resolution

Not sure yet. We could introduce a structured that would translated into a nested structure with associative arrays. But it would also be nice if we can keep the YAML structure easy an straight forward for APIs that are not that complex.

Remaining tasks

-
-
- Review MR
- Merge MR

User interface changes

N/A

API changes

Changes in YAML structure to use for modelling the query in the migration definition.

Data model changes

N/A

Feature request
Status

Fixed

Version

2.0

Component

Code

Created by

🇳🇱Netherlands ricovandevin

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @ricovandevin
  • 🇮🇹Italy reinchek Napoli 🌋, 🇮🇹

    Hi @ricovandevin,
    the first thing (maybe simple) that comes to my mind is to embed some recursive logic to \Plugin\migrate\source\GraphQL::getGenerator() buildQuery() instead to call directly $this->buildQuery() we could call $this->buildQueryRecursive (but it would works only for first "arguments" match) that will builds the query recursively.
    Somethings like this:

      private function buildQueryRecursive(string $queryName, array $query, ?Query &$qlQuery = null) {
        $arguments = NULL;
        $keys = array_keys($query);
    
        if (in_array('fields', $keys)) {
          // $queryName has "fields"
          $fields = $query['fields'];
          if (in_array('arguments', $keys)) {
            // $queryName has "arguments"
            $arguments = $query['arguments'];
          }
    
          $subQlQuery = $this->buildQuery($queryName, $fields, $arguments);
          $qlQuery->setSelectionSet([$subQlQuery]);
        } else {
          if (is_null($qlQuery)) {
            $qlQuery = new Query($queryName);
            $subQlQuery = $qlQuery;
          } else {
            $subQlQuery = (new Query($queryName));
            $qlQuery->setSelectionSet([$subQlQuery]);
          }
    
          return $this->buildQueryRec(array_key_first($query), reset($query), $subQlQuery);
        }
      }
    

    So, using this code, i tried to launch your migration (just for obtains compiled query) and the query generated was as follows:

    query {
      search {
        listings(
          locale: nl_BE
          metadata: { size: 400 }
          publication: { changedDateFrom: "2023-08-12T00:00:00.000Z" }
        ) {
          metadata {
            currentPage
            totalPages
          }
          listings {
            id
            details {
              vehicle {
                classification {
                  make {
                    formatted
                  }
                  model {
                    formatted
                  }
                  modelVersionInput
                }
                numberOfDoors
              }
            }
          }
        }
      }
    }
    

    Maybe this could be the way, what do you think?

  • 🇮🇹Italy reinchek Napoli 🌋, 🇮🇹

    Edit:
    the above code breaks simple query parsing like as the following one:

      query:
        posts:
          fields:
            -
              data:
                - id
                - title
                - body
    

    This code, instead, will work with anyway:

      private function buildQueryRecursive(string $queryName, array $query, ?Query &$qlQuery = null) {
        $arguments = NULL;
        $keys = array_keys($query);
    
        if (in_array('fields', $keys)) {
          // $queryName has "fields"
          $fields = $query['fields'];
          if (in_array('arguments', $keys)) {
            // $queryName has "arguments"
            $arguments = $query['arguments'];
          }
    
          $subQlQuery = $this->buildQuery($queryName, $fields, $arguments);
          // If $qlQuery is null means that we found 'fields' || 'arguments' at the first recursive operation...
          if (is_null($qlQuery)) {
            $qlQuery = $subQlQuery;
          } else {
            $qlQuery->setSelectionSet([$subQlQuery]);
          }
        } else {
          if (is_null($qlQuery)) {
            $qlQuery = new Query($queryName);
            $subQlQuery = $qlQuery;
          } else {
            $subQlQuery = (new Query($queryName));
            $qlQuery->setSelectionSet([$subQlQuery]);
          }
    
          return $this->buildQueryRecursive(array_key_first($query), reset($query), $subQlQuery);
        }
      }
    
  • 🇳🇱Netherlands ricovandevin

    @reinchek Thanks for looking into this so quickly. I have also been looking into this and came up with a solution that involves defining a "path" from the query name to the level holding the actual field list. That was after trying to get a recursive solution working. Good to see that you did. I have tried the compiled query you have posted in #2 using the API suppliers testing tool and it works great.

    Other issues I ran into while working on this where the fields() method which expects the field list to be on a "higher level" in the query and the actual fetching of the result rows in the getGenerator() method. Both were solved using the "path" configuration mentioned in the previous paragraph.

    As a next step I will try to integrate your recursive query compilation code with solutions to this issues in the fields() and getGenerator() methods. Although I'm not sure whether we really need fields()? I will check some other source plugins to see how they implement the method.

  • @ricovandevin opened merge request.
  • Status changed to Needs review over 1 year ago
  • 🇳🇱Netherlands ricovandevin

    The recursive code works great for my original example query.

      query:
        search:
          listings:
            arguments:
              locale: nl_BE
              metadata:
                size: 400
              publication:
                changedDateFrom: '2023-08-12T00:00:00.000Z'
            fields:
              - metadata:
                - currentPage
                - totalPages
              - listings:
                - id
                - details:
                  - vehicle:
                    - classification:
                      - make:
                        - formatted
                      - model:
                        - formatted
                      - modelVersionInput
                    - numberOfDoors
    

    I have put the code in a MR and added another change in \Drupal\migrate_source_graphql\Plugin\migrate\source\GraphQL::getGenerator() to let the generator find the actual result rows in a nested structure. I have done this by allowing data_key to contain a path to the results using the property separator as defined in \Drupal\migrate\Row::PROPERTY_SEPARATOR (which is currently a forward slash). For the example above this would be:

      ...
      auth_parameter: '***'
      data_key: listings/listings
      query:
        search:
      ...
    

    With these changes a can do a successful migration. I have left fields() out-of-scope for now as it doesn't seem to do much. At least it does not prevent the migration from working.

  • 🇮🇹Italy reinchek Napoli 🌋, 🇮🇹

    @ricovandevin thanks you too.

    Sorry because i avoided to paste other code changes (to adapt, correctly the buildQueryRecursive method) but the intent was just for show you a possible (i tried as you done and can say...) working solution.

    About the "fields" property (as i wroted in "The migration" paragraph of the module's "documentation"):

    The only difference between the GraphQL query and the YAML transposition is the mandatory property fields, the usefulness of which is solely a matter of choice for the developer.

    So, we could do some semantics changes to the code to remove the priority "fields" property role. But, yes, i'm agreed with you, for now, let it out.

    I also agree with data_key solution. 👌

    I've tested and then merged the PR. I've also added a new doc section (you can read in module's home "From 2.0.6 version (issue-3381675)").

    Thanks again ;)

  • 🇮🇹Italy reinchek Napoli 🌋, 🇮🇹
  • Status changed to Fixed over 1 year ago
  • 🇮🇹Italy reinchek Napoli 🌋, 🇮🇹
  • 🇮🇹Italy reinchek Napoli 🌋, 🇮🇹

    New features inside v. 2.0.7 .

Production build 0.71.5 2024