Cookbook (Praxisbeispiele)
Verschiedene Szenarien werden als vollständige ScriptDefinition gezeigt. Die Syntaxgrundlage finden Sie im Statement-Katalog und unter Wertausdrücke; zu Ausführung und Einschränkungen siehe Ausführungssemantik, Einschränkungen und Sicherheit. In allen Beispielen ist der fields-Wert beim Schreiben eine Locale-Map ({ "<locale>": Wert }), und die Beispiel-Locale ist einheitlich en-US. Alle Beispiele laufen inline auf dem Weg, der die aufrufende Anfrage verarbeitet, und geben das Ergebnis als Antwort-Body dieses Aufrufs zurück. Bei einem Beispiel mit einem externen Aufruf (Http, EmailSend) wird das timeoutMs dieses Statements zum Zeitbudget der Ausführung addiert (bei Http × (1 + retry)), und steht dieses Statement innerhalb einer Iteration (Loop, ResourceForEach), wird es mit der Iterationsobergrenze multipliziert (siehe Zeitbudget).
Inhaltsverzeichnis
- Grundlegendes CRUD: 1. Content erstellen und veröffentlichen · 2. Update mit berechnetem Wert · 3. Meine Bestellliste sammeln und zurückgeben · 4. Einzeln lesen, guard, dann genehmigen
- Nachschlagen und upsert: 5. slug upsert · 6. Dynamischer Feld-Schlüssel und Locale-patch
- Externe API: 7. Credit vorab abziehen (CAS), LLM-Aufruf, Rückerstattung · 8. Bild-URL zu Media · 9. base64-Bild zu Media · 10. Bedingte Verarbeitung nach Moderation · 11. try/catch fallback · 12. KI-Zusammenfassung und Tags
- Parallel: 13. Nach parallelen Aufrufen zusammenführen · 14. Registrierungsprüfung
- Schleifen und Aggregation: 15. N aus einem Array-Input erstellen · 16. Counted-loop-Seed · 17. Cascade-Löschung · 18. Schleifenakkumulation: Summe · 19. Alle passenden Elemente in einem Rutsch verarbeiten · 20. id-Batch-Sammlung
- Saga und Nebenläufigkeit: 21. Zahlungs-Saga · 22. Optimistische Sperre (CAS)
- E-Mail: 23. Benachrichtigungs-E-Mail an den Käufer jeder Bestellung · 24. Mailvorlage, die im Content Studio geändert wird · 25. Von der Administration geschriebener Newsletter
- Signaturprüfung: 26. Webhook-Signatur prüfen · 27. Signaturprüfung per Hash ohne Schlüssel
- Mitglieder nachschlagen: 28. Mitglied per E-Mail finden, Coupon und Benachrichtigungsmail
- Dateiübertragung: 29. Produktfoto an einen Bildanalysedienst senden und das Ergebnis erfassen
- Dateitransformation: 30. E-Book-PDF-Kopie mit aufgedruckter E-Mail-Adresse des Käufers
Grundlegendes CRUD
1. Content erstellen und veröffentlichen
{ "method": "Post",
"statements": [
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_post" } },
"fields": { "title": { "en-US": "{ /payload/fields/title }" }, "body": { "en-US": "{ /payload/fields/body }" } },
"publish": true, "name": "post" },
{ "type": "Return", "value": { "id": "{ /post/sys/id }" }, "statusCode": 201 } ] }2. Update mit berechnetem Wert (Aufrufzähler +1)
{ "method": "Post",
"statements": [
{ "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } },
"fields": { "viewCount": { "en-US": { "$+": [ "{ /payload/fields/viewCount }", 1 ] } } } } ] }3. Meine Bestellliste sammeln und zurückgeben
{ "method": "Get",
"statements": [
{ "type": "SetVar", "var": "orders", "value": [] },
{ "type": "ResourceForEach", "resource": "Content", "contentType": { "sys": { "id": "ct_order" } },
"where": { "createdBy": ":self" }, "order": "-sys.createdAt", "from": "Current", "advanced": false,
"limit": 20, "name": "order",
"onEach": [
{ "type": "SetVar", "var": "orders", "value": { "$merge": [ "{ /vars/orders }", [ "{ /order }" ] ] } } ] },
{ "type": "Return", "value": { "orders": "{ /vars/orders }" } } ] }Mit createdBy: ":self" wird „nur das Eigene" durchlaufen, und die Elemente werden mit SetVar gesammelt und zurückgegeben. Da ResourceForEach das Iterationsergebnis nicht als Collection bindet, sammelt man sie so selbst, um eine Liste zurückzugeben. Die Iteration wird im Zeitbudget als die von onEach deklarierte Zeit multipliziert mit der Anzahl der verarbeiteten Elemente gerechnet. Hier enthält onEach keinen externen Aufruf, die deklarierte Zeit ist also 0, sodass das Grundbudget von 30 Sekunden die tatsächliche Grenze ist; legen Sie einen externen Aufruf in onEach, geht diese Multiplikation unverändert in das Budget ein, und bei Erreichen der Obergrenze von 180 Sekunden wird dort abgebrochen (siehe Zeitbudget). Wenn es genügt, eine Liste zu lesen und zurückzugeben, ist es besser, statt einer Script-Iteration im Frontend die CDA/CMA-Listen-API direkt aufzurufen.
4. Einzeln lesen, guard, dann genehmigen
{ "method": "Post",
"statements": [
{ "type": "ResourceRead", "resource": "Content", "target": { "sys": { "id": "{ /payload/fields/orderId }" } }, "name": "order" },
{ "type": "If", "condition": { "!=": [ "{ /order/fields/status/en-US }", "pending" ] },
"then": [ { "type": "Return", "value": { "reason": "not pending" }, "isError": true, "statusCode": 409 } ] },
{ "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /order/sys/id }" } },
"fields": { "status": { "en-US": "approved" } }, "publish": true },
{ "type": "Return", "value": { "ok": true } } ] }Wenn Sie mit ResourceRead einen einzelnen Datensatz an einen Namen binden, können Sie ihn direkt über { /order/fields/... } referenzieren. Existiert er nicht, führt der Lesevorgang zu einem Fehler (Sie können ihn mit Try umschließen).
Nachschlagen und upsert
5. slug upsert (find-then-upsert)
{ "method": "Post",
"statements": [
{ "type": "ResourceFind", "resource": "Content", "contentType": { "sys": { "id": "ct_article" } },
"where": { "fields.slug": { "eq": "{ /payload/fields/slug }" } }, "name": "found" },
{ "type": "If", "condition": { "!!": "{ /found/sys/id }" },
"then": [
{ "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /found/sys/id }" } },
"fields": { "body": { "en-US": "{ /payload/fields/body }" } } },
{ "type": "Return", "value": { "id": "{ /found/sys/id }", "op": "updated" } } ],
"else": [
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_article" } },
"fields": { "slug": { "en-US": "{ /payload/fields/slug }" }, "body": { "en-US": "{ /payload/fields/body }" } }, "name": "created" },
{ "type": "Return", "value": { "id": "{ /created/sys/id }", "op": "created" }, "statusCode": 201 } ] } ] }ResourceFind bindet die erste Übereinstimmung direkt (oder null, falls keine vorhanden ist), und { "!!": "{ /found/sys/id }" } verzweigt danach, ob sie existiert.
6. Dynamischer Feld-Schlüssel und dynamischer Locale-patch
{ "method": "Patch",
"statements": [
{ "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } },
"fields": { "{ /payload/fields/fieldKey }": { "{ /payload/fields/locale }": "{ /payload/fields/value }" } } } ] }Sowohl der Feld-Schlüssel als auch der Locale-Bucket-Schlüssel sind { /ptr }-Referenzen. Verwenden Sie dies, wenn Sie eine Übersetzung in einen bestimmten Locale-Bucket einfügen möchten.
Externe API
7. Credit-guard, Vorab-Abzug (CAS), LLM-Aufruf, Rückerstattung bei Fehler (repräsentatives Beispiel)
{ "method": "Post",
"statements": [
{ "type": "ResourceFind", "resource": "Content", "contentType": { "sys": { "id": "ct_wallet" } },
"where": { "createdBy": ":self" }, "name": "wallet" },
{ "type": "If", "condition": { "<": [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] },
"then": [ { "type": "Return", "value": { "ok": false, "reason": "insufficient credit" }, "isError": true, "statusCode": 402 } ] },
{ "type": "Try",
"body": [
{ "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /wallet/sys/id }" } },
"version": "{ /wallet/sys/version }",
"fields": { "balance": { "en-US": { "$-": [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] } } },
"name": "charged" } ],
"catch": [ { "type": "Return", "value": { "ok": false, "reason": "version conflict, erneut versuchen" }, "isError": true, "statusCode": 409 } ] },
{ "type": "Try",
"body": [
{ "type": "Http", "method": "POST", "url": "https://api.llm.com/v1/gen",
"headers": [ { "key": "Authorization", "value": "Bearer { /secrets/LLM_API_KEY }" } ],
"body": { "prompt": "{ /payload/fields/prompt }" }, "timeoutMs": 15000, "name": "resp" },
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_result" } },
"fields": { "text": { "en-US": "{ /resp/body/choices/0/message/content }" } }, "name": "out" },
{ "type": "Return", "value": { "ok": true, "id": "{ /out/sys/id }", "remaining": "{ /charged/fields/balance/en-US }" } } ],
"catch": [
{ "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /wallet/sys/id }" } },
"fields": { "balance": { "en-US": { "$+": [ "{ /charged/fields/balance/en-US }", "{ /payload/fields/cost }" ] } } } },
{ "type": "Return", "value": { "ok": false, "reason": "generation failed, refunded" }, "isError": true, "statusCode": 502 } ] } ],
"secrets": [ { "key": "LLM_API_KEY", "value": "sk-...", "description": "API-Schlüssel für LLM-Aufrufe" } ] }Mit einem guard wird zunächst geprüft, ob das Guthaben ausreicht, und dann vor dem externen Aufruf abgezogen. Der Abzug setzt eine optimistische Sperre (CAS) auf die sys.version des wallet. Hat eine andere Ausführung das wallet zwischen dem Lesen des Guthabens und dem Abzug verändert, wird wegen einer Versionsabweichung abgebrochen und catch gibt 409 zurück. Da noch kein externer Aufruf erfolgt ist, werden gleichzeitige Anfragen nicht doppelt belastet. Erst nachdem der Abzug festgeschrieben ist, wird das LLM aufgerufen; schlägt dieser Aufruf fehl, addiert catch den abgezogenen Betrag (cost) wieder hinzu, um eine Rückerstattung (Kompensation) vorzunehmen, und gibt dann 502 zurück. Die Reihenfolge lautet: die Abbuchung vor dem unumkehrbaren externen Aufruf festschreiben und nur im Fehlerfall kompensieren. Der LLM-API-Schlüssel wird in secrets deklariert und im Header als { /secrets/LLM_API_KEY } gelesen. Zu den Grenzen der Kompensation siehe Keine Transaktionen und Kompensation in der Ausführungssemantik.
8. Bild (URL) als Media erstellen und an Content anhängen
{ "method": "Post",
"statements": [
{ "type": "Http", "method": "POST", "url": "https://api.img.com/gen",
"headers": [ { "key": "x-api-key", "value": "{ /secrets/IMAGE_API_KEY }" } ],
"body": { "prompt": "{ /payload/fields/prompt }" }, "name": "gen" },
{ "type": "ResourceCreate", "resource": "Media",
"fields": { "title": { "en-US": "{ /payload/fields/prompt }" },
"file": { "en-US": { "source": "{ /gen/body/data/0/url }", "encoding": "url" } } }, "name": "img" },
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_artwork" } },
"fields": { "prompt": { "en-US": "{ /payload/fields/prompt }" }, "image": { "en-US": "{ /img/sys/id }" } } } ],
"secrets": [ { "key": "IMAGE_API_KEY", "value": "...", "description": "API-Schlüssel für die Bildgenerierung" } ] }Das Media wird mit einem name erstellt, und ResourceCreate (Content) steckt { /img/sys/id } in das Referenzfeld. Media verwendet dasselbe fields-Modell wie Content. file ist die Ingestion-Anweisung { source, encoding }. Der Datei-Ingest deklariert keine Zeit und geht daher vom Grundbudget von 30 Sekunden ab; auf das Limit für externe Aufrufe pro Definition wird er ebenfalls nicht angerechnet (siehe Statische Einschränkungen).
9. base64-Bild zu Media
{ "method": "Post",
"statements": [
{ "type": "Http", "method": "POST", "url": "https://api.img.com/gen",
"headers": [ { "key": "x-api-key", "value": "{ /secrets/IMAGE_API_KEY }" } ],
"body": { "prompt": "{ /payload/fields/prompt }" }, "name": "gen" },
{ "type": "ResourceCreate", "resource": "Media",
"fields": { "file": { "en-US": { "source": "{ /gen/body/data/0/b64_json }", "encoding": "base64" } } } } ],
"secrets": [ { "key": "IMAGE_API_KEY", "value": "...", "description": "API-Schlüssel für die Bildgenerierung" } ] }10. Bedingtes publish oder Löschen nach Moderation
{ "method": "Post",
"statements": [
{ "type": "Http", "method": "POST", "url": "https://api.mod.com/check",
"headers": [ { "key": "x-api-key", "value": "{ /secrets/MODERATION_API_KEY }" } ],
"body": { "text": "{ /payload/fields/body }" }, "name": "mod" },
{ "type": "If", "condition": { "==": [ "{ /mod/body/flagged }", true ] },
"then": [ { "type": "ResourceDelete", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } } } ],
"else": [ { "type": "ResourcePublish", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } } } ] } ],
"secrets": [ { "key": "MODERATION_API_KEY", "value": "...", "description": "API-Schlüssel für die Moderation" } ] }11. try/catch: fallback bei externem Fehler
{ "method": "Post",
"statements": [
{ "type": "Try",
"body": [
{ "type": "Http", "method": "POST", "url": "https://primary.api/gen",
"headers": [ { "key": "x-api-key", "value": "{ /secrets/GEN_API_KEY }" } ],
"body": { "prompt": "{ /payload/fields/prompt }" }, "timeoutMs": 8000, "name": "resp" },
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_result" } },
"fields": { "text": { "en-US": "{ /resp/body/text }" }, "source": { "en-US": "primary" } } } ],
"catch": [
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_result" } },
"fields": { "text": { "en-US": "Generierung fehlgeschlagen" }, "error": { "en-US": "{ /error/message }" }, "source": { "en-US": "fallback" } } } ] } ],
"secrets": [ { "key": "GEN_API_KEY", "value": "...", "description": "API-Schlüssel für die Generierung" } ] }12. KI-Zusammenfassung und Tags in einen Beitrag füllen
{ "method": "Post",
"statements": [
{ "type": "Http", "method": "POST", "url": "https://api.llm.com/v1/gen",
"headers": [ { "key": "Authorization", "value": "Bearer { /secrets/LLM_API_KEY }" } ],
"body": { "prompt": "{ /payload/fields/body }", "response_format": { "type": "json_object" } },
"timeoutMs": 15000, "name": "resp" },
{ "type": "Try",
"body": [
{ "type": "ParseJson", "name": "ai", "value": "{ /resp/body/choices/0/message/content }" },
{ "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } },
"fields": { "summary": { "en-US": "{ /ai/summary }" },
"tags": { "en-US": "{ /ai/tags }" } } },
{ "type": "Return", "value": { "ok": true, "tags": "{ /ai/tags }" } } ],
"catch": [
{ "type": "Return", "value": { "ok": false, "reason": "model did not return JSON" }, "isError": true, "statusCode": 502 } ] } ],
"secrets": [ { "key": "LLM_API_KEY", "value": "sk-...", "description": "API-Schlüssel für LLM-Aufrufe" } ] }Sobald ein Beitrag angelegt ist, füllt das Modell dessen Zusammenfassung und Tags (als verknüpfte Aktion eines Webhook an Content.Create gehängt). Die Antwort kommt hier in zwei Schichten. responseType von Http ist standardmäßig Json, der Antwort-Umschlag der API ist also schon ein Objekt, aber die Antwort, die das Modell erzeugt hat, steckt darin bei choices/0/message/content als String. Deshalb schälen Sie mit ParseJson eine Schicht weiter, bevor Sie Werte als { /ai/summary } und { /ai/tags } herausholen können. Die Tags gehen als das Array, das sie sind, in ein Array-Feld (Elemente vom Typ ShortText).
Auch mit einem Vertrag über strukturierte Ausgabe (response_format) kommt etwas an, das kein JSON ist, wenn die Antwort an einer Längengrenze abgeschnitten wird oder das Modell die Anfrage verweigert. Darum ist das Parsen in Try gefasst, was einen Parse-Fehlschlag in ein 502 verwandelt. Die Fehlermeldung führt den Text mit, den zu parsen versucht wurde, sodass Sie sehen, was zurückkam. Bei einer API, deren Umschlag von vornherein kein JSON ist, geben Sie Http ein responseType: "Text" und übergeben { /resp/body } direkt (siehe Http und ParseJson).
Parallel
13. Zwei parallele externe Aufrufe zu Content zusammenführen
{ "method": "Post",
"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" } ] ] },
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_merged" } },
"fields": { "left": { "en-US": "{ /a/body/value }" }, "right": { "en-US": "{ /b/body/value }" } } } ] }14. Registrierungsprüfung: parallele Scores, dann and-Entscheidung
{ "method": "Post",
"statements": [
{ "type": "Parallel", "branches": [
[ { "type": "Http", "method": "POST", "url": "https://api.fraud.com/score",
"headers": [ { "key": "x-api-key", "value": "{ /secrets/FRAUD_API_KEY }" } ],
"body": { "email": "{ /payload/fields/email }" }, "name": "fraud" } ],
[ { "type": "Http", "method": "GET", "url": "https://api.credit.com/v1/{ /payload/fields/userId }/score",
"headers": [ { "key": "x-api-key", "value": "{ /secrets/CREDIT_API_KEY }" } ], "name": "credit" } ] ] },
{ "type": "If",
"condition": { "and": [ { "<": [ "{ /fraud/body/risk }", 0.5 ] }, { ">=": [ "{ /credit/body/score }", 700 ] } ] },
"then": [
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_account" } },
"fields": { "email": { "en-US": "{ /payload/fields/email }" }, "status": { "en-US": "approved" } } },
{ "type": "Return", "value": { "decision": "approved" }, "statusCode": 201 } ],
"else": [ { "type": "Return", "value": { "decision": "manual-review" }, "statusCode": 202 } ] } ],
"secrets": [ { "key": "FRAUD_API_KEY", "value": "...", "description": "API-Schlüssel für den Betrugs-Score" },
{ "key": "CREDIT_API_KEY", "value": "...", "description": "API-Schlüssel für den Bonitäts-Score" } ] }Sie können { /ptr } auch in den URL-Pfad einfügen. Branch-Ergebnisse werden nach dem Join referenziert. Es gibt zwei externe Aufrufe. Die Anzahl externer Aufrufe, die eine Definition enthalten kann, ist ein tarifabhängiges Limit (siehe Tarife); prüfen Sie daher, ob Sie innerhalb dieses Limits liegen.
Schleifen und Aggregation
Loop und ResourceForEach werden im Zeitbudget als die vom body (onEach) deklarierte Zeit multipliziert mit der Iterationsobergrenze gerechnet. In den Beispielen dieses Abschnitts enthält der body keinen externen Aufruf, die deklarierte Zeit ist also 0, sodass das Grundbudget von 30 Sekunden die tatsächliche Grenze ist. Genau hier stößt eine Iteration in der Praxis auch an ihre Grenze (siehe Zeitbudget und Loop).
15. N Content aus einem Array-Input (Loop over)
{ "method": "Post",
"statements": [
{ "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 }" }, "qty": { "en-US": "{ /item/qty }" } } } ] } ] }16. Counted loop (for): Slots seeden
{ "method": "Post",
"statements": [
{ "type": "Loop", "for": { "from": 1, "to": 5 }, "name": "i", "maxIterations": 100,
"body": [
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_slot" } },
"fields": { "index": { "en-US": "{ /i }" }, "status": { "en-US": "open" } } } ] } ] }for läuft von from bis to, inklusive (Ganzzahl-Literale, step ist standardmäßig 1). name bindet den aktuellen Zähler an { /i }.
17. Cascade-Löschung (ForEach, Delete)
{ "method": "Delete",
"statements": [
{ "type": "ResourceForEach", "resource": "Content", "contentType": { "sys": { "id": "ct_comment" } },
"where": { "fields.postId": { "eq": "{ /payload/sys/id }" } }, "from": "Current", "advanced": false, "name": "comment",
"onEach": [ { "type": "ResourceDelete", "resource": "Content", "target": { "sys": { "id": "{ /comment/sys/id }" } } } ] },
{ "type": "ResourceDelete", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } } } ] }Da ResourceForEach die Treffer intern paginiert und jedes Element löscht, werden ohne manuelle Paginierung alle passenden Kommentare (bis zur Plattformobergrenze) gelöscht und danach der Beitrag selbst. Da onEach keine Zeit deklariert, beträgt das Zeitbudget der Ausführung dieser Definition 30 Sekunden.
18. Schleifenakkumulation: SetVar-Summe
{ "method": "Post",
"statements": [
{ "type": "SetVar", "var": "total", "value": 0 },
{ "type": "Loop", "over": "{ /payload/fields/items }", "name": "row", "maxIterations": 100,
"body": [ { "type": "SetVar", "var": "total", "value": { "$+": [ "{ /vars/total }", "{ /row/qty }" ] } } ] },
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_summary" } },
"fields": { "totalQty": { "en-US": "{ /vars/total }" } } } ] }19. Alle passenden Elemente in einem Rutsch verarbeiten
{ "method": "Post",
"statements": [
{ "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 }" } } } ] } ] }Da ResourceForEach die Treffer intern paginiert, ist keine cursor-Schleife (Loop while + SetVar-Akkumulation) nötig. Es findet alle passenden Entwürfe und veröffentlicht jeden. Ist die Anzahl so groß, dass ein vollständiger Durchlauf schwierig ist, legen Sie mit limit eine Obergrenze für einen Durchlauf fest und setzen where auf die Bedingung „unbearbeitet", um bei einer erneuten Ausführung fortzufahren.
20. id-Batch-Sammlung aus einer E-Mail-Liste (merge)
{ "method": "Post",
"statements": [
{ "type": "SetVar", "var": "ids", "value": [] },
{ "type": "SetVar", "var": "missing", "value": [] },
{ "type": "Loop", "over": "{ /payload/fields/emails }", "name": "email", "maxIterations": 100,
"body": [
{ "type": "ResourceFind", "resource": "Content", "contentType": { "sys": { "id": "ct_account" } },
"where": { "fields.email": { "eq": "{ /email }" } }, "name": "acc" },
{ "type": "If", "condition": { "!!": "{ /acc/sys/id }" },
"then": [ { "type": "SetVar", "var": "ids", "value": { "$merge": [ "{ /vars/ids }", [ "{ /acc/sys/id }" ] ] } } ],
"else": [ { "type": "SetVar", "var": "missing", "value": { "$merge": [ "{ /vars/missing }", [ "{ /email }" ] ] } } ] } ] },
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_campaign" } },
"fields": { "recipients": { "en-US": "{ /vars/ids }" }, "unresolved": { "en-US": "{ /vars/missing }" } } } ] }Ein Lesevorgang (ResourceFind) ist kein externer Aufruf und daher innerhalb eines Loop-body erlaubt. Vorhandensein und Fehlen werden jeweils mit merge akkumuliert.
Saga und Nebenläufigkeit
21. Zahlungs-Saga (Try/catch/finally)
{ "method": "Post",
"statements": [
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_order" } },
"fields": { "sku": { "en-US": "{ /payload/fields/sku }" }, "status": { "en-US": "reserved" } },
"publish": false, "name": "order" },
{ "type": "Try",
"body": [
{ "type": "Http", "method": "POST", "url": "https://api.pay.com/charge",
"headers": [ { "key": "Authorization", "value": "Bearer { /secrets/PAYMENT_API_KEY }" } ],
"body": { "amount": "{ /payload/fields/amount }", "ref": "{ /order/sys/id }" }, "timeoutMs": 10000, "name": "pay" },
{ "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /order/sys/id }" } },
"fields": { "status": { "en-US": "paid" }, "txId": { "en-US": "{ /pay/body/transactionId }" } }, "publish": true },
{ "type": "Return", "value": { "orderId": "{ /order/sys/id }", "status": "paid" }, "statusCode": 201 } ],
"catch": [
{ "type": "ResourceDelete", "resource": "Content", "target": { "sys": { "id": "{ /order/sys/id }" } } },
{ "type": "Return", "value": { "reason": "payment failed", "detail": "{ /error/message }" }, "isError": true, "statusCode": 402 } ],
"finally": [
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_paylog" } },
"fields": { "orderRef": { "en-US": "{ /order/sys/id }" }, "amount": { "en-US": "{ /payload/fields/amount }" } } } ] } ],
"secrets": [ { "key": "PAYMENT_API_KEY", "value": "sk-...", "description": "Geheimer Schlüssel der Zahlungs-API" } ] }Nach der Reservierung (draft): bei erfolgreicher Zahlung Bestätigung, publish und 201; bei einem Fehler löscht catch die Reservierung (Kompensation) und gibt 402 zurück; finally protokolliert immer. Da die löschbasierte Kompensation eine neue sys.id erzeugt, fällt sie unter die Einschränkung, bei der Referenzen brechen (siehe Keine Transaktionen und Kompensation in der Ausführungssemantik).
22. Optimistische Sperre (CAS)
{ "method": "Post",
"statements": [
{ "type": "ResourceFind", "resource": "Content", "contentType": { "sys": { "id": "ct_stock" } },
"where": { "fields.sku": { "eq": "{ /payload/fields/sku }" } }, "name": "stock" },
{ "type": "If", "condition": { "<": [ "{ /stock/fields/qty/en-US }", "{ /payload/fields/amount }" ] },
"then": [ { "type": "Return", "value": { "reason": "out of stock" }, "isError": true, "statusCode": 409 } ] },
{ "type": "Try",
"body": [
{ "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /stock/sys/id }" } },
"version": "{ /stock/sys/version }",
"fields": { "qty": { "en-US": { "$-": [ "{ /stock/fields/qty/en-US }", "{ /payload/fields/amount }" ] } } } },
{ "type": "Return", "value": { "ok": true } } ],
"catch": [
{ "type": "Return", "value": { "reason": "version conflict, erneut versuchen" }, "isError": true, "statusCode": 409 } ] } ] }Lesen Sie den Bestand, um eine frische sys.version zu erhalten, und ziehen Sie dann mit dieser Version ab (version). Hat eine andere Ausführung den Wert zwischen dem Lesen und dem Schreiben verändert, wird wegen einer Versionsabweichung abgebrochen und catch gibt 409 zurück. Der guard für unzureichenden Bestand liegt außerhalb von Try (normaler vorzeitiger Ausstieg).
23. Benachrichtigungs-E-Mail an den Käufer jeder Bestellung (ForEach + EmailSend)
{ "method": "Post",
"statements": [
{ "type": "ResourceForEach", "resource": "Content", "contentType": { "sys": { "id": "ct_order" } },
"where": { "fields.notified": { "ne": true } }, "order": "sys.createdAt,sys.id",
"from": "Current", "advanced": false, "name": "order",
"onEach": [
{ "type": "EmailSend", "account": { "sys": { "id": "eml_orders" } },
"toServiceUser": { "sys": { "id": "{ /order/fields/buyer/en-US/sys/id }" } },
"subject": "Der Versand hat begonnen",
"body": "<p>Der Versand des von Ihnen bestellten Artikels hat begonnen.</p>" },
{ "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /order/sys/id }" } },
"fields": { "notified": { "en-US": true } } } ] } ] }Es durchläuft die Bestellungen, für die noch keine Benachrichtigung gesendet wurde (bei denen fields.notified nicht true ist), sendet dem Käufer jeder Bestellung eine E-Mail und markiert unmittelbar danach notified. Da EmailSend pro E-Mail nur einen Empfänger hat, sendet man für den Mehrfachversand so mit ResourceForEach je Element (onEach kann externe Aufrufe enthalten). Übergeben mit toServiceUser, gelangt die Adresse des Mitglieds nicht in den Variablenraum des Script und wird erst unmittelbar vor dem Senden aufgelöst. Da where auf „unbearbeitet" gesetzt und am Ende von onEach die Fertigstellung markiert wird, wird bei einem Abbruch mittendrin nach einer erneuten Ausführung ab den verbliebenen Bestellungen fortgesetzt (schlägt die Markierung unmittelbar nach dem erfolgreichen Seiteneffekt fehl, kann dieser Fall in der nächsten Ausführung doppelt vorkommen; at-least-once).
24. Versand mit einer Mailvorlage, die im Content Studio geändert wird (templateBody)
{ "method": "Post",
"statements": [
{ "type": "ResourceRead", "resource": "Content", "name": "tpl",
"target": { "sys": { "id": "cnt_mail_shipping" } } },
{ "type": "ResourceRead", "resource": "Content", "name": "order",
"target": { "sys": { "id": "{ /payload/orderId }" } } },
{ "type": "EmailSend", "account": { "sys": { "id": "eml_orders" } },
"toServiceUser": { "sys": { "id": "{ /order/fields/buyer/en-US/sys/id }" } },
"subject": "Der Versand hat begonnen (Bestellnummer { /order/sys/id })",
"templateBody": {
"source": "{ /tpl/fields/html/en-US }",
"data": { "name": "{ /order/fields/buyerName/en-US }",
"tracking": "{ /order/fields/trackingNo/en-US }" } } },
{ "type": "Return", "value": { "sent": true } } ] }Die Vorlage liegt in einem Content (im Field html von cnt_mail_shipping), und nur deren Lücken werden gefüllt. Ändert die Marketingabteilung den Text im Content Studio, bleibt das Script unverändert. Die Vorlage sieht so aus.
<p>Hallo { /name }, der Versand der von Ihnen bestellten Ware hat begonnen.</p>
<p>Sendungsnummer: { /tracking }</p>source muss genau ein Pointer auf ein Content sein, das diese Definition gelesen hat. Das ResourceRead, das die Vorlage liest, steht vor dem EmailSend. Da Statements in der geschriebenen Reihenfolge ausgeführt werden, ist der Wert leer und die Ausführung schlägt fehl, wenn auf einen noch nicht gelesenen Namen gezeigt wird. Eine Lücke der Vorlage liest ausschließlich data, aus der Vorlage heraus lässt sich /payload also nicht direkt lesen. Eine Lücke, die auf einen in data nicht vorhandenen Schlüssel zeigt, wird zu einer leeren Zeichenkette.
25. Einen von der Administration geschriebenen Newsletter an die Abonnenten (autoEscape)
{ "method": "Post",
"statements": [
{ "type": "ResourceRead", "resource": "Content", "name": "issue",
"target": { "sys": { "id": "{ /payload/issueId }" } } },
{ "type": "ResourceRead", "resource": "Content", "name": "tpl",
"target": { "sys": { "id": "cnt_mail_newsletter" } } },
{ "type": "ResourceForEach", "resource": "Content", "contentType": { "sys": { "id": "ct_subscriber" } },
"where": { "fields.active": { "eq": true } }, "order": "sys.createdAt,sys.id",
"from": "Current", "advanced": false, "name": "sub",
"onEach": [
{ "type": "EmailSend", "account": { "sys": { "id": "eml_news" } },
"to": "{ /sub/fields/email/en-US }",
"subject": "{ /issue/fields/title/en-US }",
"autoEscape": false,
"templateBody": {
"source": "{ /tpl/fields/html/en-US }",
"data": { "article": "{ /issue/fields/bodyHtml/en-US }" } } } ] } ] }Der Newsletter-Rumpf ist HTML, das die Administration als Rich Text geschrieben hat. Bleibt er unverändert, erscheinen die Tags als Zeichen. Steht autoEscape auf false, wird das Markup aus data gerendert.
Fassen Sie den Bereich, in dem Sie false setzen, eng. Diese Einstellung gilt für alle interpolierten Werte dieser einen EmailSend-E-Mail. Deshalb enthält data in diesem Beispiel nur den von der Administration geschriebenen article. Mischen Sie einen von außen gekommenen Wert, etwa den Namen des Abonnenten, in dasselbe data, wird auch das Markup aus diesem Wert mitgerendert. Brauchen Sie einen solchen Wert, legen Sie ihn in die Vorlage oder teilen Sie den Versand auf. Auch die Vorlage selbst wird unabhängig von autoEscape immer roh gerendert. Prüfen Sie daher, ob sowohl die Vorlage als auch die Werte in data ausschließlich von Personen stammen, die Schreibrechte auf dieses Content haben.
Signaturprüfung
Ein Zahlungsdienstleister (PG, MoR) hängt beim Senden eines Webhooks eine Signatur an den Body. Die empfangende Seite muss, bevor sie irgendetwas tut, prüfen, ob sich diese Signatur mit dem eigenen geheimen Schlüssel reproduzieren lässt. Die beiden Beispiele unten sind die zwei Verfahren, die in der Praxis auseinandergehen. Das eine erzeugt einen Code mit dem geheimen Schlüssel (keyed), das andere hängt Felder und den geheimen Schlüssel aneinander und berechnet daraus einen Digest.
26. Webhook-Signatur prüfen (verpackten Header entpacken, Replay-Window)
{ "method": "Post",
"statements": [
{ "type": "Regex", "name": "sig", "mode": "Capture",
"pattern": "^t=(\\d+),v1=([0-9a-f]{64})$", "value": "{ /headers/x-provider-signature }" },
{ "type": "If", "condition": { "==": [ "{ /sig }", null ] },
"then": [ { "type": "Return", "value": { "ok": false, "reason": "malformed signature header" }, "isError": true, "statusCode": 400 } ] },
{ "type": "Signature", "name": "verified", "algorithm": "SHA256",
"secret": "{ /secrets/WEBHOOK_SIGNING_SECRET }",
"value": "{ /sig/1 }.{ /rawPayload }", "expected": "{ /sig/2 }" },
{ "type": "If", "condition": { "!": "{ /verified }" },
"then": [ { "type": "Return", "value": { "ok": false, "reason": "signature mismatch" }, "isError": true, "statusCode": 401 } ] },
{ "type": "If", "condition": { ">=": [ { "-": [ "{ /now/seconds }", "{ /sig/1 }" ] }, 300 ] },
"then": [ { "type": "Return", "value": { "ok": false, "reason": "timestamp outside the replay window" }, "isError": true, "statusCode": 401 } ] },
{ "type": "ResourceFind", "resource": "Content", "contentType": { "sys": { "id": "ct_order" } },
"where": { "fields.orderId": { "eq": "{ /payload/data/orderId }" } }, "name": "order" },
{ "type": "If", "condition": { "==": [ "{ /order }", null ] },
"then": [ { "type": "Return", "value": { "ok": false, "reason": "unknown order" }, "isError": true, "statusCode": 404 } ] },
{ "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /order/sys/id }" } },
"fields": { "status": { "en-US": "paid" }, "paidAt": { "en-US": "{ /now/iso }" } }, "publish": true },
{ "type": "Return", "value": { "ok": true } } ],
"secrets": [ { "key": "WEBHOOK_SIGNING_SECRET", "value": "whsec_9f2c1b7ae4", "description": "Webhook-Signaturschlüssel des Zahlungsdienstleisters" } ] }Da der Anbieter Zeitstempel und Code gemeinsam in einem Header sendet (t=1492774577,v1=<64 Zeichen hex>), lässt sich die zu signierende Nachricht nicht bilden, bevor der Header entpackt ist. Daher ergibt sich diese Reihenfolge.
- Das
CapturevonRegexentpackt den Header und teilt ihn in{ /sig/1 }(Zeitstempel) und{ /sig/2 }(Code). Index0ist der gesamte Treffer, ab1folgen die Capture-Gruppen. Passt das Format nicht, ist{ /sig }gleichnull, und es wird an dieser Stelle mit400zurückgeschickt. Signaturenimmt"<Zeitstempel>.<Original-Body>"als Nachricht, erzeugt den Code und vergleicht ihn mit{ /sig/2 }. Entscheidend ist, die Nachricht als Original vor dem Parsen ({ /rawPayload }) zu nehmen. Macht man aus dem geparsten/payloadwieder einen String, werden Leerzeichen und Zahlennotation normalisiert, und man kommt nicht zu den Bytes zurück, die die Gegenseite signiert hat. Setzt man zwei Pointer nebeneinander in einen String, werden sie unmittelbar aneinandergehängt, daher braucht es keinen Operator.- Ist
{ /verified }gleichfalse, folgt401. Eine falsche Signatur und ein fehlender Header sind beides dasselbefalse(der sendenden Seite wird nicht mitgeteilt, welches von beidem falsch war). - Auch bei passender Signatur wird eine alte Anfrage abgewiesen.
{ /now/seconds }ist der Zeitpunkt, zu dem diese Ausführung begonnen hat, daher wird geprüft, ob der Abstand zum Zeitstempel in der Signatur das Replay-Window (hier 300 Sekunden) überschreitet. Der Zeitstempel kam als String aus dem Header, wird aber von der arithmetischen Operation in eine Zahl umgewandelt. - Erst nachdem all das bestanden ist, wird die Bestellung gesucht und ihr Status geändert.
Da es keinen externen Aufruf gibt, ist auch keine Zeit deklariert; die Ausführung endet somit innerhalb des Grundbudgets von 30 Sekunden, und der Anbieter erhält die Antwort an dieser Stelle. Der Signaturschlüssel ist in secrets deklariert, daher speichert WEEGLOO ihn verschlüsselt und sicher. Im Statement wird er als { /secrets/WEBHOOK_SIGNING_SECRET } gelesen. Wer dieses Script abruft, sieht allerdings den ursprünglichen Wert, halten Sie daher die Rollen, die dieses Script lesen können, eng (siehe secrets im Sicherheitsmodell).
Es gibt zwei Wege, dem Anbieter das Aufrufen dieser Anlaufstelle zu ermöglichen, und die Weggabelung ist, ob dieser Anbieter benutzerdefinierte Header senden kann.
- Kann er es, geben Sie ein Token aus, das nur die Execute-Berechtigung auf dieses Script enthält, lassen es in den
Authorization-Header setzen und/executeaufrufen. Eine Rolle, die auf ein einzelnes bestimmtes Script eingeengt ist, wird unter script-Berechtigung der SpaceRole behandelt, das Token unter Space Access Token. Das ist der Standardweg. - Kann er es nicht (ein Anbieter, bei dem sich nur eine Callback-URL registrieren lässt und es keine Einstellung für zusätzliche Header gibt), schalten Sie
anonymousCallEnableddieses Script ein und registrieren die Adresse/execute/anonymousals Callback. Die Ausführung erfolgt dann unter der Identität des Autors, daher ist auch dasupdatedByder Bestellung, die dieses Script ändert, der Autor, und da es keine Authentifizierung gibt, ist die obige Signaturprüfung die einzige Authentifizierung dieser Anlaufstelle. Die Bedingungen und die Speicherregeln werden unter Anonymer Aufruf behandelt.
Die obige Definition erfüllt unverändert die Bedingungen eines anonymen Script. Sie verwendet keinen createdBy: ":self"-Filter, und eine Anfrage, die die Signatur und das Replay-Window nicht besteht, wird mit Return abgebrochen, bevor irgendetwas angetastet wird.
27. Signaturprüfung per Hash ohne Schlüssel
{ "method": "Post",
"statements": [
{ "type": "Hash", "name": "expectedSign", "algorithm": "SHA256", "encoding": "HexUpper",
"value": "{ /payload/orderId }{ /payload/amount }{ /secrets/MERCHANT_KEY }" },
{ "type": "If", "condition": { "!=": [ "{ /expectedSign }", "{ /payload/signature }" ] },
"then": [ { "type": "Return", "value": { "ok": false, "reason": "signature mismatch" }, "isError": true, "statusCode": 401 } ] },
{ "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /payload/orderRef }" } },
"fields": { "status": { "en-US": "paid" } }, "publish": true },
{ "type": "Return", "value": { "ok": true } } ],
"secrets": [ { "key": "MERCHANT_KEY", "value": "9f2c1b7ae4", "description": "Mit dem Zahlungsdienstleister geteilter Händlerschlüssel" } ] }Das ist statt HMAC das Verfahren „festgelegte Felder und den geheimen Schlüssel aneinanderhängen und SHA256 berechnen". Hash hat kein secret-Feld. Den geheimen Schlüssel deklarieren Sie in secrets und schreiben ihn innerhalb von value als { /secrets/MERCHANT_KEY } an die Stelle, an die ihn dieses Schema setzt. Der Schlüssel steht je Schema vorne, hinten oder in der Mitte; so geschrieben lässt sich jede dieser Positionen ausdrücken.
encoding richten Sie nach der Notation der Gegenseite (Hex, HexUpper, Base64, Base64Url). Anders als bei Signature ist das Ergebnis ein String, daher müssen Sie den Vergleich selbst durchführen, und dieser Vergleich ist ein normaler Gleichheitsvergleich. Da die Obergrenze von value 128 Zeichen ist, verwenden Sie für ein Schema, das über den gesamten Body rechnet, Signature.
Mitglieder nachschlagen
28. Mitglied per E-Mail finden, Coupon und Benachrichtigungsmail
{ "method": "Post",
"statements": [
{ "type": "ResourceFind", "resource": "ServiceUser",
"where": { "sys.email": { "eq": "{ /payload/fields/email }" } }, "name": "member" },
{ "type": "If", "condition": { "==": [ "{ /member }", null ] },
"then": [ { "type": "Return", "value": { "ok": false, "reason": "member not found" }, "isError": true, "statusCode": 404 } ] },
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_coupon" } },
"fields": { "code": { "en-US": "WELCOME-{ /member/sys/id }" },
"owner": { "en-US": "{ /member/sys/id }" } }, "publish": false, "name": "coupon" },
{ "type": "EmailSend", "account": { "sys": { "id": "eml_orders" } },
"toServiceUser": { "sys": { "id": "{ /member/sys/id }" } },
"subject": "Ein Coupon wurde ausgestellt",
"body": "<p>Hallo { /member/nickname }, wir haben Ihnen den Coupon { /coupon/fields/code/en-US } geschenkt.</p>" },
{ "type": "Return", "value": { "ok": true, "memberId": "{ /member/sys/id }" } } ] }Über eine einzelne E-Mail-Adresse wird ein Mitglied gefunden und dessen sys.id als Eigentümer des Coupons und als Empfänger der E-Mail verwendet. Beim Lesen des Mitgliederverzeichnisses gelten diese Regeln.
sys.emailwird verschlüsselt gespeichert und nimmt daher nur Operatoren der exakten Übereinstimmung an (eq,ne,in,nin). Geben Sie einen anderen Operator wieprefix, gibt es nicht stillschweigend 0 Treffer, sondern die Ausführung schlägt fehl.- Gibt es keinen Treffer, bindet
ResourceFindnull, daher verzweigen Sie über das Vorhandensein in derselben Form wie beim Suchen von Content. - Die Felder eines Mitglieds sind anders als bei Content und Media keine Locale-Map. Referenzieren Sie sie unmittelbar, etwa als
{ /member/nickname }. - Beim Senden der E-Mail holen Sie die Adresse nicht heraus, sondern übergeben die
sys.idantoServiceUser. Da die Engine die Adresse unmittelbar vor dem Senden auflöst, gelangt die Adresse des Mitglieds nicht in den Variablenraum des Script. - Um diese Definition zu speichern, müssen die
settingsder SpaceRole des AutorsSETTING_SERVICE_LOGINenthalten. Ein Statement, das ein Mitglied erstellt oder löscht, lässt sich mit keiner Rolle speichern (siehe Mit dem Mitgliederverzeichnis arbeiten).
EmailSend addiert sein timeoutMs (ohne Deklaration 10 Sekunden) zum Zeitbudget der Ausführung und zählt einmal auf das Limit für externe Aufrufe pro Definition.
Dateiübertragung
29. Produktfoto an einen Bildanalysedienst senden und das Ergebnis erfassen (multipartBody)
{ "method": "Post",
"statements": [
{ "type": "ResourceRead", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } }, "name": "product" },
{ "type": "ResourceRead", "resource": "Media",
"target": { "sys": { "id": "{ /product/fields/photo/en-US/sys/id }" } }, "name": "photo" },
{ "type": "Try",
"body": [
{ "type": "Http", "method": "POST", "url": "https://api.vision.example/v2/analyze",
"headers": [ { "key": "x-api-key", "value": "{ /secrets/VISION_API_KEY }" } ],
"multipartBody": [
{ "name": "image", "url": "{ /photo/fields/file/en-US/url }" },
{ "name": "language", "value": "ko" },
{ "name": "features", "value": "labels" },
{ "name": "features", "value": "text" } ],
"timeoutMs": 20000, "name": "analysis" },
{ "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /product/sys/id }" } },
"fields": { "aiLabels": { "en-US": "{ /analysis/body/labels }" },
"aiText": { "en-US": "{ /analysis/body/text }" } } },
{ "type": "Return", "value": { "ok": true, "labels": "{ /analysis/body/labels }" } } ],
"catch": [
{ "type": "Return", "value": { "ok": false, "reason": "image analysis failed", "detail": "{ /error/message }" }, "isError": true, "statusCode": 502 } ] } ],
"secrets": [ { "key": "VISION_API_KEY", "value": "...", "description": "API-Schlüssel für die Bildanalyse" } ] }Sobald ein Verkäufer ein Produkt anlegt, werden aus dem Produktfoto Labels und der Text im Foto extrahiert und im Produkt erfasst (als verknüpfte Aktion eines Webhook an Content.Create gehängt). Über das Referenzfeld photo des Produkt-Content wird das Media gelesen, und die url seiner Datei wird in den Datei-Part von multipartBody geschrieben. api.vision.example ist ein fiktiver Dienst für dieses Beispiel; angenommen wird, dass dieser Dienst labels (ein String-Array) und text (einen String) zurückgibt. aiLabels ist ein Array-Feld (Elemente vom Typ ShortText), daher wird das Array unverändert geschrieben. Passen Sie die Pfade { /analysis/body/... } an die Antwortform des Dienstes an, den Sie tatsächlich verwenden.
- Aufbau der Parts: Jedes Element des Arrays ist ein Part, und die Parts werden in der Reihenfolge des Arrays gesendet.
imageist ein Datei-Part mit angegebenerurl, die übrigen sind Text-Parts mit angegebenemvalue. Verwenden Sie denselben Namen mehrmals wie beifeatures, wird jeder als eigener Part gesendet. Content-Type: Die gesamte Anfrage wird immer alsmultipart/form-datagesendet, daher geben Sie ihn nicht inheadersan. Beim PartimageistcontentTypeweggelassen, daher wird derContent-Typeverwendet, mit dem die Datei-URL geantwortet hat. Auch der Dateiname ist weggelassen, daher wird das letzte Pfadsegment der Datei-URL zum Dateinamen.- Fehlerbehandlung: Überschreitet die Datei das Download-Limit, lässt sich die Datei nicht von der Datei-URL laden oder wird die Zeit überschritten, schlägt dieses
Httpfehl. Es schlägt auch fehl, wenn der Zieldienst mit einem Status von 400 oder höher antwortet. Da dieses BeispielignoreStatusCodenicht angibt (Standardwertfalse), geht es in jedem Fall zucatchüber, und dasReturnincatchgibt Status502zurück. Das Produkt wird nicht geändert. Wie die einzelnen Fehlerarten behandelt werden, ist unter Zwei Arten, wie Http fehlschlägt beschrieben. - Zeitbudget: Im Pro-Tarif sind für dieses Script etwa 60,5 Sekunden erlaubt (Grundbudget 30 Sekunden +
timeoutMs20 Sekunden + Downloadzeit für ein Foto von etwa 10,5 Sekunden). Die Berechnung ist unter Zeitbudget beschrieben.
Bei einer API, die eine einzelne Datei als gesamten Body erwartet (in der Form eines PUT auf eine Upload-URL), verwenden Sie statt multipartBody das Feld urlBody. Die Regeln beider Felder und die Reihenfolge, in der der Content-Type bestimmt wird, sind unter Eine Datei als Body senden beschrieben.
Dateitransformation
30. E-Book-PDF-Kopie mit aufgedruckter E-Mail-Adresse des Käufers (PdfWatermark)
{ "method": "Post",
"payloadSchema": {
"type": "object", "required": [ "fields" ],
"properties": {
"fields": {
"type": "object", "required": [ "ebookMediaId", "email", "orderId" ],
"properties": {
"ebookMediaId": { "type": "string" },
"email": { "type": "string", "minLength": 1, "maxLength": 41, "not": { "pattern": "[^ -~]" } },
"orderId": { "type": "string", "minLength": 1, "maxLength": 20, "not": { "pattern": "[^A-Za-z0-9-]" } } } } } },
"statements": [
{ "type": "ResourceRead", "resource": "Media",
"target": { "sys": { "id": "{ /payload/fields/ebookMediaId }" } }, "name": "ebook" },
{ "type": "ResourceCreate", "resource": "Media", "publish": false,
"fields": {
"title": { "en-US": "{ /ebook/fields/title/en-US }" },
"file": { "en-US": {
"source": "{ /ebook/fields/file/en-US/url }", "encoding": "url",
"transformations": [
{ "type": "PdfWatermark", "text": "{ /payload/fields/email } · { /payload/fields/orderId }" } ] } } },
"name": "copy" },
{ "type": "Return", "value": { "mediaId": "{ /copy/sys/id }" } } ] }Dieser Ablauf erstellt nach bestätigter Zahlung die E-Book-Kopie, die dem Käufer übergeben wird. Über den Anfrage-payload werden die id des ursprünglichen E-Book-Media, die E-Mail-Adresse des Käufers und die Bestellnummer entgegengenommen. Das ursprüngliche Media bleibt unverändert. Erstellt wird ein neues Media, auf dessen Seiten durchgehend E-Mail · Bestellnummer diagonal aufgedruckt ist, und dessen id wird zurückgegeben.
- Textlänge: Der feste Text außerhalb der Referenzen ist
·, also 3 Zeichen.payloadSchemabegrenzt die E-Mail-Adresse auf höchstens 41 Zeichen und die Bestellnummer auf höchstens 20 Zeichen, daher überschreitet der aufgedruckte Text 64 Zeichen nicht. Enthält einer der beiden Werte auch nur ein Zeichen außerhalb der erlaubten Zeichen (einschließlich eines Zeilenumbruchs), wird die Anfrage vor der Ausführung abgelehnt, sodass der Text im Transformationsschritt nicht zum Fehlschlag führt. Alle druckbaren ASCII-Zeichen, die für die E-Mail-Adresse erlaubt sind, gehören zu den zulässigen Zeichen. - Farbe:
colorist weggelassen, daher wird mit dem Standardwert#808080(Grau) gedruckt. - Veröffentlichung: Es ist eine Kopie für einen einzelnen Käufer, daher ist
publishauffalsegesetzt. Die Kopie wird nicht über CDA/ACDA ausgeliefert. - Fehlerbehandlung: Ist die Originaldatei kein PDF oder ist sie verschlüsselt, schlägt
ResourceCreatemit Status 422 fehl, und es wird kein neues Media erstellt. Die vollständige Liste der Fehlerfälle finden Sie unter Wenn eine Dateitransformation fehlschlägt. - Zeitbudget: Die Zeit für das Empfangen und Transformieren der Datei geht vom Grundbudget von 30 Sekunden ab (Zeitbudget).
- Kaufprüfung: Dieses Beispiel enthält keinen Schritt, der Zahlung und Kauf prüft. So belassen, kann jeder Aufrufer mit Execute-Berechtigung für eine beliebige Media-id eine Kopie erstellen. Lesen Sie im Praxiseinsatz zuerst den Bestell-Content und prüfen Sie, ob Käufer und Produkt übereinstimmen, bevor Sie fortfahren.
Verwandte Dokumente
- Statement-Katalog: Die Felder und Ergebnisse der in den Beispielen verwendeten Statements.
- Wertausdrücke: Referenzen, JsonLogic und Locale-Map-Regeln.
- Ausführungssemantik, Einschränkungen und Sicherheit: Ausführungsreihenfolge, Kompensation, optimistische Sperre und Einschränkungen.
- Script-Übersicht: Die oberste Struktur und die Zeit für eine einzelne Ausführung.
