منابع داده

اتصال منبع دادهٔ API

پیاده‌سازی API سازگار با موچت؛ قالب پاسخ، صفحه‌بندی، مرتب‌سازی، فیلتر تاریخ و احراز هویت، همراه با مثال‌های Laravel، Express و FastAPI.

با منبع دادهٔ API می‌توانید اطلاعات سامانه، کاتالوگ محصولات یا پایگاه دانش خود را به موچت متصل کنید. این راهنما قرارداد آدرسی را که باید ارائه کنید، روش بررسی آن توسط موچت و مثال‌های پیاده‌سازی با PHP Laravel، Node.js Express و Python FastAPI را توضیح می‌دهد.

موچت این API را برای دریافت و همگام‌سازی دانش می‌خواند. برای افزودن اتصال در داشبورد، بخش اتصال از طریق API را ببینید.

الزام‌های API

آدرس API

یک آدرس کامل HTTP یا HTTPS ارائه کنید که از سرورهای موچت قابل دسترسی باشد. برای ارسال اطلاعات احراز هویت، از HTTPS استفاده کنید.

نمونهٔ آدرس قابل دسترسیآدرس محلی که موچت به آن دسترسی ندارد
https://api.example.com/itemshttp://localhost:3000/api/items
https://example.com/api/productshttp://127.0.0.1/api/items
https://data.example.org/endpointhttp://192.168.1.100/api/items

اگر API در شبکهٔ داخلی قرار دارد، پیش از اتصال باید دسترسی سرورهای موچت به آن را فراهم کنید. آدرس نهایی را بدهید که بدون تغییر مسیر، مستقیماً JSON برگرداند. بررسی اولیهٔ اتصال، تغییر مسیر را دنبال می‌کند، اما دریافت داده هنگام همگام‌سازی این کار را نمی‌کند. صفحهٔ HTML ورود یا بررسی مرورگر، پاسخ سازگار محسوب نمی‌شود.

روش درخواست و کش

API باید درخواست GET را با این ویژگی‌ها بپذیرد:

  • پارامترهای صفحه‌بندی، مرتب‌سازی و فیلتر تاریخ را از query string دریافت کند.
  • پارامتر _nonce را که موچت برای جلوگیری از دریافت پاسخ کش‌شده اضافه می‌کند، بپذیرد.
  • تا وقتی داده تغییر نکرده است، برای فیلترها و ترتیب یکسان، نتیجهٔ یکسان برگرداند.
  • در مهلت اتصال، پاسخ موفق JSON بدهد؛ مهلت فعلی اعتبارسنجی، ۳۰ ثانیه برای هر درخواست است.

موچت علاوه بر _nonce، هدرهای جلوگیری از کش را هم می‌فرستد. API شما می‌تواند مقدار _nonce را نادیده بگیرد، اما باید آن را بپذیرد و وارد فیلتر داده‌ها نکند.

قالب پاسخ

یک شیء JSON شامل total_count و items برگردانید:

response.json
{
  "total_count": 100,
  "items": [
    {
      "id": "123",
      "title": "Product Name",
      "date_modified": "2026-10-20T15:30:45.000Z",
      "description": "Detailed product description...",
      "url": "https://example.com/products/123",
      "price": 29.99,
      "regular_price": 39.99,
      "sale_price": 29.99,
      "stock_status": "instock",
      "currency": "USD",
      "variations": [
        { "id": "456", "title": "Size L", "price": 32.99 }
      ]
    }
  ]
}

آرایهٔ items شامل صفحهٔ درخواست‌شده است. مقدار total_count باید عدد صحیح و برابر با تعداد همهٔ آیتم‌های منطبق با فیلتر، پیش از اعمال skip و take باشد. مثلاً صفحه‌ای با یک آیتم می‌تواند total_count: 100 داشته باشد. اگر فیلتر نتیجه‌ای نداشت، پاسخ موفق با {"total_count": 0, "items": []} برگردانید.

الزام‌های فیلدها

فیلدهای اجباری

هر آیتم باید این فیلدها را داشته باشد:

فیلدنوعشرطنمونه
idرشته یا عدد صحیحشناسهٔ یکتا و ثابت آیتم"123" یا 456
titleرشتهعنوان غیرخالی"Product Name"
date_modifiedرشتهزمان دقیق UTC با میلی‌ثانیه"2026-10-20T15:30:45.000Z"

شناسهٔ id هر آیتم را بین همگام‌سازی‌ها ثابت نگه دارید. با هر تغییر محتوا، از جمله تغییر قیمت، موجودی یا تنوع محصول، date_modified را به‌روز کنید. این مقدار باید زمان واقعی تغییر باشد؛ با هر درخواست GET، زمان تازه‌ای برای همان محتوای قبلی نسازید.

قالب دقیق تاریخ

قالب موردنیاز YYYY-MM-DDTHH:mm:ss.SSSZ است. حرف پایانی Z به معنی UTC است و بخش اعشاری دقیقاً سه رقم دارد.

مقدارنتیجه
2026-10-20T15:30:45.000Zمعتبر
2026-10-20T15:30:45Zنامعتبر؛ میلی‌ثانیه ندارد
2026-10-20 15:30:45نامعتبر؛ جداکننده و منطقهٔ زمانی ندارد
2026-10-20T15:30:45+00:00نامعتبر؛ از میلی‌ثانیه و پسوند Z استفاده کنید
2026-10-20T15:30:45.000000Zنامعتبر؛ به‌جای سه رقم اعشار، شش رقم دارد

پیش از قالب‌بندی، زمان را به UTC تبدیل کنید؛ اضافه‌کردن Z به ساعت محلی، آن را به UTC تبدیل نمی‌کند. زمان تغییر در دیتابیس را با دقت میلی‌ثانیه نگه دارید تا تاریخ خروجی و فیلتر دقیق تاریخ با هم سازگار باشند.

فیلدهای اختیاری

محتوایی را که موچت باید یاد بگیرد، در فیلدهای همان آیتم قرار دهید. فیلدهای زیر برای اطلاعات محصول و پایگاه دانش کاربرد دارند؛ موچت اطلاعاتی مانند قیمت، موجودی، واحد پول و تنوع‌ها را در متادیتای آیتم هم نگه می‌دارد.

فیلدنوعتوضیح
descriptionرشتهمحتوای کامل آیتم
urlرشتهلینک عمومی مرتبط با آیتم
priceعددقیمت فعلی
regular_priceعددقیمت پیش از تخفیف
sale_priceعددقیمت تخفیف‌خورده، در صورت وجود
stock_statusرشتهمثلاً instock، outofstock، onbackorder یا مقدار اختصاصی شما
currencyرشتهمثلاً USD، EUR، IRR، ریال یا تومان
variationsآرایهتنوع‌های محصول با جزئیات جداگانه
فیلدهای اختصاصیهر مقدار معتبر JSONاطلاعات تکمیلی محصول، مقاله یا پرسش‌وپاسخ

مقدار فیلدهای اختصاصی می‌تواند رشته، عدد، مقدار بولی، شیء، آرایه یا null باشد. این فیلدها در محتوای منبع آیتم قرار می‌گیرند؛ افزودن آن‌ها به‌تنهایی کنترل تازه‌ای برای فیلتر جستجو در داشبورد نمی‌سازد. دریافت و همگام‌سازی به فعال‌بودن قابلیت منبع API و ظرفیت کافی پایگاه دانش در حساب شما نیاز دارد.

تنوع‌های محصول

هر تنوع می‌تواند فیلدهای اختصاصی خودش را داشته باشد. چند نمونهٔ رایج:

فیلدنوعتوضیح
idرشته یا عدد صحیحشناسهٔ یکتای تنوع
titleرشتهنام نمایشی تنوع
priceعددقیمت فعلی تنوع
regular_priceعددقیمت عادی
sale_priceعددقیمت تخفیف‌خورده
stock_statusرشتهوضعیت موجودی
stockعددتعداد موجود
فیلدهای اختصاصیهر مقدار معتبر JSONرنگ، اندازه، کد کالا، وزن یا ویژگی‌های دیگر

مثال اول: یک محصول فروشگاهی

product.json
{
  "id": "123",
  "title": "Wireless Headphones",
  "date_modified": "2024-03-20T15:30:45.000Z",
  "description": "High-quality wireless headphones...",
  "price": 99.99,
  "regular_price": 129.99,
  "sale_price": 99.99,
  "stock_status": "instock",
  "currency": "USD",
  "category": "electronics",
  "brand": "TechBrand",
  "sku": "WH-123",
  "weight": 0.3,
  "variations": [
    {
      "id": "123-black-large",
      "title": "Wireless Headphones - Black Large",
      "price": 99.99,
      "regular_price": 129.99,
      "sale_price": 99.99,
      "stock_status": "instock",
      "stock": 25,
      "color": "black",
      "size": "large",
      "sku": "WH-123-BL",
      "weight": 0.35
    },
    {
      "id": "123-white-medium",
      "title": "Wireless Headphones - White Medium",
      "price": 104.99,
      "regular_price": 129.99,
      "sale_price": 104.99,
      "stock_status": "outofstock",
      "stock": 0,
      "color": "white",
      "size": "medium",
      "sku": "WH-123-WM",
      "expected_restock": "2024-04-15"
    },
    {
      "id": "123-red-small",
      "title": "Wireless Headphones - Red Small",
      "price": 94.99,
      "regular_price": 129.99,
      "sale_price": 94.99,
      "stock_status": "onbackorder",
      "stock": 5,
      "color": "red",
      "size": "small",
      "sku": "WH-123-RS",
      "pre_order": true
    }
  ]
}

مثال دوم: یک آیتم پرسش‌وپاسخ

question.json
{
  "id": 1,
  "title": "What is Python?",
  "date_modified": "2024-03-15T12:00:00.000Z",
  "question": "What is Python?",
  "answer": "Python is a high-level, interpreted programming language known for its simplicity and readability."
}

هر مثال، یک آیتم است. آن‌ها را داخل آرایهٔ items پاسخ برگردانید و total_count را برای کل مجموعهٔ منطبق با فیلتر حساب کنید.

پارامترهای درخواست

API باید پارامترهای زیر را پیاده‌سازی کند. ممکن است بعضی از آن‌ها در درخواست نباشند؛ مانند مثال‌های پیاده‌سازی، مقدار پیش‌فرض در نظر بگیرید.

صفحه‌بندی

پارامترنوعرفتار موردنیازنمونه
takeعدد صحیح مثبتحداکثر تعداد آیتم‌های پاسخ?take=30
skipعدد صحیح نامنفیتعداد آیتم‌هایی که از ابتدای نتیجهٔ مرتب‌شده و فیلترشده رد می‌شوند?skip=60

موچت در اعتبارسنجی و همگام‌سازی فعلی، در هر درخواست تا ۳۱ آیتم می‌خواهد. محدودکردن اندازهٔ صفحه به ۳۰ کافی نیست. مثال‌های این صفحه تا ۱۰۰ آیتم را می‌پذیرند.

مرتب‌سازی

پارامترنوعرفتار موردنیازنمونه
orderرشتهاعمال ترتیب ASC یا DESC روی فیلدهای انتخاب‌شده?order=ASC
order_byرشتهمرتب‌سازی با نام فیلدها، جداشده با ویرگول?order_by=date_modified,id

هر دو فیلد date_modified و id را پشتیبانی کنید. اگر زمان تغییر چند آیتم یکسان باشد، id ترتیب بین آن‌ها را ثابت نگه می‌دارد. مرتب‌سازی باید پیش از اعمال skip و take انجام شود.

نام عمومی فیلدها را به ستون متناظر در دیتابیس نگاشت کنید؛ مثلاً date_modified از updated_at در SQL و id از _id در MongoDB گرفته شود. فقط فیلدهای مجاز مرتب‌سازی را بپذیرید.

فیلتر تاریخ

پارامترنوعرفتار موردنیازنمونه
modified_afterرشتهٔ زمان UTCفقط آیتم‌هایی با زمان تغییر بزرگ‌تر از ورودی را برگرداند?modified_after=2026-10-20T15:30:45.000Z

شرط باید date_modified > modified_after باشد؛ یعنی بزرگ‌تر، نه بزرگ‌تر یا مساوی. بعد از این فیلتر، total_count را دوباره حساب کنید. اگر آیتم تازه‌تری وجود ندارد، آرایهٔ items خالی و total_count: 0 برگردانید.

هر دو شکل ورودی UTC، با میلی‌ثانیه و بدون آن، را بپذیرید: 2026-10-20T15:30:45.000Z و 2026-10-20T15:30:45Z. بررسی اولیهٔ اتصال، میلی‌ثانیه می‌فرستد؛ همگام‌سازی ممکن است زمان را با دقت ثانیه بفرستد. تاریخ آیتم‌ها در پاسخ همچنان باید قالب دقیق میلی‌ثانیه داشته باشد.

همگام‌سازی بر اساس id، آیتم تازه می‌سازد و آیتم موجود را به‌روز می‌کند. اگر date_modified آیتم موجود عوض نشده باشد، آن را رد می‌کند؛ حتی اگر فیلد دیگری تغییر کرده باشد. حذف آیتم از پاسخ API، منبع واردشدهٔ قبلی را خودکار حذف نمی‌کند؛ در صورت نیاز، آن منبع را از داشبورد حذف کنید.

جلوگیری از کش

پارامترنوعرفتارنمونه
_nonceرشتهموچت خودکار اضافه می‌کند؛ آن را بپذیرید، بدون اینکه آیتم‌ها را با مقدارش فیلتر کنید?_nonce=1792497600000

روش‌های احراز هویت

هنگام افزودن منبع API در پایگاه دانش، روش احراز هویت را تنظیم کنید. این اطلاعات مربوط به API خود شما هستند و موچت هنگام دریافت داده آن‌ها را می‌فرستد.

بدون احراز هویت

اگر API عمومی است، بدون احراز هویت را انتخاب کنید. موچت اطلاعات API Key یا Bearer به درخواست اضافه نمی‌کند.

کلید API

مقدار کلید و نام هدر آن، مانند X-API-Key، را وارد کنید. اگر API شما از Authorization به‌عنوان هدر اختصاصی کلید استفاده می‌کند، مقدار کامل مورد انتظارش را وارد کنید.

API key request
GET /api/items?take=10&skip=0&_nonce=1792497600000 HTTP/1.1
Host: api.example.com
X-API-Key: your-api-key-here

توکن Bearer

Bearer Token را انتخاب کنید و فقط مقدار توکن را وارد کنید. موچت پیشوند Bearer را در هدر Authorization اضافه می‌کند.

Bearer request
GET /api/items?take=10&skip=0&_nonce=1792497600000 HTTP/1.1
Host: api.example.com
Authorization: Bearer your-token-here

اگر احراز هویت فعال است، API باید درخواست بدون اطلاعات معتبر یا با اطلاعات اشتباه را رد کند. کوکی ورود مرورگر شما از طریق این اتصال به API فرستاده نمی‌شود.

بررسی سازگاری API

هنگام ثبت منبع، موچت پیش از ساخت آن، قالب پاسخ، صفحه‌بندی، مرتب‌سازی و فیلتر تاریخ را بررسی می‌کند.

ساختار اولیه

  • پاسخ باید یک شیء JSON با total_count عدد صحیح و آرایهٔ items باشد.
  • هر آیتم باید یک شیء JSON باشد و فیلدهای id، title و date_modified شرایط گفته‌شده را داشته باشند.
  • درخواست take=1 باید آیتمی برگرداند تا ساختار آن بررسی شود.

هنگام اتصال، حداقل ۲ آیتم در دسترس داشته باشید. موچت برای بررسی صفحه‌بندی و مرتب‌سازی، دو آیتم درخواست می‌کند. آرایهٔ خالی برای فیلتر تاریخیِ بدون نتیجه معتبر است، اما مجموعه‌ای که از ابتدا خالی باشد، بررسی اتصال را نمی‌گذراند.

دقت صفحه‌بندی

Pagination checks
GET /api/items?take=2&order=ASC&order_by=date_modified,id
GET /api/items?take=1&skip=0&order=ASC&order_by=date_modified,id
GET /api/items?take=1&skip=1&order=ASC&order_by=date_modified,id

درخواست دوم باید اولین آیتم پاسخ اول را برگرداند و درخواست سوم، دومین آیتم همان پاسخ را. برای همهٔ مقدارهای skip، صفحهٔ اول را تکرار نکنید.

برای مجموعه‌هایی با حداقل ۳۱ آیتم، موچت می‌تواند هم‌پوشانی صفحه‌ها را هم بررسی کند: با ترتیب یکسان، آخرین آیتم take=31&skip=0 باید با اولین آیتم take=31&skip=30 برابر باشد.

عملکرد مرتب‌سازی

Ordering checks
GET /api/items?take=2&order=ASC&order_by=date_modified,id
GET /api/items?take=2&order=DESC&order_by=date_modified,id
GET /api/items?take=1&order=ASC&order_by=date_modified,id

مرتب‌سازی صعودی و نزولی و استفاده از چند فیلد باید کار کند. ترتیب درخواست صفحه‌ها را یکسان نگه دارید تا وقتی داده تغییر نکرده است، آیتم‌ها بین صفحه‌ها جابه‌جا نشوند.

دقت فیلتر تاریخ

موچت نتیجهٔ درخواست‌ها را با این مرزهای زمانی مقایسه می‌کند:

مقدار فیلترنتیجهٔ مورد انتظار
یک دقیقه پیش از زمان قدیمی‌ترین آیتمقدیمی‌ترین آیتم همچنان در ترتیب صعودی، اولین نتیجه باشد
دقیقاً برابر با زمان قدیمی‌ترین آیتمآیتمی با همان زمان یا زمان قدیمی‌تر برنگردد
زمان فعلیاگر همهٔ زمان‌های تغییر در گذشته‌اند، آیتمی برنگردد

زمان تغییر را مطابق زمان واقعی هر ویرایش ثبت کنید. تاریخ آینده می‌تواند باعث ردشدن بررسی آخر شود.

مثال‌های پیاده‌سازی

این مثال‌ها قرارداد یکسان قالب پاسخ، فیلتر، مرتب‌سازی و صفحه‌بندی را پیاده می‌کنند. نمونهٔ Laravel از مدل SQL موجودِ Item استفاده می‌کند، نمونهٔ Express از مدل موجودِ Item در Mongoose و نمونهٔ FastAPI از فایل JSON همین صفحه.

PHP Laravel

این کنترلر را در app/Http/Controllers/DatasourceController.php قرار دهید. مدل Item باید فیلدهای title، description، price و updated_at داشته باشد. فیلد updated_at را با date cast در Eloquent و با زمان UTC و دقت میلی‌ثانیه نگه دارید.

DatasourceController.php
<?php

namespace App\Http\Controllers;

use App\Models\Item;
use Carbon\CarbonImmutable;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;

class DatasourceController extends Controller
{
    public function index(Request $request)
    {
        $params = $request->validate([
            'take' => ['sometimes', 'integer', 'min:1', 'max:100'],
            'skip' => ['sometimes', 'integer', 'min:0'],
            'order' => ['sometimes', 'in:ASC,DESC'],
            'order_by' => ['sometimes', 'string'],
            'modified_after' => [
                'sometimes', 'date',
                'regex:/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/',
            ],
        ]);
        $columns = ['date_modified' => 'updated_at', 'id' => 'id'];
        $fields = array_map('trim', explode(',', $params['order_by'] ?? 'date_modified,id'));
        foreach ($fields as $field) {
            if (!isset($columns[$field])) {
                throw ValidationException::withMessages(['order_by' => 'Unsupported field']);
            }
        }
        if (!in_array('id', $fields, true)) {
            $fields[] = 'id';
        }

        $query = Item::query();
        if (isset($params['modified_after'])) {
            $since = CarbonImmutable::parse($params['modified_after'])->utc();
            $query->where('updated_at', '>', $since->format('Y-m-d H:i:s.v'));
        }
        $total = (clone $query)->count();
        foreach (array_unique($fields) as $field) {
            $query->orderBy($columns[$field], $params['order'] ?? 'ASC');
        }
        $items = $query->offset((int) ($params['skip'] ?? 0))
            ->limit((int) ($params['take'] ?? 30))->get();

        return response()->json([
            'total_count' => $total,
            'items' => $items->map(fn ($item) => [
                'id' => (string) $item->id,
                'title' => $item->title,
                'date_modified' => $item->updated_at->copy()->utc()
                    ->format('Y-m-d\TH:i:s.v\Z'),
                'description' => $item->description,
                'price' => $item->price === null ? null : (float) $item->price,
            ])->values(),
        ]);
    }
}

مسیر را در routes/api.php ثبت کنید:

routes/api.php
use App\Http\Controllers\DatasourceController;
use Illuminate\Support\Facades\Route;

Route::get('/items', [DatasourceController::class, 'index']);

با پیشوند پیش‌فرض API در Laravel، آدرس /api/items در دسترس قرار می‌گیرد. اگر API خصوصی است، middleware احراز هویت API Key یا Bearer خود را روی همین مسیر اعمال کنید. فیلدهای دیگر محصول را هم در صورت نیاز به خروجی هر آیتم اضافه کنید. جزئیات متدهای مرتب‌سازی و صفحه‌بندی در مستندات Query Builder لاراول آمده است.

Node.js Express

این router از Mongoose استفاده می‌کند. مدل موجودِ Item باید updated_at از نوع Date داشته باشد که هنگام تغییر محتوا به‌روز شود؛ همچنین فیلدهای title، description و price. شناسهٔ عمومی id از تبدیل _id در MongoDB به رشته به دست می‌آید.

datasource-router.js
import express from "express";
import Item from "./models/item.js";

const router = express.Router();
const columns = { date_modified: "updated_at", id: "_id" };

router.get("/api/items", async (req, res, next) => {
  const {
    take = "30", skip = "0", order = "ASC",
    order_by = "date_modified,id", modified_after
  } = req.query;
  const integers = [take, skip].every((value) =>
    typeof value === "string" && /^\d+$/.test(value)
  );
  const limit = Number(take);
  const offset = Number(skip);
  if (!integers || !Number.isSafeInteger(limit) || limit < 1 || limit > 100
      || !Number.isSafeInteger(offset) || offset < 0) {
    return res.status(400).json({ error: "Invalid take or skip" });
  }
  if (!["ASC", "DESC"].includes(order) || typeof order_by !== "string") {
    return res.status(400).json({ error: "Invalid ordering" });
  }
  const fields = [...new Set(order_by.split(",").map((field) => field.trim()))];
  if (fields.some((field) => !Object.hasOwn(columns, field))) {
    return res.status(400).json({ error: "Unsupported order_by field" });
  }
  if (!fields.includes("id")) fields.push("id");

  const filter = {};
  if (modified_after !== undefined) {
    const since = typeof modified_after === "string" ? new Date(modified_after) : null;
    if (!since || Number.isNaN(since.getTime())) {
      return res.status(400).json({ error: "Invalid modified_after timestamp" });
    }
    const canonical = since.toISOString();
    if (![canonical, canonical.replace(".000Z", "Z")].includes(modified_after)) {
      return res.status(400).json({ error: "Use UTC ISO 8601 with seconds or milliseconds" });
    }
    filter.updated_at = { $gt: since };
  }
  const sort = Object.fromEntries(fields.map((field) => [
    columns[field], order === "DESC" ? -1 : 1
  ]));
  try {
    const [total_count, items] = await Promise.all([
      Item.countDocuments(filter),
      Item.find(filter).sort(sort).skip(offset).limit(limit).lean()
    ]);
    return res.json({
      total_count,
      items: items.map((item) => ({
        id: String(item._id),
        title: item.title,
        date_modified: item.updated_at.toISOString(),
        description: item.description,
        price: item.price
      }))
    });
  } catch (error) {
    next(error);
  }
});

export default router;

router را به برنامهٔ Express متصل کنید:

app.js
import datasourceRouter from "./datasource-router.js";

app.use(datasourceRouter);

برای API خصوصی، middleware کلید API یا Bearer را پیش از این router اجرا کنید. فیلدهای دیگر محصول را در نگاشت خروجی اضافه کنید. متدهای sort، skip، limit و countDocuments در مرجع Query در Mongoose توضیح داده شده‌اند.

Python FastAPI

این کد را در main.py ذخیره کنید. مسیر API، فایل items.json را از همان پوشه می‌خواند، فیلدهای اختصاصی آیتم را نگه می‌دارد و شناسه‌ها را به‌صورت رشته برمی‌گرداند.

main.py
import json
import os
from datetime import datetime, timezone
from pathlib import Path
from secrets import compare_digest
from typing import Literal

from fastapi import Depends, FastAPI, HTTPException, Query, Security
from fastapi.security import APIKeyHeader, HTTPAuthorizationCredentials, HTTPBearer

app = FastAPI()
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
bearer_auth = HTTPBearer(auto_error=False)


def verify_api_key(api_key: str | None = Security(api_key_header)):
    expected = os.environ.get("DATASOURCE_API_KEY")
    if not expected or not api_key or not compare_digest(api_key, expected):
        raise HTTPException(status_code=401, detail="Invalid API key")


def verify_bearer_token(
    credentials: HTTPAuthorizationCredentials | None = Security(bearer_auth),
):
    expected = os.environ.get("DATASOURCE_BEARER_TOKEN")
    if not expected or not credentials or not compare_digest(credentials.credentials, expected):
        raise HTTPException(status_code=401, detail="Invalid bearer token")


def parse_timestamp(value: str) -> datetime:
    try:
        parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
        canonical = parsed.astimezone(timezone.utc).isoformat(timespec="milliseconds")
        canonical = canonical.replace("+00:00", "Z")
        if parsed.tzinfo is None or value not in {canonical, canonical.replace(".000Z", "Z")}:
            raise ValueError
        return parsed
    except (TypeError, ValueError):
        raise HTTPException(status_code=422, detail="Use UTC ISO 8601 with seconds or milliseconds")


def load_json_file() -> list[dict]:
    path = Path(__file__).with_name("items.json")
    items = json.loads(path.read_text(encoding="utf-8"))
    return [{**item, "id": str(item["id"])} for item in items]


@app.get("/api/items")
def get_items(
    take: int = Query(default=30, ge=1, le=100),
    skip: int = Query(default=0, ge=0),
    order_by: str = "date_modified,id",
    order: Literal["ASC", "DESC"] = "ASC",
    modified_after: str | None = None,
    # API key authentication: uncomment the next line.
    # api_key: None = Depends(verify_api_key),
    # Bearer authentication: uncomment the next line instead.
    # bearer: None = Depends(verify_bearer_token),
):
    fields = list(dict.fromkeys(field.strip() for field in order_by.split(",")))
    if any(field not in {"date_modified", "id"} for field in fields):
        raise HTTPException(status_code=422, detail="Unsupported order_by field")
    if "id" not in fields:
        fields.append("id")
    items = load_json_file()
    if modified_after is not None:
        since = parse_timestamp(modified_after)
        items = [item for item in items if parse_timestamp(item["date_modified"]) > since]
    for field in reversed(fields):
        items.sort(
            key=lambda item: parse_timestamp(item[field]) if field == "date_modified" else item[field],
            reverse=order == "DESC",
        )
    total_count = len(items)
    return {"total_count": total_count, "items": items[skip:skip + take]}

روش احراز هویت را مطابق تنظیم منبع داده انتخاب کنید:

  • بدون احراز هویت: هر دو خط dependency را به‌صورت کامنت نگه دارید.
  • API Key: متغیر محیطی DATASOURCE_API_KEY را تنظیم کنید و خط dependency مربوط به api_key را از حالت کامنت خارج کنید. نام هدر را در موچت X-API-Key قرار دهید.
  • Bearer Token: متغیر DATASOURCE_BEARER_TOKEN را تنظیم کنید و به‌جای آن، dependency مربوط به bearer را فعال کنید.

dependency فعال، درخواست بدون توکن، توکن اشتباه و تنظیم‌نبودن کلید روی سرور را رد می‌کند. ابزارهای دریافت هدر در مرجع امنیت FastAPI توضیح داده شده‌اند.

دادهٔ نمونهٔ زیر را در items.json قرار دهید. دو آیتم زمان یکسان دارند تا بتوانید مرتب‌سازی دوم با id را هم بررسی کنید:

items.json
[
  {
    "id": "1",
    "title": "Wireless Headphones",
    "date_modified": "2026-10-20T09:30:00.000Z",
    "description": "Wireless headphones with a microphone.",
    "price": 99.99,
    "currency": "USD"
  },
  {
    "id": "2",
    "title": "USB-C Cable",
    "date_modified": "2026-10-20T09:30:00.000Z",
    "description": "A one-meter USB-C charging cable.",
    "price": 9.99,
    "currency": "USD"
  },
  {
    "id": "3",
    "title": "What is Python?",
    "date_modified": "2026-10-21T12:00:00.000Z",
    "question": "What is Python?",
    "answer": "Python is a high-level programming language."
  }
]

FastAPI را نصب و نمونه را اجرا کنید:

Terminal
pip install "fastapi[standard]"
fastapi dev main.py

برای آزمایش محلی، آدرس http://127.0.0.1:8000/api/items را باز کنید. پیش از اتصال موچت، API را روی آدرس قابل دسترسی از بیرون منتشر کنید. نمونه‌های بیشتر در مخزن مثال‌های موچت قرار دارند.

خطاهای رایج و راه‌حل‌ها

خطای قالب تاریخ

مشکل: بررسی اتصال، مقدار date_modified را رد می‌کند.

راه‌حل: مقدار date_modified را با قالب دقیق YYYY-MM-DDTHH:mm:ss.SSSZ برگردانید. زمان را به UTC تبدیل کنید، سه رقم اعشار نگه دارید و با تغییر آیتم، تاریخ را به‌روز کنید. ورودی modified_after را هم با میلی‌ثانیه و هم با دقت ثانیه بپذیرید. دقت مقایسهٔ فیلتر و زمان خروجی باید یکسان باشد.

خطای صفحه‌بندی

مشکل: یک آیتم در همهٔ صفحه‌ها تکرار می‌شود، بعضی آیتم‌ها جا می‌افتند یا بررسی صفحهٔ بزرگ‌تر شکست می‌خورد.

راه‌حل: ابتدا فیلتر تاریخ، سپس مرتب‌سازی date_modified,id و در پایان skip و take را اعمال کنید. اندازهٔ صفحه باید حداقل تا ۳۱ آیتم را بپذیرد. total_count را پیش از صفحه‌بندی حساب کنید و برای زمان‌های برابر، ترتیب ثابت داشته باشید.

خطای احراز هویت

مشکل: API پاسخ 401 یا 403 می‌دهد.

راه‌حل: نام و مقدار هدر را با API خود تطبیق دهید. برای Bearer، فقط توکن را در موچت وارد کنید. سرور باید اطلاعات اشتباه را رد کند و توکن معتبر تنظیم‌شده را بدون نیاز به نشست مرورگر بپذیرد.

مجموعهٔ خالی یا تعداد نادرست

مشکل: در بررسی اولیه آیتمی برای اعتبارسنجی وجود ندارد یا با تغییر اندازهٔ صفحه، total_count عوض می‌شود.

راه‌حل: هنگام اتصال حداقل دو آیتم در دسترس داشته باشید. تعداد همهٔ رکوردهای منطبق با فیلتر تاریخ را پیش از صفحه‌بندی حساب کنید. برای فیلتر بدون نتیجه، پاسخ موفق با items: [] و total_count: 0 برگردانید.

خطای اتصال یا پاسخ

مشکل: موچت به آدرس دسترسی ندارد، درخواست طول می‌کشد یا پاسخ HTML است.

راه‌حل: آدرس عمومی، DNS، دسترسی شبکه و احراز هویت را بررسی کنید. آدرس نهایی را بدون تغییر مسیر وارد کنید. مسیر GET باید مستقیماً JSON بدهد، همهٔ پارامترها از جمله _nonce را بپذیرد و در مهلت اتصال پاسخ بدهد. نتیجه را بیرون از نشست مرورگری که وارد حساب شده است هم آزمایش کنید.

آزمایش API

پیش از اتصال منبع، با curl یا Postman دریافت اولیه، صفحه‌بندی، هر دو جهت مرتب‌سازی، فیلتر تاریخ و ترکیب پارامترها را بررسی کنید. آدرس و تاریخ نمونه را با مقدارهای خودتان جایگزین کنید.

Basic retrieval and pagination
curl --get 'https://api.example.com/items' --data-urlencode 'take=5'
curl --get 'https://api.example.com/items' --data-urlencode 'take=5' \
  --data-urlencode 'skip=5' --data-urlencode 'order_by=date_modified,id'
Ordering and date filtering
curl --get 'https://api.example.com/items' --data-urlencode 'order=DESC' \
  --data-urlencode 'order_by=date_modified,id'
curl --get 'https://api.example.com/items' \
  --data-urlencode 'modified_after=2026-10-20T09:30:00.000Z'
curl --get 'https://api.example.com/items' \
  --data-urlencode 'modified_after=2026-10-20T09:30:00Z'
Combined parameters with an API key
curl --get 'https://api.example.com/items' \
  --header 'X-API-Key: your-api-key-here' \
  --data-urlencode 'take=31' --data-urlencode 'skip=0' \
  --data-urlencode 'order=ASC' --data-urlencode 'order_by=date_modified,id' \
  --data-urlencode 'modified_after=2026-10-20T09:30:00.000Z' \
  --data-urlencode '_nonce=1792497600000'

برای Bearer، هدر کلید API را با Authorization: Bearer your-token-here جایگزین کنید. آزمون مرزهای زمانی را هم انجام دهید و در حالت احراز هویت فعال، ردشدن درخواست بدون اطلاعات معتبر یا با اطلاعات اشتباه را بررسی کنید.

پس از موفقیت آزمون‌ها، آدرس و اطلاعات احراز هویت را در منابع داده وارد کنید. بعد از تغییر محتوای اصلی، date_modified را به‌روز کنید و با همگام‌سازی مجدد، تغییرات را دریافت کنید.

مستندات موچت

در این صفحه