loading...
Cover image for 7 challenges for querying with GROQ

7 challenges for querying with GROQ

mornir profile image Jérôme Pott ・4 min read

📃 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]

Response

    {
      "overall": 175,
      "name": "Wishiwashi"
    }

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

Response

    [
      "Bulbasaur",
      "Ivysaur",
      "Venusaur",
        ...
    ]

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]),
    }

Response

    {
      "Grass": 97,
      "Fire": 64,
      "Water": 131
    }

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)

Response

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

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(*[]))
    }

Response

    {
      "percentage": 50
    }

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)

Response

    [
      {
        "name": "Crabominable",
        "length": 12
      },
      {
        "name": "Fletchinder",
        "length": 11
      },
     ...
    ]

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"
)}

Response

[
  {
    "name": "Chansey",
    "attack": 5,
    "evaluation": "weak"
  },
  ...
]

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

Posted on Jan 3 by:

mornir profile

Jérôme Pott

@mornir

Web Developer building websites on the Jamstack.

Discussion

markdown guide
 

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)
 

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=`)

Solution

Also obscured by way of base64 encoding...

atob("WwogIHsKICAgICJsYW5ndWFnZSI6ICJlbmdsaXNoIiwKICAgICJsb25nZXN0IjogbGVuZ3RoKCp8b3JkZXIobGVuZ3RoKG5hbWUuZW5nbGlzaCkgZGVzYylbMF0ubmFtZS5lbmdsaXNoKQogIH0sCiAgewogICAgImxhbmd1YWdlIjogImphcGFuZXNlIiwKICAgICJsb25nZXN0IjogbGVuZ3RoKCp8b3JkZXIobGVuZ3RoKG5hbWUuamFwYW5lc2UpIGRlc2MpWzBdLm5hbWUuamFwYW5lc2UpCiAgfSwKICB7CiAgICAibGFuZ3VhZ2UiOiAiY2hpbmVzZSIsCiAgICAibG9uZ2VzdCI6IGxlbmd0aCgqfG9yZGVyKGxlbmd0aChuYW1lLmNoaW5lc2UpIGRlc2MpWzBdLm5hbWUuY2hpbmVzZSkKICB9LAogIHsKICAgICJsYW5ndWFnZSI6ICJmcmVuY2giLAogICAgImxvbmdlc3QiOiBsZW5ndGgoKnxvcmRlcihsZW5ndGgobmFtZS5mcmVuY2gpIGRlc2MpWzBdLm5hbWUuZnJlbmNoKQogIH0sCl18b3JkZXIobG9uZ2VzdCBkZXNjKVtdLmxhbmd1YWdl")
 

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!

 

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

*[].name.english|order(length(@) desc)
 

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