📃 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:
- 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.
- Return an array containing all and only the Pokémon names in English.
- List the numbers of Pokémon for the grass, fire and water types.
- 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.
- Return the percentage of single-type Pokémon, rounded to the nearest integer.
- List the Pokémon based on the number of letters in their English name, from the longest string to the shortest string.
- 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
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)
- An empty filter means that all Pokémon will be selected
- Addition all the stats (same syntax as JavaScript for accessing properties)
- Return the name of the Pokémon
- 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**
```json
[
"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**
```js
{
"Grass": count(*["Grass" in type]),
"Fire": count(*["Fire" in type]),
"Water": count(*["Water" in type]),
}
```
**Response**
```json
{
"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
<img src="https://thepracticaldev.s3.amazonaws.com/i/9awbl55s8z4789sg3xz2.png" alt="Pokémon Wailord" width="200"/>
**Query**
```js
*['Water' in type && length(type) == 1]{
id,
"name": name.english,
base
}|order(base.HP desc)
```
**Response**
```json
[
{
"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**
```js
{
"percentage": round(count(*[length(type) == 1]) * 100 / count(*[]))
}
```
**Response**
```json
{
"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
<img src="https://thepracticaldev.s3.amazonaws.com/i/6z7p8mxw11hivwillg4p.png" alt="Pokémon Crabominable" width="200"/>
**Query**
```js
*[]{
"name": name.english,
"length": length(name.english)
}|order(length desc)
```
**Response**
```json
[
{
"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**
```js
*[]|order(base.Attack){
"name": name.english,
"attack": base.Attack,
"evaluation": select(
base.Attack > 124 => "strong",
base.Attack > 83 => "average",
"weak"
)}
```
**Response**
```json
[
{
"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