پرش به مطلب اصلی

افزودن یک قاعدهٔ اعتبارسنجی سمت سرور

هدف​

یک قاعدهٔ سمت سرور ثبت کنید که رکورد نامعتبر را پیش از ذخیره شدن رد می‌کند — برای نمونه، نپذیرفتن فاکتوری که جمع کل آن منفی است.

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

پیش‌نیازها​

  • جدولی که گام به آن پیوست شود (برای نمونه، par_invoice با فیلد par_total).
  • نقش Maker.
  • یک راه‌حل باز — گام یکی از مؤلفه‌های آن می‌شود.

گام‌ها​

  1. در ناحیهٔ سازنده، راه‌حل خود را باز کنید و گام‌های افزونه را برگزینید.

  2. جدید را بزنید.

  3. یک نام نمایشی بدهید («رد جمع کل منفی»). نام منطقی همزمان با تایپ شما پر می‌شود، با پیشوند ناشرِ راه‌حل خودتان — گام به راه‌حل تعلق دارد، هر جدولی را هم که بپاید.

  4. جدولی که گام می‌پاید، نوشتنی که به آن واکنش نشان می‌دهد (ایجاد، به‌روزرسانی یا حذف) و مرحله را برگزینید:

    • پیش از نوشتن جایی است که رکورد را رد یا اصلاح می‌کنید. رد کردن در اینجا یعنی رکورد هرگز ذخیره نمی‌شود.
    • پس از نوشتن جایی است که بر آنچه ذخیره شد عمل می‌کنید. پس از ذخیره شدن رکورد اجرا می‌شود، پس نمی‌تواند آن را رد کند: اگر throwValidationError را صدا بزند، فرم روی رکورد ذخیره‌شده می‌ماند و می‌گوید تغییر ذخیره شد، اما گامی که پس از نوشتن اجرا می‌شود سپس گفت….
    • پیش از اعتبارسنجی زودتر از همه اجرا می‌شود، برای بررسی‌های درشت. درون همان تراکنشِ پیش از نوشتن است، پس رد کردن در آنجا هم یعنی چیزی ذخیره نمی‌شود.
  5. اجرا را روی پیش از پاسخ بگذارید مگر دلیلی خلاف آن داشته باشید — یادداشت پایین را ببینید.

  6. اگر بیش از یک گام همین جدول، نوشتن و مرحله را دارند، ترتیب را تعیین کنید؛ کوچک‌ترین عدد زودتر اجرا می‌شود.

  7. ذخیره را بزنید. گام با یک اسکریپت آغازین ساخته می‌شود.

  8. آن را در فهرست برگزینید و ویرایش کد را بزنید. فهرست جای خود را به ویرایشگر می‌دهد:

    if (getValue("par_total") < 0) {
    throwValidationError("جمع کل نمی‌تواند منفی باشد.");
    }

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

  9. رکوردی بسازید که قاعده را نقض کند. ذخیره با همان جمله‌ای که به throwValidationError داده‌اید رد می‌شود و چیزی ذخیره نمی‌گردد.

پشت صحنه چه می‌گذرد​

هنگام ایجاد، به‌روزرسانی یا حذف یک رکورد، پلتفرم هر گام فعالِ ثبت‌شده برای آن جدول و آن نوشتن را به ترتیب صعودی و در مرحلهٔ منطبق اجرا می‌کند. گامِ پیش از نوشتن درون تراکنش اجرا می‌شود، پس رد کردن در آنجا یعنی رکورد هرگز ذخیره نمی‌شود. گام می‌تواند در همان مرحله با setValue("field", value) مقداری را پیش‌فرض بگذارد یا اصلاح کند.

ثبت هنگام ذخیره اعتبارسنجی می‌شود — پوشش در برابر شمای گام افزونه و اسکریپت در برابر دستور زبان بی‌اس لنگ — پس اسکریپتی که تجزیه نشود با ذکر شمارهٔ خط رد می‌شود، نه اینکه بعدها پیش چشم کاربر شکست بخورد.

چرا این و نه یک قاعدهٔ کسب‌وکار​

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

گام افزونه روی سرور و درون تراکنش اجرا می‌شود. هرگونه ذخیرهٔ رکورد را پوشش می‌دهد — از فرم، از اپی‌آی، از درون‌ریزی راه‌حل، از یک برنامهٔ سفارشی. اگر قیدی باید واقعاً برقرار باشد، جایش اینجاست. این دو مکمل یکدیگرند: قاعده فرم را دلپذیر می‌کند، گام داده را درست نگاه می‌دارد.

خواندن و نوشتن رکوردهای دیگر​

یک گام می‌تواند به رکوردهایی فراتر از رکوردی که ذخیره می‌شود دسترسی داشته باشد:

// خواندن یک رکورد. فیلدهایش ویژگی‌های چیزی هستند که برمی‌گردد، به‌علاوهٔ 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(...))). خواندن چند رکورد حداکثر ۲۰۰ سطر است — پرسش دربارهٔ همهٔ سطرها پرسش یک گزارش است و جای آن طراح پرس‌وجوست. 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 نمی‌توانست پاسخ دهد، چون جمع‌زدن در اسکریپت یعنی اول همهٔ سطرها را واکشی کنید — دقیقاً همان چیزی که سقف ۲۰۰ سطری اجازه نمی‌دهد.

پرسیدن دربارهٔ دو جدول با هم​

یک فاکتور شناسهٔ مشتری‌اش را می‌داند و هیچ چیز دیگری دربارهٔ او. پس «فاکتورهای پرداخت‌نشدهٔ مشتری‌ای که سقف اعتبارش کمتر از ۵۰۰۰ است» پرسشی دربارهٔ دو جدول است، و 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_… نام می‌گیرد.
  • «اسکریپت بی‌اس لنگ نامعتبر است» — اسکریپت تجزیه نشد؛ پیام شمارهٔ خط را می‌گوید.
  • گام اجرا نشد — بررسی کنید که در حال اجرا باشد نه متوقف، و جدول و نوشتن و مرحلهٔ درست را هدف بگیرد. اگر روی پس از پاسخ است کمی صبر کنید: در صف است، نه درجا.
  • چیزی جلوی به‌روزرسانی را نمی‌گیرد — هر گام یک نوشتن را می‌پاید. اگر به‌روزرسانی هم باید محافظت شود، گام دومی برای به‌روزرسانی ثبت کنید.
  • import حل نمی‌شود — بررسی کنید نام ماژول دقیقاً با نام منطقی اسکریپت راه‌حل یکی باشد و اسکریپت آنچه را صدا می‌زنید صادر کرده باشد (export myFunction;). اسکریپتی بدون export هیچ چیزی را در دسترس نمی‌گذارد.

سناریوهای مرتبط​