Catalogue des statements

Chaque élément du tableau statements est un statement. Ce document catalogue les champs, le comportement et le résultat des 25 types de statement. Chaque emplacement de valeur suit les règles des Expressions de valeur (référence, littéral, JsonLogic, mappage de locale), avec deux exceptions : le pattern de Regex et le key de Cache (voir Regex et Cache).

Résumé des statements

CatégorietypeRésumé en une ligne
Écritures de ressourceResourceCreateCréer un Content/Media (publication facultative)
ResourceUpdateRemplacement intégral des champs de Content/Media (tout champ/locale non fourni est supprimé)
ResourcePatchFusion partielle des champs de Content/Media (uniquement les champs/locales spécifiés ; un null littéral supprime)
ResourceDeleteSupprimer (Draft/Archived uniquement ; si Published, dépublier d'abord)
ResourcePublish / ResourceUnpublishPublier / dépublier
ResourceArchive / ResourceUnarchiveArchiver / désarchiver
Lectures de ressourceResourceReadLecture d'un seul élément par id
ResourceFindPremier élément correspondant via filtre (null si aucun)
ResourceForEachParcourt en interne les ressources correspondant au filtre et exécute onEach pour chaque élément
ResourceCountCompte uniquement le nombre d'éléments correspondant au filtre (sans les lire)
ExterneHttpAppel HTTP externe ({ status, body })
EmailSendEnvoie un courriel via un EmailAccount enregistré
VariablesSetVarDéclarer/mettre à jour une variable à portée script
CacheCacheLire, écrire ou supprimer dans un cache à courte durée de vie propre à ce Script
Parsing de valeurParseJsonParser un texte JSON en valeur (objet, tableau, scalaire) et la lier
Signature et texteSignatureVérifier que le code de signature reçu est identique à celui calculé avec la clé secrète (Boolean)
HashCalculer un condensé sans clé (chaîne)
RegexAppliquer une expression régulière. Correspondance (Boolean) ou groupes de capture (tableau)
Flux de contrôleIfBranchement conditionnel
LoopItération (foreach / while / counted)
ParallelExécution simultanée de branches
ReturnRenvoyer un résultat et terminer prématurément
TryGestion des exceptions (catch/finally)

Tout statement Content qui ne désigne pas sa cible par id doit indiquer le Content Type qu'il traite. Pour ResourceFind, ResourceForEach et ResourceCount, contentType est obligatoire dès lors que resource vaut "Content". Il n'existe pas de requête Content qui traverse tout un Space. ResourceCreate indique lui aussi le Content Type à créer. Un Media ne porte pas de périmètre, puisqu'il n'en existe qu'un seul jeu pour tout le Space ; et les statements qui désignent leur cible par id (ResourceRead, ResourceUpdate, ResourcePatch, ResourceDelete ainsi que les statements de publication et d'archivage) n'ont pas besoin de périmètre, puisqu'ils ont target.

Les appels cycliques sont limités à 3. En activant propagateEvents sur les statements d'écriture de ressources ci-dessus (ResourceCreate, ResourceUpdate, ResourcePublish, etc.), dont la valeur par défaut est désactivée, ces écritures déclenchent des événements de modification, et ces événements peuvent exécuter à nouveau un Script via un Webhook. Une telle chaîne (Script → événement → Webhook → Script → …) se poursuit au maximum 3 fois. Au-delà, la plateforme l'interrompt automatiquement pour éviter les boucles infinies.

Champs communs

{ "type": "<StatementType>", "name": "<facultatif, unique dans le script>", /* ...champs spécifiques au type... */ }
  • type : le discriminant. L'une des valeurs du tableau ci-dessus (obligatoire).
  • name : facultatif. Lorsqu'il est défini, le résultat est lié au contexte à /<name>, de sorte que les statements suivants peuvent le référencer via { /<name>/... }. On l'omet si l'on n'utilise pas le résultat.
  • Règles de nom de liaison : name est une clé posée telle quelle à la racine du contexte ; elle est donc validée à l'enregistrement. Elle ne peut contenir que des lettres ASCII (a-z, A-Z), des chiffres, _ et - (afin de pouvoir servir de clé JSON Pointer, tout autre caractère ou un nom vide est refusé), ne peut pas coïncider avec une racine réservée (payload, rawPayload, headers, vars, error, now), et doit être unique au sein d'un même Script. En cas de violation du format, d'usage d'un mot réservé ou de doublon, l'enregistrement est refusé.

Forme des références d'entité

Les références d'entité comme contentType et target sont unifiées en une seule forme : { "sys": { "id": <expression de valeur> } }. Seul sys.id est nécessaire ; le type cible est déduit de resource (sys.type et sys.targetType sont omis).

  • contentType.sys.id est généralement un littéral (par ex. "ct_post").
  • target.sys.id est généralement une expression de valeur { /ptr } (résolue à l'exécution ; par ex. { /payload/sys/id }).

resource

Les statements de la famille ressource indiquent le type de cible avec resource: "Content" | "ContentType" | "Media" | "ServiceUser".

Seul ResourceCount accepte Content Type. L'indiquer dans un autre statement fait refuser l'enregistrement. Créer ou modifier le Content Type lui-même relève de CMA, et non d'un Script.

ServiceUser (le membre inscrit au produit) est en lecture seule. Seuls les trois statements de lecture (ResourceRead, ResourceFind, ResourceForEach) acceptent cette valeur ; l'indiquer dans un statement d'écriture fait refuser l'enregistrement (voir Erreurs). Les règles sont traitées dans Lecture de l'annuaire des membres.

Écritures de ressource

Chaque statement d'écriture possède propagateEvents (par défaut false). Le mettre à true fait que cette écriture déclenche un événement de modification, et des traitements en aval comme les Webhook s'exécutent. Par défaut, elle n'en déclenche aucun (une écriture système silencieuse).

ResourceCreate

Crée un Content ou un Media. Content et Media partagent le modèle fields, et les valeurs sont des mappages de locale.

ChampS'applique àDescription
resourceCommun"Content" ou "Media" (obligatoire)
contentTypeContentLe Content Type à créer ({ sys: { id } }). Obligatoire pour Content
fieldsCommunLe mappage de champs { "<field>": { "<locale>": valeur } }. Chaque champ renseigné requiert le bucket de la locale par défaut. Les clés de Content suivent la définition du Content Type ; les clés de Media sont fixes (title, description, file)
localeCommun(Commodité) Si fourni, enveloppe automatiquement chaque valeur de fields en { <locale>: valeur }
publishCommunPublie après l'écriture (exposé sur CDA/ACDA). Par défaut true
  • Media file : la valeur fields.file.{locale} est une instruction d'ingestion { "source": <expression de valeur>, "encoding": "url"|"base64" } (les deux sont obligatoires). Sur une écriture qui inclut un fichier, c'est le moteur qui procède à l'ingestion (téléchargement si url ; décodage, puis téléversement et traitement si base64). Cette ingestion ne déclare aucun temps : elle sort donc du budget de base de 30 secondes (voir Budget de temps), et elle n'est pas comptée dans le plafond d'appels externes. On peut aussi créer un Media sans fichier (fileless). Si publish:true mais qu'il n'y a pas de fichier ou que le traitement est incomplet, l'étape de publication échoue ; si publish:false, il reste Draft.
  • Résultat (liaison de name) : la ressource créée. { /<name>/sys/id }, { /<name>/fields/<field>/<locale> }.
// Content
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_post" } },
  "fields": { "title": { "en-US": "{ /payload/fields/title }" } }, "publish": true, "name": "post" }
 
// Media. file est une instruction d'ingestion
{ "type": "ResourceCreate", "resource": "Media",
  "fields": {
    "title": { "en-US": "{ /payload/fields/prompt }" },
    "file":  { "en-US": { "source": "{ /gen/body/data/0/url }", "encoding": "url" } }
  }, "name": "img" }

ResourceUpdate

Remplace intégralement les champs du Content ou Media cible (PUT). Ce que l'on passe dans fields devient le nouvel ensemble de champs, et tout champ et locale absent ici est supprimé. Pour n'en changer qu'une partie, on utilise ResourcePatch.

ChampDescription
resource"Content" ou "Media"
targetLa cible ({ sys: { id } }, obligatoire). L'id est généralement { /ptr }
fieldsL'ensemble complet des champs à écrire. Les valeurs sont des mappages de locale. Comme il s'agit d'un remplacement intégral, tout champ et locale absent ici est supprimé. Pour un Media, file est une instruction d'ingestion (voir ResourceCreate ci-dessus) ; les fichiers listés sont toujours réingérés, et les fichiers des locales non fournies sont supprimés
locale(Commodité) Enveloppe automatiquement fields
version(Facultatif) Une expression de valeur (Int). Verrouillage optimiste. Si fournie, la mise à jour ne s'effectue que si elle correspond au sys.version actuel de la cible ; en cas de non-correspondance, elle abandonne avec une erreur de conflit de version (interceptable avec Try). Si omise, aucune vérification (last-write-wins)
publishRepublie après la mise à jour. Par défaut true

Si l'on utilise Update pour ne modifier que les métadonnées d'un Media, le file étant absent, tous les fichiers sont supprimés (puisqu'il s'agit d'un remplacement intégral). Pour une modification partielle, on utilise impérativement ResourcePatch.

{ "type": "ResourceUpdate", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } },
  "fields": { "title": { "en-US": "Hello", "ko-KR": "안녕" }, "status": { "en-US": "published" } } }

ResourcePatch

Fusionne partiellement les champs du Content ou Media cible (PATCH). Écrase uniquement les champs (et les locales qu'ils contiennent) passés dans fields, et laisse intacts les champs et locales non mentionnés. La forme des valeurs, locale, version et publish sont identiques à ResourceUpdate.

ChampDescription
resource"Content" ou "Media"
targetLa cible ({ sys: { id } }, obligatoire). L'id est généralement { /ptr }
fieldsLes champs à écraser. Les valeurs sont des mappages de locale. Met à jour uniquement les champs et buckets de locale spécifiés (le reste est conservé). Si une valeur est un null littéral, ce couple (champ, locale) est supprimé. Pour un Media, file est une instruction d'ingestion (voir ResourceCreate ci-dessus)
locale(Commodité) Enveloppe automatiquement fields
version(Facultatif) Identique à ResourceUpdate (verrouillage optimiste)
publishRepublie après la mise à jour. Par défaut true
  • Supprimer une locale ou un fichier spécifique : on donne un null littéral comme valeur. Par ex. : "title": { "fr-FR": null } (supprime le titre fr-FR), "file": { "en-US": null } (supprime le fichier en-US). Une expression de valeur qui s'évalue à null au moment de l'exécution n'est pas une suppression mais une erreur (seul un null littéral supprime).
  • Donner une instruction d'ingestion au file d'un Media remplace le fichier de cette locale. Si l'on ne fournit pas de fichier, il est conservé.
// +1 sur viewCount(en-US) uniquement. title, les autres locales, etc. sont conservés tels quels
{ "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } },
  "fields": { "viewCount": { "en-US": { "$+": [ "{ /payload/fields/viewCount }", 1 ] } } } }

ResourceDelete

Supprime la cible. Seuls les statuts Draft et Archived peuvent être supprimés. Si l'élément est Published ou Changed, la suppression est refusée : il faut donc d'abord ResourceUnpublish. Pour un Media, la suppression est refusée pendant que le fichier est en cours de traitement. Il ne dépublie pas automatiquement (comme CMA/ACMA).

ChampDescription
resource"Content" ou "Media"
targetLa cible ({ sys: { id } }, obligatoire)
{ "type": "ResourceDelete", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } } }

ResourcePublish, ResourceUnpublish, ResourceArchive, ResourceUnarchive

Contrôle indépendamment l'état de publication et d'archivage de la cible. Les quatre partagent les mêmes champs. La précondition de statut de chaque opération est identique à CMA/ACMA. ResourcePublish est impossible depuis Archived et exige que le traitement du fichier soit terminé. ResourceUnpublish n'est possible que depuis Published ou Changed, ResourceArchive que depuis Draft, et ResourceUnarchive que depuis Archived.

ChampDescription
resource"Content" ou "Media"
targetLa cible ({ sys: { id } }, obligatoire)
version(Facultatif) Une expression de valeur (Int). Verrouillage optimiste. Si fournie, l'opération ne s'effectue que si elle correspond au sys.version actuel
{ "type": "ResourcePublish",   "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } } }
{ "type": "ResourceUnpublish", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } } }
{ "type": "ResourceArchive",   "resource": "Media",   "target": { "sys": { "id": "{ /m/sys/id }" } } }

Lectures de ressource

ResourceRead et ResourceFind lisent une ressource et la lient comme valeur, tandis que ResourceCount en compte seulement le nombre. Aucun des trois ne change d'état (pas de propagateEvents). Pour ResourceForEach aussi, la requête elle-même est une lecture, mais si onEach contient un statement d'écriture de ressource, cette écriture s'exécute pour chaque élément et modifie l'état.

Les quatre statements (ResourceRead, ResourceFind, ResourceForEach, ResourceCount) déterminent tous, via from (Current par défaut), quelle copie stockée lire. Current est le dernier brouillon que voit le studio de contenu (la valeur que lisent CMA/ACMA), et Published est l'instantané publié (celui que distribuent CDA/ACDA, la valeur au moment de la dernière publication). Un ServiceUser n'étant pas publié, il n'accepte que Current (voir Lecture de l'annuaire des membres).

ResourceFind, ResourceForEach et ResourceCount activent ou désactivent en outre la Recherche avancée (Advanced Search) via advanced (par défaut true). En l'absence d'indication, elle est activée. Réservée au Content, elle est ignorée pour les lectures de Media et de ServiceUser. Lorsqu'elle est activée, where peut utiliser les opérateurs regex, near et within ainsi que la recherche en texte intégral (sur un champ LongText dont la recherche en texte intégral est activée, eq trouve aussi les éléments qui contiennent la valeur, par correspondance partielle et approchée), et order peut trier par fields.*. Lorsqu'elle est désactivée, ces trois opérateurs sont rejetés, eq sur du texte est une correspondance exacte, et prefix ainsi que les opérateurs de comparaison et de liste fonctionnent indépendamment de la recherche avancée. Un élément qui vient d'être créé ou modifié met un court instant (environ 1 seconde) à apparaître dans la recherche avancée, si bien que la requête de recherche avancée qui suit immédiatement peut ne pas le renvoyer. Comme elle est activée par défaut, ce délai concerne toutes les requêtes tant que l'on ne met pas advanced à false. Pour lire sans attendre un élément qui vient d'être écrit, utilisez ResourceRead par id (le stockage principal, sans délai d'apparition) ou interrogez par le sys.id renvoyé par l'écriture.

Le createdBy: ":self" de where signifie « uniquement ce qu'a créé l'utilisateur qui appelle maintenant ». Il est en revanche inutilisable dans un Script qui autorise l'appel anonyme (anonymousCallEnabled). Dans ce cas, :self se résout non pas sur l'appelant mais sur l'auteur, ce qui ouvrirait silencieusement les ressources de l'auteur ; une telle définition est donc refusée à l'enregistrement (voir Appel anonyme).

Dans where et order, un champ de contenu s'écrit fields.<field> (le nom seul n'est pas reconnu). À fields.<field>, le moteur applique automatiquement la locale par défaut du Space, si bien qu'on n'y ajoute pas de locale directement. Les fields.status et fields.slug des exemples ci-dessous interrogent donc directement la locale par défaut. Ce n'est que pour interroger une locale précise (non par défaut) qu'on la spécifie avec fields.<field>.<locale> (par ex. fields.title.ko-KR). sys.* (comme sys.createdAt) et createdBy (:self) s'écrivent tels quels, sans fields.. Les règles détaillées figurent dans La locale dans where et order des Expressions de valeur.

Lecture de l'annuaire des membres (ServiceUser)

ResourceRead, ResourceFind et ResourceForEach acceptent "ServiceUser" dans resource et lisent alors l'annuaire des membres de ce Space (ResourceCount ne l'accepte pas ; voir ResourceCount ci-dessous). On s'en sert pour vérifier à qui appartient une commande, ou pour trouver un membre par son adresse e-mail et passer son sys.id au statement suivant. Les règles ci-dessous sont communes aux trois statements.

  • Seule la lecture est possible. ResourceCreate, ResourceUpdate, ResourcePatch, ResourceDelete ainsi que les statements de publication et d'archivage n'acceptent pas "ServiceUser", et une telle définition est refusée à l'enregistrement. Ce n'est pas quelque chose qui s'ouvre en ajoutant une permission : dans un Script, il n'existe tout simplement aucun moyen de modifier un membre, si bien que le refus n'est pas une erreur de permission mais un statement mal écrit.
  • L'enregistrement exige que l'auteur ait la permission sur l'annuaire des membres. La vérification ne passe pas par les mappages de permissions comme pour Content et Media : on regarde si le settings du SpaceRole de l'auteur contient SETTING_SERVICE_LOGIN (ou SETTING_ALL). L'annuaire des membres est en effet, sur tous les autres chemins également, une ressource que gouvernent les paramètres du Space. À défaut, l'enregistrement est refusé (voir le modèle de sécurité).
  • from n'accepte que Current. Un membre n'étant pas une ressource publiée, donner Published fait échouer l'exécution.
  • contentType et advanced sont ignorés. L'annuaire des membres ne se répartit pas par Content Type (il n'en existe qu'un seul pour tout le Space), et la recherche avancée est réservée au Content.
  • Le sys.email de where n'accepte que les opérateurs de la famille de l'égalité exacte (eq, ne, in, nin). L'adresse d'un membre étant stockée chiffrée, une comparaison d'ordre ou un prefix n'a aucun sens. Donner un autre opérateur ne renvoie pas silencieusement 0 élément : l'exécution échoue.
  • Le résultat est la ressource ServiceUser elle-même. On la référence via { /<name>/sys/id } ou { /<name>/nickname }. Sa structure est traitée dans la référence ServiceUser. Pour envoyer un courriel au membre trouvé, on ne sort pas son adresse : on passe son sys.id au toServiceUser de EmailSend (le moteur résolvant l'adresse juste avant l'envoi, l'adresse du membre n'entre pas dans l'espace de variables du Script).
// Trouve un seul membre par son adresse e-mail. null s'il n'y en a pas
{ "type": "ResourceFind", "resource": "ServiceUser",
  "where": { "sys.email": { "eq": "{ /payload/fields/email }" } }, "name": "member" }

ResourceRead

Une lecture d'un seul élément par id (get-by-id). Le résultat lie la ressource entière au nom.

ChampDescription
resource"Content", "Media" ou "ServiceUser"
targetLa cible ({ sys: { id } }). L'id est une expression de valeur
from(Facultatif) Current (par défaut, dernier brouillon) ou Published (instantané publié). Pour ServiceUser, uniquement Current
  • Résultat : c'est la ressource elle-même qui est liée. Si vous avez donné un name à ce statement, référencez-la directement via { /<name>/sys/id } et { /<name>/fields/<field>/<locale> } (avec le "name": "order" de l'exemple ci-dessous, { /order/sys/id }). Ce n'est pas une liste, donc aucun index de tableau n'intervient.
  • Si la cible n'existe pas, cela génère une erreur. On peut l'envelopper dans un Try pour la gérer.
{ "type": "ResourceRead", "resource": "Content",
  "target": { "sys": { "id": "{ /payload/fields/orderId }" } }, "name": "order" }

ResourceFind

Lit le premier élément correspondant, via un filtre. S'il n'y en a aucun, c'est null. À utiliser pour trouver un enregistrement par une clé métier unique (slug, email, sku).

ChampDescription
resource"Content", "Media" ou "ServiceUser"
contentTypeLe Content Type dans lequel chercher ({ sys: { id } }). Obligatoire pour Content. Ignoré pour Media et ServiceUser
whereLe filtre ({ "<field>": { "<op>": <valeur> } }). Les opérateurs sont ceux de la liste des opérateurs (regex/near/within nécessitent advanced). createdBy: ":self" pris en charge. Pour le sys.email d'un ServiceUser, uniquement eq, ne, in et nin (voir Lecture de l'annuaire des membres)
orderLe tri qui détermine le « premier » lorsque plusieurs correspondent (par ex. "-sys.createdAt")
from(Facultatif) Current (par défaut, dernier brouillon) ou Published (instantané publié). Pour ServiceUser, uniquement Current
advanced(Facultatif) Exécuter via la recherche avancée (Advanced Search). Content uniquement (Media et ServiceUser ignorés). Par défaut true. Voir la note Lectures de ressource ci-dessus
  • Résultat : lie la première ressource correspondante au name de ce statement. On la référence directement via { /<name>/fields/<field>/<locale> }. Comme elle vaut null en l'absence de résultat, on branche selon l'existence avec { "==": [ "{ /<name> }", null ] } (le schéma classique du find-then-upsert).
{ "type": "ResourceFind", "resource": "Content", "contentType": { "sys": { "id": "ct_article" } },
  "where": { "fields.slug": { "eq": "{ /payload/fields/slug }" } }, "name": "found" }

ResourceForEach

Parcourt en interne les ressources correspondant au filtre et exécute onEach pour chaque élément. C'est un statement destiné à effectuer une opération sur chaque élément plutôt qu'à constituer une collection à utiliser comme valeur. On l'utilise pour les tâches répétitives : publication en masse de brouillons, modification en masse de Content correspondant à une condition, envoi ou synchronisation de chaque élément vers l'extérieur. Pour ne lire qu'un seul élément, on utilise ResourceRead (par id) ou ResourceFind (par filtre).

ChampDescription
resource"Content", "Media" ou "ServiceUser" (obligatoire)
contentTypeLe Content Type du périmètre de parcours ({ sys: { id } }). Obligatoire pour Content. Ignoré pour Media et ServiceUser
whereLe filtre ({ "<field>": { "<op>": <valeur> } }). Sa sémantique est identique au where de ResourceFind (y compris la contrainte sur le sys.email d'un ServiceUser). Les opérateurs sont ceux de la liste des opérateurs (regex/near/within nécessitent advanced). createdBy: ":self" pris en charge
orderLe tri (par ex. "sys.createdAt,sys.id"). En son absence, l'ordre par défaut de la plateforme
fromCurrent (par défaut, dernier brouillon) ou Published (instantané publié). Pour ServiceUser, uniquement Current
advancedParcours via la recherche avancée (Advanced Search). Content uniquement (Media et ServiceUser ignorés). Par défaut true. Voir la note Lectures de ressource ci-dessus
limit(Facultatif, ≥1) Le plafond du nombre total d'éléments traités (ce n'est pas une taille de page). En son absence, le parcours va jusqu'au plafond de la plateforme (10 000 éléments)
name(Facultatif) Le nom auquel lier l'élément courant. Il est relié à nouveau à chaque itération et se référence via { /<name> } à l'intérieur de onEach (même durée de vie que le name d'un Loop ; après la fin du parcours, le dernier élément reste lié). On l'omet si l'on ne référence pas l'élément
onEachLe tableau de statements enfants à exécuter pour chaque élément (obligatoire)
  • Il ne lie pas de collection (foreach, pas map). Il n'y a ni { items, next } ni cursor. On ne récupère pas le résultat du parcours comme valeur : onEach s'exécute pour chaque élément. Si l'on a besoin d'une liste, on l'accumule soi-même avec SetVar. Si seul le décompte est nécessaire, on utilise ResourceCount.
  • Même sans limit, ce n'est pas un parcours infini. En son absence, il parcourt jusqu'au plafond de la plateforme (10 000 éléments), et s'il atteint ce plafond alors qu'il reste des correspondances, c'est un échec (afin de ne pas signaler un succès en laissant des éléments non traités). À l'inverse, atteindre le limit déclaré est un arrêt voulu, donc une terminaison normale. Un limit supérieur au plafond est refusé à l'enregistrement.
  • Il n'y a pas de cursor. Aller au bout est un succès ; une interruption en cours de route (dépassement du temps d'horloge ou du quota, échec non géré dans onEach) est un échec, et l'erreur désigne sur quel élément et pourquoi l'échec s'est produit. La reprise s'exprime par l'auteur avec ses propres données (en laissant where sur « non traité » et en marquant l'achèvement à la fin de onEach, une réexécution reprend à partir de ce qui reste).
  • Il compte dans le budget de temps comme une multiplication. Le temps que ce statement déclare est le temps déclaré par onEach multiplié par le nombre d'éléments traités (limit, ou 10 000 à défaut), voir Budget de temps. Comme c'est un statement composite qui possède des enfants, il ne compte pas lui-même dans le budget de leaf d'appels externes ; ce sont les statements d'appel externe à l'intérieur de onEach qui sont comptés dans le budget.
  • onEach peut contenir, comme tout autre statement, des appels externes (Http, EmailSend) ou l'ingestion de fichier Media (comme le body d'un Loop). La raison d'être de ce statement est de traiter une fois par élément le résultat d'une requête sur les ressources.
// Trouve tous les articles à l'état draft et publie chaque élément
{ "type": "ResourceForEach", "resource": "Content", "contentType": { "sys": { "id": "ct_post" } },
  "where": { "fields.status": { "eq": "draft" } }, "order": "sys.createdAt,sys.id",
  "from": "Current", "advanced": false, "name": "post",
  "onEach": [
    { "type": "ResourcePublish", "resource": "Content", "target": { "sys": { "id": "{ /post/sys/id }" } } }
  ] }

ResourceCount

Compte uniquement le nombre d'éléments correspondant au filtre. Comme il ne va pas lire les éléments, on l'utilise lorsqu'on a besoin d'un décompte plutôt que d'une liste. C'est le statement qui convient pour vérifier le stock restant, déterminer si une même valeur existe déjà, ou contrôler si un plafond est dépassé.

ChampDescription
resource"Content" ou "ContentType" (obligatoire). Media et ServiceUser ne peuvent pas être comptés ; les indiquer ainsi fait refuser l'enregistrement
contentTypeLe Content Type du périmètre à compter ({ sys: { id } }). Obligatoire pour Content. Ignoré lorsqu'on compte des Content Type (il n'en existe qu'un seul jeu pour tout le Space)
whereLe filtre. Sa sémantique est identique au where de ResourceFind. Tous les éléments correspondants sont comptés
from(Facultatif) Current (par défaut, dernier brouillon) ou Published (instantané publié)
advanced(Facultatif) Exécuter via la recherche avancée (Advanced Search). Content uniquement (ignoré lorsqu'on compte des Content Type). Par défaut true. Voir la note Lectures de ressource ci-dessus
name(Facultatif) Le nom auquel lier le décompte
  • Résultat : lie le nombre d'éléments correspondants au name de ce statement. On le référence via { /<name> } pour les comparaisons et les branchements.
  • Il ne renvoie pas les éléments. Si vous avez besoin des éléments, utilisez ResourceFind (premier élément correspondant) ou ResourceForEach (exécution pour chaque élément).
  • Ne comptez pas en parcourant avec ResourceForEach pour obtenir un décompte. Un parcours réserve le budget de temps multiplié par le nombre d'éléments (voir Budget de temps), et il échoue s'il atteint le plafond de la plateforme alors qu'il reste des correspondances. Si seul le décompte est nécessaire, ce statement le règle en une seule fois.
  • order et limit n'existent pas : compter ne demande aucun ordre, et tout ce qui correspond est compté.
// Compte le nombre de commentaires attachés à cet article
{ "type": "ResourceCount", "resource": "Content", "contentType": { "sys": { "id": "ct_comment" } },
  "where": { "fields.postId": { "eq": "{ /payload/sys/id }" } }, "name": "commentCount" }

Externe

Http

Appelle un endpoint HTTP externe. S'agissant d'un appel externe, il est compté dans le plafond d'appels externes propre au plan et compte, dans le budget de temps, pour timeoutMs (30 secondes à défaut) × (1 + retry).

ChampDescription
method"GET", "POST", "PUT", "PATCH", "DELETE"
urlL'URL cible (une expression de valeur ; { /ptr } peut y être inséré)
headers[{ "key", "value", "secret"? }]. value est une expression de valeur. Un en-tête secret:true est traité comme réservé à CMA (administrateur) : il n'est pas exposé aux utilisateurs finaux et n'est déchiffré que juste avant l'envoi de la requête. Si vous placez Content-Type ici, body est sérialisé dans ce format (ci-dessous)
bodyLe corps de la requête (une expression de valeur ou du JSON). Le format sous lequel il est envoyé est fixé par l'en-tête Content-Type
timeoutMsLe délai d'expiration de cet appel (ms)
retryLe nombre de nouvelles tentatives lorsque le statut de la réponse est supérieur ou égal à 400. Par défaut 0 ; le plafond est de 2
ignoreStatusCodeIndique s'il faut traiter cet appel comme un échec lorsque le statut final (après les nouvelles tentatives) est supérieur ou égal à 400. Lorsque false (par défaut), il est traité comme un échec et devient une cible de Try/catch. Lorsque true, il n'est pas traité comme un échec et { status, body } est lié tel quel (l'appelant branche lui-même sur status)
responseTypeSous quelle forme recevoir le corps de la réponse. "Json" (par défaut) le parse en objet ou tableau, "Text" le reçoit comme chaîne
  • Résultat : { status, body }. Si vous avez donné un name à ce statement, { /<name>/status }, { /<name>/body/... }. La forme de body est fixée par responseType.
  • responseType ne s'applique qu'à une réponse en succès. Le corps d'une réponse dont le statut est supérieur ou égal à 400 est lié à des fins de diagnostic quelle que soit la valeur déclarée (la valeur parsée si c'est du JSON, une chaîne sinon).
  • Si "Json" est déclaré et que le corps n'est pas du JSON, cet appel échoue (cible de Try/catch). Pour les API qui ne renvoient pas de JSON, recevez le corps en "Text", puis parsez-le avec ParseJson si vous devez le traiter comme une valeur.
  • "Text" est décodé avec le charset du Content-Type de la réponse, et considéré comme UTF-8 en l'absence de charset. Si le corps est vide, body vaut null dans les deux cas.
  • Limite de taille de réponse : le corps de la réponse est de 10MiB au maximum. S'il dépasse cette taille, cet appel échoue avec une exception et peut être géré comme n'importe quel autre échec d'exécution avec Try/catch (il s'agit d'un échec basé sur la taille, il n'est donc pas ignoré par ignoreStatusCode).
{ "type": "Http", "method": "POST", "url": "https://api.llm.com/v1/gen",
  "headers": [ { "key": "Authorization", "value": "Bearer sk-...", "secret": true } ],
  "body": { "prompt": "{ /payload/fields/prompt }" }, "timeoutMs": 15000, "retry": 1,
  "responseType": "Json", "name": "resp" }

Le format sous lequel body est envoyé

Le Content-Type placé dans headers fixe le format de sérialisation de body. La comparaison ignore la casse ainsi que les paramètres tels que ;charset=… et ne considère que la partie initiale. En l'absence de cet en-tête, ou lorsque sa valeur est vide, l'envoi se fait en application/json. Cet en-tête n'est ajouté que lorsqu'un body est présent : sans body, l'en-tête inscrit part tel quel. Lorsque la même clé est présente plusieurs fois, seule la première valeur est retenue et les occurrences sont fusionnées en une seule.

Un body que le format déclaré ne peut pas porter est corrigé vers un format capable de le porter avant l'envoi. Il n'arrive jamais que l'en-tête annonce autre chose que le body réellement envoyé.

Voici les combinaisons qui partent dans le format déclaré.

Content-Type déclaréForme de bodybody envoyé
application/jsonN'importe laquelleJSON
application/x-www-form-urlencodedObjet ou tableauorder[id]=A-2481&order[amount]=34000
text/plainScalaireLa valeur telle quelle
Autre (text/xml, etc.)N'importe laquelleJSON

Voici les combinaisons corrigées parce que le format déclaré ne peut pas les porter.

Content-Type déclaréForme de bodyContent-Type réellement envoyébody envoyé
application/x-www-form-urlencodedScalairetext/plain;charset=UTF-8La valeur telle quelle
text/plainObjet ou tableauapplication/jsonJSON

Ces deux lignes indiquent comment la requête part lorsque la paire ne concorde pas, et non comment obtenir le format visé. Lorsque body est assemblé à partir d'une expression de valeur, il peut devenir un scalaire selon le payload au moment de l'exécution, et cette correction se produit alors sans erreur. Si le destinataire conteste le format, corrigez selon votre intention soit la forme de body, soit le Content-Type.

form-urlencoded développe les objets en clés à crochets et les tableaux en index.

bodyClés et valeurs développées
{ "order": { "id": "A-2481", "amount": 34000 } }order[id]=A-2481&order[amount]=34000
{ "tags": ["outerwear", "winter"] }tags[0]=outerwear&tags[1]=winter
{ "items": [{ "sku": "TUMBLER-500" }] }items[0][sku]=TUMBLER-500
{ "memo": null }memo=

Les clés et les valeurs partent en UTF-8 avec un encodage-pourcent. Le tableau ci-dessus est présenté sous forme décodée afin de montrer la structure des clés. Même si une valeur contient & ou +, elle n'est pas prise pour un séparateur de paires ni pour une espace : elle est transmise telle quelle.

Cette notation, qui développe l'imbrication en clés à crochets, est une convention largement répandue et non une spécification du format lui-même. Vérifiez si le destinataire reconstitue order[id] en objet imbriqué ; s'il ne le reconstitue pas, composez body avec des clés plates.

{ "type": "Http", "method": "POST", "url": "https://api.example.com/oauth/token",
  "headers": [ { "key": "Content-Type", "value": "application/x-www-form-urlencoded" } ],
  "body": { "grant_type": "client_credentials", "client_id": "{ /vars/clientId }" },
  "name": "token" }

EmailSend

Envoie un courriel via un EmailAccount enregistré. Les champs acceptés sont uniquement ceux qui se mappent tels quels sur SMTP/MIME. Il n'y a ni id de modèle, ni envoi programmé, ni extension propre à un fournisseur (si une telle fonctionnalité est nécessaire, on appelle directement l'API du service de courriel concerné avec Http). L'expéditeur (l'adresse d'envoi) ne se définit pas ici : il provient de l'EmailAccount désigné par account.

ChampDescription
accountLa référence de l'EmailAccount d'envoi ({ sys: { id } }, obligatoire). Généralement un id littéral. Fourni sous forme d'expression de valeur, il est résolu au moment de l'envoi et ne peut donc pas être vérifié à l'enregistrement
toL'adresse du destinataire (expression de valeur). On utilise exactement l'un de to ou toServiceUser
toServiceUserDésigne le destinataire par une référence ServiceUser ({ sys: { id } } ; son sys.id peut être une expression de valeur). Le moteur résout l'adresse juste avant l'envoi, si bien que l'adresse du membre n'entre pas dans l'espace de variables du Script
ccLe tableau des adresses en copie (expression de valeur)
bccLe tableau des adresses en copie cachée (expression de valeur)
subjectL'objet (expression de valeur, obligatoire)
bodyLe corps (expression de valeur, obligatoire). Toujours envoyé en text/html, on y écrit donc du balisage et non du texte brut (les sauts de ligne deviennent des espaces, < est interprété comme une balise). Les résultats d'expressions de valeur interpolés sont échappés en HTML
replyTo(Facultatif) L'en-tête Reply-To (expression de valeur). Peut différer de l'expéditeur (par ex. envoyer depuis un no-reply mais rediriger les réponses vers une adresse de support)
timeoutMs(Facultatif, ≥1) Le délai d'expiration de cet envoi (ms). En son absence, la valeur par défaut de la plateforme ; une valeur dépassant le plafond est refusée à l'enregistrement
  • Le total des destinataires est de 50 au maximum. On additionne to (1 personne), cc et bcc (l'enveloppe SMTP ne distingue pas cc/bcc et tous partent comme destinataires, on les compte donc dans le total). En cas de dépassement, c'est refusé à l'enregistrement et à l'exécution. Pour envoyer à beaucoup de personnes, on envoie un courriel par élément avec ResourceForEach + EmailSend.
  • Il ne lie pas de résultat. Le succès signifie seulement que « le fournisseur a accepté le courriel » : il n'y a pas de valeur à renvoyer, donc il n'accepte pas de name. Il ne réessaie pas non plus (le courriel n'est pas idempotent : réessayer après un échec ambigu produirait un double envoi ; c'est pourquoi il ne suit pas le retry de Http). Un échec est levé (throw) et se gère avec le catch d'un Try.
  • C'est un appel externe. Il est compté dans le plafond d'appels externes propre au plan et compte, dans le budget de temps, pour un seul timeoutMs (10 secondes à défaut) : ne réessayant pas, il ne multiplie pas par un nombre de tentatives comme Http. Il peut s'utiliser à l'intérieur de onEach d'un ResourceForEach (la forme standard de l'envoi en nombre).
{ "type": "EmailSend", "account": { "sys": { "id": "eml_orders" } },
  "to": "{ /order/fields/email/en-US }",
  "subject": "Commande bien reçue (n° de commande { /order/sys/id })",
  "body": "<p>Votre commande a bien été reçue. Nous vous préviendrons dès que la livraison commencera.</p>",
  "replyTo": "support@my-shop.example" }

Variables

SetVar

Déclare ou met à jour une variable mutable à portée du script. On la référence via { /vars/<var> } (JsonLogic n'a pas de déclaration de variable, c'est donc fourni sous forme de statement).

ChampDescription
varLe nom de la variable. Référencée via { /vars/<var> }
valueUne expression de valeur. Elle peut se référencer elle-même pour accumuler
{ "type": "SetVar", "var": "total", "value": 0 }
{ "type": "SetVar", "var": "total", "value": { "$+": [ "{ /vars/total }", "{ /row/qty }" ] } }   // accumulation
{ "type": "SetVar", "var": "ids",   "value": { "$merge": [ "{ /vars/ids }", [ "{ /row/sys/id }" ] ] } }  // collecte dans un tableau

Mise en cache

Cache

Lit et écrit dans un cache à courte durée de vie propre à ce Script. C'est l'emplacement où l'on garde quelques secondes une valeur qu'il serait dommage d'aller rechercher à chaque fois, comme le résultat d'un appel externe, afin de la réutiliser à l'appel suivant. N'étant pas un appel externe, il n'est pas compté dans le nombre d'appels externes par définition et ne déclare aucun temps dans le budget de temps.

ChampDescription
actionL'une des valeurs "Set" (écrire), "Get" (lire), "Delete" (supprimer) (obligatoire)
keyLa clé de cache (obligatoire). Un littéral, et non une expression de valeur (voir ci-dessous). 128 caractères au maximum ; au-delà, l'enregistrement est refusé
valueLa valeur à stocker (Set uniquement)
ttlLa durée de vie du cache (Set uniquement, en secondes). Entre 1 et 30 ; en son absence, 5
defaultValueLa valeur que Get lie lorsqu'il n'y a pas de données en cache (Get uniquement). En son absence, null
nameLe nom qui reçoit le résultat. Obligatoire pour Get (si la valeur lue n'a nulle part où aller, il n'y a aucune raison de la lire). Set lie la valeur stockée et Delete lie le fait que la suppression a eu lieu ; il est facultatif dans les deux cas
  • On n'écrit que les champs qui correspondent à l'opération. Écrire un ttl sur un Get, ou un defaultValue sur un Set, fait refuser l'enregistrement.
  • Une donnée absente et une donnée expirée ne se distinguent pas. Dans les deux cas, c'est defaultValue qui est lié. Il en va de même lorsque l'on a stocké null.
  • La portée du stockage est ce seul Script. Un autre Script du même Space ne voit pas les données de celui-ci, même s'il emploie la même clé de cache. Modifier ou supprimer ce Script fait disparaître toutes ses données.
  • key est un littéral. Laisser une clé de cache venue de la requête choisir la donnée reviendrait à laisser l'appelant décider ce qui est lu, et un Script qui garde une donnée par membre livrerait la valeur d'un membre à un autre. C'est pourquoi un { /pointer } présent dans key n'est ni remplacé par une valeur, ni employé au caractère près : l'enregistrement lui-même est refusé.
  • Il ne peut pas être placé dans une itération. Si un Cache figure dans le bloc d'un Loop ou d'un ResourceForEach, l'enregistrement est refusé. C'est que le plafond de quantité ci-dessous ne constitue plus aucune limite à l'intérieur d'une itération : une donnée étant écrite à chaque tour, le nombre de statements inscrits dans la définition et le nombre de clés de cache réellement utilisées divergent.
  • On peut en placer 5 au maximum par définition (imbriqués inclus, toutes opérations confondues). Au-delà, l'enregistrement est refusé.
  • La valeur stockée ne dépasse pas 10 240 octets (10KiB). Au-delà, ce statement échoue (status 422). Comme tout autre échec d'exécution, il se traite localement avec Try/catch.
// Réutilise le taux de change pendant 30 secondes.
{ "type": "Cache", "action": "Get", "name": "cached", "key": "rates" }
 
// Si une valeur est déjà en réserve, on la renvoie telle quelle, sans appel externe
{ "type": "If", "condition": { "!!": [ "{ /cached }" ] },
  "then": [ { "type": "Return", "value": "{ /cached }" } ] }
 
{ "type": "Http", "name": "fetched", "method": "GET", "url": "https://api.example.com/rates" }
{ "type": "Cache", "action": "Set", "key": "rates", "value": "{ /fetched/body }", "ttl": 30 }
{ "type": "Return", "value": "{ /fetched/body }" }
 
// Jette avant son expiration la valeur que l'on gardait
{ "type": "Cache", "action": "Delete", "key": "rates" }

Parsing de valeur

ParseJson

Parse un texte JSON en la valeur qu'il désigne et la lie à un nom. Utilisez-le pour un corps reçu de Http avec responseType: "Text", pour une chaîne JSON arrivée dans le payload, ou pour du JSON conservé sous forme de chaîne dans un champ. Ce n'est pas un appel externe : il n'est donc pas compté dans le plafond d'appels externes et ne déclare aucun temps dans le budget de temps.

ChampDescription
nameLe nom qui reçoit la valeur parsée (obligatoire). Il est facultatif sur les autres statements, mais obligatoire ici. Le statement ne fait rien d'autre que lier son résultat : sans nom, il n'a donc aucun effet
valueLe texte JSON à parser (expression de valeur, obligatoire). Pointez vers une valeur d'une étape précédente, comme dans { /resp/body }, ou écrivez le texte JSON tel quel en littéral (une { à l'intérieur du littéral n'est pas lue comme un modèle { pointeur })
  • Résultat : la valeur parsée elle-même. Un objet reste un objet, un tableau reste un tableau, et une valeur unique comme 42 ou "a" est parsée également. Ensuite, vous adressez l'intérieur avec { /<name>/... }.
  • Une valeur déjà parsée est liée telle quelle. Lorsque value se résout en autre chose qu'une chaîne, il n'y a pas de texte à parser, et cette valeur est liée en l'état.
  • Un { /pointer } présent dans le texte parsé n'est pas résolu à nouveau. Même si une chaîne reçue de l'extérieur contient une expression comme { /payload/... }, elle n'est pas remplacée par une valeur et reste du texte.
  • null couvre deux cas distincts. Si le texte à parser est le seul mot null, c'est normal et le résultat vaut null. En revanche, si l'emplacement pointé par value est vide et qu'il n'y a aucune valeur, il n'y a rien à parser et le statement échoue.
  • Échec : lorsque value se résout sans valeur ou uniquement sur des espaces, et lorsque le texte n'est pas du JSON. Traitez-le avec Try/catch comme tout autre échec d'exécution ; le message d'erreur emporte le texte qu'il a tenté de parser.
  • Il compte pour un statement dans le nombre de statements par définition, mais n'a aucun rapport avec le plafond d'appels externes ni avec la limite de SetVar.
// 1) Une API qui ne renvoie pas de JSON : recevoir en Text, puis parser
{ "type": "Http", "method": "GET", "url": "https://api.partner.example/v1/quote",
  "responseType": "Text", "name": "resp" },
{ "type": "ParseJson", "name": "quote", "value": "{ /resp/body }" },
 
// 2) Parser une chaîne JSON arrivée dans le payload
{ "type": "ParseJson", "name": "spec", "value": "{ /payload/fields/specJson }" }

Vérification de signature et traitement de texte

Ce sont les statements qui vérifient la signature qu'un prestataire de paiement a envoyée par webhook, et qui déballent la chaîne dans laquelle cette signature est emballée. Tous trois étant des calculs et non des appels externes, ils ne sont pas comptés dans le plafond d'appels externes et ne déclarent aucun temps dans le budget de temps ; comme ils n'ont pas d'emplacement de données, ils n'ont rien à voir avec la règle du préfixe $. Un exemple complet qui combine les trois figure dans la vérification de signature de webhook du cookbook.

Les trois statements ont tous une limite sur la longueur de la valeur résolue. Il s'agit non pas de la longueur de l'expression mais de la longueur de la valeur qu'elle désigne (les seize caractères de { /rawPayload } peuvent désigner plusieurs dizaines de KB), et un dépassement fait échouer l'exécution, ce qui se traite avec Try. Les valeurs exactes sont rassemblées dans Limites de longueur des valeurs.

Signature

Vérifie que le code de signature reçu est identique à celui produit avec secret, et lie la réponse comme valeur Boolean. C'est avec ce statement qu'on vérifie la signature qu'un prestataire de paiement (PG, MoR) envoie par webhook.

ChampDescription
nameLe nom qui reçoit le résultat de la vérification (obligatoire). { /<name> } vaut true ou false. Vérifier sans utiliser le résultat équivaut à ne pas vérifier, il n'est donc pas omissible
algorithmLe hachage avec lequel produire le code (obligatoire). SHA1, SHA256, SHA384, SHA512
secretLa clé secrète partagée avec le correspondant (expression de valeur, obligatoire)
secretEncodingDans quelle notation secret est écrit. Utf8 (par défaut, clé texte), Hex, Base64. Laisser en texte une clé délivrée en hex ou en base64 en fait une autre clé : un code plausible est produit, mais il ne correspondra jamais
valueLe message sur lequel calculer le code (expression de valeur, obligatoire). Il doit être identique au caractère près aux octets que le correspondant a signés : c'est donc généralement { /rawPayload }, ou bien { /rawPayload } précédé de l'horodatage que le fournisseur a joint dans l'en-tête
expectedLe code envoyé par l'appelant (expression de valeur, obligatoire). Par ex. { /headers/x-signature }
  • Résultat : un Boolean. On écrit ensuite { /<name> } tel quel dans la condition d'un If.
  • value s'écrit avec /rawPayload, et non avec le /payload parsé. Refaire une chaîne à partir du payload parsé normalise les espaces, la notation des nombres et les échappements, si bien qu'on ne retrouve pas les octets signés par le correspondant (voir Racines de contexte).
  • Il n'y a pas de champ pour préciser la notation de sortie. algorithm fixe la longueur en octets du code et, pour une même longueur, les chaînes hex et base64 n'ont pas de longueur commune : le moteur restitue donc les octets sans qu'on lui indique dans quelle notation le correspondant a envoyé le code. Pour la même raison, il ne distingue ni la casse de l'hex, ni base64 de base64url (avec ou sans padding).
  • La distinction entre échec et false se fait selon qui fournit la valeur.
    • Si expected est absent ou si le code ne correspond pas, le résultat vaut simplement false : ce n'est pas un échec. Signaler séparément l'absence d'en-tête et la non-correspondance du code reviendrait à apprendre à l'expéditeur laquelle des deux est en cause.
    • Si value est vide, le calcul se fait sur un message vide. Un corps vide est aussi un objet de signature.
    • Si secret est absent ou n'est pas dans la notation déclarée par secretEncoding, c'est un échec. Des trois valeurs, c'est la seule qui vienne de l'auteur lui-même. Le message d'échec n'emporte ni secret ni value.
  • La limite de value est de 65 536 caractères (sur la valeur résolue). Elle est calée sur la taille des corps de webhook que les fournisseurs envoient réellement.
  • La comparaison détermine l'égalité à temps constant (constant-time). Le nombre d'octets de tête qui correspondaient ne fuit pas par le temps de réponse.
  • secret n'est pas stocké chiffré. Contrairement au secret: true d'un en-tête Http (stocké chiffré, puis déchiffré juste avant l'envoi), il reste tel qu'il est écrit dans la définition : sa valeur est donc visible pour les rôles capables de lire ce Script. Un membre (ServiceUser) ne peut pas lire la définition d'un Script (l'écriture et la consultation sont réservées à CMA).
// Un fournisseur qui signe le corps entier
{ "type": "Signature", "name": "verified", "algorithm": "SHA256",
  "secret": "whsec_9f2c1b7ae4", "value": "{ /rawPayload }",
  "expected": "{ /headers/x-webhook-signature }" }
 
// Un fournisseur qui délivre la clé en base64
{ "type": "Signature", "name": "verified", "algorithm": "SHA256",
  "secret": "aGVsbG8td2VlZ2xvbw==", "secretEncoding": "Base64",
  "value": "{ /rawPayload }", "expected": "{ /headers/webhook-signature }" }

Hash

Produit le condensé de value et le lie comme chaîne dans la notation fixée par encoding. Il sert à reproduire les schémas de signature qui, au lieu d'un HMAC, « concatènent quelques champs et une clé secrète puis calculent un SHA256 ».

ChampDescription
nameLe nom qui reçoit le condensé (obligatoire)
algorithmMD5, SHA1, SHA256, SHA384, SHA512 (obligatoire). MD5 est là pour reproduire les anciens schémas qui l'exigent, ce n'est pas une valeur à choisir pour une signature nouvelle
valueLe message à condenser (expression de valeur, obligatoire)
encodingLa notation du résultat. Hex (par défaut), HexUpper, Base64, Base64Url
  • Il n'y a pas de champ secret. La place de la clé variant d'un schéma à l'autre (au début, à la fin, au milieu), écrire la clé directement dans value permet d'exprimer toutes ces places.
  • Résultat : une chaîne. Pour la comparer au code envoyé par le correspondant, on écrit { "==": [ "{ /<name> }", "{ /headers/... }" ] }. Contrairement à la comparaison à temps constant de Signature, cette comparaison est une égalité ordinaire.
  • Si value se résout sans valeur ou uniquement sur des espaces, c'est un échec (puisqu'il s'agit de l'expression de l'auteur lui-même).
  • La limite de value est de 128 caractères. C'est un emplacement destiné à quelques champs concaténés, donc bien plus étroit que celui de Signature. S'il faut calculer sur le corps entier d'un webhook, on utilise Signature.
// SHA256(numéro de commande + montant + merchantKey) en hex majuscule
{ "type": "Hash", "name": "expectedSign", "algorithm": "SHA256", "encoding": "HexUpper",
  "value": "{ /payload/orderId }{ /payload/amount }9f2c1b7ae4" }

Regex

Applique pattern à value et lie ce que mode demande. Les expressions de valeur n'offrent aucun moyen de découper une chaîne (il n'y a que cat, qui concatène, et in, qui vérifie l'inclusion) : on utilise donc ce statement pour déballer plusieurs valeurs emballées dans un même en-tête, comme t=…,v1=….

ChampDescription
nameLe nom qui reçoit le résultat (obligatoire). Avec Capture, on désigne les éléments via { /<name>/1 }
mode"Match" lie la correspondance comme Boolean, "Capture" lie la première correspondance comme tableau (obligatoire)
patternL'expression régulière (obligatoire). Un littéral, et non une expression de valeur (voir ci-dessous). Les drapeaux s'écrivent dans le motif, comme (?i). 128 caractères au maximum ; au-delà, l'enregistrement est refusé
valueLe texte auquel appliquer le motif (expression de valeur, obligatoire). Si la valeur résolue dépasse 10 240 caractères (10KiB), l'exécution échoue
  • Résultat : Match donne un Boolean, Capture un tableau ou null. Dans le tableau, l'index 0 est la correspondance entière et les groupes de capture commencent à 1 ; un groupe qui n'a pas participé vaut null (et non une chaîne vide, laquelle signifierait qu'il a correspondu). Si le motif n'apparaît pas, Capture ne vaut pas un tableau vide mais null.
  • Les deux modes posent la même question : « le motif apparaît-il quelque part ? » Si le texte entier doit être égal au motif, on l'ancre avec ^…$. La question est identique afin que deux statements, l'un qui vérifie avec Match et l'autre qui extrait avec Capture, ne donnent pas des réponses différentes.
  • pattern est l'un des deux seuls champs de ce moteur qui ne soient pas des expressions de valeur (l'autre est le key de Cache). Exécuter tel quel un motif venu de la requête laisserait l'appelant choisir l'expression exécutée, et le retour sur trace des expressions régulières en ferait un moyen de déni de service. C'est pourquoi un { /pointer } présent dans le motif n'est pas remplacé par une valeur : il fait littéralement partie du motif.
  • Les motifs sont compilés une seule fois pour toute la définition, au démarrage de l'exécution. Même à l'intérieur d'un Loop ou d'un ResourceForEach, ils ne sont pas recompilés à chaque itération, et un motif inutilisable échoue avant que le premier statement n'ait fait quoi que ce soit (gérable avec Try).
// Déballe "t=1492774577,v1=<hex de 64 caractères>" : { /sig/1 } = horodatage, { /sig/2 } = code
{ "type": "Regex", "name": "sig", "mode": "Capture",
  "pattern": "^t=(\\d+),v1=([0-9a-f]{64})$", "value": "{ /headers/x-provider-signature }" }
 
// Vérifie uniquement le format
{ "type": "Regex", "name": "isOrderId", "mode": "Match",
  "pattern": "^ORD-\\d{8}-\\d{4}$", "value": "{ /payload/orderId }" }

Flux de contrôle

If

Un branchement conditionnel. condition est du JsonLogic, et l'évaluation en vrai ou faux suit les règles de Valeurs vraies et fausses.

ChampDescription
conditionJsonLogic (évalué comme un booléen)
thenLe tableau de statements à exécuter si vrai
else(Facultatif) Le tableau de statements à exécuter si faux
{ "type": "If",
  "condition": { "<": [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] },
  "then": [ { "type": "Return", "value": { "ok": false, "reason": "crédit insuffisant" } } ],
  "else": [ /* ... */ ] }

Loop

Une itération. On choisit un seul mode : over (foreach), while (condition) ou for (compté). Quel que soit le mode, le moteur impose un plafond d'itérations (pour éviter les boucles infinies). Ce plafond se déclare avec maxIterations et, à défaut de déclaration, c'est celui de la plateforme qui s'applique. Le body peut aussi contenir des appels externes (Http, EmailSend) et l'ingestion de fichier Media, et les statements d'appel externe sont réellement appelés à chaque itération lors de l'exécution. La limite du nombre maximal d'appels externes par définition s'applique toujours.

Il compte dans le budget de temps comme une multiplication. Le temps que ce statement déclare est le temps déclaré par body multiplié par maxIterations (ou 10 000 à défaut), voir Budget de temps. En l'absence d'appel externe dans le body, le temps déclaré vaut 0 : le budget de base de 30 secondes constitue donc la limite effective.

ChampDescription
overforeach : une expression de valeur qui se résout en un tableau
whilecondition : JsonLogic (répète tant que c'est vrai)
forcompté : { "from", "to", "step"? }. De from à to inclus ; step vaut 1 par défaut
maxIterationsLe nombre maximal d'itérations (facultatif). À défaut de déclaration, le plafond de la plateforme de 10 000 s'applique, et une valeur supérieure est refusée à l'enregistrement
name(Facultatif) Le nom auquel lier l'élément courant (foreach) ou l'index (while·for) ({ /<name> })
bodyLe tableau de statements du corps de la boucle
// foreach
{ "type": "Loop", "over": "{ /payload/fields/items }", "name": "item", "maxIterations": 100,
  "body": [ { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_item" } },
             "fields": { "name": { "en-US": "{ /item/name }" } } } ] }
 
// while
{ "type": "Loop", "while": "{ /vars/hasMore }", "maxIterations": 1000, "body": [ /* ... */ ] }
 
// counted (1..10 step 2)
{ "type": "Loop", "for": { "from": 1, "to": 10, "step": 2 }, "name": "i", "maxIterations": 100, "body": [ /* ... */ ] }

Parallel

Exécute les branches simultanément et poursuit après leur jonction. Les références entre branches ne sont pas autorisées (en cas de dépendance, on les place séquentiellement).

ChampDescription
branchesStatement[][]. Chaque élément est une branche (un tableau de statements)
{ "type": "Parallel", "branches": [
  [ { "type": "Http", "method": "GET", "url": "https://api.a.com/x", "name": "a" } ],
  [ { "type": "Http", "method": "GET", "url": "https://api.b.com/y", "name": "b" } ]
] }

Return

C'est le return de la programmation classique. Il renvoie le résultat du Script à l'appelant et se termine normalement à ce point.

ChampDescription
value(Facultatif) L'expression de valeur à renvoyer
isErrorPar défaut false. Si true, value ressort comme le error de la réponse (sinon comme return)
statusCodeLe code de statut de la réponse. Par défaut 200
  • Si Return n'est jamais atteint, il n'y a pas de valeur de retour. Pour renvoyer un résultat, on précise explicitement value.
  • Comme il s'agit d'une terminaison normale, et non d'une exception ou d'un throw, ce n'est pas une cible de catch (même à l'intérieur d'un Try, il termine l'intégralité du Script, mais finally s'exécute tout de même).
  • Un guard s'exprime aussi avec ce statement. Si l'on place un Return dans le then d'un If, il renvoie une valeur lorsque la condition est violée et les statements suivants ne s'exécutent pas. C'est l'un des nombreux usages de Return.
{ "type": "Return", "value": { "orderId": "{ /order/sys/id }", "status": "paid" }, "statusCode": 201 }
{ "type": "Return", "value": { "reason": "échec du paiement" }, "isError": true, "statusCode": 402 }

Try

Gestion des exceptions.

ChampDescription
bodyLe tableau de statements à tenter
catch(Facultatif) S'exécute en cas d'échec de body. Expose { message } sur /error (le statement où l'échec s'est produit n'y figure pas)
finally(Facultatif) S'exécute toujours, que ce soit en cas de succès ou d'échec
  • Si catch la gère, le Script n'est pas interrompu. Seul un échec sans catch interrompt le Script (y compris la tentative de compensation).
  • Ce qui compte comme un « échec », ainsi que les limites de la compensation, sont traités dans Sémantique d'exécution, contraintes et sécurité.
{ "type": "Try",
  "body":    [ { "type": "Http", "method": "POST", "url": "https://primary.api/gen", "name": "resp" },
               { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_result" } },
                 "fields": { "text": { "en-US": "{ /resp/body/text }" } } } ],
  "catch":   [ { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_result" } },
                 "fields": { "text": { "en-US": "Generation failed" }, "error": { "en-US": "{ /error/message }" } } } ],
  "finally": [ /* toujours exécuté */ ] }