WELクックブック
このページには、一般的な連携の問題に関する26個の実例がテーマ別にまとめられています。各例は自己完結型であるため、welバイナリに貼り付けてそのまま実行できます。
機能の提供状況
WELは現在、一部のお客様にご利用いただけます。ご利用のワークスペースで利用可能かどうかを確認するには、Customer Success Representativeにお問い合わせください。
再形成と抽出
次のソリューションでは、ペイロードを再形成し、ネストされた構造から値を抽出します。
CRMからの連絡先名を正規化する
CRMは、スペースや大文字小文字が一貫していない名前を送信します。各フィールドをクリーンアップしてから、クリーンアップした値から表示名を作成します。
Formula
let raw = {first: ' dana ', last: ' SMITH '}
do {
first: raw.first >> trim >> capitalize,
last: raw.last >> trim >> capitalize,
full: trim(raw.first) ++ ' ' ++ trim(raw.last) >> titleize
}出力
{"first": "Dana", "last": "Smith", "full": "Dana Smith"}trimは余分な空白を削除し、次にcapitalizeとtitleizeが大文字小文字を修正します。そのため、ソースでどのように書式設定されていても、3つのフィールドはすべてクリーンな状態で出力されます。
フォールバックを使用してフィールドを安全に抽出する
2つのオプションフィールドが不足しています。 nullを送信先に渡すのではなく、それぞれにデフォルトを指定します。
Formula
let contact = {name: 'Kai', email: null, phone: null}
do {
name: contact.name,
email: contact.email | 'no-reply@example.com',
phone: contact.phone | 'N/A'
}出力
{"name": "Kai", "email": "no-reply@example.com", "phone": "N/A"}fallback operator(|)はnull値のみを置き換えるため、nameは変更されず、不足している2つのフィールドにはデフォルトが設定されます。
深くネストされたレスポンスから識別子を抽出する
APIレスポンスには、複数のレベルで同じフィールド名が含まれています。各パスを指定せずに、すべての出現箇所を抽出します。
Formula
let api_response = {
data: {
users: [
{profile: {Name: 'Dana'}, orders: [{Name: 'Order-1'}]},
{profile: {Name: 'Kai'}, orders: [{Name: 'Order-2'}, {Name: 'Order-3'}]}
]
}
}
do deep_collect(api_response, 'Name')出力
["Dana", "Order-1", "Kai", "Order-2", "Order-3"]deep_collectは構造のすべてのレベルを走査するため、Nameフィールドがプロファイルに属していても注文に属していても、どちらのパスも指定せずに検出できます。
深くネストされた構造内の値を変換する
ドキュメントには、予測できない深さにファイル添付が含まれています。すべてのバイナリ値をその場でエンコードし、それ以外はすべて変更せずに残します。
Formula
let doc = {
title: 'Report',
attachments: [
{name: 'logo', data: Binary('PNG')},
{name: 'csv', data: Binary('CSV')}
]
}
do deep_map_values_by(
doc,
v ~> encode_base64(v),
v ~> type_of(v) == 'Binary'
)出力
{"title": "Report", "attachments": [{"name": "logo", "data": "UE5H"}, {"name": "csv", "data": "Q1NW"}]}条件ラムダによって、これが安全になります。 deep_map_values_byは、type_of(v) == 'Binary'となる値にのみ変換を適用するため、titleとnameはそのまま渡されます。
リストと集計
次のソリューションでは、レコードのリストを組み合わせ、削減、または再編成して、送信先が期待する形状にします。
ネストされた注文品目をフラット化する
注文は、その下に品目がネストされた状態で到着します。親注文IDを含めて、品目ごとに1つのフラットな行を生成します。
Formula
let orders = [
{id: 'A1', items: [{sku: 'X', qty: 2}, {sku: 'Y', qty: 1}]},
{id: 'A2', items: [{sku: 'Z', qty: 5}]}
]
do orders
>> map_by(o ~> o.items >> map_by(i ~> {order_id: o.id, sku: i.sku, qty: i.qty}))
>> flatten出力
[
{"order_id": "A1", "sku": "X", "qty": 2},
{"order_id": "A1", "sku": "Y", "qty": 1},
{"order_id": "A2", "sku": "Z", "qty": 5}
]内側のmap_byは、各注文に対して品目のリストを1つ生成します。 flattenは、それらのリストを1つのフラットなリストに結合します。
注文全体の品目をSKU別にグループ化する
同じSKUが複数の注文に出現します。それぞれの数量を合計します。
Formula
let lines = [
{sku: 'X', qty: 2}, {sku: 'Y', qty: 1},
{sku: 'X', qty: 3}, {sku: 'Y', qty: 4}
]
do lines
>> group_by(l ~> l.sku)
>> entries
>> map_by(e ~> {sku: e.key, total: e.value >> map_by(row ~> row.qty) >> sum})出力
[{"sku": "X", "total": 5}, {"sku": "Y", "total": 5}]group_byは、SKUをキーとするMapを生成します。 entriesはマップをリストに変換し、sumは各グループの合計数量を計算します。
最新を保持してレコードの重複を除外する
同じレコードIDが、異なるタイムスタンプで複数回出現します。各レコードの最新バージョンのみを保持します。
Formula
let events = [
{id: 'a', ts: 1, status: 'pending'},
{id: 'b', ts: 2, status: 'active'},
{id: 'a', ts: 3, status: 'active'}
]
do events
>> group_by(e ~> e.id)
>> values
>> map_by(group ~> group >> sort_by(e ~> e.ts) >> last)出力
[{"id": "a", "ts": 3, "status": "active"}, {"id": "b", "ts": 2, "status": "active"}]IDによるgroup-byは、レコードのすべてのバージョンをまとめて収集します。各グループ内で、タイムスタンプによる並べ替えの後にlastを使用すると、最新バージョンのみが保持されます。
行を列にピボットする
メトリクスAPIは、メトリクスごとに1行を返します。メトリクスごとに1つのフィールドを持つ単一のレコードに再形成します。
Formula
let rows = [
{metric: 'cpu', value: 72},
{metric: 'mem', value: 85},
{metric: 'disk', value: 40}
]
do rows
>> map_by(r ~> {key: r.metric, value: r.value})
>> from_entries出力
{"cpu": 72, "mem": 85, "disk": 40}from_entriesはentriesの逆です。{key, value}ペアのリストを受け取り、それらを単一のMapに再構築します。この操作は行を列に変換します。
レコードを成功バケットと失敗バケットに分割する
バッチジョブでは、成功と失敗を別々にルーティングする必要があります。結果が混在したリストを2つのグループに分割します。
Formula
let results = [
{id: 1, status: 'ok'}, {id: 2, status: 'error'},
{id: 3, status: 'ok'}, {id: 4, status: 'error'}
]
do let parts = partition_by(results, r ~> r.status == 'ok')
do {
succeeded: parts[0] >> map_by(r ~> r.id),
failed: parts[1] >> map_by(r ~> r.id)
}出力
{"succeeded": [1, 3], "failed": [2, 4]}partition_byは2要素のリストを返します。最初の要素には一致するレコードが含まれ、2番目の要素には残りのレコードが含まれます。この例では、parts[0]には成功が含まれ、parts[1]には失敗が含まれます。
データソースの結合
次のソリューションでは、共有キーによって2つの別々のリストのレコードを結合します。
共有キーで2つのリストを結合する
2つのリストがキーを共有しています。両方のリストで一致するレコードのみを返します。
Formula
let users = [{id: 1, name: 'Dana'}, {id: 2, name: 'Kai'}],
orders = [{user_id: 1, item: 'Book'}, {user_id: 1, item: 'Pen'}, {user_id: 3, item: 'Tape'}]
do users
>> map_by(u ~> orders
>> filter_by(o ~> o.user_id == u.id)
>> map_by(o ~> {name: u.name, item: o.item}))
>> flatten出力
[{"name": "Dana", "item": "Book"}, {"name": "Dana", "item": "Pen"}]filter_byは、各ユーザーのIDに一致する注文のみを保持します。一致する注文がないユーザーは空のリストを生成し、flattenがそれを結果から削除します。この動作により、内部結合が作成されます。
一致がなくても左結合のすべてのレコードを保持する
内部結合とは異なり、結合の左側にあるすべてのレコードは、一致の有無に関係なく結果に表示される必要があります。
Formula
let users = [{id: 1, name: 'Dana'}, {id: 2, name: 'Kai'}],
orders = [{user_id: 1, item: 'Book'}]
do users
>> map_by(u ~>
let matches = orders >> filter_by(o ~> o.user_id == u.id)
do if length(matches) > 0
then matches >> map_by(o ~> {name: u.name, item: o.item})
else [{name: u.name, item: null}])
>> flatten出力
[{"name": "Dana", "item": "Book"}, {"name": "Kai", "item": null}]注文がユーザーに一致しない場合、else分岐は空のリストではなくitem: nullを含む1要素のリストを指定するため、そのユーザーはflatten後も行として残ります。
2つのデータソースをマージして照合する
同じエンティティが2つのシステムに存在し、どちらか一方にしか存在しない場合もあります。両方をIDごとに単一のレコードに結合し、それぞれがどのシステムから来たかを記録します。
Formula
let crm = [{id: '1', name: 'Dana', source: 'crm'}, {id: '2', name: 'Kai', source: 'crm'}],
erp = [{id: '2', name: 'Lucian', source: 'erp'}, {id: '3', name: 'Sasha', source: 'erp'}]
do let all_ids = (crm ++ erp) >> map_by(r ~> r.id) >> unique
do all_ids >> map_by(id ~>
let from_crm = crm >> find_by(r ~> r.id == id),
from_erp = erp >> find_by(r ~> r.id == id)
do {
id: id,
name: if from_crm != null then from_crm.name else from_erp.name,
in_crm: from_crm != null,
in_erp: from_erp != null
})出力
[
{"id": "1", "name": "Dana", "in_crm": true, "in_erp": false},
{"id": "2", "name": "Kai", "in_crm": true, "in_erp": true},
{"id": "3", "name": "Sasha", "in_crm": false, "in_erp": true}
]これにより、どちらのシステムを検索する前に、まず両方のリストからすべてのIDが収集されるため、1つのソースにのみ存在するレコードも結果に表示されます。
数値と金額
次のソリューションでは、近似が実際のバグになるような場合に、算術演算と解析を通じて数値を正確に保ちます。
精度を失わずに通貨を換算する
請求書の金額を別の通貨に換算する必要があります。正確なソース金額を失わずに、表示用に結果をセント単位に丸めます。
Formula
let invoice = {amount: Decimal('1234.56'), rate: Decimal('0.85')}
do let converted = invoice.amount * invoice.rate
do {
original: invoice.amount,
eur: round_places(converted, 2),
label: String(round_places(converted, 2)) ++ ' EUR'
}出力
{"original": 1234.56, "eur": 1049.38, "label": "1049.38 EUR"}amountとrateはどちらも最初からDecimalであるため、乗算は正確であり、精度が失われる可能性があるのはround_placesだけです。また、それも表示用に限られます。
JSONから大きな整数と小数を精度を失わずに解析する
ほとんどの言語では、JSONから大きな整数または小数を解析するときに、気付かないうちに精度が失われます。たとえば、JavaScriptのJSON.parseは9999999999999999999を10000000000000000000に切り詰めます。
Formula
let json = '{"tx_id": 9999999999999999999, "balance": 1234567890.1234567890}'
do let data = parse_json(json, {decimal: true})
do {
tx_id: data.tx_id,
balance: data.balance,
balance_type: type_of(data.balance),
cents: data.balance * 100
}出力
{"tx_id": 9999999999999999999, "balance": 1234567890.1234567890, "balance_type": "Decimal", "cents": 123456789012.3456789000}Integerには上限がないため、tx_idはそのまま保持されます。 decimal: trueオプションにより、balanceは近似のFloatではなく正確なDecimalとして保持されます。これがない場合、小数値は標準のInteger/Floatルールを使用して解析されます。
日付
次のソリューションでは、単一の日付から複数のルーティング情報を一度に導出します。
日付でレコードをルーティングする
レコードには、その日付が週末に当たるかどうか、月末までの日数、および四半期に基づくルーティングが必要です。
Formula
let event_date = PlainDate('2025-03-15')
do let dow = day_of_week(event_date)
do let month_end = end_of_month(event_date)
do {
is_weekend: dow == 6 or dow == 7,
days_until_month_end: month_end - event_date,
quarter: ceil(month(event_date) / 3.0)
}出力
{"is_weekend": true, "days_until_month_end": 16, "quarter": 1}PlainDateの減算では日数が直接得られるため、days_until_month_endに個別の期間処理は必要ありません。番号付けが何に依存するかについては、day_of_weekを参照してください。
検証とエラー
次のソリューションでは、無効なデータが下流のステップに到達する前に拒否またはルーティングします。
受信Webhookペイロードを検証する
Webhookは本文をJSON文字列として配信します。これを解析し、含まれる金額が使用できない場合はすぐにペイロードを拒否します。
Formula
let payload = '{"amount": "42.50", "currency": "USD"}'
do let data = parse_json(payload)
do let amount = Decimal(data.amount)
guard amount > 0 else 'amount must be positive'
do {amount: amount, currency: data.currency}出力
{"amount": 42.50, "currency": "USD"}guardは、不正な値が気付かれずに流れるのを許すのではなく、ゼロ、負の値、または解析不能な金額が変換の残りの部分に到達するのを停止します。
APIエラーレスポンスをフィルタリングして要約する
APIレスポンスのバッチには、成功と失敗が混在しています。それぞれの件数と失敗の詳細をレポートします。
Formula
let responses = [
{code: 200, body: 'ok'},
{code: 422, body: 'validation failed'},
{code: 200, body: 'ok'},
{code: 500, body: 'internal error'}
]
do let failures = responses >> filter_by(r ~> r.code >= 400)
do {
total: length(responses),
failed: length(failures),
errors: failures >> map_by(r ~> String(r.code) ++ ': ' ++ r.body)
}出力
{"total": 4, "failed": 2, "errors": ["422: validation failed", "500: internal error"]}failuresのletバインディングは、失敗したレスポンスを1回だけフィルタリングし、2回フィルタリングする代わりに、その結果を件数とエラーリストの両方に再利用します。
メールアドレスのバッチをクリーンアップして検証する
メールアドレスのバッチには、大文字小文字の不一致、余分な空白、空白、および少なくとも1つの不正な形式のエントリが含まれています。実際に使用可能なものだけを保持します。
Formula
let raw = ['dana@co.com', 'bad-email', ' KAI@CO.COM ', '', 'sasha@co.com']
do raw
>> map_by(e ~> trim(e) >> lower)
>> filter_by(e ~> not blank?(e))
>> filter_by(e ~> valid_email?(e))出力
["dana@co.com", "kai@co.com", "sasha@co.com"]正規化ステップは検証の前に実行されるため、KAI@CO.COMとkai@co.comを同じアドレスとして扱います。また、valid_email?は、有効なアドレスではなかったbad-emailを削除します。
複数ステップの計算を不正な入力から保護する
税計算は、文字列として届く税率に依存しており、下流の処理がそれを信頼する前に妥当な範囲内に収まっている必要があります。
Formula
let payload = {items: [{price: 10, qty: 2}, {price: 25, qty: 1}], tax_rate: '0.08'}
do let tax = Decimal(payload.tax_rate)
guard tax >= 0 and tax < 1 else 'invalid tax rate'
do let subtotal = payload.items >> map_by(i ~> i.price * i.qty) >> sum
do {
subtotal: subtotal,
tax: round_places(Decimal(subtotal) * tax, 2),
total: round_places(Decimal(subtotal) * (1 + tax), 2)
}出力
{"subtotal": 45, "tax": 3.60, "total": 48.60}guardは、それに依存する算術演算の前、taxを変換した直後に実行されるため、範囲外の税率は、気付かれないまま誤った合計を生成するのではなく、ジョブを失敗させます。
1つの不正なレコードでジョブ全体を失敗させずにバッチを処理する
値のバッチをIntegerに変換する必要がありますが、一部は有効な数値ではありません。 1つの不正なレコードでバッチの残りが失敗してはなりません。
Formula
let inputs = ['42', 'bad', '100', '', '7.5']
do inputs >> map_by(v ~> {
raw: v,
parsed: Integer(v) |? null,
ok: (Integer(v) |? null) != null
})出力
[
{"raw": "42", "parsed": 42, "ok": true},
{"raw": "bad", "parsed": null, "ok": false},
{"raw": "100", "parsed": 100, "ok": true},
{"raw": "", "parsed": null, "ok": false},
{"raw": "7.5", "parsed": null, "ok": false}
]try-fallback演算子|?は要素ごとに変換エラーを捕捉するため、解析不能な値が1つある場合でも、バッチ全体を停止するのではなく、そのレコードにnullが生成されます。
出力形式を生成する
次のソリューションでは、別のシステムが期待する形式の文字列を安全に作成します。
特殊文字で壊れないSOQLクエリを作成する
ユーザーが入力した検索語句には、クエリの文字列リテラルから抜け出してしまう文字が含まれる場合があります。
Formula
let search = "O'Brien & Sons"
do "SELECT Id, Name FROM Account WHERE Name = '" ++ escape_for_soql(search) ++ "'"出力
SELECT Id, Name FROM Account WHERE Name = 'O\'Brien & Sons'escape_for_soqlは検索値のみをエスケープし、周囲のクエリ構文を保持します。エスケープされたアポストロフィでは、文字列リテラルを終了できません。
構造化データからCSV行を作成する
送信先はCSVファイルを想定しており、一部のフィールド値にはカンマが含まれています。カンマが列区切りとして機能しないように、これらの値をエスケープします。
Formula
let records = [
{name: 'Dana', email: 'dana@co.com', amount: 100},
{name: 'Kai, Jr.', email: 'kai@co.com', amount: 250}
]
do let header = 'Name,Email,Amount'
do let rows = records
>> map_by(r ~> escape_for_csv(r.name) ++ ',' ++ r.email ++ ',' ++ String(r.amount))
do [header] ++ rows >> join_to_string('\n')出力
Name,Email,Amount
Dana,dana@co.com,100
"Kai, Jr.",kai@co.com,250escape_for_csvは、カンマを含むフィールド"Kai, Jr."のみを引用符で囲むため、2つに分割されずに1つの列として保持されます。
API認証用のJWTを発行する
アウトバウンドAPI呼び出しには、発行者を証明する署名済みの有効期限付きトークンが必要です。
Formula
let issued_at = now() >> to_epoch >> floor,
key = '0123456789abcdef0123456789abcdef'
do let payload = {sub: 'service-account', iat: issued_at, exp: issued_at + 3600}
do let token = jwt_encode(payload, key, 'HS256')
do {
authorization: 'Bearer ' ++ token,
decoded: jwt_decode(token, key, 'HS256')
}出力
{
authorization: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
decoded: {header: {alg: "HS256", typ: "JWT"}, payload: {sub: "service-account", iat: ..., exp: ...}}
}プロダクションでは、キーに安全なシークレットストアから取得した32バイト以上のランダムバイトを使用します。 HS256がその最小値を強制する理由については、jwt_encodeを参照してください。
項目の番号付きリストを生成する
項目のリストを、プレーンテキストレポート用に番号付きの大文字行としてレンダリングする必要があります。
Formula
let items = ['alpha', 'beta', 'gamma', 'delta']
do items
>> map_with_index_by((item, i) ~> String(i + 1) ++ '. ' ++ upper(item))
>> join_to_string('\n')出力
1. ALPHA
2. BETA
3. GAMMA
4. DELTAmap_with_index_byは各要素にゼロ始まりの位置を添えて渡すため、個別に追跡するのではなく行番号を導出できます。
式を構造化する
次のソリューションでは、WEL独自の構文Skipとfunを使用して、式をクリーンに保ちます。
Skipで一部を省略しながらフィールドを条件付きでマッピングする
アウトバウンドレコードでは、空白のフィールドは完全に省略し、内部フィールドは一切転送しないようにする必要があります。
Formula
let src = {name: 'Dana', nickname: '', age: 30, internal_id: 'x-123'}
do {
name: src.name,
nickname: if not blank?(src.nickname) then src.nickname else Skip(),
age: src.age,
internal_id: Skip()
}出力
{"name": "Dana", "age": 30}Skipは結果からキーを完全に削除します。そのため、nicknameとinternal_idは、nullとして存在するのではなく、どちらも存在しません。
名前付き関数でロジックを再利用する
同じマスキングルールが複数のフィールドに適用されます。同じラムダを繰り返し記述する代わりに、一度だけ名前を付けます。
Formula
fun mask = s ~>
if length(s) > 4
then substring(s, 0, 2) ++ '***' ++ substring(s, length(s) - 2, 2)
else '****'
do let records = [{name: 'Dana', ssn: '123-45-6789'}, {name: 'Kai', ssn: '987-65-4321'}]
do records >> map_by(r ~> {name: r.name, ssn: mask(r.ssn)})出力
[{"name": "Dana", "ssn": "12***89"}, {"name": "Kai", "ssn": "98***21"}]funはマスキングルールを一度だけ名前にバインドするため、2回記述する代わりに、両方のレコードで同じロジックを適用できます。
関連情報
- クイックスタート:さまざまなWELワークフローを示す例です。
- Standard library: このページで使用されているすべての関数に関する詳細情報。
- Data types: これらのソリューションが相互に変換するデータ型。
- Error codes: 失敗した式のトラブルシューティング。
最終更新日: