Statement 카탈로그
statements 배열의 각 원소가 하나의 문(statement)입니다. 이 문서는 25종 문의 필드, 동작, 결과를 정리합니다. 모든 값 자리는 값 표현식의 규칙(참조, 리터럴, JsonLogic, 로케일 맵)을 따릅니다(예외는 Regex의 pattern과 Cache의 key 둘입니다. 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 문은 다루는 Content Type을 반드시 적습니다.
ResourceFind·ResourceForEach·ResourceCount는resource가"Content"이면contentType이 필수입니다. Space 전체를 가로지르는 Content 조회는 없습니다.ResourceCreate도 만들 Content Type을 적습니다. Media는 Space 전체가 한 벌이라 범위를 지지 않고, id로 대상을 지정하는 문(ResourceRead·ResourceUpdate·ResourcePatch·ResourceDelete와 발행·보관 문)은target이 있어 범위가 필요 없습니다.
순환 호출은 최대 3회. 위 리소스 쓰기 문(
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)와 같을 수 없고, 한 Script 안에서 유일해야 합니다. 형식 위반·예약어 사용·중복이면 저장이 거부됩니다.
엔티티 참조 형태
contentType, target 같은 엔티티 참조는 { "sys": { "id": <값표현식> } } 한 형태로 통일합니다. sys.id만 필요하고 대상 타입은 resource에서 추론합니다(sys.type과 sys.targetType은 생략).
contentType.sys.id는 보통 리터럴입니다(예:"ct_post").target.sys.id는 보통{ /ptr }값표현식입니다(런타임 resolve. 예:{ /payload/sys/id }).
resource
리소스 계열 문은 대상 종류를 resource: "Content" | "ContentType" | "Media" | "ServiceUser"로 지정합니다.
Content Type은 ResourceCount만 받습니다. 다른 문에 적으면 저장이 거부됩니다. 양식 자체를 만들거나 고치는 일은 Script가 아니라 CMA의 몫입니다.
ServiceUser(제품에 가입한 회원)는 읽기 전용입니다. 읽기 세 문(ResourceRead·ResourceFind·ResourceForEach)만 이 값을 받고, 쓰기 문에 적으면 저장이 거부됩니다(오류 참조). 규칙은 회원 디렉터리 읽기에서 다룹니다.
리소스 쓰기
모든 쓰기 문은 propagateEvents(기본 false)를 갖습니다. 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 |
Mediafile:fields.file.{locale}값은 인제스트 지시{ "source": <값표현식>, "encoding": "url"|"base64" }입니다(둘 다 필수). 파일이 포함된 쓰기에서는 엔진이 인제스트를 수행합니다(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" }ResourceUpdate
대상 Content나 Media의 필드를 전체 교체합니다(PUT). fields에 준 것이 그대로 새 필드가 되고 여기 없는 field와 locale은 지워집니다. 일부만 바꾸려면 ResourcePatch를 씁니다.
| 필드 | 설명 |
|---|---|
resource | "Content" 또는 "Media" |
target | 대상({ sys: { id } }, 필수). id는 보통 { /ptr } |
fields | 쓸 필드 전체. 값은 로케일 맵. 전체 교체이므로 여기 없는 field와 locale은 제거됩니다. Media file은 인제스트 지시(위 ResourceCreate 참조). 나열한 파일은 항상 재인제스트하고, 안 준 locale 파일은 삭제 |
locale | (편의) fields 자동 래핑 |
version | (선택) 값표현식(Int). 낙관적 잠금. 주면 대상의 현재 sys.version과 일치할 때만 갱신하고 불일치면 버전 충돌 에러로 abort합니다(Try로 catch 가능). 생략 시 검사 없음(last-write-wins) |
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의 필드를 부분 병합합니다(PATCH). fields에 준 필드(그리고 그 안의 로케일)만 덮어쓰고, 언급하지 않은 필드와 로케일은 그대로 유지합니다. 값 형태, locale, version, publish는 ResourceUpdate와 동일합니다.
| 필드 | 설명 |
|---|---|
resource | "Content" 또는 "Media" |
target | 대상({ sys: { id } }, 필수). id는 보통 { /ptr } |
fields | 덮어쓸 필드. 값은 로케일 맵. 지정한 필드와 로케일 버킷만 갱신(나머지 유지). 값이 리터럴 null이면 그 (field, locale)를 삭제. Media file은 인제스트 지시(위 ResourceCreate 참조) |
locale | (편의) fields 자동 래핑 |
version | (선택) ResourceUpdate와 동일(낙관적 잠금) |
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
대상의 발행과 보관 상태를 독립적으로 제어합니다. 넷 다 필드는 동일합니다. 각 작업의 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는 건수만 셉니다. 셋 다 상태를 바꾸지 않습니다(propagateEvents 없음). ResourceForEach도 조회 자체는 읽기이지만, onEach에 리소스 쓰기 문을 담으면 항목마다 그 쓰기가 실행되어 상태가 바뀝니다.
네 문(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.* 정렬을 쓸 수 있습니다. 끄면 이 세 연산자는 거부되고 텍스트 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를 다음 문에 넘기는 흐름에 씁니다. 아래 규칙은 세 문에 공통입니다.
- 읽기만 됩니다.
ResourceCreate·ResourceUpdate·ResourcePatch·ResourceDelete와 발행·보관 문은"ServiceUser"를 받지 않고, 그런 정의는 저장 시점에 거부됩니다. 권한을 더해서 열 수 있는 것이 아니라 Script에서 회원을 변경하는 길이 아예 없으므로, 권한 오류가 아니라 잘못 쓴 문으로 거부됩니다. - 작성자에게 회원 디렉터리 권한이 있어야 저장됩니다. Content·Media처럼 권한 맵으로 검사하지 않고, 작성자의 SpaceRole
settings에SETTING_SERVICE_LOGIN(또는SETTING_ALL)이 있는지를 봅니다. 회원 디렉터리는 다른 모든 경로에서도 Space 설정이 관장하는 리소스이기 때문입니다. 없으면 저장이 거부됩니다(보안 모델 참조). from은Current만 받습니다. 회원은 발행되는 리소스가 아니므로Published를 주면 실행이 실패합니다.contentType과advanced는 무시됩니다. 회원 디렉터리는 Content Type으로 갈리지 않고(Space 전체가 한 벌), 고급 검색도 Content 전용입니다.where의sys.email은 정확 일치 계열 연산자만 받습니다(eq·ne·in·nin). 회원의 주소는 암호화되어 저장되므로 순서 비교나prefix는 뜻이 없습니다. 그 밖의 연산자를 주면 조용히 0건을 돌려주는 대신 실행이 실패합니다.- 결과는 ServiceUser 리소스 그 자체입니다.
{ /<name>/sys/id },{ /<name>/nickname }처럼 참조합니다. 구조는 ServiceUser 레퍼런스에서 다룹니다. 찾은 회원에게 메일을 보낼 때는 주소를 꺼내지 말고EmailSend의toServiceUser에 그sys.id를 넘깁니다(엔진이 전송 직전에 주소를 resolve하므로 회원 주소가 Script 변수 공간에 들어오지 않습니다).
// 이메일로 회원 한 명을 찾는다. 없으면 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)로 한 건 찾을 때 씁니다.
| 필드 | 설명 |
|---|---|
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를 실행합니다. 값으로 쓸 컬렉션을 만들지 않고 각 항목에 작업을 수행하기 위한 문입니다. draft 일괄 발행, 조건에 맞는 Content 일괄 수정, 각 항목을 외부로 발송·동기화 같은 반복 작업에 씁니다. 한 건만 읽을 때는 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끝에서 완료를 표시하면 재실행으로 남은 것부터 이어집니다). - 시간 예산에서는 곱셈으로 잡힙니다. 이 문이 선언하는 시간은
onEach가 선언한 시간에 처리 항목 수(limit, 없으면 10,000)를 곱한 값입니다(시간 예산). 자식을 소유하는 복합 statement라 그 자체는 외부 호출 leaf 예산에 세지 않고,onEach안의 외부 호출 문이 예산에 잡힙니다. onEach에는 다른 statement처럼 외부 호출(Http·EmailSend)이나 Media 파일 인제스트를 담을 수 있습니다(Loop의body와 같습니다). 리소스 쿼리 결과를 항목마다 한 번씩 처리한다는 것이 이 문의 존재 이유입니다.
// 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 전체가 한 벌) |
where | 필터. 의미는 ResourceFind의 where와 같습니다. 매치한 항목을 모두 셉니다 |
from | (선택) Current(기본, 최신 초안) 또는 Published(발행 스냅샷) |
advanced | (선택) 고급 검색(Advanced Search)으로 실행. Content 전용(Content Type을 셀 때는 무시). 기본 true. 위 리소스 읽기 설명 참조 |
name | (선택) 건수를 바인딩할 이름 |
- 결과: 매치한 건수를
name에 바인딩합니다.{ /<name> }으로 참조해 비교와 분기에 씁니다. - 항목은 돌려주지 않습니다. 항목이 필요하면
ResourceFind(첫 매치 단건)나ResourceForEach(항목마다 실행)를 씁니다. - 건수를 얻으려고
ResourceForEach로 돌면서 세지 마세요. 순회는 시간 예산을 항목 수만큼 곱해 잡고(시간 예산), 매치가 남은 채 플랫폼 상한에 걸리면 실패합니다. 셈만 필요하면 이 문이 한 번에 끝냅니다. order와limit은 없습니다. 세는 데에는 순서가 필요 없고, 매치한 것은 모두 세기 때문입니다.
// 이 게시글에 달린 댓글이 몇 개인지 센다
{ "type": "ResourceCount", "resource": "Content", "contentType": { "sys": { "id": "ct_comment" } },
"where": { "fields.postId": { "eq": "{ /payload/sys/id }" } }, "name": "commentCount" }외부
Http
외부 HTTP를 호출합니다. 외부 호출이라 플랜별 외부 호출 한도에 세어지고, 시간 예산에는 timeoutMs(없으면 30초) × (1 + retry)로 잡힙니다.
| 필드 | 설명 |
|---|---|
method | "GET", "POST", "PUT", "PATCH", "DELETE" |
url | 대상 URL(값표현식. { /ptr } 삽입 가능) |
headers | [{ "key", "value", "secret"? }]. value는 값표현식. secret:true 헤더는 CMA(관리자) 전용으로 취급되어 최종 사용자에게 노출되지 않고 전송 직전에만 복호화됩니다. Content-Type을 여기 넣으면 body가 그 형식으로 직렬화됩니다(아래) |
body | 요청 body(값표현식 또는 JSON). 어떤 형식으로 실려 나가는지는 Content-Type 헤더가 정합니다 |
timeoutMs | 이 호출의 타임아웃(ms) |
retry | 응답 status가 400 이상이면 재시도할 횟수. 기본 0, 상한 2 |
ignoreStatusCode | (재시도까지 마친) 최종 status가 400 이상일 때 이 호출을 실패로 볼지. 기본 false면 실패로 처리되어 Try/catch 대상이 됩니다. true면 실패로 보지 않고 { status, body }를 그대로 바인딩합니다(호출자가 status로 직접 분기) |
responseType | 응답 본문을 어떤 값으로 받을지. "Json"(기본)은 객체·배열로 파싱하고, "Text"는 문자열로 받습니다 |
- 결과:
{ 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 sk-...", "secret": true } ],
"body": { "prompt": "{ /payload/fields/prompt }" }, "timeoutMs": 15000, "retry": 1,
"responseType": "Json", "name": "resp" }body가 어떤 형식으로 나가는지
headers에 넣은 Content-Type이 body의 직렬화 형식을 정합니다. 비교는 대소문자와 ;charset=… 같은 파라미터를 무시하고 앞부분만 봅니다. 헤더가 없거나 값이 비어 있으면 application/json으로 보냅니다. 이 헤더는 body가 있을 때만 붙으므로, body가 없으면 적어 둔 헤더가 그대로 나갑니다. 같은 키를 여러 번 넣으면 첫 값만 쓰이고 하나로 합쳐집니다.
선언한 형식으로 담을 수 없는 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 |
이 두 줄은 짝이 어긋났을 때 요청이 어떻게 나가는지를 밝힌 것이고, 의도한 형식을 얻는 방법이 아닙니다. 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" }EmailSend
등록된 EmailAccount를 통해 메일 1통을 보냅니다. 받는 필드는 SMTP/MIME에 그대로 매핑되는 것만입니다. 템플릿 id, 예약 발송, 제공자별 확장은 없습니다(그런 기능이 필요하면 Http로 해당 메일 서비스의 API를 직접 호출합니다). 발신자(보내는 주소)는 여기서 정하지 않고 account가 가리키는 EmailAccount에서 옵니다.
| 필드 | 설명 |
|---|---|
account | 보낼 EmailAccount 참조({ sys: { id } }, 필수). 보통 리터럴 id입니다. 값표현식으로 주면 전송 시점에 resolve되므로 저장 시에는 검사할 수 없습니다 |
to | 수신자 주소(값표현식). to와 toServiceUser 중 정확히 하나만 씁니다 |
toServiceUser | 수신자를 ServiceUser 참조로 지정({ sys: { id } }. 그 sys.id는 값표현식 가능). 엔진이 전송 직전에 주소를 resolve하므로 회원의 주소가 Script 변수 공간에 들어오지 않습니다 |
cc | 참조 수신 주소 배열(값표현식) |
bcc | 숨은 수신 주소 배열(값표현식) |
subject | 제목(값표현식, 필수) |
body | 본문(값표현식, 필수). 항상 text/html로 전송되므로 일반 텍스트가 아니라 마크업을 씁니다(줄바꿈은 공백으로, <는 태그로 해석됩니다). 보간되는 값표현식 결과는 HTML 이스케이프됩니다 |
replyTo | (선택) Reply-To 헤더(값표현식). 발신자와 다를 수 있습니다(예: no-reply로 보내되 회신은 지원 주소로) |
timeoutMs | (선택, 1 이상) 이 발송의 타임아웃(ms). 없으면 플랫폼 기본값, 상한을 넘는 값은 저장 시 거부 |
- 수신자 합계는 최대 50명입니다.
to(1명),cc,bcc를 모두 합쳐 셉니다(SMTP 봉투에는 cc/bcc 구분이 없고 전부 수신자로 나가므로 합계로 셉니다). 초과하면 저장·실행에서 거부됩니다. 많은 사람에게 보내려면ResourceForEach+EmailSend로 항목당 1통씩 보냅니다. - 결과를 바인딩하지 않습니다. 성공은 "제공자가 메일을 받아들였다"는 것뿐이라 돌려줄 값이 없어
name을 받지 않습니다. 재시도도 하지 않습니다(이메일은 비멱등이라, 애매하게 실패한 뒤 재시도하면 중복 발송이 됩니다. 그래서Http의retry를 따르지 않습니다). 실패는 throw되어Try의catch로 처리합니다. - 외부 호출입니다. 플랜별 외부 호출 한도에 카운트되고, 시간 예산에는
timeoutMs(없으면 10초) 한 번으로 잡힙니다(재시도하지 않으므로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" }변수
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만의 짧게 사는 캐시를 읽고 씁니다. 외부 호출 결과처럼 매번 다시 가져오기 아까운 값을 몇 초 동안 들고 있다가 다음 호출에서 재사용하는 자리입니다. 외부 호출이 아니므로 한 정의당 외부 호출 수에 포함되지 않고, 시간 예산에 선언하는 시간도 없습니다.
| 필드 | 설명 |
|---|---|
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 하나입니다. 같은 Space의 다른 Script는 같은 캐시키를 써도 서로의 데이터를 보지 못합니다. 그 Script를 수정하거나 삭제하면 그 Script의 데이터는 전부 사라집니다.
key는 리터럴입니다. 요청에서 온 캐시키로 데이터를 고르게 하면 호출자가 무엇을 읽을지 정하게 되고, 회원마다 하나씩 담아 둔 Script가 한 회원의 값을 다른 회원에게 내주게 됩니다. 그래서key에{ /pointer }가 들어 있으면 값으로 바뀌지도 않고 글자 그대로 쓰이지도 않습니다. 저장 자체가 거부됩니다.- 반복 안에는 둘 수 없습니다.
Loop나ResourceForEach의 블록 안에Cache가 있으면 저장이 거부됩니다. 아래 개수 상한이 반복 안에서는 아무런 제한이 되지 못하기 때문입니다. 반복마다 데이터를 하나씩 쓰게 되므로 정의에 적힌 문 수와 실제로 쓰이는 캐시키 수가 어긋납니다. - 한 정의에 5개까지 담을 수 있습니다(중첩 포함, 연산과 무관하게 합산). 넘으면 저장이 거부됩니다.
- 저장하는 값은 10,240바이트(10KiB)까지입니다. 넘으면 그 문이 실패합니다(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 | 파싱 결과를 담을 이름(필수). 다른 문에서는 선택이지만 여기서는 필수입니다. 결과를 바인딩하는 것 말고는 하는 일이 없어서, 이름이 없으면 아무 효과도 없는 문이 됩니다 |
value | 파싱할 JSON 텍스트(값표현식, 필수). { /resp/body }처럼 앞 단계의 값을 가리키거나, JSON 텍스트를 리터럴로 그대로 적습니다(리터럴 안의 {는 { 포인터 } 템플릿으로 해석되지 않습니다) |
- 결과: 파싱된 값 그 자체입니다. 객체는 객체로, 배열은 배열로,
42나"a"같은 단일 값도 그대로 파싱됩니다. 이후{ /<name>/... }로 내부를 가리킵니다. - 이미 파싱된 값이 오면 그대로 바인딩합니다.
value가 문자열이 아닌 값으로 resolve되면 파싱할 텍스트가 아니라고 보고 그 값을 그대로 담습니다. - 파싱한 텍스트 안의
{ /pointer }는 다시 해석하지 않습니다. 외부에서 받은 문자열이{ /payload/... }같은 표현을 담고 있어도 값으로 치환되지 않고 문자열로 남습니다. null은 두 경우를 구분합니다. 파싱할 텍스트가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 }" }서명 검증과 텍스트 처리
결제 대행사가 웹훅으로 보낸 서명을 확인하고, 그 서명이 포장돼 온 문자열을 풀어내는 문입니다. 셋 다 외부 호출이 아니라 계산이므로 외부 호출 한도에 세어지지 않고 시간 예산에 선언하는 시간도 없으며, 데이터 자리를 갖지 않아 $ 접두 규칙과 무관합니다. 셋을 조합한 완결 예시는 쿡북의 웹훅 서명 검증에 있습니다.
세 문 모두 resolve된 값의 길이에 상한이 있습니다. 표현식의 길이가 아니라 그 표현식이 가리킨 값의 길이이고({ /rawPayload } 열여섯 자가 수십 KB를 가리킵니다), 초과하면 실행이 실패해 Try로 처리할 수 있습니다. 수치는 값 길이 상한에 모아 두었습니다.
Signature
받은 서명 코드가 secret으로 만든 코드와 같은지 확인해, 그 답을 Boolean 값으로 바인딩합니다. 결제 대행사(PG·MoR)가 웹훅으로 보내는 서명은 이 문으로 검증합니다.
| 필드 | 설명 |
|---|---|
name | 검증 결과를 담을 이름(필수). { /<name> }이 true 또는 false입니다. 검증해 놓고 결과를 안 쓰면 검증하지 않은 것과 같으므로 생략할 수 없습니다 |
algorithm | 코드를 만들 해시(필수). SHA1·SHA256·SHA384·SHA512 |
secret | 상대와 공유한 비밀 키(값표현식, 필수) |
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이 선언한 표기가 아니면 실패입니다. 셋 중 작성자 자신의 입력은 이것뿐입니다. 실패 메시지에secret과value는 실리지 않습니다.
value상한은 65,536자입니다(resolve된 값 기준). 실제 제공자가 보내는 웹훅 본문 크기에 맞춘 값입니다.- 비교는 값이 같은지를 constant-time으로 판정합니다. 앞쪽 몇 바이트가 맞았는지가 응답 시간으로 새어 나가지 않습니다.
secret은 암호화되어 저장되지 않습니다.Http헤더의secret: true(암호화 저장 후 전송 직전 복호화)와 달리 정의에 적은 그대로 남으므로, 그 Script를 읽을 수 있는 역할에게는 값이 보입니다. 회원(ServiceUser)은 Script 정의를 읽을 수 없습니다(저작과 조회는 CMA 전용).
// 본문 전체에 서명하는 제공자
{ "type": "Signature", "name": "verified", "algorithm": "SHA256",
"secret": "whsec_9f2c1b7ae4", "value": "{ /rawPayload }",
"expected": "{ /headers/x-webhook-signature }" }
// 키를 base64로 발급하는 제공자
{ "type": "Signature", "name": "verified", "algorithm": "SHA256",
"secret": "aGVsbG8td2VlZ2xvbw==", "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안에 직접 적는 편이 모든 자리를 표현합니다.- 결과: 문자열입니다. 상대가 보낸 코드와 비교할 때는
{ "==": [ "{ /<name> }", "{ /headers/... }" ] }로 씁니다. 이 비교는Signature의 constant-time 비교와 달리 일반 동등 비교입니다. value가 값 없이 resolve되거나 공백뿐이면 실패입니다(작성자 자신의 표현식이므로).value상한은 128자입니다. 이어 붙인 필드 몇 개를 담는 자리라Signature보다 훨씬 좁습니다. 웹훅 본문 전체를 대상으로 계산해야 하면Signature를 씁니다.
// SHA256(주문번호 + 금액 + merchantKey) 를 대문자 hex 로
{ "type": "Hash", "name": "expectedSign", "algorithm": "SHA256", "encoding": "HexUpper",
"value": "{ /payload/orderId }{ /payload/amount }9f2c1b7ae4" }Regex
pattern을 value에 적용해 mode가 요구한 것을 바인딩합니다. 값 표현식에는 문자열을 자르는 수단이 없어서(이어 붙이는 cat과 포함을 보는 in만 있습니다), t=…,v1=…처럼 한 헤더에 여러 값이 포장돼 오는 것을 풀 때 이 문을 씁니다.
| 필드 | 설명 |
|---|---|
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로 꺼내는 두 문이 서로 다른 답을 내지 않도록 질문을 같게 두었습니다. pattern은 이 엔진에서 값 표현식이 아닌 두 필드 중 하나입니다(다른 하나는Cache의key입니다). 요청에서 온 패턴을 그대로 실행하면 호출자가 실행될 식을 고르게 되고, 정규식의 역추적이 그것을 서비스 거부 수단으로 만듭니다. 그래서 패턴 안의{ /pointer }도 값으로 바뀌지 않고 글자 그대로 패턴의 일부가 됩니다.- 패턴은 실행이 시작될 때 정의 전체에서 한 번 컴파일됩니다.
Loop나ResourceForEach안에 있어도 반복마다 다시 컴파일되지 않고, 쓸 수 없는 패턴은 첫 문이 무엇도 하기 전에 실패합니다(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
반복입니다. 모드 하나를 고릅니다. over(foreach), while(조건), for(카운트)입니다. 어느 모드든 엔진이 반복 상한을 강제합니다(무한 루프 방지). 상한은 maxIterations로 선언하고, 적지 않으면 플랫폼 상한이 적용됩니다. body 안에는 외부 호출(Http·EmailSend)과 Media 파일 인제스트도 담을 수 있고, 외부 호출 문은 실행 시 반복마다 실제로 호출됩니다. 한 정의당 외부 호출 최대 개수 제한은 그대로 적용됩니다.
시간 예산에서는 곱셈으로 잡힙니다. 이 문이 선언하는 시간은 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
브랜치들을 동시 실행하고 조인 후 진행합니다. 브랜치 간 참조는 불가합니다(의존이 있으면 순차로 배치합니다).
| 필드 | 설명 |
|---|---|
branches | 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도 이 문으로 표현합니다.
If의then에Return을 두면 조건을 어겼을 때 값을 반환하고 이후 문을 실행하지 않습니다. 이는Return의 여러 쓰임 가운데 하나입니다.
{ "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 } 노출(어느 문에서 실패했는지는 담기지 않습니다) |
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": [ /* 항상 실행 */ ] }관련 문서
- 값 표현식: 위 모든 필드가 따르는 값 규칙.
- 실행 시맨틱, 제약, 보안: 실행 순서, 에러, 정적 제약, 보안.
- 쿡북: 이 문들을 조합한 완결 예시.
- Script 개요: 최상위 구조와 한 번의 실행에 주어지는 시간.
