DEV Community

Cover image for 7 challenges for querying with GROQ
Jérôme Pott
Jérôme Pott

Posted on

7 challenges for querying with GROQ

📃 Introduction

Developers who are lucky to work with the headless CMS from Sanity have the opportunity to use a leaner and more expressive alternative to GraphQL: GROQ (Graph-Relational Object Queries).

The Sanity team recently launched groq.dev. There you can easily test out the GROQ syntax and play with it, without having to create a Sanity project.

I created 7 challenges that are relatively difficult. Before you try to tackle them, you should familiarize yourself with the GROQ specs by reading this introduction and having a look at this cheat-sheet.

💪 Challenges

Now you should be ready for the challenges on the Pokédex dataset:

  1. Find the weakest Pokémon overall. In other words, when comparing the sum of the base stats, return the Pokémon (only one!) with the lowest score.
  2. Return an array containing all and only the Pokémon names in English.
  3. List the numbers of Pokémon for the grass, fire and water types.
  4. List all the Pokémon that are only of water type. For each, return their ID, their names only in English and all their stats. Order them by their HP in descending order.
  5. Return the percentage of single-type Pokémon, rounded to the nearest integer.
  6. List the Pokémon based on the number of letters in their English name, from the longest string to the shortest string.
  7. Group the Pokémon in three categories (strong, average, weak) based on their attack stat. attack above 124 = strong; attack between 83 and 124 = average; attack below 83 = weak. Order the Pokémon by their attack from the lowest to the highest stat. Return their names, their attack stat and an evaluation property containing the value strong, average or weak.

💡 Solution

If you just want to see the queries without any explanations, you can read them in this gist.

Challenge 1: The one with the lowest base stats

Pokémon Wishiwashi

Query

*[]{
  "overall": base.HP + base.Attack + base.Defense + base["Sp. Attack"] + 
             base["Sp. Defense"] + base.Speed,
  "name": name.english,
}|order(overall)[0]
Enter fullscreen mode Exit fullscreen mode

Response

    {
      "overall": 175,
      "name": "Wishiwashi"
    }
Enter fullscreen mode Exit fullscreen mode

Explanation (line by line)

  1. An empty filter means that all Pokémon will be selected
  2. Addition all the stats (same syntax as JavaScript for accessing properties)
  3. Return the name of the Pokémon
  4. Order by the overall property calculated above and only return the first element (the order is ascending by default).

Challenge 2: All and only the names

Query

    *[].name.english
Enter fullscreen mode Exit fullscreen mode

Response

    [
      "Bulbasaur",
      "Ivysaur",
      "Venusaur",
        ...
    ]
Enter fullscreen mode Exit fullscreen mode

Explanation
Select all Pokémon and return an array of values only (without object wrapper) for the property english. If you use a projection with the brackets syntax, you would end up with an array of objects.

Challenge 3: Grass, fire and water

Query

    {
      "Grass": count(*["Grass" in type]),
      "Fire": count(*["Fire" in type]),
      "Water": count(*["Water" in type]),
    }
Enter fullscreen mode Exit fullscreen mode

Response

    {
      "Grass": 97,
      "Fire": 64,
      "Water": 131
    }
Enter fullscreen mode Exit fullscreen mode

Explanation (line by line)

  1. You can wrap multiple selections in brackets.
  2. Return the number of Pokémon with Grass in their type and store the number in the Grass property.
  3. Idem for Fire and Water

Challenge 4: water-type Pokémon

Pokémon Wailord

Query

    *['Water' in type && length(type) == 1]{
      id,
      "name": name.english,
      base
    }|order(base.HP desc)
Enter fullscreen mode Exit fullscreen mode

Response

    [
    {
        "id": 321,
        "name": "Wailord",
        "base": {
          "HP": 170,
          "Attack": 90,
          "Defense": 45,
          "Sp. Attack": 90,
          "Sp. Defense": 45,
          "Speed": 60
        }
      },
     ...
    ]
Enter fullscreen mode Exit fullscreen mode

Explanation (line by line)

  1. Filter Pokémon by type water and with only one type. The length function returns the length of an array.
  2. Projection: only return the id, the English name and all the stats.
  3. Sort the results by the HP in descending order.

Challenge 5: percentage of single type Pokémon

Query

    {
      "percentage": round(count(*[length(type) == 1]) * 100 / count(*[]))
    }
Enter fullscreen mode Exit fullscreen mode

Response

    {
      "percentage": 50
    }
Enter fullscreen mode Exit fullscreen mode

Explanation
All standard arithmetic operations are supported in GROQ: get the number of single type Pokémon times 100 and divided by the total number of Pokémon in the dataset. (50% of Pokémon only have one type 😯)

Challenge 6: The longest name

Pokémon Crabominable

Query

    *[]{
      "name": name.english,
      "length": length(name.english)
    }|order(length desc)
Enter fullscreen mode Exit fullscreen mode

Response

    [
      {
        "name": "Crabominable",
        "length": 12
      },
      {
        "name": "Fletchinder",
        "length": 11
      },
     ...
    ]
Enter fullscreen mode Exit fullscreen mode

Explanation (line by line)

  1. An empty filter means that all Pokémon will be selected
  2. Projection with English name and the value of the length function, which can also return the length of a string.
  3. Sort the results by the length property calculated above, in descending order.

Challenge 7: The weak, the average and the strong

Query

*[]|order(base.Attack){
  "name": name.english,
  "attack": base.Attack,
  "evaluation": select(
  base.Attack  > 124  => "strong",
  base.Attack > 83 => "average",
  "weak"
)}
Enter fullscreen mode Exit fullscreen mode

Response

[
  {
    "name": "Chansey",
    "attack": 5,
    "evaluation": "weak"
  },
  ...
]
Enter fullscreen mode Exit fullscreen mode

Explanation (line by line)

  1. The ordering can also happen right after the filtering. In this case, the filtering doesn't work after the projection for some reason (probably because of the select function)
  2. Projection with the name, the attack and a conditional. If none of the conditions are met, weak is returned.

😌 Closing words

I hope you had fun playing around with GROQ and solving the challenges. If you caught a mistake or found alternative queries, don't hesitate to leave a comment.

🐍 The GROQ logo in the upper right part hides an Easter egg. Have you found it?

Image sources:
Pokédex: https://dribbble.com/shots/2908884-I-Saw-It-On-Twitch-Pokedex
Wishiwashi: https://bulbapedia.bulbagarden.net/wiki/Wishiwashi_(Pokémon)
Wailord: https://www.serebii.net/pokedex-swsh/wailord/
Crabominable: https://www.pokemon.com/us/pokedex/crabominable

Top comments (5)

Collapse
 
billymoon profile image
Billy Moon • Edited

Great challenges. For most of them my answers were near identical to what's published here, which I think is a testament to the GROQ language itself that there seems to be a kind of natural solution to be arrived at for a specific problem.

I propose another challenge

Challenge 8:

An array of language names, ordered by the length of their longest Pokemon name, languages with longer Pokemon names appearing before shorter Pokemon names.

Answer

Obscured by way of base64 encoding...

atob(`WwogICJmcmVuY2giLAogICJlbmdsaXNoIiwKICAiamFwYW5lc2UiLAogICJjaGluZXNlIgpdCgouLi4gb3IgLi4uCgpbCiAgImVuZ2xpc2giLAogICJmcmVuY2giLAogICJqYXBhbmVzZSIsCiAgImNoaW5lc2UiCl0=`)
Enter fullscreen mode Exit fullscreen mode

Solution

Also obscured by way of base64 encoding...

atob("WwogIHsKICAgICJsYW5ndWFnZSI6ICJlbmdsaXNoIiwKICAgICJsb25nZXN0IjogbGVuZ3RoKCp8b3JkZXIobGVuZ3RoKG5hbWUuZW5nbGlzaCkgZGVzYylbMF0ubmFtZS5lbmdsaXNoKQogIH0sCiAgewogICAgImxhbmd1YWdlIjogImphcGFuZXNlIiwKICAgICJsb25nZXN0IjogbGVuZ3RoKCp8b3JkZXIobGVuZ3RoKG5hbWUuamFwYW5lc2UpIGRlc2MpWzBdLm5hbWUuamFwYW5lc2UpCiAgfSwKICB7CiAgICAibGFuZ3VhZ2UiOiAiY2hpbmVzZSIsCiAgICAibG9uZ2VzdCI6IGxlbmd0aCgqfG9yZGVyKGxlbmd0aChuYW1lLmNoaW5lc2UpIGRlc2MpWzBdLm5hbWUuY2hpbmVzZSkKICB9LAogIHsKICAgICJsYW5ndWFnZSI6ICJmcmVuY2giLAogICAgImxvbmdlc3QiOiBsZW5ndGgoKnxvcmRlcihsZW5ndGgobmFtZS5mcmVuY2gpIGRlc2MpWzBdLm5hbWUuZnJlbmNoKQogIH0sCl18b3JkZXIobG9uZ2VzdCBkZXNjKVtdLmxhbmd1YWdl")
Enter fullscreen mode Exit fullscreen mode
Collapse
 
djeglin profile image
David Eglin • Edited

If the data had also had references for evolved pokemon to their earlier evolutions, I could have suggested another challenge – To find all the evolutions and output the pokemon english names in their evolution graphs, for example: Pichu > Pikachu > Raichu. I've had to perform a similar task in the wild and it wasn't easy. If you have any hierarchical content, this is a great task to perform.

Great list of tasks. There were a couple of "shortcuts" in here that I hadn't thought of before. A fun, solid, introduction to the world of Groq!

Collapse
 
billymoon profile image
Billy Moon

Spoiler alert...

For challenge 7 I was able to filter after the projection, but must use the column name of the projection, not the original data.

*[]{
  "name": name.english,
  "attack": base.Attack,
  "evaluation": select(
    base.Attack < 83 => "weak",
    base.Attack < 124 => "average",
    "strong"
  )
}|order(attack)
Collapse
 
billymoon profile image
Billy Moon

Challenge 6, assuming you want a simple array of names can be done like this...

*[].name.english|order(length(@) desc)
Enter fullscreen mode Exit fullscreen mode
Collapse
 
kmelve profile image
Knut Melvær

This is a really nice introduction to advanced features in GROQ! Thanks for putting it up 🙇‍♂️