إضافة قاعدة تحقق على الخادم
الهدف
سجّل قاعدة على الخادم ترفض السجل غير الصالح قبل حفظه — على سبيل المثال، رفض فاتورة إجماليها سالب.
قاعدة كهذه هي خطوة إضافة: نص BS Lang مرتبط بكتابة جدول. إنها اللغة نفسها التي تشغّلها الواجهة البرمجية المخصصة، لكنها موجَّهة إلى عملية حفظ بدل مسار.
المتطلبات المسبقة
- جدول تُرفق به الخطوة (مثل
par_invoiceمع الحقلpar_total). - دور
Maker. - حل مفتوح — تصبح الخطوة أحد مكوناته.
الخطوات
-
في منطقة المصمم، افتح الحل واختر خطوات الإضافة.
-
اضغط جديد.
-
أعطِ الخطوة اسم عرض ("رفض الإجمالي السالب"). يُملأ الاسم المنطقي أثناء الكتابة مسبوقًا ببادئة ناشر الحل الذي تعمل فيه — فالخطوة تخص الحل، مهما كان الجدول الذي تراقبه.
-
اختر الجدول الذي تراقبه الخطوة، والكتابة التي تستجيب لها (إنشاء أو تحديث أو حذف)، والمرحلة:
- قبل الكتابة هي المكان الذي ترفض فيه السجل أو تعدّله. الرفض هنا يعني ألّا يُخزَّن السجل أبدًا.
- بعد الكتابة هي المكان الذي تتصرف فيه بناءً على ما خُزِّن. تعمل بعد حفظ السجل، فلا
تستطيع رفضه: إذا استدعت
throwValidationError، ينتقل النموذج إلى السجل المحفوظ ويقول حُفظ التغيير، لكن خطوة تعمل بعد الكتابة قالت بعد ذلك…. - قبل التحقق تعمل أولًا، للفحوص العامة. وهي داخل معاملة قبل الكتابة نفسها، فالرفض فيها يعني أيضًا ألّا يُخزَّن شيء.
-
اترك التشغيل على قبل الاستجابة ما لم يكن لديك سبب لغير ذلك — انظر الملاحظة أدناه.
-
حدّد الترتيب إذا كانت أكثر من خطوة تشترك في هذا الجدول والكتابة والمرحلة؛ الرقم الأصغر يعمل أولاً.
-
اضغط حفظ. تُنشأ الخطوة بنص بدئي.
-
اخترها من القائمة واضغط تحرير الشفرة. تحلّ المحرّرة محل القائمة:
if (getValue("par_total") < 0) {throwValidationError("لا يمكن أن يكون الإجمالي سالبًا.");}يعرض زر العين في شريط الأدوات كل دالة يمكن للنص استدعاؤها، بما تأخذه وما تعيده. اضغط حفظ ثم إغلاق للعودة إلى القائمة.
-
جرّب إنشاء سجل يخالف القاعدة. يُرفض الحفظ بالجملة التي مرّرتها إلى
throwValidationError، ولا يُخزَّن شيء.
ماذا يحدث خلف الكواليس
عند إنشاء سجل أو تحديثه أو حذفه، يشغّل النظام كل خطوة مفعّلة مسجّلة لذلك الجدول وتلك الكتابة،
بترتيب تصاعدي وفي المرحلة المطابقة. خطوة قبل الكتابة تعمل داخل المعاملة، فالرفض فيها يعني
ألّا يُخزَّن السجل أبدًا. ويمكن للخطوة كذلك أن تضبط قيمة افتراضية أو تصحّح قيمة عبر
setValue("field", value) في تلك المرحلة.
يُتحقَّق من التسجيل عند الحفظ — الغلاف مقابل مخطط خطوة الإضافة، والنص مقابل قواعد BS Lang — فالنص الذي لا يُحلَّل يُرفض مع ذكر رقم السطر بدل أن يفشل لاحقًا أمام مستخدم.
لماذا هذه وليست قاعدة عمل
قاعدة العمل تعمل في المتصفح وعلى نموذج. وهي الأداة الصحيحة لما يراه الشخص: إظهار حقل، جعله مطلوبًا، ضبط قيمة افتراضية أثناء الكتابة.
خطوة الإضافة تعمل على الخادم وداخل المعاملة. وتسري مهما كانت طريقة حفظ السجل — من نموذج، أو من الواجهة البرمجية، أو من استيراد حل، أو من تطبيق مخصص. إذا كان القيد يجب أن يتحقق فعلاً، فمكانه هنا. والاثنتان متكاملتان: القاعدة تجعل النموذج مريحًا، والخطوة تجعل البيانات صحيحة.
قراءة سجلات أخرى والكتابة إليها
يمكن للخطوة أن تصل إلى سجلات غير السجل الجاري حفظه:
// قراءة سجل. حقوله هي خصائص ما يعود إليك، إضافةً إلى id.
var account = getRecord("par_account", getValue("par_account_id"));
if (account.par_status == "on hold") {
throwValidationError("هذا الحساب موقوف.");
}
// قراءة عدة سجلات، والمرور عليها.
var open = queryRecords("par_invoice", "par_status", "open");
for (var i: int = 0; i < open.length; i++) {
// open[i].par_total و open[i].id و…
}
// الكتابة: كل الحقول دفعة واحدة. ويعود إليك معرّف السجل الجديد.
var noteId = createRecord("par_audit_note", { par_text: "تمت الموافقة تلقائياً." });
updateRecord("par_account", account.id, { par_reviewed: true });
ثلاثة أمور تستحق المعرفة:
- تعمل بهوية من حفظ السجل، لا بهوية مسؤول. فإن لم يكن بوسعه قراءة جدول، فلا تستطيع خطوتك ذلك أيضاً. والنص البرمجي ليس أبداً طريقاً للالتفاف على صلاحية.
- الكتابة واحدة. مرّر كل الحقول في حزمة واحدة. وهي تشغّل القواعد المعتادة، فتعمل أيضاً أي خطوة مسجّلة على ذلك الجدول.
- لا تكتب في الجدول الذي تراقبه خطوتك إلا بشرط يحميك — فتلك الكتابة تُشغّل خطوتك من جديد.
يوقف النظام الحلقة بعد ثمانية مستويات، لكن العلاج شرط لا سقف. و
depth()يخبرك كم أنت عميق.
السجل غير الموجود يعود «لا شيء»، لذا افحص قبل قراءة أي حقل منه:
if (isEmpty(getRecord(...))). وقراءة عدة سجلات محدودة بـ200 صف — فالسؤال عن كل الصفوف سؤال
تقرير، ومكانه مصمّم الاستعلامات. أما countRecords فغير محدود: يطلب من قاعدة البيانات عدداً بدل أن
يجلب صفوفاً، فيصحّ مهما بلغ العدد.
سؤال أصعب
queryRecords يقارن حقلاً واحداً بقيمة واحدة. وحين لا يكفي ذلك — مدى، أو عدة شروط، أو ترتيب، أو
عمود على جدول مرتبط — استخدم runQuery، الذي يأخذ مستند الاستعلام الخاص بالمنصة نفسه: وهو ما
يُنتجه مُنشئ «البحث المتقدم» وما يخزّنه الاستعلام المحفوظ.
var cheap = runQuery({
entity: "par_product",
filter: {
connector: "And",
conditions: [
{ field: "par_price", operator: "LessThan", value: 10 },
{ field: "par_discontinued", operator: "Equals", value: false }
]
},
order: [{ field: "par_price", descending: true }],
top: 20
});
والمعاملات هي Equals وNotEquals وContains وGreaterThan وLessThan و
GreaterOrEqual وLessOrEqual وIsNull وIsNotNull وIn — وIn يأخذ قائمة، فتصبح
«الحالة New أو Active» شرطاً واحداً بدل شرطين تجمعهما بيدك.
وهو يَعُدّ ويُجمّع أيضاً. أضِف aggregate لتحصل على مجموعات بدل صفوف، كل مجموعة كائن مسطّح من
مفاتيحها ومقاييسها:
var byOwner = runQuery({
entity: "par_order",
aggregate: {
measures: [{ alias: "total", function: "Sum", field: "par_amount" }],
groupBy: [{ field: "par_owner" }]
}
});
// byOwner[0].par_owner, byOwner[0].total
هذا سؤال لم يكن أي عدد من استدعاءات queryRecords ليجيب عنه، لأن الجمع داخل السكربت يعني جلب كل
الصفوف أولاً — وهو بالضبط ما يمنعه حد الـ200 صف.
السؤال عن جدولين معاً
الفاتورة تعرف معرّف عميلها ولا تعرف عنه شيئاً آخر. فـ«الفواتير غير المدفوعة لعميلٍ سقف ائتمانه أقل
من 5000» سؤال عن جدولين، وlinks تربط الجدول الثاني تحت اسم مستعار:
var rows = runQuery({
entity: "par_invoice",
select: ["par_name", "par_amount"],
links: [
{
alias: "cust",
entity: "par_customer",
'from': "par_customerid",
to: "id",
kind: "Inner",
select: ["par_name", "par_credit_limit"]
}
],
filter: { conditions: [
{ field: "cust.par_credit_limit", operator: "LessThan", value: 5000 }
] }
});
report(rows[0].cust.par_name);
ثلاثة أمور اعرفها، وأولها سيعضّك:
ضع 'from' بين علامتَي اقتباس. فـfrom كلمة تستعملها BS Lang أصلاً، ومن ثم فإن from: بلا
اقتباس خطأ تحليل — لا يُنفَّذ سكربتك أصلاً، والرسالة تتحدث عن اللغة لا عن الاستعلام. أما to و
select وoperator وسواها فأسماء عادية لا تحتاج اقتباساً. وداخل page، number هو المفتاح
الآخر الوحيد الذي يحتاجه.
from وto هما اتجاه السير. from هو الحقل على الجدول الذي تأتي منه، وto الحقل على
الجدول الذي تذهب إليه. وإن لم يُعِد ارتباطٌ شيئاً وأنت واثق أنه ينبغي أن يفعل، فابدأ بفحص هذا.
العمود المرتبط يُقرأ تحت اسمه المستعار — rows[0].cust.par_name لا rows[0].par_name. ولهذا
وُجد الاسم المستعار: قد يحمل جدولان كلاهما par_name، وعليهما داخل صفٍّ واحد أن يبقيا منفصلين.
والمرشّح يسمّي العمود المرتبط بالطريقة نفسها: cust.par_credit_limit.
استعمل kind: "Outer" لتُبقي الصفوف بلا مطابقة — فالصفقة بلا حساب تظل صفقة. وتعود حقول اسمها
المستعار فارغة، فيكون isEmpty(rows[0].cust.par_name) هو الفحص. وكل جدول مرتبط يُفحص بحقّه أمام
صلاحيات المستدعي: فالارتباط ليس طريقاً لقراءة جدول لا يستطيع فتحه.
إعادة استخدام استعلام بناه غيرك
للاستعلام المعقّد حقاً، لا تكتب المستند في سكربتك أصلاً. ابنِه في البحث المتقدم، واحفظه، ثم سمِّه:
var overdue = runSavedQuery("par_overdue_invoices");
وهذا عادةً الجواب الأفضل، لثلاثة أسباب لا علاقة لأيٍّ منها بقلة الكتابة. فالاستعلام يُبنى ويُجرَّب في مصمّم، على يد من يفهم السؤال. وهو مكوّن حلّ، فيُصدَّر بإصداره ويسافر إلى الاختبار والإنتاج مع سائر المكونات. وحين يتغيّر السؤال يتغيّر في مكان واحد — فتتحرك معه كل خطوة وكل واجهة مخصصة تسمّيه.
وكلاهما يعمل بصلاحيات المستدعي، تماماً كـqueryRecords، وكل جدول مرتبط يُفحص بحقّه. فالاستعلام
المحفوظ ليس طريقاً لقراءة ما لا تستطيع قراءته بنفسك.
مشاركة المنطق بين الخطوات
يمكن للخطوة أن تستورد نص حل، تمامًا كما تفعل قاعدة العمل:
import 'par_order_rules';
if (needsApproval(getValue("par_total"))) {
throwValidationError("هذا يحتاج إلى موافقة.");
}
اكتب القاعدة مرة واحدة في النصوص، ثم استوردها من كل خطوة — ومن واجهة برمجية مخصصة عليها
أن تجيب عن السؤال نفسه. هذا ما يمنع نسختين من عتبة واحدة أن تختلفا يوم ينقلها أحدهم. وعينة
business-rules-shared-server-logic مبنية على ذلك بالضبط.
الخطوات غير المتزامنة
التشغيل ← بعد الاستجابة يسلّم الخطوة إلى طابور بدل تشغيلها في الحال. يُحفظ السجل، ويحصل المستدعي على جوابه، ثم تعمل الخطوة بعد قليل.
ثلاثة أمور تتبع ذلك، وهي معنى «بعد» أكثر منها قيودًا:
- يجب أن تكون بعد الكتابة. تنقل الشاشة المرحلة نيابةً عنك عند اختيارها. فالخطوة التي تعمل بعد إرسال الاستجابة لا تستطيع تغيير الكتابة ولا رفضها، فتصير المراحل الأسبق لحظة لا تأتي أبدًا.
- لا تستطيع رفض شيء.
throwValidationErrorفي خطوة غير متزامنة تسجّل فشلًا للخطوة نفسها؛ ويبقى السجل محفوظًا. وكل ما يجب أن يمنع الحفظ فهو متزامن. - قد تعمل مرتين. التسليم «مرة واحدة على الأقل»، فاكتب الخطوة بحيث يكون تشغيلها مرتين غير ضار — القاعدة نفسها التي تتبعها إضافات Dataverse غير المتزامنة.
- تعمل باسم من حفظ، بأدوار الأمان التي كانت لديه عند الحفظ. تستطيع قراءة السجلات وكتابتها وإرسال البريد كما تستطيع خطوة تعمل أثناء الحفظ. ولا تستطيع كتابة ملفات في التخزين. وعلى حفظ قام به زائر بوابة لا تستطيع شيئًا من ذلك.
- تقول
recordId()أي سجل هو. ما تُسلَّمه الخطوة هو الحفظ، لا السجل كما هو الآن، فاقرأ السجل بـgetRecord(entityName(), recordId())حين تحتاج قيمه الحالية، واكتب عليه بـupdateRecord. افحص السجل قبل أن تتصرف: إن كتبت خطوتك على السجل الذي تعمل له، شغّلت تلك الكتابة الخطوة من جديد.
استخدمها لعمل لا ينبغي أن ينتظره المستدعي: إرسال إشعار، أو استدعاء نظام آخر، أو تحديث مجموع.
استكشاف الأخطاء
- «يجب أن يكون الاسم المنطقي مسبوقًا بـ …» — يجب أن يبدأ الاسم ببادئة ناشر حلّك وشرطة سفلية
(مثل
par_). والجدول الذي تختاره لا يغيّرها: خطوة على جدول Account القياسي تظلpar_…. - «شفرة BS Lang غير صالحة» — لم يُحلَّل النص؛ وتذكر الرسالة رقم السطر.
- لم تعمل الخطوة — تأكد من أنها قيد التشغيل لا متوقفة، وأنها تستهدف الجدول والكتابة والمرحلة الصحيحة. وإن كانت بعد الاستجابة فامهلها لحظة: هي في طابور لا في الحال.
- لا شيء يمنع التحديث — كل خطوة تراقب كتابة واحدة. سجّل خطوة ثانية لـالتحديث إذا وجب حماية التحديثات أيضًا.
importلا يُحَل — تأكد من أن اسم الوحدة يطابق الاسم المنطقي لنص الحل تمامًا، وأن النص يصدّر ما تستدعيه (export myFunction;). فالنص الذي لا يحتويexportلا يتيح شيئًا.