إنتقل إلى المحتوى الرئيسي

نشر واجهة برمجية مخصصة

الهدف​

امنح نظام شركة أخرى عنواناً واحداً يستدعيه — POST /api/par/v1/invoices/{id}/approve — واجعل ذلك العنوان يعمل بالطريقة نفسها بعد أن تعيد تسمية عمود.

لماذا يوجد هذا​

لدى بارسوير أصلاً واجهة برمجية لجداولك، مُولَّدة من بيانات الجدول الوصفية، ما يعني أن شكلها يتغير كلما تغير الجدول. هذا بالضبط ما تريده داخل المنصة، وبالضبط ما يجب ألّا تسلّمه لمن هو خارجها: لقد كتب شفرته على أساس شكل معيّن، ولن يعيد كتابتها لأنك غيّرت اسم par_total إلى par_amount.

الواجهة المخصصة وعدٌ بأن يحافظ المسار على شكله بينما تتغير الجداول خلفه.

وهي مكوّن من مكوّنات حلّك، كالنموذج أو العرض: تُؤلَّف في بيئة التطوير، وتسافر داخل حزمة الحل، وتستجيب في بيئتَي الاختبار والإنتاج لأنك استوردتها هناك.

قبل أن تبدأ​

  • حلّ، وله ناشر (تصبح بادئة الناشر جزءاً من كل مسار تنشره).
  • الجدول الذي تريد كشفه، إن كانت هذه الواجهة عن جدول.
  • تسجيل تطبيق للنظام الذي سيستدعيك، يحمل النطاق الذي أنت على وشك اشتراطه. تسجيله من عمل المسؤول — تسجيل تطبيق خارجي.

الخطوات​

  1. افتح حلّك واختر الواجهات البرمجية المخصصة.
  2. اختر جديد.
  3. أعطها اسم عرض. سيُملأ الاسم المنطقي تلقائياً مسبوقاً ببادئة ناشرك.
  4. اترك الإصدار على 1. سترفعه لاحقاً، وسيظل الإصدار القديم مستجيباً ريثما يلحق من يستدعيك.
  5. اختر الأسلوب واكتب المسار — بلا شرطة مائلة في البداية. المقطع بين قوسين معقوفين معاملُ مسار: invoices/{id}/approve. تعرض الشاشة العنوان الكامل وأنت تكتب.
  6. اختر النطاق المطلوب — النطاق الذي يجب أن يحمله تسجيل تطبيق النظام المستدعي، مثل modelapps.read. وهو قائمة لا مربع نص: نطاق لا يُمنح لأحد يرفض كل استدعاء، ولن تعرف بذلك حتى يصطدم به شريك.
  7. اختر بماذا يجيب — JSON أو نص أو HTML. انظر القسم أدناه؛ فهذا الاختيار يحدد أيضاً النص البدئي، لذا حدّده قبل كتابة أي شفرة.
  8. مخطط الطلب ومخطط الاستجابة اختياريان. إن تُركا فارغين يُقرأ العقد من نصّك البرمجي — كل getInput يقرؤه وكل setOutput يضبطه — ويُنشر في مستند OpenAPI. اكتب مخطط JSON فقط لتكون أكثر صرامة (الأنواع، الحقول المطلوبة، الحدود)؛ مخطط الطلب المكتوب يُفحص في كل استدعاء، فيُرفض الطلب المشوّه عند الباب.
  9. أبقِ مستجيبة مؤشَّرة، ثم احفظ.
  10. اختر الصف واضغط تحرير الشفرة. تحلّ المحرّرة محل القائمة — وهنا يعيش سلوك هذه الواجهة، مكتوباً بـ BS Lang. اضغط العين لترى كل دالة يمكنك استدعاؤها. احفظ ثم أغلق.

بماذا تجيب​

تجيب الواجهة البرمجية المخصصة بواحد من ثلاثة، وأنت تختار أيّها على العقد:

تجيب بـيكتب النصمناسب لـ
JSONsetOutput("name", value) لكل حقلنظام آخر يقرأ بياناتك
نصsetBody("...")سطر واحد — مرجع أو رمز أو سطر CSV
HTMLsetBody("<p>...</p>")رسالة تأكيد، أو صفحة يفتحها المتصفح
setOutput("reference", "BOOKING-42"); // JSON: { "reference": "BOOKING-42" }
setBody("BOOKING-42"); // نص أو HTML: هذا بالضبط، ولا شيء غيره

استخدم واحداً منهما، لا كليهما أبداً. النص الذي يستدعي setOutput و setBody يُرفض عند التشغيل بدل أن يفوز أحدهما بصمت — فالإجابة التي لا تستطيع تفسيرها أسوأ من خطأ تستطيع قراءته.

وتأتي معهما دالتان أخريان:

  • setStatus(404) — الحالة التي تريد الإجابة بها. وهي تختلف عن throwValidationError التي تعني أن المستدعي أخطأ (خطأ ٤٠٠)؛ أما setStatus فهو قرار عقدك أنت. ويجب ألّا تحمل 204 جسماً.
  • setContentType("html") — يغيّر النوع لاستدعاء واحد. ولا تحتاجه معظم الواجهات، لأن العقد يعلن أصلاً بماذا تجيب.

لماذا يعلنه العقد لا شفرتك وحدها: تُولَّد وثيقة OpenAPI قبل أن يستدعيك أحد، فلا تستطيع أن تصف إلا ما يقوله العقد. أعلن HTML فتقول الوثيقة HTML؛ واتركه JSON فيكتب المتكامل محلل JSON حول صفحتك.

وضع قيمة مخزّنة في صفحة​

تُبنى الصفحة من نص، وبعض هذا النص سجلاتك. مرِّر كل قيمة مخزّنة عبر htmlEncode قبل أن تضعها في الصفحة:

var booking = getRecord('par_booking', getInput('id'));
setBody('<h1>Receipt ' + htmlEncode(booking.par_name) + '</h1>'
+ '<p>' + htmlEncode(booking.par_guest) + '</p>');

عندها يظهر الضيف الذي كتب <b>، أو أسوأ من ذلك <script>، في اسمه كما كتبه نصاً، بدلاً من أن يصبح جزءاً من الإيصال الذي يفتحه شخص آخر. والحقل الذي لم يملأه أحد يعود فارغاً، فيترك فراغاً بدلاً من كلمة null. وينطبق الأمر نفسه على بريد إلكتروني ترسله مع html: true. أما وسومك أنت، أي التي كتبتها في النص البرمجي، فلا تُرمَّز.

الإجابة بقائمة من السجلات​

يحمل setOutput السجل وصفحة من السجلات كاملةً:

var rows = runQuery({
entity: 'par_invoice',
select: ['par_name', 'par_total'],
filter: { connector: 'And', conditions: [{ field: 'par_status', operator: 'Equals', value: 'Open' }] }
});

setOutput("count", rows.length());
setOutput("invoices", rows); // مصفوفة JSON، كائن لكل سطر

يتلقّى المستدعي { "count": 2, "invoices": [ { "id": "...", "par_name": "INV-1", ... } ] }، بأسماء حقولك كما هي على الجدول تماماً — وهو ما تكتبه في مخطط الإجابة كي تصفه وثيقة OpenAPI.

لا تستطيع بناء قائمة من عندك. ليس في بي‌إس لانغ طريقة للإضافة إلى قائمة، فالسطور التي تجيب بها هي السطور التي سلّمها لك الاستعلام. استخدم select لاختيار الأعمدة بدل محاولة تجميعها: العمود الذي لم تخترْه عمودٌ لا يسافر، وهذا ما يُبقي ملاحظةً داخلية خارج إجابةٍ تنشرها.

الإجابة لعميل مسجَّل الدخول​

كل ما سبق يفترض أن المستدعي نظام — تسجيل تطبيق واحد يفعل ما يسمح به دوره الأمني. أما البوابة فمختلفة: فـparsware.com، أو أي موقع تبنيه على البوابات، يستدعيك نيابةً عن شخصٍ سجّل دخوله هناك، والشيء الوحيد الذي يجب أن تصيبه عمليتك هو أن تجيب عن شركته هو لا عن شركة أحدٍ غيره.

وهناك طريقة واحدة بالضبط لتسأل من يكون:

الدالةتجيب بـ
portalCustomerId()العميل (سجل Account) الذي ينتمي إليه الشخص المسجَّل
portalUserId()مستخدم البوابة نفسه
portalContactId()سجل Contact الذي يمثّله، أو فارغاً إن لم يكن مرتبطاً بأحد
portalEmail()العنوان الذي سجّل الدخول به
portalRole()«Owner» أو «Billing» أو «Member» — ما يجوز له فعله نيابةً عن شركته
var customerId = portalCustomerId();

var rows = runQuery({
entity: 'par_invoice',
select: ['par_name', 'par_total'],
filter: { connector: 'And', conditions: [{ field: 'par_customer', operator: 'Equals', value: customerId }] }
});

setOutput("invoices", rows);

لا تأخذ العميل مُدخَلاً أبداً. ليس سهواً أن لا وجود لـgetInput("customerId") في ذلك النص — فمعرّف العميل الذي يستطيع المستدعي إرساله هو معرّف يستطيع المستدعي تغييره، وطلبٌ واحد معدَّل يقرأ فواتير شخصٍ آخر. تأتي القيمة من رمزٍ موقَّع لا يستطيع أحدٌ بينك وبين الزائر إعادة كتابته.

ويتبع ذلك أمران، وكلاهما مقصود:

  • الاستدعاء بلا شخصٍ مسجَّل يُرفض، ولا يُجاب بلا شيء. إن استدعى نصّك portalCustomerId() ولم يحمل الطلب تسجيل دخول بوابة، تلقّى المستدعي خطأ 401 — بدل أن يتلقّى نصّك قيمةً فارغة ثم يرشّح بها ربما.

  • portalRole() هو ما تُبقي به شيئاً لمن ينبغي أن يراه. فمبلغ الفاتورة أو وسيلة الدفع لمالك الحساب أو لمن يدفع، لا لكل من يملك تسجيل دخول:

    if (portalRole() == "Member") {
    throwValidationError("اطلب هذا من مالك الحساب أو من مسؤول المدفوعات.");
    } else {
    setOutput("total", rows[0].par_total);
    }

    والأدوار الثلاثة سُلَّم — فالمالك يفعل كل ما يفعله مسؤول المدفوعات، وهذا يفعل كل ما يفعله العضو — ولذا فاختبار الدور الذي لا يجوز له أقصرُ عادةً من تعداد من يجوز لهم.

  • ما يجوز للزائر أضيق مما يجوز للموقع. العمليات المسموحة هي تقاطع صلاحيات تطبيق البوابة مع صلاحيات الشخص المسجَّل. فموقعٌ بصلاحيات واسعة لا يجعل زوّاره أقوياء.

الرفض يوقف نصّك​

ينهي throwValidationError النص عند السطر الذي يُستدعى فيه. لا يعمل شيء بعده — لا بقية الكتلة، ولا الأسطر التي تليه، ولا catch وضعته حوله. يتلقّى المستدعي خطأ 400 يحمل جملتك، ولا يُكتب شيء مما كان النص سيكتبه بعد ذلك.

فتحقّق أولاً، واكتب بعد أن تصبح الفحوص وراءك:

if (isEmpty(getInput("reference"))) {
throwValidationError("المرجع مطلوب.");
}
createRecord('par_invoice', { par_name: getInput("reference") });
setStatus(201);

الشيء الوحيد الذي لا يستطيع الرفض التراجع عنه هو كتابة سبقته: نصٌّ ينشئ سجلاً ثم يرفض قد أنشأ السجل. ضع الرفض فوق الكتابة.

ختم الوقت​

يجيب now() باللحظة الحالية وtoday() باليوم الحالي، وكلاهما نصّ تستطيع كتابته مباشرةً في حقل تاريخ/وقت:

createRecord('par_lead', { par_name: getInput("name"), par_received_at: now() });

استخدمهما بدل وقتٍ أرسله لك المستدعي. «متى وصل هذا» حقيقة عن منصّتك؛ أما الطابع الزمني في جسم الطلب فادّعاء من المستدعي، وقد يكون أي تاريخ يشاء. وكلاهما يجيب بالتوقيت العالمي (UTC) — ويُطبَّق تقويم القارئ نفسه عند العرض لا عند التخزين.

تأكّد أنها تعمل​

  • يعرض الصف الجديد المسار المنشور — /api/par/v1/invoices أو ما بنيتَه.
  • اختر OpenAPI من شريط الأدوات. يُنسخ المستند المُولَّد إلى الحافظة: ألصقه لمن سألك عن شكل نقطة النهاية. إنه مشتقّ من تعريفاتك، فلا يمكن أن يصف شيئاً مختلفاً عمّا يفعله الخادم فعلاً.
  • اطلب من النظام المستدعي تجربتها برمزٍ من تسجيل تطبيقه.

الإصدارات: الجزء الجدير بالفهم​

هوية الواجهة هي اسمها وإصدارها معاً. par_approve_invoice بإصداريه 1 و2 عقدٌ واحد في عمرين، ويستجيبان جنباً إلى جنب.

فحين تحتاج إلى تغيير شكل طلب أو استجابة بما يلاحظه المستدعي:

  1. أنشئ واجهة مخصصة جديدة باسم العرض نفسه وإصدار 2. ليست تكراراً — ستقبلها الشاشة.
  2. أخبر شركاء التكامل، وامنحهم وقتاً.
  3. حين لا يعود أحد يستدعي الإصدار 1، أزل تأشير «مستجيبة» عنه.

هذه الخطوة الأخيرة هي التي يخطئ فيها الناس. أزل التأشير؛ ولا تحذف. الإصدار المتقاعد يتوقف عن الاستجابة لكنه يبقى موجوداً، ويسافر في حلّك، ويظل يوثّق ما كان الإصدار 1 يعد به. أما الحذف فيزيل المسار بلا أي إشعار — فيتلقّى كل من يستدعيه خطأ 404 لا يمكن تمييزه عن كون خدمتك معطّلة.

ما لا تفعله الواجهة المخصصة​

لا توسّع الوصول أبداً. تمر عملية الجدول عبر الباب نفسه الذي تمر منه شاشات المنصة: صلاحية المستدعي على ذلك الجدول، وسياسة البيانات خلفها، هما ما يقرّر ما يعود. العقد المخصص يمنح النظام الخارجي رؤية ثابتة لبياناتك، لا رؤية أوسع.

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

استكشاف الأخطاء​

ما تراهما يعنيه
عند الحفظ: «... مُجاب عليه بالفعل»واجهة أخرى في هذه البيئة تطالب بالأسلوب والمسار نفسيهما عند ذلك الإصدار. لا يجوز لعقدين اقتسام عنوان واحد.
«... لديه بالفعل إصدار 1»أنت تعيد إنشاء العقد بدل ترقيمه. ارفع الإصدار.
«... يجب أن يعلن مقطع {id}»القراءة والتحديث والحذف تعمل على سجل واحد، والمسار هو المكان الوحيد الذي يأتي منه معرّفه.
«طلب GET لا يحمل جسماً»احذف مخطط الطلب أو غيّر الأسلوب.
خطأ 403 لدى المستدعيتسجيل تطبيقه لا يحمل النطاق الذي اشترطتَه.
«استُدعي setOutput و setBody معاً»نصّك يبني الإجابة مرتين. اختر واحداً: setOutput يبني كائن JSON وsetBody يكتب جسماً نصياً واحداً.
«جسم مع حالة 204»الحالة 204 تخبر المستدعي أن لا شيء ليقرأه، فلن يرى ما كتبته أبداً. استخدم 200، أو احذف الجسم.
«حالة HTTP بين 100 و599»أُعطي setStatus شيئاً ليس حالة.
تصل الصفحة محاطة بعلامات اقتباساستخدم النص setOutput بدل setBody. فـsetOutput يجيب بـ JSON دائماً مهما أعلن العقد.
خطأ 404 لدى المستدعيلا توجد واجهة مستجيبة تجيب على ذلك الأسلوب والمسار عند ذلك الإصدار. تحقّق من «مستجيبة» ومن مقطع الإصدار.
خطأ 401 مع «يحتاج مستخدم بوابة مسجَّل»نصّك يستدعي portalCustomerId() ولم يحمل الطلب تسجيل دخول بوابة. فإمّا أن المستدعي نظام لا بوابة، وإمّا أن جلسة الزائر انتهت.
خطأ 401 مع «غير صالح»قُدِّم تسجيل دخول بوابة ولم يُقبل — منتهٍ، أو صادر لبوابة أخرى.
خطأ 403 مع «لا ينتمي إلى شركة بعد»الشخص مسجَّل لكنه لم يُوضع في شركة. اعرض له ذلك بدل قائمة فارغة.
أُنشئ سجل رغم رفضك الطلبجرت الكتابة قبل الرفض. يوقف throwValidationError النص حيث يُستدعى، فانقل الفحص فوق الكتابة.

انظر أيضاً​