Eine benutzerdefinierte API veröffentlichen
Ziel
Dem System eines anderen Unternehmens eine Adresse zum Aufrufen geben –
POST /api/par/v1/invoices/{id}/approve – und dafür sorgen, dass diese Adresse genauso weiter
funktioniert, nachdem Sie eine Spalte umbenannt haben.
Warum es das gibt
Parsware hat bereits eine API für Ihre Tabellen. Sie wird aus den Metadaten Ihrer Tabelle erzeugt,
ändert also ihre Form, wann immer Ihre Tabelle das tut. Innerhalb der Plattform ist das genau das, was
Sie wollen, und genau das, was Sie niemandem außerhalb in die Hand geben dürfen: Er hat seinen Code
gegen eine Form geschrieben und wird ihn nicht neu schreiben, weil Sie par_total in par_amount
umbenannt haben.
Eine benutzerdefinierte API ist das Versprechen, dass eine Route ihre Form behält, während sich die Tabellen dahinter bewegen.
Sie ist eine Komponente Ihrer Lösung, wie ein Formular oder eine Ansicht. Sie wird in der Entwicklung erstellt, reist im Lösungspaket mit und antwortet in Test und Produktion, weil Sie sie dort importiert haben.
Bevor Sie beginnen
- Eine Lösung mit einem Herausgeber (dessen Präfix Teil jeder Route wird, die Sie veröffentlichen).
- Die Tabelle, die Sie bereitstellen möchten, falls sich diese API um eine Tabelle dreht.
- Eine App-Registrierung für das System, das Sie aufrufen wird, mit dem Scope, den Sie gleich verlangen werden. Eine zu registrieren ist Aufgabe eines Administrators – Eine externe App registrieren.
Schritte
- Öffnen Sie Ihre Lösung und wählen Sie Benutzerdefinierte APIs aus.
- Wählen Sie Neu aus.
- Geben Sie ihr einen Anzeigenamen. Der logische Name füllt sich selbst aus, mit Ihrem Herausgeber als Präfix.
- Lassen Sie die Version auf 1. Sie erhöhen sie später, und die alte Version antwortet weiter, während Ihre Aufrufer nachziehen.
- Wählen Sie die Methode und geben Sie die Route ein – ohne führenden Schrägstrich. Ein
Segment in geschweiften Klammern ist ein Pfadparameter:
invoices/{id}/approve. Der Bildschirm zeigt Ihnen während der Eingabe die vollständige Adresse. - Wählen Sie den erforderlichen Scope – den Scope, den die App-Registrierung des aufrufenden
Systems besitzen muss, etwa
modelapps.read. Es ist eine Liste, kein Textfeld: Ein Scope, den niemand erhalten kann, würde jeden Aufruf ablehnen, und Sie würden es erst erfahren, wenn ein Partner es tut. - Wählen Sie, womit sie antwortet – JSON, Text oder HTML. Siehe den Abschnitt unten; das bestimmt auch, was das Startskript tut, wählen Sie es also, bevor Sie Code schreiben.
- Anfrageschema und Antwortschema sind optional. Leer gelassen, wird der Vertrag aus Ihrem
Skript gelesen – jedes
getInputund jedessetOutput– und im OpenAPI-Dokument veröffentlicht. Schreiben Sie ein JSON Schema nur, um strenger zu sein (Typen, Pflichtfelder, Grenzen); ein geschriebenes Anfrageschema wird bei jedem Aufruf geprüft, sodass eine fehlerhafte Anfrage schon an der Tür abgelehnt wird. - Lassen Sie Antwortet aktiviert und wählen Sie Speichern aus.
- Markieren Sie die Zeile und wählen Sie Code bearbeiten aus. Die Liste wird durch den Editor ersetzt – hier lebt das Verhalten der API, geschrieben in BS Lang. Wählen Sie das Auge aus, um jede Funktion zu sehen, die Sie aufrufen können. Wählen Sie Speichern und dann Schließen aus.
Womit sie antwortet
Eine benutzerdefinierte API antwortet mit einer von drei Arten, und Sie wählen im Vertrag, mit welcher:
| Antwortet mit | Das Skript schreibt | Gut für |
|---|---|---|
| JSON | setOutput("name", value), einmal pro Feld | Ein anderes System, das Ihre Daten liest |
| Text | setBody("...") | Eine Zeile – eine Referenz, ein Token, eine CSV-Zeile |
| HTML | setBody("<p>...</p>") | Eine Bestätigungs-E-Mail, eine Seite, die ein Browser öffnet |
setOutput("reference", "BOOKING-42"); // JSON: { "reference": "BOOKING-42" }
setBody("BOOKING-42"); // text or HTML: exactly that, and nothing else
Verwenden Sie das eine oder das andere, nie beides. Ein Skript, das setOutput und setBody
aufruft, wird bei der Ausführung abgelehnt, statt dass eines von beiden stillschweigend gewinnt – eine
Antwort, die Sie nicht erklären können, ist schlimmer als ein Fehler, den Sie lesen können.
Zwei weitere Funktionen gehören dazu:
setStatus(404)– der Status, mit dem Sie antworten möchten. Etwas anderes alsthrowValidationError, das bedeutet: Der Aufrufer hat etwas falsch gemacht (ein 400); mitsetStatusentscheidet Ihr Vertrag. Ein204darf keinen Body tragen.setContentType("html")– ändert den Typ für einen Aufruf. Die meisten APIs brauchen das nie, weil der Vertrag bereits festlegt, womit sie antworten.
Warum der Vertrag das festlegt und nicht nur Ihr Code: Das OpenAPI-Dokument wird erzeugt, bevor irgendjemand Sie aufruft, und kann daher nur beschreiben, was der Vertrag sagt. Legen Sie HTML fest, sagt das Dokument HTML; belassen Sie es bei JSON, schreibt ein Integrator einen JSON-Parser um Ihre Seite herum.
Einen gespeicherten Wert in eine Seite setzen
Eine Seite besteht aus Text, und ein Teil dieses Textes sind Ihre Datensätze. Geben Sie jeden
gespeicherten Wert durch htmlEncode, bevor er in die Seite kommt:
var booking = getRecord('par_booking', getInput('id'));
setBody('<h1>Receipt ' + htmlEncode(booking.par_name) + '</h1>'
+ '<p>' + htmlEncode(booking.par_guest) + '</p>');
Ein Gast, der <b> oder gar <script> in seinen eigenen Namen getippt hat, erscheint dann als
genau dieser Text, statt Teil der Quittung zu werden, die jemand anderes öffnet. Ein leeres Feld
kommt leer zurück und hinterlässt eine Lücke statt des Wortes null. Dasselbe gilt für eine E-Mail,
die Sie mit html: true senden. Ihr eigenes Markup, die Tags im Skript, wird nicht kodiert.
Mit einer Liste von Datensätzen antworten
setOutput transportiert einen Datensatz und eine Seite von Datensätzen vollständig:
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); // a JSON array, one object per row
Der Aufrufer erhält { "count": 2, "invoices": [ { "id": "...", "par_name": "INV-1", ... } ] }, mit
Ihren Feldnamen genau so, wie sie in der Tabelle stehen – und genau das schreiben Sie ins
Antwortschema, damit das OpenAPI-Dokument es beschreibt.
Sie können keine eigene Liste aufbauen. BS Lang bietet keine Möglichkeit, etwas an eine Liste
anzuhängen; die Zeilen, mit denen Sie antworten, sind also die Zeilen, die Ihnen eine Abfrage geliefert
hat. Wählen Sie die Spalten mit select, statt sie zusammenzusetzen: Eine Spalte, die Sie nicht
ausgewählt haben, reist nicht mit – so halten Sie eine interne Notiz aus einer Antwort heraus, die
Sie veröffentlichen.
Einem angemeldeten Kunden antworten
Alles oben geht davon aus, dass der Aufrufer ein System ist – eine App-Registrierung, die tut, was ihre Sicherheitsrolle erlaubt. Ein Portal ist anders: parsware.com oder jede Website, die Sie auf Portal Apps bauen, ruft Sie im Namen einer Person auf, die sich dort angemeldet hat, und das Eine, was Ihre Operation richtig machen muss, ist, über deren Unternehmen zu antworten und über kein anderes.
Es gibt genau einen Weg, zu fragen, wer das ist:
| Funktion | Antwortet mit |
|---|---|
portalCustomerId() | dem Kunden (einem Firmendatensatz), zu dem die angemeldete Person gehört |
portalUserId() | dem Portalbenutzer selbst |
portalContactId() | dem Kontaktdatensatz, der die Person ist, oder leer, wenn sie mit keinem verknüpft ist |
portalEmail() | der Adresse, mit der sie sich angemeldet hat |
portalRole() | "Owner", "Billing" oder "Member" – was sie im Namen ihres Unternehmens tun darf |
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);
Nehmen Sie den Kunden nie als Eingabe entgegen. Es ist kein Versehen, dass es in diesem Skript
kein getInput("customerId") gibt – eine Kunden-ID, die der Aufrufer senden kann, ist eine Kunden-ID,
die der Aufrufer ändern kann, und eine einzige bearbeitete Anfrage liest die Rechnungen eines anderen.
Der Wert stammt aus einem signierten Token, das niemand zwischen dem Besucher und Ihnen umschreiben
kann.
Daraus folgen einige Dinge, und alle sind beabsichtigt:
-
Ein Aufruf ohne angemeldete Person wird abgelehnt, nicht mit nichts beantwortet. Ruft Ihr Skript
portalCustomerId()auf und enthielt die Anfrage keine Portalanmeldung, erhält der Aufrufer einen 401 – statt dass Ihr Skript einen leeren Wert erhält und womöglich danach filtert. -
Mit
portalRole()behalten Sie etwas den Personen vor, die es sehen sollen. Eine Rechnungssumme oder eine Zahlungsmethode ist für einen Owner oder die Person, die bezahlt, nicht für jeden mit einem Login:if (portalRole() == "Member") {throwValidationError("Ask an Owner or a Billing contact for this.");} else {setOutput("total", rows[0].par_total);}Die drei Rollen sind eine Leiter – ein Owner darf alles, was Billing darf, und Billing alles, was ein Member darf –, daher ist es meist kürzer, auf die eine Rolle zu prüfen, die etwas nicht darf, als alle aufzuzählen, die es dürfen.
-
Was der Besucher tun darf, ist enger als das, was die Website tun darf. Die erlaubten Operationen sind die Schnittmenge aus den Berechtigungen der Portalanwendung und denen der angemeldeten Person. Eine Website mit weitreichenden Berechtigungen macht ihre Besucher nicht mächtig.
Ablehnen beendet Ihr Skript
throwValidationError beendet das Skript in der Zeile, in der es aufgerufen wird. Danach läuft
nichts mehr – nicht der Rest des Blocks, nicht die Zeilen darunter und auch kein catch, das Sie
darum gelegt haben. Der Aufrufer erhält einen 400 mit Ihrem Satz, und nichts, was das Skript danach
geschrieben hätte, wird geschrieben.
Prüfen Sie also zuerst, und schreiben Sie, wenn die Prüfungen hinter Ihnen liegen:
if (isEmpty(getInput("reference"))) {
throwValidationError("A reference is required.");
}
createRecord('par_invoice', { par_name: getInput("reference") });
setStatus(201);
Das Einzige, was eine Ablehnung nicht rückgängig machen kann, ist ein Schreibvorgang, der davor kam: Ein Skript, das einen Datensatz erstellt und dann ablehnt, hat den Datensatz erstellt. Stellen Sie die Ablehnungen über die Schreibvorgänge.
Die Zeit stempeln
now() liefert den aktuellen Zeitpunkt und today() den aktuellen Tag, beide als Text, den Sie
direkt in ein Datum/Uhrzeit-Feld schreiben können:
createRecord('par_lead', { par_name: getInput("name"), par_received_at: now() });
Verwenden Sie diese statt einer Zeit, die der Aufrufer Ihnen gesendet hat. „Wann ist das eingetroffen“ ist eine Tatsache über Ihre Plattform; ein Zeitstempel im Anfrage-Body ist eine Behauptung des Aufrufers und kann jedes beliebige Datum sein. Beide antworten in UTC – der eigene Kalender des Lesers wird bei der Anzeige angewendet, nicht beim Speichern.
Prüfen, ob es funktioniert hat
- Die neue Zeile zeigt ihren ausgelieferten Pfad –
/api/par/v1/invoicesoder was Sie gebaut haben. - Wählen Sie in der Symbolleiste OpenAPI aus. Das erzeugte Dokument wird in Ihre Zwischenablage kopiert: Fügen Sie es für den Integrator ein, der gefragt hat, wie Ihr Endpunkt aussieht. Es wird aus Ihren Definitionen abgeleitet und kann daher nichts anderes beschreiben, als der Server tatsächlich tut.
- Lassen Sie das aufrufende System es ausprobieren, mit einem Token aus seiner App-Registrierung.
Versionierung: der Teil, den man verstehen sollte
Die Identität einer API ist ihr Name und ihre Version zusammen. par_approve_invoice v1 und v2
sind ein Vertrag in zwei Altersstufen, und sie antworten nebeneinander.
Wenn Sie also die Form einer Anfrage oder Antwort so ändern müssen, dass ein Aufrufer es bemerken würde:
- Erstellen Sie eine neue benutzerdefinierte API mit demselben Anzeigenamen und Version 2. Sie ist kein Duplikat – der Bildschirm akzeptiert sie.
- Informieren Sie Ihre Integrationspartner und geben Sie ihnen Zeit.
- Wenn niemand mehr v1 aufruft, deaktivieren Sie dort „Antwortet“.
Beim letzten Schritt machen viele Fehler. Deaktivieren Sie „Antwortet“; löschen Sie nicht. Eine außer Dienst gestellte Version antwortet nicht mehr, existiert aber weiter, reist weiter in Ihrer Lösung mit und dokumentiert weiterhin, was v1 einmal versprochen hat. Löschen entfernt die Route ganz ohne Vorwarnung – alles, was sie noch aufruft, erhält einen schlichten 404, der sich nicht davon unterscheiden lässt, dass Ihr Dienst defekt ist.
Was eine benutzerdefinierte API nicht tut
Sie erweitert nie den Zugriff. Eine Tabellenoperation geht durch genau dieselbe Tür, die auch die eigenen Bildschirme der Plattform verwenden: Die Berechtigung des Aufrufers für diese Tabelle und die dahinterstehende Datenrichtlinie entscheiden, was zurückkommt. Ein kuratierter Vertrag gibt einem externen System eine stabile Sicht auf Ihre Daten, nie eine größere.
Erhält das aufrufende System weniger als erwartet, liegt die Antwort fast immer in der Sicherheitsrolle hinter seiner App-Registrierung – nicht in der API.
Fehlerbehebung
| Was Sie sehen | Was es bedeutet |
|---|---|
| „…wird bereits beantwortet von…“ beim Speichern | Eine andere API in dieser Umgebung beansprucht diese Methode und diesen Pfad in dieser Version bereits. Zwei Verträge können sich keine Adresse teilen. |
| „…hat bereits eine Version 1…“ | Sie erstellen einen Vertrag neu, statt ihn zu versionieren. Erhöhen Sie die Version. |
„…muss ein {id}-Segment deklarieren“ | Lesen, Aktualisieren und Löschen wirken auf einen Datensatz, und die Route ist der einzige Ort, von dem dessen ID kommen kann. |
| „Eine GET-Anfrage hat keinen Body“ | Entfernen Sie das Anfrageschema oder ändern Sie die Methode. |
| 403 bei einem Aufrufer | Seine App-Registrierung besitzt den verlangten Scope nicht. |
| „hat sowohl setOutput als auch setBody aufgerufen“ | Ihr Skript baut seine Antwort doppelt. Wählen Sie eines: setOutput setzt ein JSON-Objekt zusammen, setBody schreibt einen einzigen Textkörper. |
| „hat einen Body und einen 204-Status gesetzt“ | Ein 204 sagt dem Aufrufer, dass es nichts zu lesen gibt, er sieht also nie, was Sie geschrieben haben. Verwenden Sie 200 oder lassen Sie den Body weg. |
| „Ein HTTP-Status liegt zwischen 100 und 599“ | setStatus wurde etwas übergeben, das kein Status ist. |
| Eine Seite kommt in Anführungszeichen verpackt an | Das Skript hat setOutput statt setBody verwendet. setOutput antwortet immer mit JSON, gleich was der Vertrag festlegt. |
| 404 bei einem Aufrufer | Keine aktivierte API beantwortet diese Methode und diesen Pfad in dieser Version. Prüfen Sie „Antwortet“ und das Versionssegment. |
| 401 mit „benötigt einen angemeldeten Portalbenutzer“ | Ihr Skript ruft portalCustomerId() auf, und die Anfrage enthielt keine Portalanmeldung. Entweder ist der Aufrufer ein System statt eines Portals, oder die Sitzung des Besuchers ist abgelaufen. |
| 401 mit „nicht gültig“ | Eine Portalanmeldung wurde vorgelegt und nicht akzeptiert – abgelaufen oder für ein anderes Portal ausgestellt. |
| 403 mit „gehört noch zu keinem“ | Die Person ist angemeldet, wurde aber noch keinem Unternehmen zugeordnet. Zeigen Sie ihr das, statt einer leeren Liste. |
| Ein Datensatz wurde erstellt, obwohl Sie die Anfrage abgelehnt haben | Der Schreibvorgang lief vor der Ablehnung. throwValidationError hält das Skript dort an, wo es aufgerufen wird; verschieben Sie die Prüfung also über den Schreibvorgang. |