कुकबुक (व्यावहारिक उदाहरण संग्रह)

विभिन्न परिदृश्यों को पूर्ण ScriptDefinition के रूप में दिखाया गया है। सिंटैक्स के आधार के लिए Statement कैटलॉग और मान अभिव्यक्ति देखें, तथा निष्पादन और बाधाओं के लिए निष्पादन सिमेंटिक्स, बाधाएँ, सुरक्षा देखें। सभी उदाहरणों में लिखते समय fields का मान एक Locale मैप ({ "<locale>": मान }) होता है, और उदाहरण Locale को en-US पर एकरूप रखा गया है। सभी उदाहरण कॉल के अनुरोध को संभालने वाले पथ पर इनलाइन निष्पादित होते हैं, और कॉल की प्रतिक्रिया की body में परिणाम लौटा देते हैं। जिन उदाहरणों में बाहरी कॉल (Http·EmailSend) है, उनमें उस statement का timeoutMs निष्पादन के समय बजट में जुड़ जाता है (Http में × (1 + retry)), और वह statement किसी पुनरावृत्ति (Loop·ResourceForEach) के भीतर हो, तो पुनरावृत्ति की ऊपरी सीमा से गुणा हो जाता है (समय बजट)।

विषय-सूची

बुनियादी CRUD

1. Content बनाना और publish करना

{ "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 (व्यू काउंट +1)

{ "method": "Post",
  "statements": [
    { "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } },
      "fields": { "viewCount": { "en-US": { "$+": [ "{ /payload/fields/viewCount }", 1 ] } } } } ] }

3. मेरी ऑर्डर सूची इकट्ठा करके लौटाना

{ "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 }" } } ] }

createdBy: ":self" से "केवल अपने ही" आइटम पर पुनरावृत्ति करते हुए आइटम को SetVar से इकट्ठा करके लौटाया जाता है। ResourceForEach पुनरावृत्ति के परिणाम को collection के रूप में bind नहीं करता, इसलिए सूची के रूप में लौटाने के लिए इसे ऐसे ही स्वयं इकट्ठा किया जाता है। पुनरावृत्ति समय बजट में onEach द्वारा घोषित समय को संसाधित आइटम की संख्या से गुणा किए हुए मान के रूप में आती है। यहाँ onEach में कोई बाहरी कॉल न होने से घोषित समय 0 है, इसलिए 30 सेकंड का मूल बजट ही वास्तविक सीमा है; और onEach में बाहरी कॉल रखने पर वह गुणा ज्यों-का-त्यों बजट में आ जाता है, तथा 180 सेकंड की ऊपरी सीमा पर पहुँचते ही वहीं रुक जाता है (समय बजट)। जब केवल सूची पढ़कर लौटाना ही हो, तो Script की पुनरावृत्ति के बजाय फ़्रंटएंड से CDA/CMA सूची API को सीधे कॉल करना बेहतर है।

4. एकल पठन के बाद guard, फिर स्वीकृति

{ "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 } } ] }

ResourceRead से किसी एकल रिकॉर्ड को नाम पर बाइंड करने पर उसे { /order/fields/... } से सीधे संदर्भित किया जा सकता है। यदि वह मौजूद न हो, तो पठन पर एरर आता है (Try से लपेटा जा सकता है)।

लुकअप और 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 पहली मैच को सीधे बाइंड करता है (न होने पर null), और { "!!": "{ /found/sys/id }" } से अस्तित्व के आधार पर शाखा बनाता है।

6. डायनामिक फ़ील्ड कुंजी और डायनामिक 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 }" } } } ] }

फ़ील्ड कुंजी और Locale बकेट कुंजी दोनों { /ptr } संदर्भ हैं। किसी अनुवाद को किसी विशिष्ट Locale बकेट में डालते समय इसका उपयोग करें।

बाहरी API

7. क्रेडिट guard, अग्रिम कटौती (CAS), LLM कॉल, विफलता पर रिफंड (प्रतिनिधि उदाहरण)

{ "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, फिर से प्रयास करें" }, "isError": true, "statusCode": 409 } ] },
    { "type": "Try",
      "body": [
        { "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, "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 } ] } ] }

guard से यह जांचा जाता है कि शेष राशि पर्याप्त है या नहीं, फिर बाहरी कॉल से पहले कटौती की जाती है। कटौती wallet की sys.version पर आशावादी लॉकिंग (CAS) लगाती है। यदि पढ़ी गई शेष राशि और कटौती के बीच किसी अन्य निष्पादन ने wallet को बदल दिया हो, तो वर्शन मेल न खाने के कारण abort हो जाता है और catch 409 लौटाता है। चूंकि कोई बाहरी कॉल नहीं होती, इसलिए समवर्ती अनुरोधों पर दोहरी कटौती नहीं होती। कटौती के तय हो जाने के बाद ही LLM को कॉल किया जाता है, और यदि वह कॉल विफल हो, तो catch में कटौती की गई राशि (cost) को वापस जोड़कर रिफंड (क्षतिपूर्ति) किया जाता है और फिर 502 लौटाया जाता है। क्रम यह है: अपरिवर्तनीय बाहरी कॉल से पहले शुल्क तय करना, और केवल विफलता की स्थिति में क्षतिपूर्ति करना। गुप्त कुंजी को secret:true हेडर में रखा जाता है। क्षतिपूर्ति की सीमाओं के लिए निष्पादन सिमेंटिक्स में ट्रांज़ैक्शन का अभाव और क्षतिपूर्ति देखें।

8. इमेज (URL) को Media बनाकर Content में संलग्न करना

{ "method": "Post",
  "statements": [
    { "type": "Http", "method": "POST", "url": "https://api.img.com/gen",
      "headers": [ { "key": "x-api-key", "value": "...", "secret": true } ],
      "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 }" } } } ] }

Media को name के साथ बनाया जाता है, और ResourceCreate (Content) { /img/sys/id } को संदर्भ फ़ील्ड में डालता है। Media भी Content जैसा ही fields मॉडल उपयोग करता है। file इंजेस्ट निर्देश { source, encoding } है। फ़ाइल इंजेस्ट का कोई घोषित समय नहीं होता, इसलिए वह 30 सेकंड के मूल बजट से जाता है, और प्रति परिभाषा बाहरी कॉल की सीमा में भी नहीं आता (स्थैतिक बाधाएँ)।

9. base64 इमेज को Media बनाना

{ "method": "Post",
  "statements": [
    { "type": "Http", "method": "POST", "url": "https://api.img.com/gen",
      "headers": [ { "key": "x-api-key", "value": "...", "secret": true } ],
      "body": { "prompt": "{ /payload/fields/prompt }" }, "name": "gen" },
    { "type": "ResourceCreate", "resource": "Media",
      "fields": { "file": { "en-US": { "source": "{ /gen/body/data/0/b64_json }", "encoding": "base64" } } } } ] }

10. मॉडरेशन के बाद सशर्त publish या डिलीट

{ "method": "Post",
  "statements": [
    { "type": "Http", "method": "POST", "url": "https://api.mod.com/check",
      "headers": [ { "key": "x-api-key", "value": "...", "secret": true } ],
      "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 }" } } } ] } ] }

11. try/catch: बाहरी विफलता पर fallback

{ "method": "Post",
  "statements": [
    { "type": "Try",
      "body": [
        { "type": "Http", "method": "POST", "url": "https://primary.api/gen",
          "headers": [ { "key": "x-api-key", "value": "...", "secret": true } ],
          "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": "जनरेशन विफल" }, "error": { "en-US": "{ /error/message }" }, "source": { "en-US": "fallback" } } } ] } ] }

12. लेख में AI से सारांश और टैग भरना

{ "method": "Post",
  "statements": [
    { "type": "Http", "method": "POST", "url": "https://api.llm.com/v1/gen",
      "headers": [ { "key": "Authorization", "value": "Bearer sk-...", "secret": true } ],
      "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 } ] } ] }

लेख बन जाने पर उसका सारांश और टैग मॉडल भरता है (Webhook की लिंक्ड ऐक्शन के रूप में Content.Create पर लगा दें)। यहाँ प्रतिक्रिया दो परतों में आती है। Http का responseType डिफ़ॉल्ट रूप से Json है, इसलिए API की प्रतिक्रिया का envelope पहले से object है, पर मॉडल ने जो उत्तर बनाया वह उसके भीतर choices/0/message/content में string के रूप में पड़ा है। इसीलिए ParseJson से एक परत और खोलनी पड़ती है, तभी { /ai/summary }·{ /ai/tags } के रूप में मान निकाले जा सकते हैं। टैग को array के रूप में जैसा है वैसा ही Array (element ShortText) field में लिखा जाता है।

संरचित आउटपुट (response_format) से अनुबंध बाँधने पर भी, प्रतिक्रिया लंबाई सीमा से कट जाए या मॉडल अनुरोध ठुकरा दे, तो JSON न होने वाली चीज़ आती है। इसलिए पार्सिंग को Try में लपेटा गया है, जो पार्स विफलता को 502 में बदल देता है। विफलता संदेश में वह टेक्स्ट आता है जिसे पार्स करने की कोशिश हुई, तो देखा जा सकता है कि क्या लौटा। जिस API का envelope ही JSON नहीं है, उसके लिए Http को responseType: "Text" दें और { /resp/body } सीधे सौंप दें (Http, ParseJson देखें)।

समानांतर

13. दो समानांतर बाहरी कॉल मर्ज करके Content

{ "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. साइन-अप समीक्षा: समानांतर स्कोर के बाद and निर्णय

{ "method": "Post",
  "statements": [
    { "type": "Parallel", "branches": [
      [ { "type": "Http", "method": "POST", "url": "https://api.fraud.com/score",
          "headers": [ { "key": "x-api-key", "value": "...", "secret": true } ],
          "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": "...", "secret": true } ], "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 } ] } ] }

{ /ptr } को URL पथ में भी डाला जा सकता है। ब्रांच के परिणाम जॉइन के बाद संदर्भित किए जाते हैं। बाहरी कॉल 2 हैं। एक परिभाषा कितनी बाहरी कॉल रख सकती है यह प्लान-वार सीमा है (मूल्य योजनाएं देखें), इसलिए जाँच लें कि वह उस सीमा के भीतर है।

लूप और एग्रीगेशन

Loop और ResourceForEach समय बजट में body (onEach) द्वारा घोषित समय को पुनरावृत्ति की ऊपरी सीमा से गुणा किए हुए मान के रूप में आते हैं। इस खंड के उदाहरणों की body में कोई बाहरी कॉल नहीं है, इसलिए घोषित समय 0 है और 30 सेकंड का मूल बजट ही वास्तविक सीमा है। पुनरावृत्ति वास्तव में जहाँ अटकती है वह जगह भी यही है (समय बजट, Loop)।

15. ऐरे इनपुट से N Content (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): स्लॉट सीड

{ "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 from से to तक समावेशी होता है (पूर्णांक लिटरल, step डिफ़ॉल्ट 1)। name वर्तमान काउंटर को { /i } पर बाइंड करता है।

17. cascade डिलीट (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 }" } } } ] }

ResourceForEach मैच को आंतरिक रूप से पेज करते हुए हर आइटम को डिलीट करता है, इसलिए बिना किसी मैनुअल पेजिनेशन के शर्त से मेल खाती सभी टिप्पणियाँ (प्लेटफ़ॉर्म सीमा तक) मिटाकर फिर पोस्ट को स्वयं मिटाया जाता है। onEach का कोई घोषित समय नहीं है, इसलिए इस परिभाषा का निष्पादन समय बजट 30 सेकंड है।

18. लूप संचय: SetVar योग

{ "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. शर्त से मेल खाते सभी आइटम का थोक प्रोसेसिंग

{ "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 }" } } } ] } ] }

ResourceForEach मैच को आंतरिक रूप से पेज करता है, इसलिए किसी cursor loop (Loop while + SetVar संचय) की आवश्यकता नहीं। शर्त से मेल खाते सभी draft ढूँढकर हर आइटम को publish किया जाता है। संख्या बहुत अधिक होने के कारण पूरा करना कठिन हो, तो limit से एक बार में संसाधित की जाने वाली ऊपरी सीमा तय करें, और where को "असंसाधित" शर्त पर रखकर पुनः निष्पादन से आगे संसाधित करें।

20. ईमेल सूची से id बैच संग्रह (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 }" } } } ] }

पठन (ResourceFind) कोई बाहरी कॉल नहीं है, इसलिए यह Loop body में अनुमत है। अस्तित्व और अनुपस्थिति को अलग-अलग merge से संचित किया जाता है।

सागा और कॉनकरेंसी

21. भुगतान सागा (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 sk-...", "secret": true } ],
          "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 }" } } } ] } ] }

आरक्षण (draft) के बाद, भुगतान सफल होने पर पुष्टि, publish और 201; विफलता पर catch आरक्षण को डिलीट (क्षतिपूर्ति) करता है और 402; finally हमेशा लॉग करता है। डिलीट-आधारित क्षतिपूर्ति एक नई sys.id बनाता है, इसलिए यह उस सीमा में आता है जहां संदर्भ टूट जाते हैं (निष्पादन सिमेंटिक्स में ट्रांज़ैक्शन का अभाव और क्षतिपूर्ति देखें)।

22. आशावादी लॉकिंग 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, फिर से प्रयास करें" }, "isError": true, "statusCode": 409 } ] } ] }

स्टॉक को पढ़कर एक ताज़ा sys.version प्राप्त करने के बाद उसी वर्शन से कटौती की जाती है (version)। यदि पठन और लेखन के बीच किसी अन्य निष्पादन ने मान बदल दिया हो, तो वर्शन मेल न खाने के कारण abort हो जाता है और catch 409 लौटाता है। स्टॉक अपर्याप्त होने वाला guard Try के बाहर है (सामान्य पूर्व-समाप्ति)।

ईमेल

23. हर ऑर्डर के खरीदार को सूचना मेल (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": "आपकी शिपिंग शुरू हो गई है",
          "body": "<p>आपके ऑर्डर किए गए उत्पाद की शिपिंग शुरू हो गई है।</p>" },
        { "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /order/sys/id }" } },
          "fields": { "notified": { "en-US": true } } } ] } ] }

अभी तक जिन ऑर्डर की सूचना नहीं भेजी गई (जिनका fields.notified true नहीं है) उन पर पुनरावृत्ति करते हुए, हर ऑर्डर के खरीदार को मेल भेजा जाता है और तुरंत notified चिह्नित कर दिया जाता है। EmailSend एक मेल में एक ही प्राप्तकर्ता लेता है, इसलिए बहु-मेल भेजना इसी तरह ResourceForEach से हर आइटम पर भेजा जाता है (onEach में बाहरी कॉल रखी जा सकती हैं)। toServiceUser से देने पर सदस्य का पता Script के वेरिएबल स्थान में नहीं आता और भेजने से ठीक पहले resolve होता है। where को "असंसाधित" पर रखकर और onEach के अंत में पूर्णता चिह्नित करने के कारण, बीच में रुकने पर भी पुनः निष्पादन शेष ऑर्डर से आगे बढ़ता है (साइड-इफ़ेक्ट सफल होने के तुरंत बाद यदि चिह्नित करना विफल हो जाए, तो वह मामला अगले निष्पादन में डुप्लिकेट हो सकता है; at-least-once)।

हस्ताक्षर सत्यापन

भुगतान प्रदाता (PG·MoR) webhook भेजते समय body के साथ एक हस्ताक्षर जोड़ते हैं। लेने वाले पक्ष को कुछ भी करने से पहले यह जाँचना चाहिए कि वह हस्ताक्षर उसके पास मौजूद गुप्त key से पुनःनिर्मित होता है या नहीं। नीचे दिए दोनों उदाहरण व्यवहार में मिलने वाले दो अलग तरीके हैं। एक में गुप्त key से कोड बनाया जाता है (keyed), और दूसरे में field और गुप्त key को जोड़कर डाइजेस्ट की गणना की जाती है।

24. webhook हस्ताक्षर सत्यापन (लिपटे हेडर को खोलना, 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": "whsec_9f2c1b7ae4",
      "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 } } ] }

प्रदाता timestamp और कोड को एक ही हेडर में साथ भेजता है (t=1492774577,v1=<64 अक्षर hex>), इसलिए हेडर को खोलने से पहले हस्ताक्षर के लिए संदेश बनाया ही नहीं जा सकता। इसीलिए क्रम इस तरह तय होता है।

  1. Regex का Capture हेडर को खोलकर उसे { /sig/1 } (timestamp) और { /sig/2 } (कोड) में बाँट देता है। index 0 पूरा मिलान होता है और 1 से capture group शुरू होते हैं। प्रारूप मेल न खाए, तो { /sig } null होता है और वहीं से 400 लौटा दिया जाता है।
  2. Signature "<timestamp>.<मूल body>" को संदेश मानकर कोड बनाता है और उसकी { /sig/2 } से तुलना करता है। मुख्य बात संदेश को पार्स करने से पहले की मूल body ({ /rawPayload }) के रूप में लेना है। पार्स किए गए /payload को दोबारा string बनाने पर space और अंकों का लेखन सामान्यीकृत हो जाते हैं और वह सामने वाले पक्ष द्वारा हस्ताक्षरित bytes पर नहीं लौटता। दो पॉइंटर को एक ही string में साथ रखने पर वे ज्यों-के-त्यों जुड़ जाते हैं, इसलिए किसी ऑपरेटर की आवश्यकता नहीं होती।
  3. { /verified } false हो, तो 401 है। हस्ताक्षर गलत होना और हेडर न होना, दोनों false के रूप में एक ही हैं (इनमें से क्या गलत था, यह भेजने वाले पक्ष को नहीं बताया जाता)।
  4. हस्ताक्षर सही होने पर भी पुराने अनुरोध अस्वीकार किए जाते हैं। { /now/seconds } इस निष्पादन के शुरू होने का समय है, इसलिए हस्ताक्षर में शामिल timestamp से उसका अंतर replay window (यहाँ 300 सेकंड) से अधिक है या नहीं, यह देखा जाता है। timestamp हेडर से string के रूप में आया है, पर अंकगणितीय संक्रिया उसे संख्या में बदल देती है।
  5. यहाँ तक पास होने के बाद ही ऑर्डर ढूँढकर उसकी स्थिति बदली जाती है।

कोई बाहरी कॉल न होने से कोई घोषित समय नहीं है, इसलिए यह 30 सेकंड के मूल बजट के भीतर पूरा हो जाता है और प्रदाता को प्रतिक्रिया वहीं मिल जाती है। सत्यापन में उपयोग होने वाला secret, Http हेडर के secret: true के विपरीत एन्क्रिप्ट होकर संग्रहीत नहीं होता, इसलिए इस Script को पढ़ सकने वाली भूमिकाओं को सीमित रखें (सुरक्षा मॉडल का secret हेडर)।

प्रदाता इस खिड़की को बुला सके, इसके दो तरीके हैं, और मोड़ यह है कि वह प्रदाता कस्टम हेडर भेज सकता है या नहीं।

  • भेज सकता हो, तो उस Script की केवल Execute अनुमति रखने वाला टोकन जारी करके उसे Authorization हेडर में डलवाएँ और /execute बुलवाएँ। किसी एक विशिष्ट Script तक सीमित करने वाली भूमिका SpaceRole की script अनुमति में और टोकन Space Access Token में दिया गया है। यही मुख्य तरीका है।
  • न भेज सकता हो (ऐसा प्रदाता जो केवल कॉलबैक URL दर्ज कर सकता है और जिसके पास हेडर जोड़ने की कोई सेटिंग नहीं है), तो उस Script का anonymousCallEnabled चालू करें और /execute/anonymous का पता कॉलबैक के रूप में दर्ज करें। इस स्थिति में निष्पादन लेखक की पहचान से होता है, इसलिए यह Script जिस ऑर्डर को बदलती है उसका updatedBy भी लेखक ही होता है, और प्रमाणीकरण न होने के कारण ऊपर किया गया हस्ताक्षर सत्यापन ही इस खिड़की का एकमात्र प्रमाणीकरण बन जाता है। शर्तें और सेव के नियम अनाम कॉल में दिए गए हैं।

ऊपर दी गई परिभाषा ज्यों-की-त्यों अनाम Script की शर्तें पूरी करती है। वह createdBy: ":self" फ़िल्टर का उपयोग नहीं करती, और हस्ताक्षर एवं replay window पास न करने वाले अनुरोध को कुछ भी छूने से पहले Return से रोक देती है।

25. बिना key वाले हैश हस्ताक्षर का सत्यापन

{ "method": "Post",
  "statements": [
    { "type": "Hash", "name": "expectedSign", "algorithm": "SHA256", "encoding": "HexUpper",
      "value": "{ /payload/orderId }{ /payload/amount }9f2c1b7ae4" },
    { "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 } } ] }

यह HMAC के बजाय "तय किए गए field और गुप्त key को जोड़कर SHA256 की गणना" करने वाला तरीका है। Hash में secret field नहीं होता, और गुप्त key (9f2c1b7ae4) को उस स्कीम में जिस जगह रखा जाता है, वहीं value के भीतर सीधे लिखा जाता है। हर स्कीम में key आगे, पीछे या बीच में आ सकती है, इसलिए यही तरीका सभी जगहों को व्यक्त कर पाता है।

encoding सामने वाले पक्ष के लेखन के अनुसार रखें (Hex·HexUpper·Base64·Base64Url)। Signature के विपरीत यहाँ परिणाम एक string है, इसलिए तुलना स्वयं करनी पड़ती है, और वह तुलना सामान्य समता तुलना है। value की ऊपरी सीमा 128 अक्षर है, इसलिए पूरी body पर गणना करने वाली स्कीम के लिए Signature का उपयोग करें।

सदस्य लुकअप

26. ईमेल से सदस्य ढूँढकर कूपन और सूचना मेल

{ "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": "आपका कूपन जारी हो गया है",
      "body": "<p>{ /member/nickname } जी को कूपन { /coupon/fields/code/en-US } दिया गया है।</p>" },
    { "type": "Return", "value": { "ok": true, "memberId": "{ /member/sys/id }" } } ] }

एक ईमेल से सदस्य ढूँढकर, उसकी sys.id को कूपन के स्वामी और मेल के प्राप्तकर्ता के रूप में उपयोग किया जाता है। सदस्य डायरेक्टरी पढ़ते समय के नियम इस प्रकार हैं।

  • sys.email एन्क्रिप्ट होकर संग्रहीत होता है, इसलिए यह केवल ठीक-ठीक मिलान वाले operators लेता है (eq·ne·in·nin)। prefix जैसा कोई दूसरा operator देने पर चुपचाप 0 परिणाम नहीं, बल्कि निष्पादन विफल हो जाता है।
  • मिलान न हो, तो ResourceFind null bind करता है, इसलिए अस्तित्व के आधार पर शाखा उसी आकार में बनाई जाती है जैसी Content ढूँढते समय बनती है।
  • सदस्य के field Content और Media के विपरीत locale map नहीं होते। इन्हें { /member/nickname } की तरह ज्यों-का-त्यों संदर्भित करें।
  • मेल भेजते समय पता निकाले बिना toServiceUser में sys.id दी जाती है। engine भेजने से ठीक पहले पते को resolve करता है, इसलिए सदस्य का पता Script के वेरिएबल स्थान में नहीं आता।
  • इस परिभाषा को सेव करने के लिए लेखक की SpaceRole के settings में SETTING_SERVICE_LOGIN होना आवश्यक है। सदस्य को बनाने, बदलने या हटाने वाला statement किसी भी भूमिका से सेव नहीं होता (सदस्य डायरेक्टरी का पठन)।

EmailSend अपने timeoutMs (घोषित न हो तो 10 सेकंड) को निष्पादन के समय बजट में जोड़ता है, और प्रति परिभाषा बाहरी कॉल की सीमा में एक के रूप में गिना जाता है।