Statement 카탈로그
statements 배열의 각 원소가 하나의 Statement입니다. 이 문서는 25종 Statement의 필드, 동작, 결과를 정리합니다. 모든 값 자리는 값 표현식의 규칙(참조, 리터럴, JsonLogic, 로케일 맵)을 따릅니다(예외는 Regex의 pattern, Cache의 key, PdfWatermark의 color 셋입니다. 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 전체가 한 벌이라 범위를 지지 않고, 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)와 같을 수 없고, 한 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
리소스 계열 Statement는 대상 종류를 resource: "Content" | "ContentType" | "Media" | "ServiceUser"로 지정합니다.
Content Type은 ResourceCount만 받습니다. 다른 Statement에 적으면 저장이 거부됩니다. 양식 자체를 만들거나 고치는 일은 Script가 아니라 CMA의 몫입니다.
ServiceUser(제품에 가입한 회원)를 받는 Statement는 읽기 세 Statement(ResourceRead·ResourceFind·ResourceForEach)와 ResourceUpdate·ResourcePatch입니다. 이 둘이 쓸 수 있는 필드는 roleOverride와 enableLogin 둘뿐입니다. 그 밖의 쓰기 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 |
Mediafile: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~64자여야 합니다 |
color | 선택 | 글자 색. #RRGGBB(16진수 여섯 자리)로 적고, 생략하면 #808080(회색)입니다. 값표현식이 아니라 리터럴만 받으며, 형식이 다르면 Script 저장이 거부됩니다 |
- 표에 없는 필드는 무시합니다. 글자 크기, 위치, 투명도는 지정할 수 없습니다.
- 모든 페이지에 찍습니다. 문구 한 줄이 페이지 가운데를 지나는 대각선(왼쪽 아래에서 오른쪽 위)을 따라 놓입니다.
- 본문 위에 불투명도 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" }구매자 한 사람을 위한 사본이므로 publish를 false로 두어 CDA/ACDA로 전달되지 않게 했습니다. 생략하면 기본값 true에 따라 발행됩니다.
PdfWatermark 문구의 제한
text에는 PDF 표준 글꼴이 그릴 수 있는 글자(WinAnsi 문자 집합)만 쓸 수 있습니다.
| 종류 | 글자 |
|---|---|
| ASCII 출력 문자 | 공백, 영문 대소문자, 숫자, @ . # { } 같은 기본 기호 |
| 라틴-1 문자 | é ü ñ ß · © ° 같은 서유럽 문자와 기호. 줄바꿈 없는 공백(U+00A0)과 소프트 하이픈(U+00AD)은 제외 |
| 추가 기호 | € ‚ ƒ „ … † ‡ ˆ ‰ Š ‹ Œ Ž ‘ ’ “ ” • ˜ ™ š › œ ž Ÿ, 그리고 대시 두 종류(U+2013, U+2014) |
이 밖의 글자는 쓸 수 없습니다. 표에 없는 라틴 확장 문자(ł, ő, ș 같은 것), 라틴 문자가 아닌 글자, 이모지, 그리고 줄바꿈과 탭이 여기에 해당합니다.
Script를 저장할 때는 작성한 그대로의 text를 검사합니다. { /pointer }가 든 text는 해석된 값을 실행 시점에야 알 수 있으므로, 실행할 때 해석된 값을 다시 검사합니다. 저장할 때 발생하는 오류의 코드는 오류에 있습니다.
| 제한 | 리터럴 text | { /pointer }가 든 text |
|---|---|---|
| 쓸 수 있는 글자로 된 한 줄 | Script 저장 거부 | 작성한 문자열 전체(포인터 경로 포함)를 저장할 때 검사해 Script 저장 거부. 해석된 값은 실행할 때 다시 검사해 status 400으로 실패 |
| 1~64자 | Script 저장 거부 | 저장할 때는 검사하지 않음. 실행할 때 해석된 값의 길이로 검사해 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로 실패 |
해석된 text가 비어 있거나 공백뿐임 | status 400으로 실패 |
해석된 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
대상의 발행과 보관 상태를 독립적으로 제어합니다. 넷 다 필드는 동일합니다. 각 작업의 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에 리소스 쓰기 Statement를 담으면 항목마다 그 쓰기가 실행되어 상태가 바뀝니다.
네 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.* 정렬을 쓸 수 있습니다. 끄면 이 세 연산자는 거부되고 텍스트 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에 넘기는 흐름에 씁니다. 아래 규칙은 회원 디렉터리를 다루는 다섯 Statement에 걸칩니다.
- 고칠 수 있는 필드는 둘뿐입니다.
ResourceUpdate·ResourcePatch가"ServiceUser"를 받아roleOverride와enableLogin을 바꿉니다.ResourcePatch에서roleOverride에null을 주면 지정해 둔 역할이 해제됩니다.ResourceCreate·ResourceDelete와 발행·보관 Statement는"ServiceUser"를 받지 않고, 그런 정의는 저장 시점에 거부됩니다. 권한을 더해서 열 수 있는 것이 아니라 Script가 회원을 만들거나 지울 수 없는 것이므로, 권한 오류가 아니라 잘못 쓴 Statement로 거부됩니다. - 회원에는
version과propagateEvents가 없습니다. 둘 중 하나를 적으면 실행이 실패합니다.ResourceUpdate는 쓸 수 있는 필드를 전부 교체하므로enableLogin이 필수이고,roleOverride를 적지 않으면 지정해 둔 역할이 해제됩니다. 한쪽만 바꾸려면ResourcePatch를 쓰고, 그 경우 두 필드 중 적어도 하나는 적어야 합니다. - 작성자에게 회원 디렉터리 권한이 있어야 저장됩니다. 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를 실행합니다. 값으로 쓸 컬렉션을 만들지 않고 각 항목에 작업을 수행하기 위한 Statement입니다. 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끝에서 완료를 표시하면 재실행으로 남은 것부터 이어집니다). - 시간 예산에서는 곱셈으로 잡힙니다. 이 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 전체가 한 벌) |
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는 셋 중 하나만 씁니다. 둘 이상을 적으면 저장이 거부됩니다. 셋 다 없으면 본문 없이 보냅니다.- 결과:
{ 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가 없으면 적어 둔 헤더가 그대로 나갑니다. 같은 키를 여러 번 넣으면 첫 값만 쓰이고 하나로 합쳐집니다.
선언한 형식으로 담을 수 없는 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" }파일을 본문으로 보내기
Media에 올려 둔 파일을 외부 API로 넘겨야 할 때가 있습니다. 파일의 바이트는 값표현식에 담을 수 없습니다. 대신 파일이 있는 URL을 지정하면, 플랫폼이 그 URL에서 파일을 받아 요청 본문으로 보냅니다. 파일 하나를 본문 전체로 보내려면 urlBody를, 파일과 텍스트 필드를 함께 폼으로 보내려면 multipartBody를 사용합니다.
urlBody: 파일 하나를 본문 전체로 보내기
지정한 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로 보냅니다. 배열의 원소 하나가 part 하나가 되며, 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중 정확히 하나를 적습니다. 둘 다 적거나 둘 다 빠지면 저장이 거부됩니다.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에서 생기는 실패도 두 종류로 나뉩니다. 시간 초과는 status 504 응답으로 처리되므로 retry와 ignoreStatusCode가 적용됩니다. 아래 표의 파일 관련 실패에는 둘 다 적용되지 않습니다. Http 실패 전체의 분류는 Http가 실패하는 두 방식에 정리되어 있습니다.
ignoreStatusCode: true이면 시간이 초과되었을 때 { "status": 504, "body": "timed out after …ms" }가 결과로 바인딩됩니다.
요청 한 번은 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 하나당 적용됩니다.
multipartBody에 파일 part가 여럿이면 파일마다 한도가 따로 적용됩니다. - 보낸 바이트는 파일을 포함해 요금제의 데이터 전송 "외부 호출"에 합산됩니다.
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로 전송되므로 일반 텍스트가 아니라 마크업을 씁니다(줄바꿈은 공백으로, <는 태그로 해석됩니다). 보간되는 값표현식 결과는 autoEscape가 false가 아닌 한 HTML 이스케이프됩니다. body와 templateBody 중 정확히 하나만 씁니다 |
templateBody | 본문을 양식에서 만듭니다. { source, data } 모양이며 body와 정확히 하나만 씁니다. 아래 양식으로 본문 만들기 참조 |
autoEscape | (선택, 기본 true) 본문에 보간되는 값을 HTML 이스케이프할지. body의 { /pointer } 결과와 templateBody.data 값이 대상입니다. false로 두면 값에 든 마크업이 그대로 렌더됩니다. 작성자가 직접 쓴 마크업과 templateBody.source 양식은 애초에 대상이 아니고, 헤더 필드도 영향받지 않습니다 |
replyTo | (선택) Reply-To 헤더(값표현식). 발신자와 다를 수 있습니다(예: no-reply로 보내되 회신은 지원 주소로) |
timeoutMs | (선택, 1 이상) 이 발송의 타임아웃(ms). 없으면 플랫폼 기본값, 상한을 넘는 값은 저장 시 거부 |
- 본문은
body나templateBody중 하나로 씁니다. 둘을 함께 적거나 둘 다 비우면 저장이 거부됩니다. - 호출자가 보낸 값은 기본적으로 글자로 렌더됩니다.
autoEscape가 켜져 있으면 값에 든<a>같은 마크업이 태그가 아니라 글자로 보입니다. 남이 보내온 값이 다른 사람의 메일에 링크나 태그를 심지 못하게 하는 장치입니다. 관리자가 직접 쓴 뉴스레터 본문처럼 마크업을 살려야 할 때만false로 둡니다. - 헤더 필드에는 줄바꿈이 든 값을 넣을 수 없습니다.
subject·replyTo와 모든 주소가 해당합니다. 줄바꿈이 들어오면 실행이 실패합니다. 본문에서는 줄바꿈이 평범한 내용이라 검사하지 않습니다. - 수신자 합계는 최대 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" }양식으로 본문 만들기 (templateBody)
메일 본문을 Script 정의에 직접 쓰면 문구를 고칠 때마다 Script를 수정해야 합니다. templateBody는 본문 양식을 Content에 두고 그 빈칸만 채우게 해, 문구는 콘텐츠 스튜디오에서 고치고 Script는 그대로 두게 합니다.
| 키 | 설명 |
|---|---|
source | 양식이 든 자리를 가리키는 포인터 하나(필수). 이 Script의 ResourceRead나 ResourceFind가 읽은 Content를 가리켜야 하고, 그 포인터 앞뒤에 다른 글자를 쓸 수 없습니다. 값은 문자열이어야 하므로 다국어 Field는 로케일까지 적습니다(/tpl/fields/html/en-US) |
data | 양식의 빈칸을 채울 값(선택). 키 이름이 곧 빈칸 이름입니다. 데이터 자리라서 연산을 쓰려면 연산자에 $를 붙이고($cat), 이름이 $로 시작하는 빈칸은 $$로 적습니다(데이터 자리와 식 자리) |
양식 안의 빈칸은 { /<키> }로 씁니다. 이 빈칸이 읽는 것은 data뿐이며, 양식에서 /payload나 /secrets를 직접 읽을 수는 없습니다. data에 없는 키를 가리킨 빈칸은 빈 글자로 렌더됩니다. 값이 null인 키도 같습니다. body에서 null을 가리킨 포인터가 글자 null로 찍히는 것과 반대이니 두 방식을 오갈 때 유의하세요.
양식 안에 { 다음에 /가 오는 자리가 있으면(사이에 공백이 있어도 마찬가지입니다. 예: CSS의 a{/*주석*/}) 빈칸으로 읽히므로 \{로 적습니다.
저장은 통과했는데 실행에서 끊기는 자리가 셋 있습니다. 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만의 짧게 사는 캐시를 읽고 씁니다. 외부 호출 결과처럼 매번 다시 가져오기 아까운 값을 몇 초 동안 들고 있다가 다음 호출에서 재사용하는 자리입니다. 외부 호출이 아니므로 한 정의당 외부 호출 수에 포함되지 않고, 시간 예산에 선언하는 시간도 없습니다.
| 필드 | 설명 |
|---|---|
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가 있으면 저장이 거부됩니다. 아래 개수 상한이 반복 안에서는 아무런 제한이 되지 못하기 때문입니다. 반복마다 데이터를 하나씩 쓰게 되므로 정의에 적힌 Statement 수와 실제로 쓰이는 캐시키 수가 어긋납니다. - 한 정의에 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은 두 경우를 구분합니다. 파싱할 텍스트가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 }" }서명 검증과 텍스트 처리
결제 대행사가 웹훅으로 보낸 서명을 확인하고, 그 서명이 포장돼 온 문자열을 풀어내는 Statement입니다. 셋 다 외부 호출이 아니라 계산이므로 외부 호출 한도에 세어지지 않고 시간 예산에 선언하는 시간도 없으며, 데이터 자리를 갖지 않아 $ 접두 규칙과 무관합니다. 셋을 조합한 완결 예시는 쿡북의 웹훅 서명 검증에 있습니다.
세 Statement 모두 resolve된 값의 길이에 상한이 있습니다. 표현식의 길이가 아니라 그 표현식이 가리킨 값의 길이이고({ /rawPayload } 열여섯 자가 수십 KB를 가리킵니다), 초과하면 실행이 실패해 Try로 처리할 수 있습니다. 수치는 값 길이 상한에 모아 두었습니다.
Signature
받은 서명 코드가 secret으로 만든 코드와 같은지 확인해, 그 답을 Boolean 값으로 바인딩합니다. 결제 대행사(PG·MoR)가 웹훅으로 보내는 서명은 이 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이 선언한 표기가 아니면 실패입니다. 셋 중 작성자 자신의 입력은 이것뿐입니다. 실패 메시지에secret과value는 실리지 않습니다.
value상한은 65,536자입니다(resolve된 값 기준). 실제 제공자가 보내는 웹훅 본문 크기에 맞춘 값입니다.- 비교는 값이 같은지를 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보다 훨씬 좁습니다. 웹훅 본문 전체를 대상으로 계산해야 하면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=…처럼 한 헤더에 여러 값이 포장돼 오는 것을 풀 때 이 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로 꺼내는 두 Statement가 서로 다른 답을 내지 않도록 질문을 같게 두었습니다. pattern은 이 엔진에서 값 표현식이 아닌 세 필드 중 하나입니다(나머지는Cache의key와PdfWatermark의color입니다). 요청에서 온 패턴을 그대로 실행하면 호출자가 실행될 식을 고르게 되고, 정규식의 역추적이 그것을 서비스 거부 수단으로 만듭니다. 그래서 패턴 안의{ /pointer }도 값으로 바뀌지 않고 글자 그대로 패턴의 일부가 됩니다.- 패턴은 실행이 시작될 때 정의 전체에서 한 번 컴파일됩니다.
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
반복입니다. 모드 하나를 고릅니다. over(foreach), while(조건), for(카운트)입니다. 어느 모드든 엔진이 반복 상한을 강제합니다(무한 루프 방지). 상한은 maxIterations로 선언하고, 적지 않으면 플랫폼 상한이 적용됩니다. body 안에는 외부 호출(Http·EmailSend)과 Media 파일 인제스트도 담을 수 있고, 외부 호출 Statement는 실행 시 반복마다 실제로 호출됩니다. 한 정의당 외부 호출 최대 개수 제한은 그대로 적용됩니다.
시간 예산에서는 곱셈으로 잡힙니다. 이 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
브랜치들을 동시 실행하고 조인 후 진행합니다. 브랜치 간 참조는 불가합니다(의존이 있으면 순차로 배치합니다).
| 필드 | 설명 |
|---|---|
branches | Statement[][]. 각 원소가 한 브랜치(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의 여러 쓰임 가운데 하나입니다.
{ "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 개요: 최상위 구조와 한 번의 실행에 주어지는 시간.
