Direkt zum Hauptinhalt

Eine serverseitige Validierungsregel hinzufügen

Ziel​

Eine serverseitige Regel registrieren, die einen ungültigen Datensatz vor dem Speichern abweist – zum Beispiel eine Rechnung, deren Gesamtbetrag negativ ist.

Eine solche Regel ist ein Plug-in-Schritt: ein BS Lang-Skript, das an einen Schreibvorgang einer Tabelle angehängt ist. Es ist dieselbe Sprache, die eine benutzerdefinierte API ausführt – nur auf einen Speichervorgang statt auf eine Route gerichtet.

Voraussetzungen​

  • Eine Tabelle, an die der Schritt angehängt wird (zum Beispiel par_invoice mit einem Feld par_total).
  • Die Rolle Maker.
  • Eine geöffnete Lösung – der Schritt wird eine ihrer Komponenten.

Schritte​

  1. Öffnen Sie im Erstellerbereich Ihre Lösung und wählen Sie Plug-in-Schritte aus.

  2. Wählen Sie Neu aus.

  3. Geben Sie dem Schritt einen Anzeigenamen („Negativen Gesamtbetrag abweisen“). Der logische Name füllt sich während der Eingabe selbst aus, mit dem Herausgeberpräfix Ihrer Lösung davor – der Schritt gehört zur Lösung, gleich welche Tabelle er überwacht.

  4. Wählen Sie die Tabelle, die der Schritt überwacht, den Schreibvorgang, auf den er reagiert (Erstellen, Aktualisieren oder Löschen), und die Phase:

    • Vor dem Schreiben ist der Ort, an dem Sie einen Datensatz abweisen oder ändern. Ein Abweisen hier bedeutet, dass der Datensatz nie gespeichert wird.
    • Nach dem Schreiben ist der Ort, an dem Sie auf das Gespeicherte reagieren. Der Schritt läuft, wenn der Datensatz gespeichert ist, und kann ihn daher nicht abweisen: Ruft er throwValidationError auf, landet das Formular auf dem gespeicherten Datensatz und meldet Die Änderung wurde gespeichert, aber ein Schritt, der nach dem Schreiben läuft, meldete danach….
    • Vor der Validierung läuft am frühesten, für grobe Prüfungen. Er liegt in derselben Transaktion wie Vor dem Schreiben, ein Abweisen dort bedeutet also ebenfalls, dass nichts gespeichert wird.
  5. Lassen Sie Ausführung auf Innerhalb des Schreibvorgangs, sofern Sie keinen Grund dagegen haben – siehe den Hinweis unten.

  6. Legen Sie die Reihenfolge fest, wenn sich mehrere Schritte dieselbe Tabelle, denselben Schreibvorgang und dieselbe Phase teilen; die niedrigste Zahl läuft zuerst.

  7. Wählen Sie Speichern aus. Der Schritt existiert nun mit einem Startskript.

  8. Markieren Sie ihn in der Liste und wählen Sie Code bearbeiten aus. Die Liste wird durch den Editor ersetzt:

    if (getValue("par_total") < 0) {
    throwValidationError("Total cannot be negative.");
    }

    Wählen Sie in der Symbolleiste das Auge aus, um jede Funktion zu sehen, die das Skript aufrufen kann, samt dem, was sie erwartet und zurückgibt. Wählen Sie Speichern und dann Schließen aus, um zur Liste zurückzukehren.

  9. Versuchen Sie, einen Datensatz zu erstellen, der gegen die Regel verstößt. Das Speichern wird mit dem Satz abgewiesen, den Sie an throwValidationError übergeben haben, und nichts wird gespeichert.

Was im Hintergrund passiert​

Wenn ein Datensatz erstellt, aktualisiert oder gelöscht wird, führt die Plattform jeden aktivierten Schritt aus, der für diese Tabelle und diesen Schreibvorgang registriert ist – in aufsteigender Reihenfolge und in der passenden Phase. Ein Schritt Vor dem Schreiben läuft innerhalb der Transaktion; ein Abweisen dort bedeutet also, dass der Datensatz nie gespeichert wird. In dieser Phase kann ein Schritt mit setValue("field", value) auch einen Wert vorbelegen oder korrigieren.

Die Registrierung wird beim Speichern validiert – der Rahmen gegen das Plug-in-Schritt-Schema, das Skript gegen die BS Lang-Grammatik –, sodass ein Skript, das sich nicht parsen lässt, mit der betroffenen Zeile abgewiesen wird, statt später vor den Augen eines Benutzers zu scheitern.

Warum dies und keine Geschäftsregel​

Eine Geschäftsregel läuft im Browser, auf einem Formular. Sie ist das richtige Werkzeug für das, was eine Person sieht: ein Feld anzeigen, es verpflichtend machen, während der Eingabe einen Wert vorbelegen.

Ein Plug-in-Schritt läuft auf dem Server, innerhalb der Transaktion. Er greift, gleichgültig, wie der Datensatz gespeichert wurde – über ein Formular, über die API, über einen Lösungsimport, über eine benutzerdefinierte App. Muss eine Bedingung tatsächlich gelten, gehört sie hierher. Beide ergänzen sich: Die Regel macht das Formular angenehm, der Schritt macht die Daten korrekt.

Andere Datensätze lesen und schreiben​

Ein Schritt kann Datensätze über den gerade gespeicherten hinaus erreichen:

// Read a record. Its fields are the properties of what comes back, plus id.
var account = getRecord("par_account", getValue("par_account_id"));
if (account.par_status == "on hold") {
throwValidationError("That account is on hold.");
}

// Read several, and walk them.
var open = queryRecords("par_invoice", "par_status", "open");
for (var i: int = 0; i < open.length; i++) {
// open[i].par_total, open[i].id, ...
}

// Write: the fields you want, in one go. You get the new record's id back.
var noteId = createRecord("par_audit_note", { par_text: "Approved automatically." });
updateRecord("par_account", account.id, { par_reviewed: true });

Drei Dinge sind wissenswert:

  • Er läuft als die Person, die den Datensatz gespeichert hat, nicht als Administrator. Kann sie eine Tabelle nicht lesen, kann Ihr Schritt es auch nicht. Ein Skript ist nie ein Weg, eine Berechtigung zu umgehen.
  • Ein Schreibvorgang ist ein Schreibvorgang. Übergeben Sie alle Felder in einem Objekt. Er durchläuft die normalen Regeln, sodass auch ein Schritt läuft, der auf jener Tabelle registriert ist.
  • Schreiben Sie nicht in die Tabelle, die Ihr Schritt überwacht, es sei denn, Sie sichern es ab – der Schreibvorgang löst Ihren eigenen Schritt erneut aus. Die Plattform stoppt die Schleife nach acht Durchläufen, doch die Lösung ist eine Bedingung, nicht die Grenze. depth() sagt Ihnen, wie tief Sie sich befinden.

Ein nicht vorhandener Datensatz kommt als leerer Wert zurück; mit if (isEmpty(getRecord(...))) prüfen Sie das, bevor Sie ein Feld daraus lesen. Das Lesen mehrerer Datensätze ist auf 200 Zeilen begrenzt – eine Frage über alle Zeilen ist ein Bericht, und dafür ist der Abfrage-Designer da. countRecords ist nicht begrenzt: Es fragt die Datenbank nach einer Anzahl, statt Zeilen abzurufen, und stimmt daher unabhängig davon, wie viele es sind.

Eine schwierigere Frage stellen​

queryRecords vergleicht ein Feld mit einem Wert. Reicht das nicht – ein Bereich, mehrere Bedingungen, eine Sortierung, eine Spalte aus einer verknüpften Tabelle –, verwenden Sie runQuery. Es nimmt das plattformeigene Abfragedokument entgegen: dasselbe, das der Builder der erweiterten Suche erzeugt und eine gespeicherte Abfrage ablegt.

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
});

Die Operatoren sind Equals, NotEquals, Contains, GreaterThan, LessThan, GreaterOrEqual, LessOrEqual, IsNull, IsNotNull und In – und In nimmt eine Liste, sodass „Status ist Neu oder Aktiv“ eine einzige Bedingung ist statt zweier, die Sie von Hand kombinieren.

Es zählt und gruppiert auch. Fügen Sie ein aggregate hinzu, und Sie erhalten Gruppen statt Zeilen zurück, jede ein flaches Objekt aus ihren Schlüsseln und Kennzahlen:

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

Diese Frage könnte keine noch so große Zahl von queryRecords-Aufrufen beantworten, denn Summieren im Skript heißt, zuerst jede Zeile abzurufen – genau das, was die Grenze von 200 Zeilen verbietet.

Zwei Tabellen auf einmal abfragen​

Eine Rechnung kennt die ID ihres Kunden und sonst nichts über ihn. „Unbezahlte Rechnungen von Kunden, deren Kreditlimit unter 5000 liegt“ ist daher eine Frage über zwei Tabellen, und links verknüpft die zweite unter einem Alias:

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);

Drei Dinge sollten Sie wissen, und das erste wird Sie erwischen:

Setzen Sie 'from' in Anführungszeichen. from ist ein Wort, das BS Lang bereits verwendet, daher ist ein from: ohne Anführungszeichen ein Parse-Fehler – Ihr Skript läuft nicht, und die Meldung spricht von der Sprache statt von der Abfrage. to, select, operator und die übrigen sind gewöhnliche Namen und brauchen keine Anführungszeichen. Innerhalb von page ist number der einzige weitere Schlüssel, der sie braucht.

from und to sind die Richtung. from ist das Feld der Tabelle, von der Sie kommen; to ist das Feld der Tabelle, zu der Sie gehen. Liefert eine Verknüpfung nichts, obwohl Sie sicher sind, dass sie etwas liefern sollte, prüfen Sie dies zuerst.

Eine verknüpfte Spalte wird unter ihrem Alias gelesen – rows[0].cust.par_name, nicht rows[0].par_name. Dafür ist der Alias da: Zwei Tabellen können beide ein par_name haben, und innerhalb einer Zeile müssen sie getrennt bleiben. Ein Filter benennt eine verknüpfte Spalte auf dieselbe Weise, als cust.par_credit_limit.

Mit kind: "Outer" behalten Sie Zeilen ohne Treffer – ein Geschäft ohne Firma ist trotzdem ein Geschäft. Deren Alias-Felder kommen leer zurück; mit isEmpty(rows[0].cust.par_name) prüfen Sie das. Und jede verknüpfte Tabelle wird eigenständig gegen die Berechtigungen des Aufrufers geprüft: Eine Verknüpfung ist kein Weg, eine Tabelle zu lesen, die er nicht öffnen dürfte.

Eine Abfrage wiederverwenden, die jemand bereits gebaut hat​

Für eine wirklich komplizierte Abfrage schreiben Sie das Dokument gar nicht erst in Ihr Skript. Bauen Sie sie in der erweiterten Suche, speichern Sie sie und rufen Sie sie über ihren Namen auf:

var overdue = runSavedQuery("par_overdue_invoices");

Das ist meist die bessere Antwort, aus drei Gründen, die nichts mit weniger Tipparbeit zu tun haben. Die Abfrage wird in einem Designer gebaut und getestet, von der Person, die die Frage versteht. Sie ist eine Lösungskomponente, wird also versioniert und reist mit allem anderen nach Test und Produktion. Und wenn sich die Frage ändert, ändert sie sich an einer Stelle – jeder Schritt und jede benutzerdefinierte API, die sie aufruft, zieht sofort mit.

Beide laufen mit den Berechtigungen des Aufrufers, genau wie queryRecords, und jede verknüpfte Tabelle wird eigenständig geprüft. Eine gespeicherte Abfrage ist kein Weg, etwas zu lesen, das Sie selbst nicht lesen dürften.

Logik zwischen Schritten teilen​

Ein Schritt kann ein Lösungsskript importieren, genau wie eine Geschäftsregel:

import 'par_order_rules';

if (needsApproval(getValue("par_total"))) {
throwValidationError("This needs approval.");
}

Schreiben Sie die Regel einmal, unter Skripte, und importieren Sie sie in jeden Schritt – und in eine benutzerdefinierte API, die dieselbe Frage beantworten muss. Das verhindert, dass zwei Kopien eines Schwellenwerts an dem Tag auseinanderlaufen, an dem jemand ihn verschiebt. Das Beispiel business-rules-shared-server-logic ist genau darauf aufgebaut.

Asynchrone Schritte​

Ausführung → Nach der Antwort übergibt den Schritt an eine Warteschlange, statt ihn direkt auszuführen. Der Datensatz wird gespeichert, der Aufrufer erhält seine Antwort, und der Schritt läuft kurz danach.

Daraus folgen einige Dinge – weniger Einschränkungen als das, was „danach“ bedeutet:

  • Er muss Nach dem Schreiben sein. Der Bildschirm stellt die Phase automatisch um, wenn Sie diese Option wählen. Ein Schritt, der erst läuft, wenn die Antwort bereits verschickt ist, kann den Schreibvorgang weder ändern noch abweisen; die früheren Phasen wären also ein Moment, der nie eintreten kann.
  • Er kann nichts abweisen. throwValidationError in einem asynchronen Schritt verbucht einen Fehler für den Schritt; der Datensatz bleibt gespeichert. Alles, was ein Speichern blockieren muss, ist synchron.
  • Er kann zweimal laufen. Die Zustellung erfolgt mindestens einmal; schreiben Sie den Schritt also so, dass ein doppelter Lauf unschädlich ist – dieselbe Regel, der die asynchronen Plug-ins von Dataverse folgen.
  • Er läuft als die Person, die gespeichert hat, mit den Sicherheitsrollen, die sie beim Speichern hatte. Er kann Datensätze lesen und schreiben und E-Mails senden, genau wie ein Schritt, der während des Speicherns läuft. Er kann keine Dateien in den Speicher schreiben. Bei einem Speichern durch einen Portalbesucher kann er nichts davon.
  • recordId() sagt, um welchen Datensatz es geht. Er erhält den Speichervorgang, nicht den Datensatz in seinem aktuellen Zustand; lesen Sie den Datensatz also mit getRecord(entityName(), recordId()), wenn Sie seine aktuellen Werte brauchen, und schreiben Sie mit updateRecord zurück. Prüfen Sie den Datensatz, bevor Sie handeln: Schreibt Ihr Schritt in den Datensatz, auf dem er läuft, führt dieser Schreibvorgang den Schritt erneut aus.

Verwenden Sie das für Arbeit, auf die der Aufrufer nicht warten soll: eine Benachrichtigung senden, ein anderes System aufrufen, eine Summe aktualisieren.

Fehlerbehebung​

  • „Der logische Name muss … als Präfix haben“ – Der Name muss mit dem Herausgeberpräfix Ihrer Lösung und einem Unterstrich beginnen (zum Beispiel par_). Die gewählte Tabelle ändert daran nichts: Ein Schritt auf der Standardtabelle Firma heißt trotzdem par_….
  • „Das BS Lang-Skript ist ungültig“ – Das Skript ließ sich nicht parsen; die Meldung nennt die Zeile.
  • Der Schritt ist nicht gelaufen – Prüfen Sie, ob er Läuft statt Angehalten ist und ob er die richtige Tabelle, den richtigen Schreibvorgang und die richtige Phase anspricht. Läuft er Nach der Antwort, geben Sie ihm einen Moment: Er ist in der Warteschlange, nicht direkt.
  • Nichts blockiert eine Aktualisierung – Ein Schritt überwacht einen Schreibvorgang. Registrieren Sie einen zweiten Schritt für Aktualisieren, wenn auch Aktualisierungen abgesichert werden müssen.
  • import wird nicht aufgelöst – Prüfen Sie, ob der Modulname genau dem logischen Namen des Lösungsskripts entspricht und ob das Skript exportiert, was Sie aufrufen (export myFunction;). Ein Skript ohne export-Anweisung stellt nichts bereit.

Verwandte Szenarien​