Statement カタログ
statements 配列の各要素が 1 つの Statement です。このドキュメントでは 25 種の Statement のフィールド、動作、結果を整理します。すべての値の箇所は 値式 の規則(参照、リテラル、JsonLogic、ロケールマップ)に従います(例外は Regex の pattern、Cache の key、PdfWatermark の color の 3 つです。Regex、Cache、ファイル変換 参照)。
Statement 概要
| 分類 | type | 一行要約 |
|---|---|---|
| リソース書き込み | ResourceCreate | Content/Media を生成(任意で公開) |
ResourceUpdate | Content/Media フィールドの全体置換(渡さなかった field・locale は削除) | |
ResourcePatch | Content/Media フィールドの部分マージ(指定した field・locale のみ。リテラル null は削除) | |
ResourceDelete | 削除(Draft・Archived のみ。Published の場合はまず unpublish) | |
ResourcePublish / ResourceUnpublish | 公開 / 公開取り消し | |
ResourceArchive / ResourceUnarchive | アーカイブ / アーカイブ解除 | |
| リソース読み取り | ResourceRead | id で単一件取得 |
ResourceFind | フィルターで最初にマッチした単一件(なければ null) | |
ResourceForEach | フィルターにマッチするリソースを内部的に巡回し、項目ごとに onEach を実行 | |
ResourceCount | フィルターにマッチする件数だけを数える(項目は読み取らない) | |
| 外部 | Http | 外部 HTTP 呼び出し({ status, body })。ファイルを本文として送信可能 |
EmailSend | 登録済みの EmailAccount でメール 1 通を送信 | |
| 変数 | SetVar | script-scoped 変数の宣言/更新 |
| キャッシュ | Cache | その Script だけの短命なキャッシュを読み取り/書き込み/削除 |
| 値のパース | ParseJson | JSON テキストを値(オブジェクト・配列・スカラー)にパースしてバインド |
| 署名とテキスト | Signature | 受け取った署名コードが秘密鍵で作ったコードと同じかを検証(Boolean) |
Hash | 鍵なしのダイジェスト計算(文字列) | |
Regex | 正規表現の適用。一致の有無(Boolean)またはキャプチャグループ(配列) | |
| 制御フロー | If | 条件分岐 |
Loop | 繰り返し(foreach / while / counted) | |
Parallel | ブランチの同時実行 | |
Return | 結果の返却と早期終了 | |
Try | 例外処理(catch/finally) |
id で対象を指定せずに Content を扱う Statement には、Content Type を必ず書きます。
ResourceFind・ResourceForEach・ResourceCountは、resourceが"Content"ならcontentTypeが必須です。Space 全体を横断する Content の照会はありません。ResourceCreateも作成する Content Type を書きます。Media は Space 全体で 1 組のため範囲を切らず、id で対象を指定する Statement(ResourceRead・ResourceUpdate・ResourcePatch・ResourceDeleteと公開・アーカイブの Statement)はtargetがあるため範囲が不要です。
循環呼び出しは最大 3 回まで。 上のリソース書き込み Statement(
ResourceCreate・ResourceUpdate・ResourcePublishなど)でpropagateEventsを有効にすると(デフォルトは無効)、変更イベントが発生し、そのイベントが Webhook を通じて再び Script を実行することがあります。このように続く連鎖(Script → イベント → Webhook → Script → …)は最大 3 回までしか続かず、それを超えると自動的に打ち切られて無限循環を防ぎます。
共通フィールド
{ "type": "<StatementType>", "name": "<任意、script 内で一意>", /* ...タイプ別フィールド... */ }type: 判別子です。上表の値のいずれかです(必須)。name: 任意です。付けると結果が/<name>としてコンテキストにバインドされ、以降の Statement が{ /<name>/... }で参照します。結果を使わない場合は省略します。- バインディング名の規則:
nameはコンテキストルートにそのまま載るキーなので、保存時に検証されます。使えるのは英数字と_・-だけで(JSON Pointer のキーとして使える必要があるため、それ以外の文字や空の名前は拒否されます)、予約ルート(payload・rawPayload・headers・vars・error・now・secrets)と同じであってはならず、1 つの Script 内で一意でなければなりません。違反した場合(形式違反・予約語の使用・重複)は保存が拒否されます。
エンティティ参照の形式
contentType、target などのエンティティ参照は { "sys": { "id": <値式> } } という 1 つの形式に統一します。sys.id だけが必要で、対象タイプは resource から推論します(sys.type と sys.targetType は省略)。
contentType.sys.idは通常リテラルです(例:"ct_post")。target.sys.idは通常{ /ptr }値式です(実行時に resolve。例:{ /payload/sys/id })。
resource
リソース系の Statement は対象の種類を resource: "Content" | "ContentType" | "Media" | "ServiceUser" で指定します。
Content Type を受け取るのは ResourceCount だけです。他の Statement に書くと保存が拒否されます。様式そのものを作ったり直したりする作業は、Script ではなく CMA の担当です。
ServiceUser(製品にサインアップした会員)を受け取る Statement は、読み取りの 3 つの Statement(ResourceRead・ResourceFind・ResourceForEach)と ResourceUpdate・ResourcePatch です。この 2 つが書き込めるフィールドは roleOverride と enableLogin の 2 つだけです。それ以外の書き込み Statement に書くと保存が拒否されます(エラー 参照)。規則は 会員ディレクトリを扱う で扱います。
リソース書き込み
書き込み Statement は propagateEvents(デフォルト false)を持ちます(ServiceUser に書き込む ResourceUpdate・ResourcePatch はこのフィールドを受け取りません)。true にすると、その書き込みが変更イベントを発生させ、Webhook のような後続の動作が実行されます。デフォルトは発生させません(サイレントなシステム書き込み)。
ResourceCreate
Content または Media を作成します。Content と Media は fields モデルを共有し、値はロケールマップです。
| フィールド | 対象 | 説明 |
|---|---|---|
resource | 共通 | "Content" または "Media"(必須) |
contentType | Content | 作成する Content Type({ sys: { id } })。Content の場合は必須 |
fields | 共通 | フィールドマップ { "<field>": { "<locale>": 値 } }。populate するフィールドごとにデフォルトロケールのバケットが必須。Content のキーは Content Type 定義に従い、Media のキーは固定(title・description・file) |
locale | 共通 | (便宜)指定すると fields の各値を { <locale>: 値 } に自動ラップ |
publish | 共通 | 書き込み後に公開(CDA/ACDA へ露出)。デフォルト true |
Mediaのfile:fields.file.{locale}の値はインジェスト指示{ "source": <値式>, "encoding": "url"|"base64", "transformations": [ … ] }です。sourceとencodingは必須で、transformationsは受け取ったファイルを保存する前に変更する場合にのみ書きます(ファイル変換)。ファイルを含む書き込みでは、エンジンがインジェストを実行します(urlならダウンロード、base64ならデコードしたうえでアップロードして処理します)。このインジェストは宣言する時間がないため 30 秒の基本予算から出ていき(時間予算)、外部呼び出しの上限には数えられません。ファイルのない(fileless)Media も作成できます。publish:trueでファイルがない、または処理が未完了の場合は公開段階でエラーになり、publish:falseの場合はそのままDraftです。- 結果(
nameバインディング): 作成されたリソースです。{ /<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 はインジェスト指示
{ "type": "ResourceCreate", "resource": "Media",
"fields": {
"title": { "en-US": "{ /payload/fields/prompt }" },
"file": { "en-US": { "source": "{ /gen/body/data/0/url }", "encoding": "url" } }
}, "name": "img" }ファイル変換 (transformations)
電子書籍を販売するとき、購入した人のメールアドレスを PDF のすべてのページに印字したコピーを渡したい場合があります。インジェスト指示に transformations を書くと、受け取ったファイルを保存する前に変更できます。対応している変換は PdfWatermark だけです。
| インジェスト指示のフィールド | 必須 | 説明 |
|---|---|---|
transformations | 任意 | 変換オブジェクトの配列。省略するか空の配列にすると、受け取ったファイルを変更せずに Media に入れます。配列でなければ Script の保存が拒否されます |
- 配列の順序どおりに適用します。 前の変換の結果が次の変換の入力になり、最後の結果が Media のファイルになります。
ResourceCreate・ResourceUpdate・ResourcePatchのインジェスト指示のどこにでも書けます。sourceが指す元のファイルは変更されません。 すでに Space にあるファイルに文言を印字するには、ResourceReadでその Media を読み取り、{ /<name>/fields/file/<locale>/url }をsourceに入れて新しい Media を作成します。- 変換もインジェストの一部です。 かかる時間は 30 秒の基本予算から出ていきます(時間予算)。
PdfWatermark: PDF のすべてのページに文言を印字する
| フィールド | 必須 | 説明 |
|---|---|---|
type | 必須 | "PdfWatermark"。他の値の場合は Script の保存が拒否されます |
text | 必須 | 印字する文言。文字列で、{ /pointer } 参照を混ぜて書けます。JsonLogic の演算オブジェクトは受け付けません。使える文字からなる 1 行、1〜64 文字 でなければなりません |
color | 任意 | 文字の色。#RRGGBB(16 進数 6 桁)で書き、省略すると #808080(グレー)です。値式ではなくリテラルのみを受け付け、形式が異なると Script の保存が拒否されます |
- 表にないフィールドは無視します。文字サイズ、位置、透明度は指定できません。
- すべてのページに印字します。 1 行の文言が、ページの中央を通る対角線(左下から右上)に沿って配置されます。
- 本文の上に不透明度 30% で重ねて印字します。 文言の下にある元の内容はそのまま読めます。
- 文字サイズはページごとに自動で決まります。 文言が対角線の長さの 80% を占めるサイズで印字しますが、ページの短辺の 15% を超えることはありません。短い文言は大きく、長い文言は小さく印字され、ページの外にはみ出しません。
- フォントは PDF 標準フォントの Helvetica です。
- PDF かどうかは、ファイル名や拡張子ではなく内容で判別します。
// statements 配列の一部。購入した電子書籍 PDF に購入者のメールアドレスと注文番号を印字した新しい Media
{ "type": "ResourceRead", "resource": "Media",
"target": { "sys": { "id": "{ /payload/fields/ebookMediaId }" } }, "name": "ebook" },
{ "type": "ResourceCreate", "resource": "Media", "locale": "en-US", "publish": false,
"fields": {
"title": "{ /ebook/fields/title/en-US }",
"file": {
"source": "{ /ebook/fields/file/en-US/url }", "encoding": "url",
"transformations": [
{ "type": "PdfWatermark", "text": "{ /payload/fields/email } · { /payload/fields/orderId }", "color": "#B22222" }
]
}
}, "name": "stamped" }購入者 1 人のためのコピーなので、publish を false にして CDA/ACDA で配信されないようにしています。省略するとデフォルト値 true に従って公開されます。
PdfWatermark の文言の制限
text には、PDF 標準フォントが描画できる文字(WinAnsi 文字セット)だけを使えます。
| 種類 | 文字 |
|---|---|
| ASCII 印字可能文字 | 空白、英字の大文字・小文字、数字、@ . # { } などの基本的な記号 |
| Latin-1 文字 | é ü ñ ß · © ° などの西ヨーロッパの文字と記号。ノーブレークスペース(U+00A0)とソフトハイフン(U+00AD)は除く |
| 追加の記号 | € ‚ ƒ „ … † ‡ ˆ ‰ Š ‹ Œ Ž ‘ ’ “ ” • ˜ ™ š › œ ž Ÿ、そして 2 種類のダッシュ(U+2013、U+2014) |
これ以外の文字は使えません。表にないラテン拡張文字(ł、ő、ș など)、ラテン文字以外の文字、絵文字、そして改行とタブがこれに該当します。
Script を保存するときは、書いたとおりの text を検査します。{ /pointer } を含む text は resolve された値が実行時点まで分からないため、実行時に resolve された値を改めて検査します。保存時に発生するエラーのコードは エラー にあります。
| 制限 | リテラルの text | { /pointer } を含む text |
|---|---|---|
| 使える文字からなる 1 行 | Script の保存を拒否 | 書いた文字列全体(ポインターのパスを含む)を保存時に検査して Script の保存を拒否。resolve された値は実行時に改めて検査し、status 400 で失敗 |
| 1〜64 文字 | Script の保存を拒否 | 保存時には検査しない。実行時に resolve された値の長さで検査し、status 400 で失敗 |
- 固定文言の長さを計算して書いてください。
"Purchased by { /payload/fields/email } · Order { /payload/fields/orderId }"はテンプレート外の固定文言が 22 文字なので、メールアドレスと注文番号に残る長さは合わせて 42 文字です。これより長い実際の値が入ると、Script は保存されますが、その実行は失敗します。 - 名前よりも識別子を印字してください。 表にない文字を含む購入者名は、実行時に失敗します。ASCII からなるメールアドレスや注文番号は印字できます。
ファイル変換が失敗する場合
以下の場合、その Statement は失敗します。他のランタイム失敗と同様に Try/catch で処理できます。変換は Media を書き込む前に行われるため、失敗すると新しい Media は作成されず、ResourceUpdate・ResourcePatch の対象も変更されません。
| 状況 | 結果 |
|---|---|
source のファイルが PDF ではない | status 422 で失敗 |
| PDF を読み取れない、または暗号化されている(開くときにパスワードを求めず、権限だけを制限した PDF を含む) | status 422 で失敗 |
resolve された text が空、または空白だけである | status 400 で失敗 |
resolve された text が 1〜64 文字の範囲を外れている、または使えない文字を含んでいる | status 400 で失敗 |
ResourceUpdate
対象の Content・Media・ServiceUser のフィールドを全体置換します(PUT)。fields に渡したものがそのまま新しいフィールドになり、ここにない field と locale は消去されます。一部だけ変更するには ResourcePatch を使います。
| フィールド | 説明 |
|---|---|
resource | "Content"・"Media"・"ServiceUser"。ServiceUser に書き込めるフィールドと制約は 会員ディレクトリを扱う |
target | 対象({ sys: { id } }、必須)。id は通常 { /ptr } |
fields | 書き込むフィールド全体。値はロケールマップ。全体置換なので、ここにない field と locale は削除されます。Media file はインジェスト指示(上記 ResourceCreate 参照)。列挙したファイルは常に再インジェストし、渡さなかった locale のファイルは削除 |
locale | (便宜)fields を自動ラップ |
version | (任意)値式(Int)。楽観的ロック。指定すると対象の現在の sys.version と一致する場合のみ更新し、不一致の場合はバージョン競合エラーで abort します(Try で catch 可能)。省略時は検査なし(last-write-wins)。ServiceUser は受け取りません |
publish | 更新後に republish。デフォルト true |
Media で metadata だけを変更しようと Update を使うと file が抜けてファイルがすべて削除されます(全体置換なので)。部分的な変更には必ず ResourcePatch を使います。
{ "type": "ResourceUpdate", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } },
"fields": { "title": { "en-US": "Hello", "ko-KR": "안녕" }, "status": { "en-US": "published" } } }ResourcePatch
対象の Content・Media・ServiceUser のフィールドを部分マージします(PATCH)。fields に渡したフィールド(およびその中のロケール)のみを上書きし、言及しなかったフィールドとロケールはそのまま維持します。値の形式、locale、version、publish は ResourceUpdate と同じです。
| フィールド | 説明 |
|---|---|
resource | "Content"・"Media"・"ServiceUser"。ServiceUser に書き込めるフィールドと制約は 会員ディレクトリを扱う |
target | 対象({ sys: { id } }、必須)。id は通常 { /ptr } |
fields | 上書きするフィールド。値はロケールマップ。指定したフィールドとロケールバケットのみを更新(残りは維持)。値がリテラル null の場合はその (field, locale) を削除。Media file はインジェスト指示(上記 ResourceCreate 参照) |
locale | (便宜)fields を自動ラップ |
version | (任意)ResourceUpdate と同じ(楽観的ロック)。ServiceUser は受け取りません |
publish | 更新後に republish。デフォルト true |
- 特定のロケールやファイルの削除: 値にリテラル
nullを指定します。例:"title": { "fr-FR": null }(fr-FR のタイトルを削除)、"file": { "en-US": null }(en-US のファイルを削除)。値式が実行時に null と評価されるのは削除ではなくエラーです(リテラル null のみ削除)。 - Media
fileにインジェスト指示を与えると、そのロケールのファイルを置き換えます。ファイルを与えなければ維持します。
// viewCount(en-US) だけ +1。title やその他のロケールなど残りはそのまま維持
{ "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } },
"fields": { "viewCount": { "en-US": { "$+": [ "{ /payload/fields/viewCount }", 1 ] } } } }ResourceDelete
対象を削除します。Draft と Archived 状態のみ削除できます。Published や Changed の場合は拒否されるため、まず ResourceUnpublish する必要があります。Media はファイルを処理している最中であれば削除が拒否されます。auto-unpublish はしません(CMA/ACMA で同じ)。
| フィールド | 説明 |
|---|---|
resource | "Content" または "Media" |
target | 対象({ sys: { id } }、必須) |
{ "type": "ResourceDelete", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } } }ResourcePublish, ResourceUnpublish, ResourceArchive, ResourceUnarchive
対象の公開状態とアーカイブ状態を独立して制御します。4 つともフィールドは同じです。各操作の status 前提条件は CMA/ACMA と同じです。ResourcePublish は Archived からは実行できず、ファイルの処理が終わっている必要があります。ResourceUnpublish は Published・Changed からのみ、ResourceArchive は Draft からのみ、ResourceUnarchive は Archived からのみ実行できます。
| フィールド | 説明 |
|---|---|
resource | "Content" または "Media" |
target | 対象({ sys: { id } }、必須) |
version | (任意)値式(Int)。楽観的ロック。指定すると現在の sys.version と一致する場合のみ実行 |
{ "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 }" } } }リソース読み取り
ResourceRead・ResourceFind はリソースを読み取って値にバインドし、ResourceCount は件数だけを数えます。3 つとも状態を変更しません(propagateEvents はありません)。ResourceForEach も照会自体は読み取りですが、onEach にリソース書き込み Statement を含めると項目ごとにその書き込みが実行され、状態が変わります。
4 つの Statement(ResourceRead・ResourceFind・ResourceForEach・ResourceCount)はいずれも from(デフォルト Current)で、どの保存版を読み取るかを指定します。Current はコンテンツスタジオに表示される最新の下書き(CMA/ACMA が読む値)で、Published は公開スナップショット(CDA/ACDA が配信する、最後に公開した時点の値)です(ServiceUser は公開されないため Current のみを受け取ります。会員ディレクトリを扱う 参照)。
ResourceFind・ResourceForEach・ResourceCount はこれに加えて advanced(デフォルト true)で 高度な検索(Advanced Search) の有効と無効を切り替えます。書かなければ有効です。 Content 専用のため、Media・ServiceUser の読み取りでは無視されます。有効にすると where の regex・near・within 演算子とテキスト全文検索(全文検索が有効な LongText フィールドでは、eq が値を含む項目まで部分・類似マッチで見つけます)、そして fields.* での並べ替えを使えます。無効にするとこの 3 つの演算子は拒否され、テキストの eq は完全一致になり、prefix と比較・リスト演算子は高度な検索に関係なく動作します。作成または変更したばかりの項目は高度な検索への反映に少し(約 1 秒)かかるため、その直後の高度な検索では拾えないことがあります。デフォルトが有効なので、この遅延は advanced を false にしない限りすべての照会に当てはまります。書き込んだばかりの項目をすぐに読み取る必要がある場合は、id で ResourceRead(基本の保存版で、反映の遅延はありません)を使うか、書き込みが返した sys.id で取得します。
where の createdBy: ":self" は「今呼び出したユーザーが作成したものだけ」を意味します。ただし匿名呼び出しを許可した Script(anonymousCallEnabled)では使えません。その場合 :self が呼び出し元ではなく作成者に解決され、作成者のリソースが黙って開いてしまうため、そうした定義は保存が拒否されます(匿名呼び出し 参照)。
where と order では、コンテンツのフィールドは fields.<field> と書きます(名前だけでは認識されません)。fields.<field> には Space の既定ロケールが自動的に適用されるため、ロケールを直接付けません。下の例の fields.status・fields.slug がそのまま既定ロケールの照会です。特定(非既定)のロケールだけを照会する場合にのみ、fields.<field>.<locale>(例: fields.title.ko-KR)で明示します。sys.*(sys.createdAt など)と createdBy(:self)は fields. を付けずにそのまま書きます。詳しい規則は 値式の where と order のロケール にあります。
会員ディレクトリを扱う (ServiceUser)
ResourceRead・ResourceFind・ResourceForEach は resource に "ServiceUser" を受け取り、その Space の会員ディレクトリを読み取ります(ResourceCount は受け取りません。下の ResourceCount 参照)。注文の持ち主が誰かを確認したり、メールアドレスで会員を探してその sys.id を次の Statement に渡す流れで使います。以下の規則は、会員ディレクトリを扱う 5 つの Statement にまたがります。
- 直せるフィールドは 2 つだけです。
ResourceUpdate・ResourcePatchが"ServiceUser"を受け取り、roleOverrideとenableLoginを変更します。ResourcePatchでroleOverrideにnullを渡すと、指定してあったロールが解除されます。ResourceCreate・ResourceDeleteと公開・アーカイブの Statement は"ServiceUser"を受け取らず、そうした定義は保存の時点で拒否されます。権限を足して開けるものではなく、Script が会員を作ったり消したりできないためなので、権限エラーではなく書き方を誤った Statement として拒否されます。 - 会員には
versionとpropagateEventsがありません。 どちらかを書くと実行が失敗します。ResourceUpdateは書き込めるフィールドをすべて置き換えるためenableLoginが必須で、roleOverrideを書かなければ指定してあったロールが解除されます。片方だけを変更するにはResourcePatchを使い、その場合は 2 つのフィールドのうち少なくとも 1 つを書かなければなりません。 - 作成者に会員ディレクトリの権限がなければ保存できません。 Content・Media のように権限マップで検査するのではなく、作成者の SpaceRole
settingsにSETTING_SERVICE_LOGIN(またはSETTING_ALL)があるかを見ます。会員ディレクトリは他のすべての経路でも Space 設定が管掌するリソースだからです。なければ保存が拒否されます(セキュリティモデル 参照)。 fromはCurrentのみを受け取ります。会員は公開されるリソースではないため、Publishedを渡すと実行が失敗します。contentTypeとadvancedは無視されます。 会員ディレクトリは Content Type で分かれず(Space 全体で 1 組)、高度な検索も Content 専用です。whereのsys.emailは完全一致系の演算子のみを受け取ります(eq・ne・in・nin)。会員のアドレスは暗号化して保存されるため、順序比較やprefixには意味がありません。それ以外の演算子を渡すと、黙って 0 件を返すのではなく実行が失敗します。- 結果は ServiceUser リソースそのものです。
{ /<name>/sys/id }、{ /<name>/nickname }のように参照します。構造は ServiceUser リファレンス で扱います。見つけた会員にメールを送るときはアドレスを取り出さず、EmailSendのtoServiceUserにそのsys.idを渡します(エンジンが送信の直前にアドレスを resolve するため、会員のアドレスが Script の変数空間に入りません)。
// メールアドレスで会員 1 名を探す。なければ null
{ "type": "ResourceFind", "resource": "ServiceUser",
"where": { "sys.email": { "eq": "{ /payload/fields/email }" } }, "name": "member" }ResourceRead
id で単一件取得します(get-by-id)。結果はリソース全体を名前にバインドします。
| フィールド | 説明 |
|---|---|
resource | "Content"・"Media"・"ServiceUser" |
target | 対象({ sys: { id } })。id は値式 |
from | (任意)Current(デフォルト、最新の下書き)または Published(公開スナップショット)。ServiceUser は Current のみ |
- 結果: リソースそのものがバインドされます。
nameを付けていれば{ /<name>/sys/id }、{ /<name>/fields/<field>/<locale> }で直接参照します(下の例の"name": "order"なら{ /order/sys/id })。一覧ではないので配列インデックスを経由しません。 - 対象がなければエラーです。
Tryで囲んで処理できます。
{ "type": "ResourceRead", "resource": "Content",
"target": { "sys": { "id": "{ /payload/fields/orderId }" } }, "name": "order" }ResourceFind
フィルターで最初にマッチした単一件を読み取ります。なければ null です。ユニークな業務キー(slug、email、sku)で 1 件を探すときに使います。
| フィールド | 説明 |
|---|---|
resource | "Content"・"Media"・"ServiceUser" |
contentType | 検索範囲の Content Type({ sys: { id } })。Content のとき必須。Media・ServiceUser では無視 |
where | フィルター({ "<field>": { "<op>": <値> } })。使える演算子は 演算子一覧 に従います(regex・near・within は advanced が必要)。createdBy: ":self" をサポート。ServiceUser の sys.email は eq・ne・in・nin のみ(会員ディレクトリを扱う) |
order | 複数マッチ時に「最初」を決定するソート(例: "-sys.createdAt") |
from | (任意)Current(デフォルト、最新の下書き)または Published(公開スナップショット)。ServiceUser は Current のみ |
advanced | (任意)高度な検索(Advanced Search)で実行。Content 専用(Media・ServiceUser は無視)。デフォルト true。上の リソース読み取り の説明を参照 |
- 結果: 最初にマッチしたリソースを
nameにバインドします。{ /<name>/fields/<field>/<locale> }で直接参照します。なければnullなので、{ "==": [ "{ /<name> }", null ] }で存在の有無を分岐します(find-then-upsert の典型)。
{ "type": "ResourceFind", "resource": "Content", "contentType": { "sys": { "id": "ct_article" } },
"where": { "fields.slug": { "eq": "{ /payload/fields/slug }" } }, "name": "found" }ResourceForEach
フィルターにマッチするリソースを内部的に巡回し、項目ごとに onEach を実行します。値として使うコレクションを作るのではなく、各項目に対して作業を実行するための Statement です。draft の一括公開、条件にマッチする Content の一括修正、各項目を外部へ送信・同期するといった反復作業に使います。1 件だけ読むときは ResourceRead(id)や ResourceFind(フィルター)を使います。
| フィールド | 説明 |
|---|---|
resource | "Content"・"Media"・"ServiceUser"(必須) |
contentType | 巡回範囲の Content Type({ sys: { id } })。Content のとき必須。Media・ServiceUser では無視 |
where | フィルター({ "<field>": { "<op>": <値> } })。意味は ResourceFind の where と同じです(ServiceUser の sys.email の制約も同じです)。使える演算子は 演算子一覧 に従います(regex・near・within は advanced が必要)。createdBy: ":self" をサポート |
order | ソート(例: "sys.createdAt,sys.id")。なければプラットフォームの既定順 |
from | Current(デフォルト、最新の下書き)または Published(公開スナップショット)。ServiceUser は Current のみ |
advanced | 高度な検索(Advanced Search)で巡回。Content 専用(Media・ServiceUser は無視)。デフォルト true。上の リソース読み取り の説明を参照 |
limit | (任意、1 以上) 総処理件数の上限(ページサイズではありません)。なければプラットフォームの上限(10,000 件)まで巡回 |
name | (任意) 現在の項目をバインドする名前。繰り返しごとに新しくバインドされ、onEach 内で { /<name> } で参照します(Loop の name と同じ寿命。巡回が終わった後も最後の項目がバインドされたまま残ります)。項目を参照しない場合は省略します |
onEach | 各項目ごとに実行する子 Statement 配列(必須) |
- コレクションをバインドしません(
mapではなくforeach)。{ items, next }も cursor もありません。巡回の結果を値として受け取るのではなく、項目ごとにonEachを回します。一覧が必要ならSetVarで自分で集めます。件数だけが必要ならResourceCountを使います。 limitがなくても無限巡回ではありません。 なければプラットフォームの上限(10,000 件)まで回り、マッチが残ったままその上限に達すると失敗します(手をつけていない項目を残したまま成功と報告しないためです)。逆に 宣言したlimitに到達するのは意図した停止なので正常終了です。上限を超えるlimitは保存時に拒否されます。- cursor がありません。 完走すれば成功で、途中で止まれば(実時間・クォータ超過、
onEachの未処理の失敗)失敗であり、どの項目でなぜ失敗したかをエラーが指し示します。再開は作成者が自分のデータで表現します(whereを「未処理」にしてonEachの最後で完了を記録すれば、再実行で残ったものから続きます)。 - 時間予算では掛け算として計上されます。 この Statement が宣言する時間は、
onEachが宣言した時間に処理する項目数(limit、なければ 10,000)を掛けた値です(時間予算)。子を所有する複合 Statement なので、それ自体は 外部呼び出しの leaf 予算 には数えず、onEach内の外部呼び出し Statement が予算に数えられます。 onEachには他の Statement と同様に外部呼び出し(Http・EmailSend)や Media ファイルのインジェストを含められます(Loopのbodyと同じです)。リソースのクエリ結果を項目ごとに一度ずつ処理することが、この Statement の存在理由です。
// draft 状態の投稿をすべて見つけて項目ごとに公開
{ "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
フィルターにマッチする件数だけを数えます。項目を読み取ってこないため、一覧ではなく個数が必要なときに使います。残っている在庫を確認したり、同じ値がすでにあるかを判定したり、限度を超えたかを検査する場面です。
| フィールド | 説明 |
|---|---|
resource | "Content" または "ContentType"(必須)。Media・ServiceUser は数えられず、そう書くと保存が拒否されます |
contentType | 数える範囲の Content Type({ sys: { id } })。Content のとき必須。Content Type を数えるときは無視されます(Space 全体で 1 組) |
where | フィルター。意味は ResourceFind の where と同じです。マッチした項目をすべて数えます |
from | (任意)Current(デフォルト、最新の下書き)または Published(公開スナップショット) |
advanced | (任意)高度な検索(Advanced Search)で実行。Content 専用(Content Type を数えるときは無視)。デフォルト true。上の リソース読み取り の説明を参照 |
name | (任意)件数をバインドする名前 |
- 結果: マッチした件数を
nameにバインドします。{ /<name> }で参照して、比較や分岐に使います。 - 項目は返しません。 項目が必要なら
ResourceFind(最初にマッチした単一件)やResourceForEach(項目ごとに実行)を使います。 - 件数を得るために
ResourceForEachで巡回して数えないでください。 巡回は時間予算を項目数の分だけ掛けて確保し(時間予算)、マッチが残ったままプラットフォームの上限に達すると失敗します。数えるだけでよいなら、この Statement が一度で終わらせます。 orderとlimitはありません。数えるのに順序は必要なく、マッチしたものはすべて数えるからです。
// この投稿に付いたコメントが何件あるかを数える
{ "type": "ResourceCount", "resource": "Content", "contentType": { "sys": { "id": "ct_comment" } },
"where": { "fields.postId": { "eq": "{ /payload/sys/id }" } }, "name": "commentCount" }外部
Http
外部 HTTP を呼び出します。外部呼び出しなので、プランごとの 外部呼び出しの上限 に数えられます。時間予算 には timeoutMs が加算され、ファイルを送る場合はファイルごとにダウンロード時間も一緒に加算されます。retry を指定すると、その合計に試行回数(1 + retry)を掛けます。
| フィールド | 説明 |
|---|---|
method | "GET", "POST", "PUT", "PATCH", "DELETE" |
url | 対象 URL(値式。{ /ptr } を挿入可能) |
headers | [{ "key", "value" }]。value は値式。API キーのようなシークレット値はそのまま書かず、secrets に宣言したうえで { /secrets/<key> } として読みます(例: "Bearer { /secrets/LLM_API_KEY }")。Content-Type をここに入れると、body がその形式でシリアライズされます(下記) |
body | リクエスト body(値式または JSON)。どの形式で送られるかは Content-Type ヘッダーが決めます |
urlBody | リクエスト body として送るファイルの URL(値式)。プラットフォームがこの URL からファイルを受け取り、そのバイトをそのまま送ります(ファイルを本文として送る) |
multipartBody | multipart/form-data で送る part の配列(1 つ以上)。テキスト part とファイル part を混在させられます(ファイルを本文として送る) |
timeoutMs | この呼び出しのタイムアウト(ms) |
retry | 応答 status が 400 以上の場合に再試行する回数。デフォルト 0、上限は 2 |
ignoreStatusCode | (再試行を終えた)最終 status が 400 以上のとき、この呼び出しを失敗とみなすか。false(デフォルト)の場合は失敗として扱われ、Try/catch の対象になります。true の場合は失敗とみなさず、{ status, body } をそのままバインドします(呼び出し元が status で直接分岐) |
responseType | レスポンス本文を何として受け取るか。"Json"(デフォルト)はオブジェクト・配列にパースし、"Text" は文字列として受け取ります |
body・urlBody・multipartBodyは 3 つのうち 1 つだけを使います。 2 つ以上を書くと保存が拒否されます。3 つともなければ本文なしで送ります。- 結果:
{ status, body }です。nameを付けていれば{ /<name>/status }、{ /<name>/body/... }。bodyの形はresponseTypeが決めます。 responseTypeは成功レスポンスにのみ適用されます。 status が 400 以上のレスポンスの本文は、宣言した値に関わらず診断用としてバインドされます(JSON ならパースされた値、そうでなければ文字列)。"Json"なのに本文が JSON でない場合、この呼び出しは失敗します(Try/catchの対象)。JSON を返さない API は"Text"で受け取り、値として扱う必要があればParseJsonでパースします。"Text"はレスポンスのContent-Typeの charset でデコードし、charset がなければ UTF-8 とみなします。本文が空の場合、bodyはどちらもnullです。- 応答サイズの上限: 応答本文は最大 10MiB です。これを超えると、この呼び出しは例外で失敗し、他のランタイム失敗と同様に
Try/catchで処理できます(サイズによる失敗なのでignoreStatusCodeでは無視されません)。
{ "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, "retry": 1,
"responseType": "Json", "name": "resp" }body がどの形式で送られるか
headers に入れた Content-Type が body のシリアライズ形式を決めます。比較では大文字と小文字の違いや ;charset=… のようなパラメーターを無視し、前の部分だけを見ます。ヘッダーがない場合や値が空の場合は application/json で送ります。このヘッダーは body があるときにだけ付くので、body がなければ書いておいたヘッダーがそのまま送られます。同じキーを複数回入れると、最初の値だけが使われて 1 つにまとめられます。
宣言した形式に収められない body は、収められる形式に訂正して送ります。 ヘッダーの宣言と実際の body が食い違うことはありません。
宣言した値のまま送られる組み合わせです。
宣言した Content-Type | body の形 | 送られる body |
|---|---|---|
application/json | 何であれ | JSON |
application/x-www-form-urlencoded | オブジェクト・配列 | order[id]=A-2481&order[amount]=34000 |
text/plain | スカラー | 値そのまま |
その他(text/xml など) | 何であれ | JSON |
宣言した形式には収められないため訂正される組み合わせです。
宣言した Content-Type | body の形 | 実際に送られる Content-Type | 送られる body |
|---|---|---|---|
application/x-www-form-urlencoded | スカラー | text/plain;charset=UTF-8 | 値そのまま |
text/plain | オブジェクト・配列 | application/json | JSON |
この 2 行は、噛み合わない組み合わせのときにリクエストがどう送られるかを明らかにしたものであり、意図した形式を得る方法ではありません。body を値式で組み立てると、実行時点の payload によってスカラーになることがあり、そのときこの訂正はエラーなしに行われます。受け取る側が形式を問題にする場合は、body の形と Content-Type のどちらかを意図に合わせて修正してください。
form-urlencoded はオブジェクトをブラケットキーに、配列をインデックスに展開します。
body | 展開されるキーと値 |
|---|---|
{ "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= |
キーと値は UTF-8 でパーセントエンコードされて送られます。上の表は、キーの構造を示すためにデコードした形です。値に & や + が含まれていても、ペアの区切り文字や空白と誤解されることなくそのまま伝わります。
ネストをブラケットキーに展開する表記は広く使われている慣例であり、形式そのものの規格ではありません。 受け取る側が order[id] をネストしたオブジェクトに復元するかを確認し、復元しない場合はフラットなキーで body を構成してください。
{ "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" }ファイルを本文として送る
Media にアップロードしておいたファイルを外部 API に渡す必要がある場合があります。ファイルのバイトは値式に入れられません。代わりにファイルのある URL を指定すると、プラットフォームがその URL からファイルを受け取り、リクエスト本文として送ります。ファイル 1 つを本文全体として送るには urlBody を、ファイルとテキストフィールドを一緒にフォームとして送るには multipartBody を使います。
urlBody: ファイル 1 つを本文全体として送る
指定した URL から受け取ったファイルを変換せず、そのままリクエスト本文として送ります。アップロード URL にファイルを PUT でアップロードする API に適しています。以下は、契約書の PDF(Media)を読み取り、電子署名サービスのアップロード URL へ送る例です。
// statements 配列の一部
{ "type": "ResourceRead", "resource": "Media",
"target": { "sys": { "id": "{ /payload/fields/contractMediaId }" } }, "name": "contract" },
{ "type": "Http", "method": "PUT",
"url": "https://api.esign.example/v1/envelopes/{ /payload/fields/envelopeId }/document",
"headers": [ { "key": "Authorization", "value": "Bearer { /secrets/ESIGN_API_KEY }" },
{ "key": "Content-Type", "value": "application/pdf" } ],
"urlBody": "{ /contract/fields/file/en-US/url }", "name": "upload" }リクエストの Content-Type は次の順序で決まります。
headersにContent-Typeを指定した場合は、その値をそのまま使います。body のシリアライズ の訂正は適用されません。- 指定していない場合や値が空の場合は、ファイル URL が応答した
Content-Typeを使います。 - それもなければ
application/octet-streamを使います。
multipartBody: ファイルとテキストをフォームとして送る
multipart/form-data で送ります。配列の要素 1 つが part 1 つになり、part は配列の順序どおりに送信されます。配列には 1 つ以上の要素が必要で、空の配列は保存が拒否されます。Content-Type は常に multipart/form-data(境界文字列は自動生成)であり、headers に書いた Content-Type は無視されます。
| part フィールド | 説明 |
|---|---|
name | form フィールド名(必須)。値式ではなくリテラルです。同じ名前を複数回使うと、それぞれ別の part として送られます |
value | テキスト part の内容(値式)。結果が空なら空の値として送ります |
url | ファイル part のバイトを取得する URL(値式)。例: Media ファイルの url |
fileName | part の Content-Disposition に載るファイル名(値式)。url part でのみ使えます |
contentType | part の Content-Type(値式) |
valueとurlのうち、ちょうど 1 つを書きます。 両方を書いたり両方とも省いたりすると保存が拒否されます。valuepart にfileNameを書いても保存が拒否されます。fileNameを省略すると、urlの最後のパスセグメントをデコードして使います(クエリ文字列は除きます)。そのセグメントが空であるか制御文字を含んでいる場合はfileとして送ります。contentTypeを省略すると、valuepart はContent-Typeなしで送られ、urlpart はファイル URL が応答したContent-Typeを使います(なければapplication/octet-stream)。メディアタイプの形式ではない値を指定すると、実行時にそのHttpが status 400 で失敗します。fileNameとcontentTypeの値式の結果に改行文字が含まれていると、リクエストを送る前にそのHttpが status 400 で失敗します。
以下は、商品写真(Media)と分析オプションを画像分析サービスに一緒に送る例です。image part は fileName を書いていないため、Media ファイル URL の最後のセグメント(例: tumbler.png)がファイル名として送られます。
// statements 配列の一部
{ "type": "ResourceRead", "resource": "Media",
"target": { "sys": { "id": "{ /payload/fields/photoMediaId }" } }, "name": "photo" },
{ "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" }ファイルを送る Http で起きる失敗も 2 種類に分かれます。 タイムアウトは status 504 の応答として扱われるため、retry と ignoreStatusCode が適用されます。下の表のファイル関連の失敗には、どちらも適用されません。Http の失敗全体の分類は Http が失敗する 2 つの方式 にまとめてあります。
ignoreStatusCode: true の場合、タイムアウトすると { "status": 504, "body": "timed out after …ms" } が結果としてバインドされます。
1 回のリクエストは timeoutMs 以内に終わる必要があり、ファイル URL の応答を待つ時間もこれに含まれます。ファイルを送る場合は、そのファイルを毎秒 10,000,000 バイトの速度で送るのにかかる時間の分だけ制限時間が延びます。延びる時間は、プランのダウンロード時間にファイル数を掛けた値を超えません。ただし、ファイル URL がファイルサイズを知らせない場合は実際に送った量の分だけしか延びないため、転送が遅かったり途中で止まったりすると、より早くタイムアウトすることがあります。
以下の場合、その Http はすぐに失敗します。retry で再試行せず、ignoreStatusCode: true を指定しても失敗として扱われます。他のランタイム失敗と同様に Try/catch で処理できます。
| 状況 | 結果 |
|---|---|
| 受け取るファイルがプランの ダウンロード上限 より大きい | status 413 で失敗。ファイル URL がサイズを事前に知らせる場合は受け取る前に、知らせない場合は受け取る途中で上限を超えた時点で失敗します |
| ファイル URL が 2xx 以外の status を応答した(リダイレクトを含む)、またはファイル URL に接続できない | status 502 で失敗 |
part の contentType がメディアタイプの形式ではない | status 400 で失敗 |
part の fileName・contentType の値に改行文字が含まれている | status 400 で失敗。リクエストを送る前に失敗します |
- ダウンロード上限はファイル URL 1 つごとに適用されます。
multipartBodyにファイル part が複数ある場合は、ファイルごとに上限が別々に適用されます。 - 送ったバイトは、ファイルを含めて 料金プラン のデータ転送「外部呼び出し」に合算されます。
EmailSend
登録済みの EmailAccount を通じて メール 1 通を送ります。受け取るフィールドは SMTP/MIME にそのままマッピングされるものだけです。テンプレート id、予約送信、プロバイダー固有の拡張はありません(そうした機能が必要なら Http で該当メールサービスの API を直接呼び出します)。送信者(送信元アドレス)はここで定めず、account が指す EmailAccount から来ます。
| フィールド | 説明 |
|---|---|
account | 送信に使う EmailAccount 参照({ sys: { id } }、必須)。通常はリテラル id です。値式で渡すと送信時点で resolve されるため、保存時には検査できません |
to | 受信者アドレス(値式)。to と toServiceUser のうち ちょうど 1 つだけを使います |
toServiceUser | 受信者を ServiceUser 参照で指定({ sys: { id } }。その sys.id は値式が可能)。エンジンが 送信の直前にアドレスを resolve するため、メンバーのアドレスが Script の変数空間に入りません |
cc | Cc 受信アドレスの配列(値式) |
bcc | Bcc 受信アドレスの配列(値式) |
subject | 件名(値式、必須) |
body | 本文(値式)。常に text/html で送信されるため、プレーンテキストではなくマークアップを書きます(改行は空白に、< はタグとして解釈されます)。補間される値式の結果は、autoEscape が false でない限り HTML エスケープされます。body と templateBody のうち ちょうど 1 つだけを使います |
templateBody | 本文をテンプレートから作ります。{ source, data } の形で、body と ちょうど 1 つだけを使います。下記の テンプレートで本文を作る を参照 |
autoEscape | (任意、デフォルト true)本文に補間される値を HTML エスケープするかどうか。body の { /pointer } の結果と templateBody.data の値が対象です。false にすると、値に含まれるマークアップがそのままレンダリングされます。作成者が自分で書いたマークアップと templateBody.source のテンプレートはそもそも対象ではなく、ヘッダーフィールドも影響を受けません |
replyTo | (任意)Reply-To ヘッダー(値式)。送信者と異なることがあります(例: no-reply で送りつつ返信はサポートアドレスへ) |
timeoutMs | (任意、1 以上)この送信のタイムアウト(ms)。なければプラットフォームのデフォルト値、上限を超える値は保存時に拒否 |
- 本文は
bodyかtemplateBodyのどちらか 1 つで書きます。 両方を書いても、両方を空にしても保存が拒否されます。 - 呼び出し元が送った値は、既定では文字としてレンダリングされます。
autoEscapeが有効だと、値に含まれる<a>のようなマークアップがタグではなく文字として見えます。他人から送られてきた値が、別の人のメールにリンクやタグを埋め込めないようにするための仕組みです。管理者が自分で書いたニュースレター本文のようにマークアップを生かす必要があるときだけfalseにします。 - ヘッダーフィールドには改行が含まれる値を入れられません。
subject・replyToとすべてのアドレスが該当します。改行が入ると実行が失敗します。本文では改行がふつうの内容なので検査しません。 - 受信者の合計は最大 50 名です。
to(1 名)、cc、bccをすべて合わせて数えます(SMTP のエンベロープには cc/bcc の区別がなく、すべて受信者として送られるため合計で数えます)。超えると保存・実行で拒否されます。多くの人に送るにはResourceForEach+EmailSendで 項目ごとに 1 通ずつ送ります。 - 結果をバインドしません。 成功は「プロバイダーがメールを受け付けた」ことだけなので返す値がなく、
nameを受け取りません。再試行もしません(メールは非冪等であり、曖昧に失敗した後に再試行すると重複送信になります。そのためHttpのretryに従いません)。失敗は throw され、Tryのcatchで処理します。 - 外部呼び出しです。 プランごとの 外部呼び出しの上限 にカウントされ、時間予算 には
timeoutMs(なければ 10 秒)1 回分として計上されます(再試行しないため、Httpのように回数を掛けません)。ResourceForEachのonEach内で使えます(多件送信の標準形)。
{ "type": "EmailSend", "account": { "sys": { "id": "eml_orders" } },
"to": "{ /order/fields/email/en-US }",
"subject": "注文受付完了 (注文番号 { /order/sys/id })",
"body": "<p>ご注文を受け付けました。配送が始まりましたら再度お知らせします。</p>",
"replyTo": "support@my-shop.example" }テンプレートで本文を作る (templateBody)
メールの本文を Script の定義に直接書くと、文言を直すたびに Script を修正しなければなりません。templateBody は本文のテンプレートを Content に置き、その空欄だけを埋めさせることで、文言はコンテンツスタジオで直し、Script はそのままにしておけるようにします。
| キー | 説明 |
|---|---|
source | テンプレートが入っている場所を指す ポインター 1 つ(必須)。この Script の ResourceRead や ResourceFind が読んだ Content を指す必要があり、そのポインターの前後に他の文字を書けません。値は文字列でなければならないため、多言語 Field はロケールまで書きます(/tpl/fields/html/en-US) |
data | テンプレートの空欄を埋める値(任意)。キー名がそのまま空欄の名前になります。データ位置なので、演算を使うには演算子に $ を付け($cat)、名前が $ で始まる空欄は $$ と書きます(データ位置と式位置) |
テンプレートの中の空欄は { /<キー> } と書きます。この空欄が読むのは data だけで、テンプレートから /payload や /secrets を直接読むことはできません。data にないキーを指した空欄は 空文字でレンダリングされます。値が null のキーも同じです。body で null を指したポインターが文字 null として出力されるのとは逆なので、2 つの書き方を行き来するときは注意してください。
テンプレートの中に { の次に / が来る箇所があると(あいだに空白があっても同じです。例: CSS の a{/*コメント*/})空欄として読まれるため、\{ と書きます。
保存は通ったのに実行で止まる箇所が 3 つあります。source がオブジェクトとして解決されると(多言語 Field でロケールを書き漏らした場合)失敗します。source が空の値だと失敗します(ResourceFind が見つけられなかったか、テンプレートを読む Statement が EmailSend より後ろにある場合です)。空欄をすべて埋めた結果が空白だけでも失敗します。
source が指すテンプレートそのものはエスケープしません。エスケープの対象は data の値です。そのため、呼び出し元が送った HTML をメールに生かして入れるには、その値を data に入れて autoEscape を false にします。
{ "type": "ResourceRead", "resource": "Content", "name": "tpl",
"target": { "sys": { "id": "cnt_mail_shipping" } } },
{ "type": "EmailSend", "account": { "sys": { "id": "eml_orders" } },
"toServiceUser": { "sys": { "id": "{ /order/fields/buyer/en-US/sys/id }" } },
"subject": "配送が始まりました",
"templateBody": {
"source": "{ /tpl/fields/html/en-US }",
"data": { "name": "{ /order/fields/buyerName/en-US }",
"tracking": "{ /order/fields/trackingNo/en-US }" } } }テンプレート(cnt_mail_shipping の html Field)はこのような形です。
<p>{ /name } 様、ご注文の商品の配送が始まりました。</p>
<p>送り状番号: { /tracking }</p>変数
SetVar
script-scoped な可変変数を宣言または更新します。{ /vars/<var> } で参照します(JsonLogic には変数宣言がないため Statement として提供します)。
| フィールド | 説明 |
|---|---|
var | 変数名。{ /vars/<var> } で参照 |
value | 値式。自分自身を参照して累積できます |
{ "type": "SetVar", "var": "total", "value": 0 }
{ "type": "SetVar", "var": "total", "value": { "$+": [ "{ /vars/total }", "{ /row/qty }" ] } } // 累積
{ "type": "SetVar", "var": "ids", "value": { "$merge": [ "{ /vars/ids }", [ "{ /row/sys/id }" ] ] } } // 配列の収集キャッシュ
Cache
その Script だけの短命なキャッシュを読み書きします。外部呼び出しの結果のように、毎回取り直すのが惜しい値を数秒のあいだ持っておき、次の呼び出しで再利用するための箇所です。外部呼び出しではないので、1 定義あたりの外部呼び出し数には含まれず、時間予算に宣言する時間もありません。
| フィールド | 説明 |
|---|---|
action | "Set"(書き込み)、"Get"(読み取り)、"Delete"(削除)のいずれか(必須) |
key | キャッシュキー(Cache Key)(必須)。値式ではなくリテラルです(下記参照)。最大 128 文字で、超えると保存が拒否されます |
value | 保存する値(Set 専用)です |
ttl | キャッシュが生きている時間(Set 専用、秒)。1 から 30 のあいだで、省略すると 5 です |
defaultValue | キャッシュされたデータがないときに Get がバインドする値(Get 専用)。省略すると null です |
name | 結果を入れる名前。Get は必須です(読み取った値の行き先がなければ、読み取る理由がありません)。Set は保存した値を、Delete は削除を実行したかどうかをバインドし、どちらも任意です |
- その演算に該当するフィールドだけを書きます。
Getにttlを書いたり、SetにdefaultValueを書いたりすると保存が拒否されます。 - ないことと期限切れは区別されません。 どちらも
defaultValueがバインドされます。nullを保存しておいた場合も同じです。 - 保存の範囲はその Script 1 つです。 同じ Space の他の Script は、同じキャッシュキーを使っても互いのデータを見られません。その Script を修正または削除すると、その Script のデータはすべて消えます。
keyはリテラルです。 リクエストから来たキャッシュキーでデータを選ばせると、呼び出し元が何を読むかを決めることになり、会員ごとに 1 つずつ入れておいた Script が、ある会員の値を別の会員に渡してしまいます。そのためkeyの中に{ /pointer }があると、値に置き換えられることも文字どおり使われることもありません。保存そのものが拒否されます。- 繰り返しの中には置けません。
LoopやResourceForEachのブロックの中にCacheがあると保存が拒否されます。下記の個数の上限が、繰り返しの中では何の制限にもならないからです。繰り返しごとに 1 つずつデータを書くことになるため、定義に書かれた Statement の数と実際に使われるキャッシュキーの数が食い違います。 - 1 つの定義に 5 個まで入れられます(ネスト含む、演算に関係なく合算)。超えると保存が拒否されます。
- 保存する値は 10,240 バイト(10KiB)までです。 超えるとその Statement が失敗します(status 422)。他のランタイム失敗と同じなので、
Try/catchで局所的に処理できます。
// 為替レートを 30 秒のあいだ再利用します。
{ "type": "Cache", "action": "Get", "name": "cached", "key": "rates" }
// 持っている値があれば、外部呼び出しなしでそのまま返します
{ "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 }" }
// 持っていた値を期限切れの前に捨てます
{ "type": "Cache", "action": "Delete", "key": "rates" }値のパース
ParseJson
JSON テキストを、それが表す値にパースして名前にバインドします。Http を responseType: "Text" で受け取った本文、payload で渡された JSON 文字列、フィールドに文字列として保存しておいた JSON を扱うときに使います。外部呼び出しではないので外部呼び出しの上限には数えられず、時間予算に宣言する時間もありません。
| フィールド | 説明 |
|---|---|
name | パース結果を入れる名前(必須)。他の Statement では任意ですが、ここでは必須です。結果をバインドする以外に何もしないため、名前がないと何の効果もない Statement になります |
value | パースする JSON テキスト(値式、必須)。{ /resp/body } のように前の段階の値を指すか、JSON テキストをリテラルでそのまま書きます(リテラル内の { は { ポインター } テンプレートとして解釈されません) |
- 結果: パースされた値そのものです。オブジェクトはオブジェクト、配列は配列、
42や"a"のような単一の値もそのままパースされます。以降は{ /<name>/... }で内部を指します。 - すでにパースされた値が来た場合はそのままバインドします。
valueが文字列でない値に resolve された場合、パースするテキストではないと判断してその値をそのまま入れます。 - パースしたテキスト内の
{ /pointer }は再度解釈しません。 外部から受け取った文字列が{ /payload/... }のような表現を含んでいても、値に置き換えられず文字列として残ります。 nullは 2 つの場合を区別します。 パースするテキストがnullの一語であれば正常で、結果もnullです。一方valueが指す場所が空で値そのものがない場合は、パースするものがないため失敗です。- 失敗:
valueが値なしに resolve された場合や空白だけの場合、そしてテキストが JSON でない場合です。他のランタイム失敗と同じくTry/catchで処理し、エラーメッセージにパースしようとしたテキストが載ります。 - 定義あたりの Statement 数には 1 つとして数えますが、外部呼び出しの上限や
SetVarの上限とは無関係です。
// 1) JSON を返さない API: Text で受け取ってパース
{ "type": "Http", "method": "GET", "url": "https://api.partner.example/v1/quote",
"responseType": "Text", "name": "resp" },
{ "type": "ParseJson", "name": "quote", "value": "{ /resp/body }" },
// 2) payload で渡された JSON 文字列をパース
{ "type": "ParseJson", "name": "spec", "value": "{ /payload/fields/specJson }" }署名の検証とテキスト処理
決済代行会社が Webhook で送ってきた署名を確認し、その署名が包まれて届いた文字列を解きほどく Statement です。3 つとも外部呼び出しではなく計算なので、外部呼び出しの上限には数えられず、時間予算に宣言する時間もありません。また、データの箇所を持たないため $ プレフィックスの規則 とは無関係です。3 つを組み合わせた完結した例は クックブックの Webhook 署名検証 にあります。
3 つの Statement はいずれも resolve された値の長さに上限があります。式の長さではなく、その式が指した値の長さであり({ /rawPayload } の 16 文字が数十 KB を指します)、超えると実行が失敗するので Try で処理できます。数値は 値の長さの上限 にまとめてあります。
Signature
受け取った署名コードが secret で作ったコードと同じかを確認し、その答えを Boolean 値としてバインドします。決済代行会社(PG・MoR)が Webhook で送ってくる署名は、この Statement で検証します。
| フィールド | 説明 |
|---|---|
name | 検証結果を入れる名前(必須)。{ /<name> } が true または false です。検証しておいて結果を使わないのは検証していないのと同じなので、省略できません |
algorithm | コードを作るハッシュ(必須)。SHA1・SHA256・SHA384・SHA512 |
secret | 相手と共有した秘密鍵(値式、必須)。鍵は secrets に宣言し、{ /secrets/<key> } と書きます |
secretEncoding | secret が指す鍵の文字列がどの表記か。Utf8(デフォルト、テキストの鍵)・Hex・Base64。hex や base64 で発行された鍵をテキストのまま置くと別の鍵になり、それらしいコードは作られるものの永遠に一致しません |
value | コードを計算するメッセージ(値式、必須)。相手が署名したバイトと文字どおり同じでなければならないため、通常は { /rawPayload } か、プロバイダーがヘッダーに一緒に載せて送ってきたタイムスタンプをその前に付けたものです |
expected | 呼び出し元が送ってきたコード(値式、必須)。例: { /headers/x-signature } |
- 結果:
Booleanです。以降はIfの条件に{ /<name> }をそのまま書きます。 valueはパースされた/payloadではなく/rawPayloadで書きます。 パースした payload を再び文字列に戻すと、空白・数値表記・エスケープが正規化され、相手が署名したバイトには戻りません(コンテキストルート)。- 出力の表記を指定するフィールドはありません。
algorithmがコードのバイト長を固定し、同じ長さの hex と base64 は文字列の長さが重ならないため、相手がどちらで送ってきたかを教えてもらわなくてもエンジンがバイトを復元します。hex の大文字・小文字、base64 と base64url(パディングの有無を含む)も同じ理由で区別しません。 - 失敗と
falseは、その値を誰が与えるかで分かれます。expectedがない場合やコードが一致しない場合は、結果がfalseになるだけで失敗ではありません。ヘッダーの欠落とコードの不一致を別々に知らせると、どちらが違っているのかを送信元に教えることになるからです。valueが空なら空のメッセージで計算します。空の本文も署名の対象です。secretがない場合やsecretEncodingが宣言した表記になっていない場合は失敗です。3 つのうち作成者自身の入力はこれだけです。失敗メッセージにsecretとvalueは載りません。
valueの上限は 65,536 文字です(resolve された値が基準)。実際のプロバイダーが送ってくる Webhook 本文の大きさに合わせた値です。- 比較は値が同じかどうかを constant-time で判定します。先頭の何バイトが一致したかが応答時間から漏れません。
- 署名鍵は
secretにそのまま書きません。 定義のsecretsに宣言し、"{ /secrets/<key> }"と書きます。secretsの値は WEEGLOO が暗号化して安全に保存します。secretEncodingは、secretsに保存した鍵の文字列がどの表記かを表します。
// 本文全体に署名するプロバイダー
{ "type": "Signature", "name": "verified", "algorithm": "SHA256",
"secret": "{ /secrets/WEBHOOK_SIGNING_SECRET }", "value": "{ /rawPayload }",
"expected": "{ /headers/x-webhook-signature }" }
// 鍵を base64 で発行するプロバイダー (secrets に base64 文字列をそのまま入れておく)
{ "type": "Signature", "name": "verified", "algorithm": "SHA256",
"secret": "{ /secrets/WEBHOOK_SIGNING_KEY }", "secretEncoding": "Base64",
"value": "{ /rawPayload }", "expected": "{ /headers/webhook-signature }" }Hash
value をダイジェストして、encoding が定めた表記の文字列としてバインドします。HMAC ではなく「いくつかのフィールドと秘密鍵をつなげて SHA256 を計算する」署名スキームを再現するときに使います。
| フィールド | 説明 |
|---|---|
name | ダイジェストを入れる名前(必須) |
algorithm | MD5・SHA1・SHA256・SHA384・SHA512(必須)。MD5 はそれを要求する以前のスキームを再現するためのもので、新しく作る署名で選ぶ値ではありません |
value | ダイジェストするメッセージ(値式、必須) |
encoding | 結果の表記。Hex(デフォルト)・HexUpper・Base64・Base64Url |
secretフィールドはありません。valueの中で、そのスキームが鍵を置く位置に{ /secrets/<key> }を書きます。スキームごとに鍵が前・後・中間と分かれますが、こうすればどの位置でも表現できます。鍵はsecretsに宣言しておきます。- 結果: 文字列です。相手が送ってきたコードと比較するときは
{ "==": [ "{ /<name> }", "{ /headers/... }" ] }と書きます。この比較はSignatureの constant-time 比較とは違い、通常の等価比較です。 valueが値なしに resolve された場合や空白だけの場合は失敗です(作成者自身の値式だからです)。valueの上限は 128 文字です。つなげたフィールドをいくつか入れる箇所なのでSignatureよりはるかに狭いです。Webhook の本文全体を対象に計算する必要がある場合はSignatureを使います。
// SHA256(注文番号 + 金額 + MERCHANT_KEY) を大文字 hex で
{ "type": "Hash", "name": "expectedSign", "algorithm": "SHA256", "encoding": "HexUpper",
"value": "{ /payload/orderId }{ /payload/amount }{ /secrets/MERCHANT_KEY }" }Regex
pattern を value に適用して、mode が求めたものをバインドします。値式には文字列を切り出す手段がないため(つなげる cat と、含むかを見る in だけです)、t=…,v1=… のように 1 つのヘッダーに複数の値が包まれて届くものを解くときにこの Statement を使います。
| フィールド | 説明 |
|---|---|
name | 結果を入れる名前(必須)。Capture は要素を { /<name>/1 } で指します |
mode | "Match" は一致の有無を Boolean で、"Capture" は最初のマッチを配列でバインド(必須) |
pattern | 正規表現(必須)。値式ではなくリテラルです(下記参照)。フラグは (?i) のようにパターンの中に書きます。最大 128 文字で、超えると保存が拒否されます |
value | パターンを適用するテキスト(値式、必須)。resolve された値が 10,240 文字(10KiB)を超えると実行が失敗します |
- 結果:
MatchはBoolean、Captureは配列またはnullです。配列はインデックス0がマッチ全体で、1からがキャプチャグループであり、参加しなかったグループはnullです(空文字列ではありません。それはマッチしたということです)。パターンが現れなければCaptureは空の配列ではなくnullです。 - どちらのモードも「パターンがどこかに現れるか」を問います。 テキスト全体がパターンと同じでなければならない場合は
^…$で固定します。Matchで検査してからCaptureで取り出す 2 つの Statement が互いに違う答えを出さないよう、問いを同じにしてあります。 patternはこのエンジンで値式ではない 3 つのフィールドのうちの 1 つです(残りはCacheのkeyとPdfWatermarkのcolorです)。リクエストから来たパターンをそのまま実行すると、呼び出し元が実行される式を選べることになり、正規表現のバックトラッキングがそれをサービス拒否の手段にします。そのため、パターンの中の{ /pointer }も値に置き換えられず、文字どおりパターンの一部になります。- パターンは実行が始まるときに定義全体で 1 回コンパイルされます。
LoopやResourceForEachの中にあっても繰り返しごとに再コンパイルされることはなく、使えないパターンは最初の Statement が何かをする前に失敗します(Tryで処理可能)。
// "t=1492774577,v1=<64 文字の hex>" を解いて { /sig/1 } = タイムスタンプ、{ /sig/2 } = コード
{ "type": "Regex", "name": "sig", "mode": "Capture",
"pattern": "^t=(\\d+),v1=([0-9a-f]{64})$", "value": "{ /headers/x-provider-signature }" }
// 形式だけを検査
{ "type": "Regex", "name": "isOrderId", "mode": "Match",
"pattern": "^ORD-\\d{8}-\\d{4}$", "value": "{ /payload/orderId }" }制御フロー
If
条件分岐です。condition は JsonLogic で、真偽は 真偽判定 の規則に従います。
| フィールド | 説明 |
|---|---|
condition | JsonLogic(boolean として評価) |
then | 真の場合に実行する Statement 配列 |
else | (任意)偽の場合に実行する Statement 配列 |
{ "type": "If",
"condition": { "<": [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] },
"then": [ { "type": "Return", "value": { "ok": false, "reason": "insufficient credit" } } ],
"else": [ /* ... */ ] }Loop
繰り返しです。モードを 1 つ選びます。over(foreach)、while(条件)、for(カウント)です。どのモードでもエンジンが繰り返しの上限を強制します(無限ループの防止)。上限は maxIterations で宣言し、書かなければプラットフォームの上限が適用されます。body 内には外部呼び出し(Http・EmailSend)や Media ファイルのインジェストも含められ、外部呼び出し Statement は実行時に繰り返しごとに実際に呼び出されます。1 つの定義あたりの外部呼び出しの最大数の制限は、そのまま適用されます。
時間予算では掛け算として計上されます。 この Statement が宣言する時間は、body が宣言した時間に maxIterations(なければ 10,000)を掛けた値です(時間予算)。body に外部呼び出しがなければ宣言時間は 0 なので、30 秒の基本予算が実質的な上限です。
| フィールド | 説明 |
|---|---|
over | foreach: 配列に resolve される値式 |
while | 条件: JsonLogic(真である間繰り返す) |
for | カウント: { "from", "to", "step"? }。from から to まで含む、step はデフォルト 1 |
maxIterations | 最大繰り返し回数(任意)。書かなければプラットフォームの上限 10,000 が適用され、それより大きい値は保存時に拒否されます |
name | (任意)現在の項目(foreach)やインデックス(while・for)をバインドする名前({ /<name> }) |
body | 繰り返し本体の Statement 配列 |
// 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
ブランチを同時に実行し、join してから進みます。ブランチ間の参照は不可です(依存がある場合は順次に配置します)。
| フィールド | 説明 |
|---|---|
branches | Statement[][]。各要素が 1 つのブランチ(Statement の配列) |
{ "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
一般的なプログラミングの return です。Script の結果を呼び出し元に返し、その地点で正常終了します。
| フィールド | 説明 |
|---|---|
value | (任意)返す値式 |
isError | デフォルト false。true の場合は value が応答の error として返ります(そうでなければ return) |
statusCode | 応答ステータスコード。デフォルト 200 |
Returnに到達しなければ返り値はありません。結果を返すにはvalueを明示します。- 例外や throw ではなく正常終了なので
catchの対象ではありません(Tryの中でも Script 全体を終了しますがfinallyは実行します)。 - guard もこの Statement で表現します。
IfのthenにReturnを置くと、条件に反したときに値を返し、以降の Statement を実行しません。これはReturnの複数ある使い方のうちの 1 つです。
{ "type": "Return", "value": { "orderId": "{ /order/sys/id }", "status": "paid" }, "statusCode": 201 }
{ "type": "Return", "value": { "reason": "payment failed" }, "isError": true, "statusCode": 402 }Try
例外処理です。
| フィールド | 説明 |
|---|---|
body | 試行する Statement 配列 |
catch | (任意)body 失敗時に実行。/error に { message } を露出(どの Statement で失敗したかは入りません) |
finally | (任意)成功・失敗に関わらず常に実行 |
catchが処理すれば Script は中断されません。catchのない失敗のみが Script を中断させます(補償の試行を含む)。- 何が「失敗」なのか、補償(compensation)の限界は 実行セマンティクス、制約、セキュリティ で扱います。
{ "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": [ /* 常に実行 */ ] }関連ドキュメント
- 値式: 上記すべてのフィールドが従う値の規則。
- 実行セマンティクス、制約、セキュリティ: 実行順序、エラー、静的制約、セキュリティ。
- クックブック: これらの Statement を組み合わせた完結した例。
- Script 概要: 最上位の構造と 1 回の実行に与えられる時間。
