Warehouse Syncs API
استيرادات مجدولة من مستودع البيانات لديك - BigQuery أو Snowflake أو Redshift - عبر مسارات الكتابة نفسها التي يستخدمها استيراد CSV.
جميع نقاط النهاية في هذه الصفحة نسبية إلى العنوان الأساسي: https://api-eu1.joryio.com/api - راجع نظرة عامة على API.
نظرة عامة
المزامنة تقرأ جدولاً أو عرضاً (view) تجهّزه أنت، وتكتب كل صفحة صفوف إلى Joryio فور وصولها. لا شيء يتراكم في الذاكرة، فمزامنة عشرة ملايين صف تكلّف الذاكرة نفسها التي تكلّفها مزامنة عشرة آلاف.
أمران ليست المزامنة إيّاهما:
- ليست SQL حرّة. يُولَّد الاستعلام في كل تشغيل ليحمل شرط القراءة التزايدية، لذا تعطينا اسم جدول أو عرض وعمود علامة مائية، لا
SELECT. - ليست مسار إدخال منفصلاً. تدخل الصفوف عبر الشيفرة نفسها التي يستخدمها استيراد الملفات، بما في ذلك قواعد انتقال الموافقة - فإلغاء الاشتراك في مستودعك يُحترم، وإعادة الاشتراك لا تُحترم إلا إن طلبت ذلك صراحة.
المصادقة
Authorization: Bearer YOUR_API_KEY
قراءة المزامنة تتطلب integrations:read، وإنشاؤها أو تحديثها أو تشغيلها أو حذفها يتطلب integrations:write. وتفعيل overrideConsent يتطلب إضافةً compliance:write.
كائن المزامنة
| الحقل | النوع | ملاحظات |
|---|---|---|
integrationId | uuid | اتصال المستودع المحفوظ الذي نقرأ عبره |
connectorId | string | bigquery أو snowflake أو redshift |
syncType | string | users أو events أو deletions - يحدد مسار الكتابة الذي تسلكه الصفوف |
sourceTable | string | table أو schema.table أو db.schema.table. هذا الحقل أو sourceQuery |
sourceQuery | string? | جملة SELECT محفوظة للقراءة فقط، بدل الجدول |
watermarkColumn | string? | العمود الذي يدل على تغيّر الصف. احذفه لتحديث كامل في كل تشغيل |
keyColumn | string? | فاصل تعادل فريد وقابل للترتيب. موصى به - انظر أدناه |
watermark | timestamp? | إلى أين وصلت القراءة. نديره نحن، وتمسحه بـ Rewind |
columnMapping | object | عمود المستودع إلى مسار الحقل الهدف |
schedule | string | manual، every_5_min، every_15_min، hourly، every_6_hours، daily، weekly، monthly |
cadence | object | للقراءة فقط. configuredMs وeffectiveMs وthrottled - انظر أدناه |
enabled | boolean | الإيقاف يعني أن المجدول يتخطّاها مع الاحتفاظ بالجدولة لاحقاً |
overrideConsent | boolean | اعتبار المستودع مرجعاً للموافقة. يتطلب compliance:write |
lastRunStats | object? | الصفوف المقروءة والمنشأة والمحدّثة والمتخطّاة والفاشلة، والمدة، وعيّنة محدودة من الأخطاء |
ربط الأعمدة
اربط كل عمود في المستودع بمسار حقل هدف:
| الهدف | المعنى |
|---|---|
email، phone، externalId | الهوية. يلزم على الأقل email أو externalId لكل صف |
attributes.<key> | سمة في الملف الشخصي. لا يجوز أن يحتوي المفتاح على نقطة |
subscriptions.<channel> | موافقة عامة على القناة - email، sms، whatsapp، push، viber |
subscriptions.list.<listId>.<channel> | الموافقة داخل مجموعة اشتراك واحدة |
userId، eventName، timestamp، properties.<key> | لـ syncType: events |
عمليات الحذف
مزامنة deletions تقرأ جدولاً بأشخاص يجب محوهم. وهي لا تحذف شيئاً بنفسها، بل تنشئ طلبات محو على مسار الامتثال نفسه الذي تستخدمه واجهة DSR، فيمرّ المحو على الملفات الشخصية والاشتراكات لكل قائمة والأحداث وبطاقات المحفظة والملفات المخزّنة، ويحترم إعدادك بإخفاء الهوية بدل الحذف، ويترك سجل تدقيق يمكن عرضه على جهة تنظيمية.
نتيجتان تستحقان المعرفة:
- الصف الذي يسمّي شخصاً لم يُستورد قط، أو مُحي بالفعل، يُتخطّى ولا يفشل. قوائم الحذف تحتوي الحالتين بشكل معتاد.
- من هو قيد المحو بالفعل لا يُدرَج في الطابور مجدداً. يبقى الصف في جدولك حتى تزيله، فبدون هذه الحماية كان كل تشغيل سيُدرج محواً آخر للشخص نفسه.
الموافقة
تنتقل الموافقة عبر المزامنة بالقواعد نفسها التي يتبعها استيراد الملفات:
- إلغاء الاشتراك يُحترم دائماً، بلا صلاحية ولا راية. مزامنة الاستبعادات من نظام CRM لا تحتاج تفعيل أي شيء.
- المزامنة لن تعيد اشتراك من ألغى اشتراكه، ولن تخفّض موافقة مزدوجة مؤكدة، إلا إذا كان
overrideConsentمفعّلاً.
overrideConsent يعني في المزامنة أكثر مما يعني في ملف. الملف يُطبَّق مرة واحدة، أما المزامنة فتعيد تطبيق مصدرها كل دورة، لذا فإن تركه مفعّلاً يعكس كل إلغاء اشتراك يصل بين التشغيلات، طوال بقاء المزامنة.
نقاط النهاية
عرض المزامنات
GET /warehouse-syncs
الحصول على مزامنة
GET /warehouse-syncs/:id
تعيد المزامنة مع lastRunStats والعلامة المائية watermark الحالية.
معاينة صفوف المصدر
POST /warehouse-syncs/preview
{ "integrationId": "0f8e...", "sourceTable": "analytics.public.contacts", "limit": 20 }
قراءة محدودة للمصدر كما هو - بلا علامة مائية وبلا شرط تزايدي - لتبني ربط الأعمدة على أسماء الأعمدة الحقيقية لا على ما تتذكره. أرسل واحداً فقط من sourceTable أو sourceQuery. تُنفَّذ مباشرة وبحد أقصى صارم 100 صف.
كتابة SQL بدل تسمية جدول
يقبل sourceQuery جملة SELECT محفوظة للقراءة فقط، للمصادر التي لا يستطيع العرض تشكيلها. ويُفحص بحارس القراءة-فقط نفسه عند الحفظ، لا في الثالثة فجراً عند أول تشغيل.
يُطبَّق الشرط التزايدي عبر تغليف استعلامك:
SELECT * FROM (<استعلامك>) AS jry_src WHERE updated_at > :watermark ORDER BY updated_at ASC
لذا يجب أن يُخرج استعلامك عمود العلامة المائية، وعمود المفتاح إن حدّدت واحداً. لا تسمّهما بأسماء بديلة بنفسك: سمِّ العمود في watermarkColumn وتأكد أن SELECT يعيده. وإن لم يفعل، يفشل حفظ المزامنة برسالة تعدّد الأعمدة التي يعيدها مصدرك فعلاً، بدل أن تفشل المزامنة في أول تشغيل لها فجراً. هذا العقد هو سبب كون الجدول أو العرض هو الافتراضي، وكون هذا مخرج الطوارئ. جملة واحدة فقط - الثانية ستُنفَّذ خارج الاستعلام الفرعي.
عرض التشغيلات
GET /warehouse-syncs/:id/runs?limit=50
التشغيلات الأخيرة، الأحدث أولاً. يحمل كل تشغيل عدّاداته، والعلامة المائية التي بدأ وانتهى عندها، وعيّنة محدودة من أخطاء الصفوف، وخطأ على مستوى التشغيل إن فشل التشغيل نفسه.
كائن المزامنة يخبرك إن كانت المزامنة تعمل الآن. وهذا يخبرك متى بدأت تفشل، وهو السؤال الحقيقي حين تبدو الأرقام غريبة: المزامنة التي تتخطّى 3% من صفوفها منذ شهر تبدو مطابقة، في كائن المزامنة، لتلك التي تخطّت 3% هذا الصباح.
يُحتفظ بأحدث 50 تشغيلاً لكل مزامنة. وعيّنة الأخطاء محدودة لأن ربط أعمدة خاطئاً يُفشل كل صف، ومليون رسالة متطابقة لا تفيد أحداً: العدّادات هي الحقيقة عن الحجم، والعيّنة هي الحقيقة عن السبب.
إنشاء مزامنة
POST /warehouse-syncs
{
"integrationId": "0f8e...",
"connectorId": "snowflake",
"name": "Contacts sync",
"syncType": "users",
"sourceTable": "analytics.public.contacts",
"watermarkColumn": "updated_at",
"schedule": "hourly",
"columnMapping": {
"EMAIL": "email",
"CUSTOMER_ID": "externalId",
"PLAN": "attributes.plan",
"EMAIL_CONSENT": "subscriptions.email"
}
}
يجب أن يكون sourceTable وwatermarkColumn معرّفين بسيطين. وأي شيء آخر - علامات اقتباس أو مسافات أو فواصل منقوطة أو بادئات تعليق - يُرفض بـ 400، لأن هذه الأسماء تُدمج في SQL مولَّد.
تحديث مزامنة
PATCH /warehouse-syncs/:id
يقبل الإنشاء والتحديث الحقول القابلة للتعيين فقط: name, integrationId, syncType, sourceTable, sourceQuery, watermarkColumn, keyColumn, columnMapping, schedule, enabled, overrideConsent. أي حقل آخر - id أو organizationId أو workspaceId أو status أو watermark أو عدادات التشغيل - يُرفض برمز 400؛ فهذه الحقول يديرها Joryio. يجب أن يكون integrationId اتصال مستودع بيانات في مساحة العمل نفسها التي تنتمي إليها المزامنة، وإلا يُرجع الطلب 404. الحقل connectorId اختياري ويُؤخذ من ذلك الاتصال - وإذا أرسلته فيجب أن يطابقه.
تغيير schedule أو enabled يعيد حساب موعد الاستحقاق التالي.
التشغيل الآن
POST /warehouse-syncs/:id/run
تعيد { "success": true, "queued": true }، أو queued: false إن كانت المزامنة في الطابور أو قيد التشغيل. المزامنة التي تتداخل مع نفسها تعيد قراءة الصفوف ذاتها وتضاعف فاتورة المستودع بلا بيانات جديدة.
إرجاع العلامة المائية (Rewind)
POST /warehouse-syncs/:id/rewind
يمسح العلامة المائية ليعيد التشغيل التالي قراءة الجدول من البداية. وهذا آمن - فكل كتابة هي upsert خامل التكرار مفتاحه الهوية - وهو العلاج حين يكون ربط الأعمدة خاطئاً: صحّح الربط، ثم أرجِع، ثم شغّل.
الحذف
DELETE /warehouse-syncs/:id
يحذف الإعداد. أما البيانات المستوردة سابقاً فلا تُمَس.
الإيقاع الفعلي
التشغيل الذي يستغرق أطول من فترته يُرفض ولا يُدرج في الطابور - فالمزامنة لا تتداخل مع نفسها أبداً. لذا فجدولة كل 5 دقائق لمزامنة تستغرق 7 دقائق تعمل فعلياً كل 7 دقائق، وcadence.throttled هو ما يخبرك بذلك بدل أن تظل تتساءل.
القيمة every_5_min هي الحد العملي الأدنى للقراءة بالاستطلاع، والسبب التكلفة لا القدرة: Snowflake يحاسب بالثانية على تشغيل المستودع بحد أدنى 60 ثانية عند كل استئناف، فمزامنة بهذا التواتر لا تدع المستودع ينام. وBigQuery يحاسب على البايتات الممسوحة، والشرط التزايدي لا يتجنّب المسح الكامل إلا إذا كان الجدول مقسّماً أو معنقداً على عمود العلامة المائية.
شهرياً
monthly يحتفظ بيوم الشهر الذي بدأت فيه المزامنة. فالمزامنة المثبّتة على 31 تعمل في 28 فبراير (أو 29 في السنة الكبيسة) وتعود إلى 31 مارس - ولا تنتقل إلى 28 وتبقى هناك. وكل حساب الجدولة بتوقيت UTC، لأن المزامنة لا تحمل منطقة زمنية، وحساب التوقيت المحلي كان سيعني ضمنياً المنطقة التي يصادف أن يعمل فيها المجدول.
كيف تعمل القراءات التزايدية
مع تحديد watermarkColumn، يقرأ كل تشغيل الصفوف عند العلامة المائية المخزّنة أو بعدها، مرتّبة بذلك العمود. ولا تتقدم العلامة المائية إلا بعد كتابة الصفحة كتابة دائمة، فالتشغيل المنقطع يعيد قراءة ذيله بدل تخطّيه.
مع keyColumn تختفي مشكلة الحدّ بدل أن تُدار. يصبح المؤشر (watermarkColumn, keyColumn)، ويقرأ كل تشغيل ما بعد آخر صف قرأه تماماً:
WHERE updated_at > :ts OR (updated_at = :ts AND id > :lastId)
ORDER BY updated_at, id
فالصفوف التي تتشارك الطابع الزمني تُقرأ مرة واحدة بالضبط - لا تُفقد ولا تُعاد قراءتها - ولا حاجة لاستعلام عدّ إضافي. اختر شيئاً متزايداً باطراد: معرّفاً تلقائي التزايد أو ULID. أما بصمة المحتوى فمستقرة لكن ترتيبها عشوائي، فالصف المُدرج لاحقاً في اللحظة نفسها سيقع قبل المؤشر ولن يُقرأ أبداً.
وبدون keyColumn نعود إلى مؤشر بالطابع الزمني وحده، ويُعالَج الحدّ بإحدى طريقتين:
- البصمات (الافتراضي، حتى 5000 صف على الحدّ). نتذكّر أي صفوف وقعت عند لحظة الحدّ ونتخطّاها بعينها في التشغيل التالي. ويُعاد قراءة الحدّ في كل مرة كي يمكن التعرف عليها، ولا يُفقد شيء.
- عدّ الصفوف (بعد 5000). نتذكّر كم صفاً كان هناك ولا نعيد القراءة إلا حين يزيد ذلك العدد. أرخص لأن الحدّ لا يُعاد قراءته، وفيه ثغرة واحدة: صف محذوف وصف مُدرج في اللحظة ذاتها يُبقيان العدد كما هو، فلا يُقرأ الصف الجديد أبداً.
يتجنّب keyColumn كلا المقايضتين: تُستبعد صفوف الحدّ في جملة WHERE، فلا تُعاد قراءتها ولا تُفقد. ولهذا بالضبط هو الإعداد الموصى به. فالحدّ الحصري يفقد بصمت الصفوف التي تتشارك الطابع الزمني نفسه ولم تكن قد قُرئت عند انتهاء التشغيل، أما الحدّ الشامل فيعيد قراءة صفوف قليلة، وهذا لا يكلّف شيئاً: كل كتابة upsert خاملة التكرار، فالصف الذي يصل مرتين يُخزَّن مرة.