# التقسيم إلى صفحات في واجهة مُجيب بالمؤشر nextCursor

> التقسيم إلى صفحات في واجهة مُجيب البرمجية يتم بالمؤشر nextCursor. تعرّف إلى حجم الصفحة وترتيب النتائج، وكود يقرأ كل الصفحات، وطريقة جلب الجديد فقط.

التقسيم إلى صفحات في واجهة مُجيب البرمجية يتم بمؤشر (cursor): كل قائمة تعيد صفحة من النتائج ومعها `nextCursor`، فتمرّره في الطلب التالي باسم `cursor` حتى تصبح قيمته `null`. حجم الصفحة 50 عنصرًا، ويمكنك رفعه إلى 100.

يعمل هذا في القائمتين: `GET /api/v1/contacts` و`GET /api/v1/bookings`. أما طلبات العنصر الواحد فلا صفحات فيها.

## كيف تعمل الصفحات في الواجهة؟

ترسل الطلب الأول دون `cursor`، فيعود الرد بالعناصر في `data` وبمؤشر الصفحة التالية في `nextCursor`:

```json
{
  "data": [ { "id": "5d8f2a61-0c4e-4b7a-9f13-6e2b8c0d4a57" }, { "id": "9b1e3c74-2a5d-4f08-8e6b-1c7d0a3f5e92" } ],
  "nextCursor": "MjAyNi0xMC0yOFQxNDowNToxMi40ODFafDliMWUzYzc0LTJhNWQtNGYwOC04ZTZiLTFjN2QwYTNmNWU5Mg"
}
```

(العناصر هنا مختصرة إلى `id` للتوضيح. شكلها الكامل في صفحة كل طلب.)

| المعامل | معناه |
| --- | --- |
| `limit` | عدد العناصر في الصفحة، من واحد إلى 100. إذا لم ترسله فهو 50 |
| `cursor` | قيمة `nextCursor` من الصفحة السابقة، كما هي حرفًا بحرف |

- عندما تكون `nextCursor` قيمتها `null` فقد وصلت إلى آخر صفحة.
- قد تأتي الصفحة الأخيرة بعناصر أقل من `limit`، وقد تأتي `data` فارغة إذا لم يطابق الطلب شيئًا.
- المؤشر نص مغلق: لا تحاول قراءته أو تركيبه بنفسك. المؤشر الذي لا يُقرأ يعود بالخطأ 400 ورسالة `Invalid cursor`.

## كيف أقرأ كل الصفحات حتى النهاية؟

اطلب الصفحة، واجمع عناصرها، ثم اطلب التالية بمؤشرها، وتوقّف عندما يصبح المؤشر `null`. هذه دالة تفعل ذلك وتنتظر إذا وصلت إلى حدود الطلبات:

```js
const BASE = 'https://api.mujib.ai/api/v1';
const headers = { Authorization: `Bearer ${process.env.MUJIB_API_KEY}` };

async function fetchAll(path, params = {}) {
  const items = [];
  let cursor = null;
  for (;;) {
    const url = new URL(BASE + path);
    for (const [k, v] of Object.entries({ ...params, limit: 100 })) url.searchParams.set(k, String(v));
    if (cursor) url.searchParams.set('cursor', cursor);
    const res = await fetch(url, { headers });
    if (res.status === 429) {
      const wait = Number(res.headers.get('retry-after') ?? '60');
      await new Promise((r) => setTimeout(r, wait * 1000));
      continue;
    }
    const body = await res.json();
    if (!res.ok) throw new Error(`${res.status} ${body.error.code}: ${body.error.message}`);
    items.push(...body.data);
    if (body.nextCursor === null) return items;
    cursor = body.nextCursor;
  }
}

const bookings = await fetchAll('/bookings', { from: '2026-11-01', to: '2026-11-30' });
console.log(bookings.length);
```

```python
import os
import time
import requests

BASE = "https://api.mujib.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['MUJIB_API_KEY']}"}

def fetch_all(path, params=None):
    items, cursor = [], None
    while True:
        query = {**(params or {}), "limit": 100}
        if cursor:
            query["cursor"] = cursor
        res = requests.get(BASE + path, params=query, headers=HEADERS, timeout=30)
        if res.status_code == 429:
            time.sleep(int(res.headers.get("Retry-After", "60")))
            continue
        body = res.json()
        if not res.ok:
            raise RuntimeError(f"{res.status_code} {body['error']['code']}: {body['error']['message']}")
        items.extend(body["data"])
        if body["nextCursor"] is None:
            return items
        cursor = body["nextCursor"]

contacts = fetch_all("/contacts", {"since": "2026-10-01T00:00:00Z"})
print(len(contacts))
```

## بأي ترتيب تأتي النتائج؟

لكل قائمة ترتيب ثابت لا يتغيّر بين الصفحات:

| القائمة | الترتيب |
| --- | --- |
| العملاء | الأحدث أولًا، بحسب `createdAt` (أول ظهور للعميل)، ثم بالمعرّف `id` عند التساوي |
| الحجوزات | الأقدم أولًا، بحسب الوقت المحلي للموعد في فرعه (`localTime`)، ثم بالمعرّف `id` عند التساوي |

إذا كانت فروعك في مناطق زمنية مختلفة، فترتيب الحجوزات يتبع الوقت المحلي لكل فرع، لا اللحظة بتوقيت UTC.

## هل يتغيّر شيء إذا أُضيفت بيانات أثناء القراءة؟

لا يتكرر عنصر قرأته ولا يُتخطّى عنصر بسبب إضافة جديدة، لأن المؤشر يحفظ موضعك بقيمة آخر عنصر قرأته لا برقم صفحة.

- **عميل جديد** أثناء القراءة يظهر في أول القائمة، فلا تراه في الصفحات التالية. تلتقطه في المزامنة التالية بالمعامل `since`.
- **حجز جديد** يقع وقته بعد موضعك يظهر في صفحة تالية، والذي يقع قبله تراه في القراءة التالية.
- **حجز نُقل موعده** أثناء القراءة ينتقل إلى موضعه الجديد في الترتيب، فقد تراه مرتين أو لا تراه. اعتمد على `id` لتمنع التكرار.

## كيف أجلب الجديد فقط بدل القائمة كلها؟

احفظ موضعك من المزامنة السابقة، واطلب ما بعده فقط.

- **العملاء:** احفظ أحدث قيمة `createdAt` قرأتها، وأرسلها في المزامنة التالية باسم `since`. المعامل يشمل اللحظة نفسها، فقد يعود آخر عميل قرأته مرة أخرى؛ تجاهله بمعرّفه.
- **الحجوزات:** اطلب نافذة أيام بـ `from` و`to`، مثل الأيام السبعة القادمة. لا يوجد معامل لما تغيّر حديثًا، فإذا أردت أن تعرف بكل حجز جديد أو ملغى لحظة حدوثه فاستخدم [Webhooks](https://docs.mujib.ai/developers/webhooks/).
- أبقِ معاملات الاختيار نفسها في كل صفحات القراءة الواحدة: المؤشر يحفظ الموضع فقط، ولا يحفظ `since` أو `from` أو `to` أو `status`.

## الأسئلة الشائعة

### هل أستطيع طلب الصفحة الثالثة مباشرة؟

لا. لا توجد أرقام صفحات. تصل إلى كل صفحة بالمؤشر الذي أعادته الصفحة التي قبلها.

### كيف أعرف أنني وصلت إلى آخر صفحة؟

عندما تكون قيمة nextCursor في الرد null. قد تأتي الصفحة الأخيرة بعناصر أقل من limit.

### هل أستطيع تغيير limit بين صفحة وأخرى؟

نعم. المؤشر يحفظ موضعك لا حجم الصفحة. أما المعاملات التي تختار النتائج، مثل since وfrom وto وstatus، فأبقِها كما هي.

### ما أكبر حجم للصفحة؟

100 عنصر. وإذا لم ترسل limit فحجم الصفحة 50.

---

المصدر: https://docs.mujib.ai/developers/pagination/ · آخر تحديث: 2026-11-04
