# Pagination

> Listes paginées par curseur, comme Stripe : has_more, next_cursor, sans doublon ni trou.

Documentation de l’API de WoWDB des Défias : https://docs.wowdb.assemblee-defias.fr/pagination

## Principe

Toutes les listes ont la même enveloppe :

- `object` (chaîne) : Toujours « list ». Valeurs : `list`.
- `data` (tableau d’objets) : Les éléments de cette page (objets du type de la liste).
- `has_more` (booléen) : Vrai s’il reste des éléments après cette page.
- `next_cursor` (chaîne ou null) : Curseur de la page suivante, à passer tel quel dans cursor ; null en fin de liste.
- `url` (chaîne) : Adresse de la liste dans l’API, filtres compris (sans cursor ni limit).
- `total_count` (entier, facultatif) : Nombre total d’éléments avec ces filtres ; seulement avec include_total=true.

`limit` règle la taille de la page, de 1 à 100 (20 par défaut). Tant que `has_more` vaut `true`, repassez la valeur de `next_cursor` dans le paramètre `cursor`, **avec les mêmes filtres et le même tri**. La dernière page a `has_more: false` et `next_cursor: null`.

La pagination suit la clé de tri (« keyset ») : chaque page reprend juste après le dernier élément de la précédente. Pas de doublon, pas de trou, et la centième page est aussi rapide que la première.

**Requête (Première page)**

#### cURL

```bash
curl -G https://api.wowdb.assemblee-defias.fr/v1/classic/items \
  -d type=dagger \
  -d rogue=true \
  -d quality=rare,epic \
  -d max_level=30 \
  -d sort=-item_level \
  -d limit=2
```

#### JavaScript

```js
const params = new URLSearchParams({
  type: "dagger",
  rogue: "true",
  quality: "rare,epic",
  max_level: "30",
  sort: "-item_level",
  limit: "2",
});
const res = await fetch(`https://api.wowdb.assemblee-defias.fr/v1/classic/items?${params}`);
if (!res.ok) throw new Error((await res.json()).error.message);
const page = await res.json();
for (const item of page.data) {
  console.log(item.name);
}
```

#### Python

```python
import json
import urllib.parse
import urllib.request

params = urllib.parse.urlencode({
    "type": "dagger",
    "rogue": "true",
    "quality": "rare,epic",
    "max_level": "30",
    "sort": "-item_level",
    "limit": "2",
})
req = urllib.request.Request(
    f"https://api.wowdb.assemblee-defias.fr/v1/classic/items?{params}",
    headers={"User-Agent": "mon-application/1.0 (contact@example.com)"},
)
with urllib.request.urlopen(req) as res:
    page = json.load(res)
for item in page["data"]:
    print(item["name"])
```

#### PHP

```php
<?php
$params = http_build_query([
    'type' => 'dagger',
    'rogue' => 'true',
    'quality' => 'rare,epic',
    'max_level' => '30',
    'sort' => '-item_level',
    'limit' => '2',
]);
$context = stream_context_create([
    'http' => ['header' => "User-Agent: mon-application/1.0 (contact@example.com)\r\n"],
]);
$json = file_get_contents('https://api.wowdb.assemblee-defias.fr/v1/classic/items?' . $params, false, $context);
$page = json_decode($json, true);
foreach ($page['data'] as $item) {
    echo $item['name'], PHP_EOL;
}
```

#### Ruby

```ruby
require "json"
require "net/http"

uri = URI("https://api.wowdb.assemblee-defias.fr/v1/classic/items")
uri.query = URI.encode_www_form(
  type: "dagger",
  rogue: "true",
  quality: "rare,epic",
  max_level: "30",
  sort: "-item_level",
  limit: "2",
)
res = Net::HTTP.get_response(uri, { "User-Agent" => "mon-application/1.0 (contact@example.com)" })
page = JSON.parse(res.body)
page["data"].each do |item|
  puts item["name"]
end
```

#### Go

```go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
)

func main() {
	params := url.Values{}
	params.Set("type", "dagger")
	params.Set("rogue", "true")
	params.Set("quality", "rare,epic")
	params.Set("max_level", "30")
	params.Set("sort", "-item_level")
	params.Set("limit", "2")
	req, err := http.NewRequest("GET", "https://api.wowdb.assemblee-defias.fr/v1/classic/items?"+params.Encode(), nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("User-Agent", "mon-application/1.0 (contact@example.com)")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	var page struct {
		Data    []map[string]any `json:"data"`
		HasMore bool             `json:"has_more"`
	}
	if err := json.NewDecoder(res.Body).Decode(&page); err != nil {
		panic(err)
	}
	for _, item := range page.Data {
		fmt.Println(item["name"])
	}
}
```

#### Java

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Exemple {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder(URI.create(
                "https://api.wowdb.assemblee-defias.fr/v1/classic/items?type=dagger&rogue=true&quality=rare,epic&max_level=30&sort=-item_level&limit=2"))
            .header("User-Agent", "mon-application/1.0 (contact@example.com)")
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        // JSON à lire avec Jackson, Gson ou le client WowDb.java (sans dépendance)
        System.out.println(response.body());
    }
}
```

#### .NET

```csharp
using System.Net.Http;
using System.Text.Json;

using var http = new HttpClient();
http.DefaultRequestHeaders.UserAgent.ParseAdd("mon-application/1.0 (contact@example.com)");
var json = await http.GetStringAsync(
    "https://api.wowdb.assemblee-defias.fr/v1/classic/items?type=dagger&rogue=true&quality=rare,epic&max_level=30&sort=-item_level&limit=2");
using var doc = JsonDocument.Parse(json);
foreach (var item in doc.RootElement.GetProperty("data").EnumerateArray())
    Console.WriteLine(item.GetProperty("name").GetString());
```

#### Rust

```rust
// Cargo.toml : ureq = { version = "2", features = ["json"] } et serde_json = "1"
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let page: serde_json::Value = ureq::get("https://api.wowdb.assemblee-defias.fr/v1/classic/items")
        .set("User-Agent", "mon-application/1.0 (contact@example.com)")
        .query("type", "dagger")
        .query("rogue", "true")
        .query("quality", "rare,epic")
        .query("max_level", "30")
        .query("sort", "-item_level")
        .query("limit", "2")
        .call()?
        .into_json()?;
    for item in page["data"].as_array().into_iter().flatten() {
        println!("{}", item["name"].as_str().unwrap_or_default());
    }
    Ok(())
}
```

**Réponse** (statut 200)

```json
{
  "object": "list",
  "data": [
    {
      "object": "item",
      "id": 23168,
      "key": "classic:item:23168",
      "version": "classic",
      "name": "Dague focale de Dédain",
      "quality": "rare",
      "quality_name": "Rare",
      "icon": "https://wowdb.assemblee-defias.fr/database-icons/inv_weapon_shortblade_07.jpg",
      "item_level": 35,
      "required_level": 30,
      "slot": "one_hand",
      "slot_name": "À une main",
      "type": "dagger",
      "type_name": "Dague",
      "weapon": {
        "damage_min": 22,
        "damage_max": 42,
        "speed": 1.4,
        "dps": 22.9
      },
      "sell_price": 2665,
      "rogue": true,
      "internal": false,
      "tooltip": [
        "Dague focale de Dédain",
        "Niveau d'objet 35",
        "Lié quand ramassé",
        "À une main",
        "Dague",
        "Dégâts : 22 - 42",
        "Vitesse 1.40",
        "(22.9 dégâts par seconde)",
        "Durabilité 65 / 65",
        "Niveau 30 requis",
        "Équipé : Augmente les dégâts et les soins produits par les sorts et effets magiques de 9 au maximum."
      ],
      "url": "https://wowdb.assemblee-defias.fr/?id=classic:item:23168",
      "api_url": "https://api.wowdb.assemblee-defias.fr/v1/classic/items/23168"
    },
    {
      "object": "item",
      "id": 6691,
      "key": "classic:item:6691",
      "version": "classic",
      "name": "Surin de brochepourceau",
      "quality": "rare",
      "quality_name": "Rare",
      "icon": "https://wowdb.assemblee-defias.fr/database-icons/inv_misc_bone_06.jpg",
      "item_level": 35,
      "required_level": 30,
      "slot": "one_hand",
      "slot_name": "À une main",
      "type": "dagger",
      "type_name": "Dague",
      "weapon": {
        "damage_min": 24,
        "damage_max": 45,
        "speed": 1.5,
        "dps": 23
      },
      "sell_price": 8866,
      "rogue": true,
      "internal": false,
      "tooltip": [
        "Surin de brochepourceau",
        "Niveau d'objet 35",
        "Lié quand ramassé",
        "Unique",
        "À une main",
        "Dague",
        "Dégâts : 24 - 45",
        "Vitesse 1.50",
        "(23.0 dégâts par seconde)",
        "+6 Endurance",
        "+4 Esprit",
        "Durabilité 65 / 65",
        "Niveau 30 requis"
      ],
      "url": "https://wowdb.assemblee-defias.fr/?id=classic:item:6691",
      "api_url": "https://api.wowdb.assemblee-defias.fr/v1/classic/items/6691"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJrIjpbLTM1LCJzdXJpbiBkZSBicm9jaGVwb3VyY2VhdSIsImNsYXNzaWM6aXRlbTo2NjkxIl0sImgiOiJqY1BDbFRNSFQyIn0",
  "url": "https://api.wowdb.assemblee-defias.fr/v1/classic/items?max_level=30&quality=rare%2Cepic&rogue=true&sort=-item_level&type=dagger"
}
```

## Parcourir toute une liste

Une boucle suffit : demandez des pages de 100 tant qu’il y a un curseur. Les [bibliothèques clientes](https://docs.wowdb.assemblee-defias.fr/bibliotheques) le font pour vous (`iterate`, `paginate`).

> **Bon à savoir** : Pour parcourir tout le catalogue d’une version (39 133 objets en WotLK), comptez quelques centaines de requêtes : gardez le résultat en cache plutôt que de recommencer ([limites de débit](https://docs.wowdb.assemblee-defias.fr/limites)).

**Toutes les pages**

#### cURL

```bash
# Page suivante : reprenez next_cursor de la page précédente.
curl -G https://api.wowdb.assemblee-defias.fr/v1/classic/items \
  -d rogue=true -d type=dagger -d limit=100
curl -G https://api.wowdb.assemblee-defias.fr/v1/classic/items \
  -d rogue=true -d type=dagger -d limit=100 \
  -d cursor=CURSEUR_DE_LA_PAGE_PRECEDENTE
```

#### JavaScript

```js
// Toutes les dagues utiles au voleur en Vanilla, page après page.
async function* tous(chemin, filtres) {
  let cursor;
  do {
    const params = new URLSearchParams({ ...filtres, limit: "100", ...(cursor && { cursor }) });
    const res = await fetch(`https://api.wowdb.assemblee-defias.fr/v1${chemin}?${params}`);
    if (!res.ok) throw new Error((await res.json()).error.message);
    const page = await res.json();
    yield* page.data;
    cursor = page.next_cursor;
  } while (cursor);
}

for await (const item of tous("/classic/items", { rogue: "true", type: "dagger" })) {
  console.log(item.name);
}
```

#### Python

```python
import json
import urllib.parse
import urllib.request

def tous(chemin, **filtres):
    """Tous les éléments d'une liste, page après page."""
    cursor = None
    while True:
        params = {**filtres, "limit": 100, **({"cursor": cursor} if cursor else {})}
        url = "https://api.wowdb.assemblee-defias.fr/v1" + chemin + "?" + urllib.parse.urlencode(params)
        req = urllib.request.Request(url, headers={"User-Agent": "mon-application/1.0"})
        with urllib.request.urlopen(req) as res:
            page = json.load(res)
        yield from page["data"]
        cursor = page["next_cursor"]
        if not cursor:
            return

for item in tous("/classic/items", rogue="true", type="dagger"):
    print(item["name"])
```

#### PHP

```php
<?php
function tous(string $chemin, array $filtres): Generator {
    $cursor = null;
    do {
        $params = $filtres + ['limit' => 100] + ($cursor ? ['cursor' => $cursor] : []);
        $context = stream_context_create(['http' => ['header' => "User-Agent: mon-application/1.0\r\n"]]);
        $page = json_decode(file_get_contents('https://api.wowdb.assemblee-defias.fr/v1' . $chemin . '?' . http_build_query($params), false, $context), true);
        yield from $page['data'];
        $cursor = $page['next_cursor'];
    } while ($cursor);
}

foreach (tous('/classic/items', ['rogue' => 'true', 'type' => 'dagger']) as $item) {
    echo $item['name'], PHP_EOL;
}
```

#### Ruby

```ruby
require "json"
require "net/http"

def tous(chemin, filtres)
  return enum_for(:tous, chemin, filtres) unless block_given?
  cursor = nil
  loop do
    uri = URI("https://api.wowdb.assemblee-defias.fr/v1#{chemin}")
    uri.query = URI.encode_www_form(filtres.merge(limit: 100).merge(cursor ? { cursor: cursor } : {}))
    page = JSON.parse(Net::HTTP.get_response(uri, { "User-Agent" => "mon-application/1.0" }).body)
    page["data"].each { |element| yield element }
    cursor = page["next_cursor"]
    break unless cursor
  end
end

tous("/classic/items", rogue: true, type: "dagger") { |item| puts item["name"] }
```

#### Go

```go
// Toutes les pages d'une liste ; f est appelée pour chaque élément.
func tous(chemin string, filtres url.Values, f func(map[string]any)) error {
	cursor := ""
	for {
		params := url.Values{}
		for k, v := range filtres {
			params[k] = v
		}
		params.Set("limit", "100")
		if cursor != "" {
			params.Set("cursor", cursor)
		}
		req, _ := http.NewRequest("GET", "https://api.wowdb.assemblee-defias.fr/v1"+chemin+"?"+params.Encode(), nil)
		req.Header.Set("User-Agent", "mon-application/1.0")
		res, err := http.DefaultClient.Do(req)
		if err != nil {
			return err
		}
		var page struct {
			Data       []map[string]any `json:"data"`
			NextCursor *string          `json:"next_cursor"`
		}
		err = json.NewDecoder(res.Body).Decode(&page)
		res.Body.Close()
		if err != nil {
			return err
		}
		for _, element := range page.Data {
			f(element)
		}
		if page.NextCursor == nil {
			return nil
		}
		cursor = *page.NextCursor
	}
}
```

#### Java

```java
// Avec le client sans dépendance WowDb.java (bibliothèques clientes).
WowDb wowdb = new WowDb("mon-application/1.0");
for (Map<String, Object> item : wowdb.iterate("/classic/items", Map.of("rogue", "true", "type", "dagger"))) {
    System.out.println(item.get("name"));
}
```

#### .NET

```csharp
// Avec le client sans dépendance WowDb.cs (bibliothèques clientes).
var wowdb = new WowDb.Client("mon-application/1.0");
await foreach (var item in wowdb.IterateAsync("/classic/items", new() { ["rogue"] = "true", ["type"] = "dagger" }))
    Console.WriteLine(item.GetProperty("name").GetString());
```

#### Rust

```rust
// Cargo.toml : ureq = { version = "2", features = ["json"] } et serde_json = "1"
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut cursor: Option<String> = None;
    loop {
        let mut req = ureq::get("https://api.wowdb.assemblee-defias.fr/v1/classic/items")
            .set("User-Agent", "mon-application/1.0")
            .query("rogue", "true")
            .query("type", "dagger")
            .query("limit", "100");
        if let Some(c) = &cursor {
            req = req.query("cursor", c);
        }
        let page: serde_json::Value = req.call()?.into_json()?;
        for item in page["data"].as_array().into_iter().flatten() {
            println!("{}", item["name"].as_str().unwrap_or_default());
        }
        match page["next_cursor"].as_str() {
            Some(c) => cursor = Some(c.to_string()),
            None => break,
        }
    }
    Ok(())
}
```

## Nombre total

Par défaut, une liste ne compte pas ses éléments (c’est plus rapide). Ajoutez `include_total=true` pour recevoir `total_count`, le nombre total d’éléments avec ces filtres.

**include_total**

```bash
curl -G https://api.wowdb.assemblee-defias.fr/v1/classic/items \
  -d rogue=true -d include_total=true -d limit=1
```

## En-tête Link

Quand il y a une page suivante, la réponse porte aussi un en-tête `Link` (RFC 8288) avec son adresse complète, `rel="next"` : pratique pour les outils qui savent le suivre.

**En-tête de réponse**

```http
Link: <https://api.wowdb.assemblee-defias.fr/v1/classic/items?rogue=true&cursor=eyJrIjpb…&limit=100>; rel="next"
```

## Curseur invalide

Un curseur est opaque : ne le fabriquez pas, ne le modifiez pas. Il est lié à la requête qui l’a produit ; réutilisé avec d’autres filtres ou un autre tri, il est refusé avec `400 cursor_invalid`, au lieu de donner une page incohérente. `limit` peut, lui, changer d’une page à l’autre.

## Listes incluses dans une fiche

Certaines fiches contiennent une liste longue : le butin et les ventes d’un PNJ. La fiche en donne les 10 premiers éléments dans la même enveloppe `list`, avec `has_more` et l’`url` de la liste complète (`/npcs/{id}/loot`), qui se pagine de la même façon.

---

API de WoWDB des Défias, v1 (2026-10-01). Adresse de base : https://api.wowdb.assemblee-defias.fr/v1. Index : https://docs.wowdb.assemblee-defias.fr/llms.txt.
