Naar hoofdinhoud gaan

Een validatieregel aan de serverkant toevoegen

Doel​

Een regel aan de serverkant registreren die een ongeldig record weigert voordat het wordt opgeslagen — bijvoorbeeld een factuur met een negatief totaal.

Zo'n regel is een plug-instap: een BS Lang-script dat aan de schrijfbewerking van een tabel is gekoppeld. Het is dezelfde taal die een aangepaste API uitvoert, alleen gericht op het opslaan in plaats van op een route.

Vereisten​

  • Een tabel om de stap aan te koppelen (bijvoorbeeld par_invoice met een veld par_total).
  • De rol Maker.
  • Een geopende oplossing — de stap wordt een van haar componenten.

Stappen​

  1. Open in het makergedeelte je oplossing en kies Plug-instappen.

  2. Druk op Nieuw.

  3. Geef de stap een weergavenaam ("Negatief totaal weigeren"). De logische naam vult zich in terwijl je typt, voorafgegaan door het uitgeversvoorvoegsel van je oplossing — de stap hoort bij de oplossing, welke tabel hij ook bewaakt.

  4. Kies de tabel die de stap bewaakt, de schrijfbewerking waarop hij reageert (Maken, Bijwerken of Verwijderen), en de fase:

    • Vóór de schrijfbewerking is waar je een record weigert of wijzigt. Weigeren betekent hier dat het record nooit wordt opgeslagen.
    • Na de schrijfbewerking is waar je reageert op wat is opgeslagen. De stap draait als het record al is opgeslagen en kan het dus niet weigeren: roept hij throwValidationError aan, dan gaat het formulier naar het opgeslagen record en meldt De wijziging is opgeslagen, maar een stap die na het schrijven draait, meldde daarna….
    • Vóór validatie draait het vroegst, voor grove controles. Hij zit in dezelfde transactie als Vóór de schrijfbewerking, dus weigeren daar betekent ook dat er niets wordt opgeslagen.
  5. Laat Wordt uitgevoerd op Binnen de schrijfbewerking staan, tenzij je een reden hebt om dat niet te doen — zie de opmerking hieronder.

  6. Stel de volgorde in als meer dan één stap deze tabel, schrijfbewerking en fase deelt; het laagste nummer draait eerst.

  7. Druk op Opslaan. De stap bestaat nu, met een startscript.

  8. Selecteer hem in de lijst en druk op Code bewerken. De lijst maakt plaats voor de editor:

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

    Druk op het oog in de werkbalk om elke functie te zien die het script kan aanroepen, met wat elke functie aanneemt en teruggeeft. Druk op Opslaan en daarna op Sluiten om terug te gaan naar de lijst.

  9. Probeer een record te maken dat de regel overtreedt. Het opslaan wordt geweigerd met de zin die je aan throwValidationError hebt meegegeven, en er wordt niets opgeslagen.

Wat er achter de schermen gebeurt​

Wanneer een record wordt gemaakt, bijgewerkt of verwijderd, voert het platform elke ingeschakelde stap uit die voor die tabel en schrijfbewerking is geregistreerd, in oplopende volgorde, in de bijbehorende fase. Een stap Vóór de schrijfbewerking draait binnen de transactie, dus weigeren betekent daar dat het record nooit wordt opgeslagen. In die fase kan een stap ook een waarde standaard invullen of corrigeren met setValue("field", value).

De registratie wordt gevalideerd wanneer je hem opslaat — de wrapper tegen het plug-instapschema, het script tegen de BS Lang-grammatica — dus een script dat niet kan worden geparsed, wordt geweigerd met de regel waar het probleem zit, in plaats van later te mislukken voor de ogen van een gebruiker.

Waarom dit en geen bedrijfsregel​

Een bedrijfsregel draait in de browser, op een formulier. Dat is het juiste gereedschap voor wat iemand ziet: een veld tonen, het verplicht maken, een waarde standaard invullen terwijl er wordt getypt.

Een plug-instap draait op de server, binnen de transactie. Hij geldt hoe het record ook is opgeslagen — via een formulier, via de API, via een oplossingsimport, via een aangepaste app. Als een beperking echt moet gelden, hoort ze hier. De twee vullen elkaar aan: de regel maakt het formulier prettig, de stap maakt de gegevens waar.

Andere records lezen en schrijven​

Een stap kan records bereiken buiten het record dat wordt opgeslagen:

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

Drie dingen die goed zijn om te weten:

  • Hij draait als degene die het record opsloeg, niet als beheerder. Als die persoon een tabel niet kan lezen, kan je stap dat ook niet. Een script is nooit een manier om een machtiging te omzeilen.
  • Een schrijfbewerking is één schrijfbewerking. Geef elk veld mee in één verzameling. Ze doorloopt de gewone regels, dus een stap die op die tabel is geregistreerd, draait ook.
  • Schrijf niet naar de tabel die je stap bewaakt tenzij je dat afschermt — de schrijfbewerking activeert je eigen stap opnieuw. Het platform stopt de lus na acht keer, maar de oplossing is een voorwaarde, niet de limiet. depth() vertelt je hoe diep je zit.

Een record dat niet bestaat, komt terug als niets, dus met if (isEmpty(getRecord(...))) controleer je dat voordat je er een veld uit leest. Meerdere records lezen is begrensd op 200 rijen — een vraag over elke rij is een rapport, en die hoort thuis in de queryontwerper. countRecords is niet begrensd: het vraagt de database om een telling in plaats van rijen op te halen, dus het klopt hoeveel het er ook zijn.

Een moeilijkere vraag stellen​

queryRecords vergelijkt één veld met één waarde. Als dat niet genoeg is — een bereik, meerdere voorwaarden, een sortering, een kolom op een gekoppelde tabel — gebruik dan runQuery, dat het eigen querydocument van het platform aanneemt: hetzelfde wat de bouwer van Geavanceerd zoeken produceert en wat een opgeslagen query bewaart.

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

De operators zijn Equals, NotEquals, Contains, GreaterThan, LessThan, GreaterOrEqual, LessOrEqual, IsNull, IsNotNull en In — en In neemt een lijst aan, dus "status is Nieuw of Actief" is één voorwaarde in plaats van twee die je zelf combineert.

Het telt en groepeert ook. Voeg een aggregate toe en je krijgt groepen terug in plaats van rijen, elk een plat object met zijn sleutels en zijn metingen:

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

Dat is een vraag die geen enkel aantal aanroepen van queryRecords kan beantwoorden, omdat optellen in een script betekent dat je eerst elke rij ophaalt — en dat is precies wat de grens van 200 rijen verbiedt.

Een vraag over twee tabellen tegelijk​

Een factuur kent de id van zijn klant en verder niets over die klant. "Onbetaalde facturen van een klant met een kredietlimiet onder 5000" is dus een vraag over twee tabellen, en links koppelt de tweede onder een 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);

Drie dingen om te weten, en het eerste gaat je gegarandeerd een keer overkomen:

Zet 'from' tussen aanhalingstekens. from is een woord dat BS Lang al gebruikt, dus een from: zonder aanhalingstekens is een parsefout — je script draait niet, en de melding gaat over de taal in plaats van over de query. to, select, operator en de rest zijn gewone namen en hebben geen aanhalingstekens nodig. Binnen page is number de enige andere sleutel die ze wel nodig heeft.

from en to geven de reisrichting aan. from is het veld op de tabel waar je vandaan komt; to is het veld op de tabel waar je naartoe gaat. Als een koppeling niets teruggeeft terwijl je zeker weet dat dat niet klopt, is dit het eerste wat je controleert.

Een gekoppelde kolom lees je onder zijn alias — rows[0].cust.par_name, niet rows[0].par_name. Daar is de alias voor: twee tabellen kunnen allebei een par_name hebben, en binnen één rij moeten die gescheiden blijven. Een filter noemt een gekoppelde kolom op dezelfde manier, als cust.par_credit_limit.

Gebruik kind: "Outer" om rijen te behouden die geen overeenkomst hebben — een deal zonder account is nog steeds een deal. Hun aliasvelden komen leeg terug, dus met isEmpty(rows[0].cust.par_name) controleer je dat. En elke gekoppelde tabel wordt op zichzelf gecontroleerd tegen de machtigingen van je aanroeper: een koppeling is geen manier om een tabel te lezen die ze niet konden openen.

Een query hergebruiken die iemand al heeft gebouwd​

Schrijf voor een echt ingewikkelde query het document helemaal niet in je script. Bouw hem in Geavanceerd zoeken, sla hem op en geef hem een naam:

var overdue = runSavedQuery("par_overdue_invoices");

Dit is meestal het betere antwoord, om drie redenen die niets te maken hebben met minder typen. De query wordt gebouwd en getest in een ontwerper, door wie de vraag begrijpt. Het is een oplossingscomponent, dus hij heeft versiebeheer en reist met al het andere mee naar Test en Productie. En als de vraag verandert, verandert hij op één plek — elke stap en elke aangepaste API die hem bij naam noemt, gaat tegelijk mee.

Beide draaien onder de machtigingen van je aanroeper, precies zoals queryRecords, en elke gekoppelde tabel wordt op zichzelf gecontroleerd. Een opgeslagen query is geen manier om iets te lezen wat je zelf niet zou kunnen lezen.

Logica delen tussen stappen​

Een stap kan een oplossingsscript importeren, op dezelfde manier als een bedrijfsregel:

import 'par_order_rules';

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

Schrijf de regel één keer, in Scripts, en importeer hem vanuit elke stap — en vanuit een aangepaste API die dezelfde vraag moet beantwoorden. Zo voorkom je dat twee kopieën van één drempel het oneens worden op de dag dat iemand hem verschuift. Het voorbeeld business-rules-shared-server-logic is precies daaromheen gebouwd.

Asynchrone stappen​

Wordt uitgevoerd → Na het antwoord geeft de stap door aan een wachtrij in plaats van hem direct uit te voeren. Het record wordt opgeslagen, de aanroeper krijgt zijn antwoord, en de stap draait kort daarna.

Daar volgen drie dingen uit, en het zijn niet zozeer beperkingen als wel wat "daarna" betekent:

  • Hij moet op Na de schrijfbewerking staan. Het scherm verplaatst de fase voor je als je dit kiest. Een stap die draait als het antwoord al weg is, kan de schrijfbewerking niet wijzigen of weigeren, dus de eerdere fasen zouden een moment zijn dat nooit kan aanbreken.
  • Hij kan niets weigeren. throwValidationError in een asynchrone stap registreert een fout bij de stap; het record blijft opgeslagen. Alles wat het opslaan moet tegenhouden, is synchroon.
  • Hij kan twee keer draaien. Aflevering is minstens één keer, dus schrijf de stap zo dat twee keer draaien geen kwaad kan — dezelfde regel die de asynchrone plug-ins van Dataverse volgen.
  • Hij draait als de persoon die opsloeg, met de beveiligingsrollen die die persoon had op het moment van opslaan. Hij kan records lezen en schrijven en e-mail versturen, net als een stap die tijdens het opslaan draait. Hij kan geen bestanden naar opslag schrijven. Bij een opslag door een portalbezoeker kan hij geen van deze dingen.
  • recordId() zegt om welk record het gaat. Hij krijgt het opslaan mee, niet het record zoals het er nu voor staat, dus lees het record met getRecord(entityName(), recordId()) als je de huidige waarden nodig hebt, en schrijf terug met updateRecord. Controleer het record voordat je iets doet: als je stap schrijft naar het record waarop hij draait, voert die schrijfbewerking de stap opnieuw uit.

Gebruik dit voor werk waarop de aanroeper niet hoeft te wachten: een melding versturen, een ander systeem aanroepen, een totaal bijwerken.

Problemen oplossen​

  • "Logical name must be … prefixed" — de naam moet beginnen met het uitgeversvoorvoegsel van je oplossing en een onderstrepingsteken (bijvoorbeeld par_). De tabel die je kiest, verandert dat niet: een stap op de standaardtabel Account heet nog steeds par_….
  • "BS Lang script is invalid" — het script kon niet worden geparsed; de melding noemt de regel.
  • De stap draaide niet — controleer of hij Actief is in plaats van Gestopt, en of hij op de juiste tabel, schrijfbewerking en fase is gericht. Draait hij Na het antwoord, geef hem dan even de tijd: hij staat in een wachtrij en draait niet direct.
  • Niets houdt een bijwerking tegen — een stap bewaakt één schrijfbewerking. Registreer een tweede stap voor Bijwerken als ook bijwerkingen moeten worden afgeschermd.
  • import wordt niet gevonden — controleer of de modulenaam precies overeenkomt met de logische naam van het oplossingsscript, en of het script exporteert wat je aanroept (export myFunction;). Een script zonder export-opdracht maakt niets beschikbaar.

Gerelateerde scenario's​