Een aangepaste API publiceren
Doel
Geef het systeem van een ander bedrijf één adres om aan te roepen —
POST /api/par/v1/invoices/{id}/approve — en zorg dat dat adres op dezelfde manier blijft werken
nadat je een kolom hebt hernoemd.
Waarom dit bestaat
Parsware heeft al een API voor je tabellen. Die wordt gegenereerd uit de metagegevens van je tabel,
wat betekent dat hij van vorm verandert zodra je tabel dat doet. Dat is precies wat je binnen het
platform wilt, en precies wat je niet aan iemand buiten het platform moet geven: die heeft zijn code
tegen een bepaalde vorm geschreven, en gaat die niet herschrijven omdat jij par_total hebt hernoemd
naar par_amount.
Een aangepaste API is de belofte dat een route zijn vorm houdt terwijl de tabellen erachter veranderen.
Het is een component van je oplossing, net als een formulier of een weergave. Je maakt hem in Ontwikkeling, hij reist mee in het oplossingspakket, en hij antwoordt in Test en Productie omdat je hem daar hebt geïmporteerd.
Voordat je begint
- Een oplossing, met een uitgever (het voorvoegsel daarvan wordt deel van elke route die je publiceert).
- De tabel die je wilt openstellen, als deze API over een tabel gaat.
- Een app-registratie voor het systeem dat jou gaat aanroepen, met de scope die je zo meteen gaat vereisen. Er een registreren is het werk van een beheerder — Een externe app registreren.
Stappen
- Open je oplossing en kies Aangepaste API's.
- Kies Nieuw.
- Geef hem een weergavenaam. De logische naam vult zichzelf in, met het voorvoegsel van je uitgever ervoor.
- Laat de versie op 1 staan. Je verhoogt die later, en de oude versie blijft antwoorden terwijl degene die jou aanroept bijwerkt.
- Kies de methode en typ de route — zonder schuine streep aan het begin. Een segment tussen
accolades is een padparameter:
invoices/{id}/approve. Het scherm toont het volledige adres terwijl je typt. - Kies de vereiste scope — de scope die de app-registratie van het aanroepende systeem moet
hebben, zoals
modelapps.read. Het is een lijst, geen tekstvak: een scope die niemand kan krijgen, zou elke aanroep weigeren, en dat zou je pas merken als een partner het merkte. - Kies waarmee hij antwoordt — JSON, tekst of HTML. Zie de sectie hieronder; dit bepaalt ook wat het startscript doet, dus kies het voordat je code schrijft.
- Het aanvraagschema en het antwoordschema zijn optioneel. Leeg gelaten wordt het contract
uit uw script gelezen — elke
getInputen elkesetOutput— en in het OpenAPI-document gepubliceerd. Schrijf alleen een JSON Schema om strenger te zijn (typen, verplichte velden, grenzen); een geschreven aanvraagschema wordt bij elke aanroep gecontroleerd, zodat een onjuiste aanvraag al aan de deur wordt geweigerd. - Laat Antwoordt aangevinkt en kies Opslaan.
- Selecteer de rij en druk op Code bewerken. De lijst maakt plaats voor de editor — hier leeft het gedrag van de API, geschreven in BS Lang. Druk op het oog om elke functie te zien die je kunt aanroepen. Kies Opslaan en daarna Sluiten.
Waarmee hij antwoordt
Een aangepaste API antwoordt met een van drie dingen, en je kiest welke op het contract:
| Antwoordt met | Het script schrijft | Geschikt voor |
|---|---|---|
| JSON | setOutput("name", value), één keer per veld | Een ander systeem dat je gegevens leest |
| Tekst | setBody("...") | Eén regel — een referentie, een token, een CSV-rij |
| HTML | setBody("<p>...</p>") | Een bevestigingsmail, een pagina die een browser opent |
setOutput("reference", "BOOKING-42"); // JSON: { "reference": "BOOKING-42" }
setBody("BOOKING-42"); // text or HTML: exactly that, and nothing else
Gebruik het een of het ander, nooit beide. Een script dat setOutput en setBody aanroept,
wordt bij het uitvoeren geweigerd, in plaats van dat een van beide stilletjes wint — een antwoord dat
je niet kunt verklaren, is erger dan een foutmelding die je kunt lezen.
Er horen nog twee functies bij:
setStatus(404)— de status waarmee je wilt antwoorden. Anders danthrowValidationError, dat betekent de aanroeper deed het fout (een 400); metsetStatusbeslist je contract. Een204mag geen body hebben.setContentType("html")— wijzigt het type voor één aanroep. De meeste API's hebben het nooit nodig, omdat het contract al zegt waarmee ze antwoorden.
Waarom het contract dit vastlegt en niet alleen je code: het OpenAPI-document wordt gegenereerd voordat iemand je aanroept, dus het kan alleen beschrijven wat het contract zegt. Leg je HTML vast, dan zegt het document HTML; laat je het op JSON staan, dan schrijft een integrator een JSON-parser rond je pagina.
Een opgeslagen waarde in een pagina zetten
Een pagina is opgebouwd uit tekst, en een deel van die tekst komt uit uw records. Haal elke
opgeslagen waarde door htmlEncode voordat die in de pagina komt:
var booking = getRecord('par_booking', getInput('id'));
setBody('<h1>Receipt ' + htmlEncode(booking.par_name) + '</h1>'
+ '<p>' + htmlEncode(booking.par_guest) + '</p>');
Een gast die <b>, of erger <script>, in zijn eigen naam heeft getypt, verschijnt dan als die
tekst, in plaats van onderdeel te worden van het ontvangstbewijs dat iemand anders opent. Een veld
dat niemand heeft ingevuld komt leeg terug, dus er staat een open plek in plaats van het woord
null. Hetzelfde geldt voor een e-mail die u verstuurt met html: true. Uw eigen opmaak, de tags
die u in het script schreef, wordt niet gecodeerd.
Antwoorden met een lijst records
setOutput draagt een record, en een pagina met records, in zijn geheel:
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
De aanroeper ontvangt { "count": 2, "invoices": [ { "id": "...", "par_name": "INV-1", ... } ] }, met
je veldnamen precies zoals ze op de tabel staan — en dat schrijf je in het antwoordschema, zodat het
OpenAPI-document het beschrijft.
Je kunt geen eigen lijst opbouwen. BS Lang kan niets aan een lijst toevoegen, dus de rijen waarmee
je antwoordt, zijn de rijen die een query je gaf. Gebruik select om de kolommen te kiezen in plaats
van ze te proberen samen te stellen: een kolom die je niet hebt geselecteerd, reist niet mee — zo
houd je een interne notitie uit een antwoord dat je publiceert.
Een aangemelde klant antwoorden
Alles hierboven gaat ervan uit dat de aanroeper een systeem is — één app-registratie die doet wat haar beveiligingsrol toestaat. Een portal is anders: parsware.com, of een andere site die je met Portal Apps bouwt, roept je aan namens een persoon die zich daar heeft aangemeld, en het enige wat je bewerking goed moet doen, is antwoorden over zijn bedrijf en dat van niemand anders.
Er is precies één manier om te vragen wie dat is:
| Functie | Antwoordt |
|---|---|
portalCustomerId() | de klant (een Account-record) waartoe de aangemelde persoon behoort |
portalUserId() | de portalgebruiker zelf |
portalContactId() | het Contact-record dat hij is, of leeg als hij er niet aan is gekoppeld |
portalEmail() | het adres waarmee hij zich heeft aangemeld |
portalRole() | "Owner", "Billing" of "Member" — wat hij namens zijn bedrijf mag doen |
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);
Neem de klant nooit als invoer aan. Het is geen vergissing dat er in dat script geen
getInput("customerId") staat — een klant-id die de aanroeper kan sturen, is een klant-id die de
aanroeper kan wijzigen, en één bewerkte aanvraag leest de facturen van iemand anders. De waarde komt uit
een ondertekend token, dat niemand tussen de bezoeker en jou kan herschrijven.
Daar volgen twee dingen uit, en beide zijn bewust:
-
Een aanroep zonder aangemelde persoon wordt geweigerd, niet met niets beantwoord. Roept je script
portalCustomerId()aan en bevatte de aanvraag geen portalaanmelding, dan krijgt de aanroeper een 401 — in plaats van dat je script een lege waarde krijgt en daar misschien op filtert. -
Met
portalRole()houd je iets voor de mensen die het mogen zien. Een factuurtotaal of een betaalmethode is voor een Owner of de persoon die betaalt, niet voor iedereen met een login:if (portalRole() == "Member") {throwValidationError("Ask an Owner or a Billing contact for this.");} else {setOutput("total", rows[0].par_total);}De drie rollen vormen een ladder — een Owner mag alles wat Billing mag, en Billing alles wat een Member mag — dus testen op de ene rol die iets niet mag, is meestal korter dan de rollen opsommen die het wel mogen.
-
Wat de bezoeker mag, is beperkter dan wat de site mag. De toegestane bewerkingen zijn de overlap tussen de machtigingen van de portalapplicatie en die van de aangemelde persoon. Een site met ruime machtigingen maakt zijn bezoekers niet machtig.
Weigeren stopt je script
throwValidationError beëindigt het script op de regel waar het wordt aangeroepen. Niets erna
wordt uitgevoerd — niet de rest van het blok, niet de regels eronder, en ook geen catch die je
eromheen hebt gezet. De aanroeper krijgt een 400 met jouw zin, en niets wat het script daarna zou
hebben geschreven, wordt geschreven.
Controleer dus eerst, en schrijf pas als de controles achter je liggen:
if (isEmpty(getInput("reference"))) {
throwValidationError("A reference is required.");
}
createRecord('par_invoice', { par_name: getInput("reference") });
setStatus(201);
Het enige wat een weigering niet ongedaan kan maken, is een schrijfactie die ervoor kwam: een script dat een record maakt en daarna weigert, heeft het record gemaakt. Zet de weigeringen boven de schrijfacties.
De tijd vastleggen
now() geeft het huidige moment en today() de huidige dag, allebei als tekst die je direct in een
veld van het type Datum/tijd kunt schrijven:
createRecord('par_lead', { par_name: getInput("name"), par_received_at: now() });
Gebruik deze in plaats van een tijd die de aanroeper je stuurde. "Wanneer kwam dit binnen" is een feit over jouw platform; een tijdstempel in de body van de aanvraag is een bewering van de aanroeper, en dat kan elke datum zijn die hij wil. Beide antwoorden in UTC — de eigen kalender van de lezer wordt toegepast bij het weergeven, niet bij het opslaan.
Controleer of het werkte
- De nieuwe rij toont het geserveerde pad —
/api/par/v1/invoicesof wat je ook hebt gebouwd. - Kies OpenAPI in de werkbalk. Het gegenereerde document wordt naar je klembord gekopieerd: plak het voor de integrator die vroeg hoe je endpoint eruitziet. Het wordt afgeleid uit je definities, dus het kan niets anders beschrijven dan wat de server werkelijk doet.
- Laat het aanroepende systeem het proberen, met een token van zijn app-registratie.
Versiebeheer: het deel dat de moeite waard is om te begrijpen
De identiteit van een API is zijn naam en zijn versie samen. par_approve_invoice v1 en v2 zijn
één contract op twee leeftijden, en ze antwoorden naast elkaar.
Moet je dus de vorm van een aanvraag of antwoord veranderen op een manier die een aanroeper zou merken:
- Maak een nieuwe aangepaste API met dezelfde weergavenaam en versie 2. Het is geen duplicaat — het scherm accepteert hem.
- Informeer je integratiepartners en geef ze de tijd.
- Roept niemand v1 meer aan, vink daar dan Antwoordt uit.
Die laatste stap is waar mensen de fout in gaan. Vink Antwoordt uit; verwijder niet. Een versie die met pensioen is, antwoordt niet meer, maar bestaat nog, reist nog mee in je oplossing en documenteert nog wat v1 vroeger beloofde. Verwijderen haalt de route zonder enige aankondiging weg — alles wat hem nog aanroept, krijgt een kale 404, die niet te onderscheiden is van een kapotte service.
Wat een aangepaste API niet doet
Hij verruimt de toegang nooit. Een tabelbewerking gaat door precies dezelfde deur als de eigen schermen van het platform: de machtiging van de aanroeper op die tabel, en het gegevensbeleid erachter, bepalen wat er terugkomt. Een zorgvuldig samengesteld contract geeft een extern systeem een stabiele kijk op je gegevens, nooit een ruimere.
Krijgt het aanroepende systeem minder dan je verwacht, dan ligt het antwoord bijna altijd bij de beveiligingsrol achter zijn app-registratie — niet bij de API.
Problemen oplossen
| Wat je ziet | Wat het betekent |
|---|---|
| "...is already answered by..." bij opslaan | Een andere API in deze omgeving claimt die methode en dat pad al voor die versie. Twee contracten kunnen niet één adres delen. |
| "...already has a version 1..." | Je maakt een contract opnieuw in plaats van er een nieuwe versie van te maken. Verhoog de versie. |
"...must declare an {id} segment" | Lezen, Bijwerken en Verwijderen werken op één record, en de route is de enige plek waar de id van dat record vandaan kan komen. |
| "A GET request carries no body" | Verwijder het aanvraagschema, of wijzig de methode. |
| 403 bij een aanroeper | Zijn app-registratie heeft de scope die je vereiste niet. |
| "called both setOutput and setBody" | Je script bouwt zijn antwoord twee keer op. Kies er een: setOutput stelt een JSON-object samen, setBody schrijft één body met tekst. |
| "set a body and a 204 status" | Een 204 vertelt de aanroeper dat er niets te lezen is, dus hij ziet nooit wat je schreef. Gebruik 200, of laat de body weg. |
| "An HTTP status is between 100 and 599" | setStatus kreeg iets wat geen status is. |
| Een pagina komt aan tussen aanhalingstekens | Het script gebruikte setOutput in plaats van setBody. setOutput antwoordt altijd JSON, wat het contract ook vastlegt. |
| 404 bij een aanroeper | Geen ingeschakelde API antwoordt op die methode en dat pad voor die versie. Controleer Antwoordt, en controleer het versiesegment. |
| 401 met "needs a signed-in portal user" | Je script roept portalCustomerId() aan en de aanvraag bevatte geen portalaanmelding. De aanroeper is een systeem in plaats van een portal, of de sessie van de bezoeker is verlopen. |
| 401 met "not valid" | Er werd een portalaanmelding aangeboden die niet wordt geaccepteerd — verlopen, of uitgegeven voor een andere portal. |
| 403 met "does not belong to one yet" | De persoon is aangemeld maar nog niet bij een bedrijf ondergebracht. Laat hem dat zien, in plaats van een lege lijst. |
| Er is een record gemaakt, ook al heb je de aanvraag geweigerd | De schrijfactie liep vóór de weigering. throwValidationError stopt het script waar het wordt aangeroepen, dus zet de controle boven de schrijfactie. |