Aller au contenu principal

Publier une API personnalisée

Objectif​

Donner au système d’une autre entreprise une adresse unique à appeler — POST /api/par/v1/invoices/{id}/approve — et faire en sorte que cette adresse continue de fonctionner de la même façon après que vous avez renommé une colonne.

Pourquoi cela existe​

Parsware dispose déjà d’une API pour vos tables. Elle est générée à partir des métadonnées de votre table, ce qui signifie qu’elle change de forme chaque fois que votre table change. C’est exactement ce que vous voulez à l’intérieur de la plateforme, et exactement ce que vous ne devez pas confier à quelqu’un de l’extérieur : cette personne a écrit son code pour une certaine forme, et elle ne va pas le réécrire parce que vous avez renommé par_total en par_amount.

Une API personnalisée est la promesse qu’une route garde sa forme pendant que les tables qui se trouvent derrière évoluent.

C’est un composant de votre solution, comme un formulaire ou une vue. Elle est créée en Dev, voyage dans le paquet de la solution, et répond en Test et en Production parce que vous l’y avez importée.

Avant de commencer​

  • Une solution, avec un éditeur (son préfixe fait partie de chaque route que vous publiez).
  • La table que vous voulez exposer, si cette API porte sur une table.
  • Une inscription d’application pour le système qui vous appellera, détenant l’étendue que vous allez exiger. En inscrire une est le travail d’un administrateur — Inscrire une application externe.

Étapes​

  1. Ouvrez votre solution et choisissez API personnalisées.
  2. Choisissez Nouveau.
  3. Donnez-lui un nom d’affichage. Le nom logique se remplit tout seul, préfixé par votre éditeur.
  4. Laissez la version à 1. Vous l’augmenterez plus tard, et l’ancienne version continuera de répondre le temps que vos appelants se mettent à jour.
  5. Choisissez la méthode et saisissez la route — sans barre oblique initiale. Un segment entre accolades est un paramètre de chemin : invoices/{id}/approve. L’écran affiche l’adresse complète au fil de la saisie.
  6. Choisissez l’étendue requise — celle que doit détenir l’inscription d’application du système appelant, par exemple modelapps.read. C’est une liste et non une zone de texte : une étendue que personne ne peut se voir accorder refuserait tous les appels, et vous ne le découvririez qu’au moment où un partenaire le découvrirait.
  7. Choisissez ce avec quoi elle répond — JSON, texte ou HTML. Voir la section ci-dessous ; ce choix détermine aussi ce que fait le script de départ, faites-le donc avant d’écrire du code.
  8. Le schéma de requête et le schéma de réponse sont facultatifs. Laissés vides, le contrat est lu dans votre script — chaque getInput et chaque setOutput — et publié dans le document OpenAPI. Écrivez un JSON Schema seulement pour être plus strict (types, champs obligatoires, limites) ; un schéma de requête écrit est vérifié à chaque appel, donc une requête mal formée est refusée dès l’entrée.
  9. Laissez Répond coché, puis Enregistrez.
  10. Sélectionnez la ligne et appuyez sur Modifier le code. La liste est remplacée par l’éditeur — c’est là que vit le comportement de l’API, écrit en BS Lang. Appuyez sur l’œil pour voir toutes les fonctions que vous pouvez appeler. Enregistrez, puis Fermez.

Ce avec quoi elle répond​

Une API personnalisée répond l’une de trois choses, et vous choisissez laquelle dans le contrat :

Répond avecLe script écritIdéal pour
JSONsetOutput("name", value), une fois par champUn autre système qui lit vos données
TextesetBody("...")Une ligne — une référence, un jeton, une ligne CSV
HTMLsetBody("<p>...</p>")Un e-mail de confirmation, une page qu’ouvre un navigateur
setOutput("reference", "BOOKING-42"); // JSON: { "reference": "BOOKING-42" }
setBody("BOOKING-42"); // text or HTML: exactly that, and nothing else

Utilisez l’un ou l’autre, jamais les deux. Un script qui appelle setOutput et setBody est refusé à l’exécution, au lieu que l’un des deux l’emporte discrètement — une réponse que vous ne pouvez pas expliquer est pire qu’une erreur que vous pouvez lire.

Deux autres fonctions les accompagnent :

  • setStatus(404) — le statut avec lequel vous voulez répondre. À ne pas confondre avec throwValidationError, qui signifie l’appelant s’est trompé (un 400) ; setStatus, c’est votre contrat qui décide. Un 204 ne doit porter aucun corps.
  • setContentType("html") — change le type pour un seul appel. La plupart des API n’en ont jamais besoin, car le contrat déclare déjà ce avec quoi elles répondent.

Pourquoi c’est le contrat qui le déclare, et pas seulement votre code : le document OpenAPI est généré avant que quiconque ne vous appelle, il ne peut donc décrire que ce que dit le contrat. Déclarez HTML et le document dira HTML ; laissez JSON et un intégrateur écrira un analyseur JSON autour de votre page.

Mettre une valeur enregistrée dans une page​

Une page est faite de texte, et une partie de ce texte provient de vos enregistrements. Passez chaque valeur enregistrée par htmlEncode avant de la mettre dans la page :

var booking = getRecord('par_booking', getInput('id'));
setBody('<h1>Receipt ' + htmlEncode(booking.par_name) + '</h1>'
+ '<p>' + htmlEncode(booking.par_guest) + '</p>');

Un client qui a tapé <b>, ou pire <script>, dans son propre nom apparaît alors comme ce texte, au lieu de faire partie du reçu qu'une autre personne ouvre. Un champ que personne n'a rempli revient vide : il laisse un blanc plutôt que le mot null. Il en va de même pour un e-mail envoyé avec html: true. Votre propre balisage, les balises écrites dans le script, n'est pas encodé.

Répondre avec une liste d’enregistrements​

setOutput transporte un enregistrement, et une page d’enregistrements, en entier :

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

L’appelant reçoit { "count": 2, "invoices": [ { "id": "...", "par_name": "INV-1", ... } ] }, avec vos noms de champ exactement tels qu’ils figurent dans la table — c’est ce que vous écrivez dans le schéma de réponse pour que le document OpenAPI le décrive.

Vous ne pouvez pas construire votre propre liste. BS Lang n’offre aucun moyen d’y ajouter des éléments : les lignes avec lesquelles vous répondez sont celles qu’une requête vous a remises. Utilisez select pour choisir les colonnes plutôt que d’essayer de les assembler : une colonne non sélectionnée est une colonne qui ne voyage pas, et c’est ainsi que vous tenez une note interne à l’écart d’une réponse que vous publiez.

Répondre à un client connecté​

Tout ce qui précède suppose que l’appelant est un système — une inscription d’application, qui fait ce que son rôle de sécurité autorise. Un portail est différent : parsware.com, ou tout site que vous construisez avec les applications de portail, vous appelle au nom d’une personne qui s’y est connectée, et la seule chose que votre opération doit absolument réussir est de répondre au sujet de son entreprise et de personne d’autre.

Il n’existe qu’une seule façon de demander de qui il s’agit :

FonctionRenvoie
portalCustomerId()le client (un enregistrement Account) auquel appartient la personne connectée
portalUserId()l’utilisateur du portail lui-même
portalContactId()l’enregistrement Contact qui lui correspond, ou vide s’il n’est lié à aucun
portalEmail()l’adresse avec laquelle il s’est connecté
portalRole()"Owner", "Billing" ou "Member" — ce qu’il peut faire au nom de son entreprise
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);

Ne recevez jamais le client comme entrée. Ce n’est pas un oubli s’il n’y a pas de getInput("customerId") dans ce script — un identifiant client que l’appelant peut envoyer est un identifiant client que l’appelant peut modifier, et une seule requête retouchée permet de lire les factures de quelqu’un d’autre. La valeur provient d’un jeton signé, que personne entre le visiteur et vous ne peut réécrire.

Plusieurs conséquences en découlent, toutes voulues :

  • Un appel sans personne connectée est refusé, et non traité par une réponse vide. Si votre script appelle portalCustomerId() et que la requête ne portait aucune connexion de portail, l’appelant reçoit un 401 — plutôt que votre script ne reçoive une valeur vide et ne filtre éventuellement sur elle.

  • portalRole() est la façon de réserver quelque chose aux personnes qui doivent le voir. Le total d’une facture ou un moyen de paiement concerne un Owner ou la personne qui paie, pas tous ceux qui ont un identifiant :

    if (portalRole() == "Member") {
    throwValidationError("Ask an Owner or a Billing contact for this.");
    } else {
    setOutput("total", rows[0].par_total);
    }

    Les trois rôles forment une échelle — un Owner peut faire tout ce que peut Billing, et Billing tout ce que peut un Member — si bien que tester le seul rôle qui ne peut pas est généralement plus court que d’énumérer ceux qui peuvent.

  • Ce que le visiteur peut faire est plus restreint que ce que peut faire le site. Les opérations autorisées sont l’intersection entre les autorisations de l’application du portail et celles de la personne connectée. Un site aux autorisations étendues ne rend pas ses visiteurs puissants.

Refuser arrête votre script​

throwValidationError met fin au script à la ligne où il est appelé. Rien ne s’exécute ensuite — ni le reste du bloc, ni les lignes suivantes, ni un catch placé autour. L’appelant reçoit un 400 portant votre phrase, et rien de ce que le script aurait écrit ensuite n’est écrit.

Vérifiez donc d’abord, et écrivez une fois les vérifications passées :

if (isEmpty(getInput("reference"))) {
throwValidationError("A reference is required.");
}
createRecord('par_invoice', { par_name: getInput("reference") });
setStatus(201);

La seule chose qu’un refus ne peut pas annuler est une écriture qui l’a précédé : un script qui crée un enregistrement puis refuse a bel et bien créé l’enregistrement. Placez les refus avant les écritures.

Horodater​

now() renvoie l’instant présent et today() le jour en cours, tous deux sous forme de texte que vous pouvez écrire directement dans un champ Date/Heure :

createRecord('par_lead', { par_name: getInput("name"), par_received_at: now() });

Utilisez-les plutôt qu’une heure envoyée par l’appelant. « Quand est-ce arrivé » est un fait qui concerne votre plateforme ; un horodatage dans le corps de la requête est une affirmation de l’appelant, qui peut indiquer la date qu’il veut. Les deux répondent en UTC — le calendrier du lecteur est appliqué à l’affichage, pas au stockage.

Vérifier que cela a fonctionné​

  • La nouvelle ligne affiche son chemin servi — /api/par/v1/invoices ou ce que vous avez construit.
  • Choisissez OpenAPI dans la barre d’outils. Le document généré est copié dans votre presse-papiers : collez-le à l’intégrateur qui vous a demandé à quoi ressemble votre point de terminaison. Il est dérivé de vos définitions, il ne peut donc pas décrire autre chose que ce que fait réellement le serveur.
  • Demandez au système appelant de l’essayer, avec un jeton issu de son inscription d’application.

Gestion des versions : la partie à bien comprendre​

L’identité d’une API, c’est son nom et sa version ensemble. par_approve_invoice v1 et v2 sont un même contrat à deux âges, et ils répondent côte à côte.

Lorsque vous devez donc changer la forme d’une requête ou d’une réponse d’une manière qu’un appelant remarquerait :

  1. Créez une nouvelle API personnalisée avec le même nom d’affichage et la version 2. Ce n’est pas un doublon — l’écran l’acceptera.
  2. Prévenez vos partenaires d’intégration et laissez-leur du temps.
  3. Lorsque plus personne n’appelle la v1, décochez Répond sur celle-ci.

C’est cette dernière étape que l’on se trompe le plus souvent. Décochez Répond ; ne supprimez pas. Une version retirée cesse de répondre, mais existe toujours, voyage toujours dans votre solution et documente toujours ce que promettait la v1. La supprimer retire la route sans aucun préavis — tout ce qui l’appelle encore reçoit un simple 404, impossible à distinguer d’une panne de votre service.

Ce qu’une API personnalisée ne fait pas​

Elle n’élargit jamais l’accès. Une opération sur une table passe exactement par la même porte que les écrans de la plateforme : l’autorisation de l’appelant sur cette table, et la stratégie de données qui se trouve derrière, décident de ce qui est renvoyé. Un contrat soigné offre à un système externe une vue stable de vos données, jamais une vue plus large.

Si le système appelant obtient moins que prévu, la réponse est presque toujours le rôle de sécurité associé à son inscription d’application — pas l’API.

Dépannage​

Ce que vous voyezCe que cela signifie
« ...is already answered by... » à l’enregistrementUne autre API de cet environnement revendique déjà cette méthode et ce chemin pour cette version. Deux contrats ne peuvent pas partager une adresse.
« ...already has a version 1... »Vous recréez un contrat au lieu de le versionner. Augmentez la version.
« ...must declare an {id} segment »Lecture, Mise à jour et Suppression agissent sur un seul enregistrement, et la route est le seul endroit d’où peut venir l’identifiant de cet enregistrement.
« A GET request carries no body »Supprimez le schéma de requête, ou changez de méthode.
403 renvoyé à un appelantSon inscription d’application ne détient pas l’étendue que vous avez exigée.
« called both setOutput and setBody »Votre script construit sa réponse deux fois. Choisissez : setOutput compose un objet JSON, setBody écrit un corps de texte unique.
« set a body and a 204 status »Un 204 indique à l’appelant qu’il n’y a rien à lire : il ne verra donc jamais ce que vous avez écrit. Utilisez 200, ou supprimez le corps.
« An HTTP status is between 100 and 599 »setStatus a reçu quelque chose qui n’est pas un statut.
Une page arrive entourée de guillemetsLe script a utilisé setOutput au lieu de setBody. setOutput répond toujours en JSON, quoi que déclare le contrat.
404 renvoyé à un appelantAucune API activée ne répond à cette méthode et à ce chemin pour cette version. Vérifiez Répond, ainsi que le segment de version.
401 avec « needs a signed-in portal user »Votre script appelle portalCustomerId() et la requête ne portait aucune connexion de portail. Soit l’appelant est un système et non un portail, soit la session du visiteur a expiré.
401 avec « not valid »Une connexion de portail a été présentée et n’est pas acceptée — expirée, ou émise pour un autre portail.
403 avec « does not belong to one yet »La personne est connectée mais n’a pas encore été rattachée à une entreprise. Montrez-le-lui plutôt qu’une liste vide.
Un enregistrement a été créé alors que vous avez refusé la requêteL’écriture s’est exécutée avant le refus. throwValidationError arrête le script là où il est appelé : placez donc le contrôle avant l’écriture.

Voir aussi​