前言
欄位 Mapping 儲存成功後,最危險的下一步就是直接打開自動同步。只要一個 Field ID 選錯、狀態 transform 不完整,第一批 Webhook 就可能把資料寫進錯誤欄位。
這個案例因此把「看預覽」與「真正送出」拆成兩個 action。商家先選一筆 Shopify 訂單或會員,由 server 讀取真實資料、套用 active Mapping,顯示 Ragic destination、unique key、payload 與 warnings;只有再次明確確認,才執行 create 或 update。
上一篇建立了 Ragic API 文件到 Shopify Mapping Editor 的 schema 流程。這篇把它接到第一筆受控測試資料。
原本的做法與問題症狀
最早的 Dry Run UI 是一個 JSON textarea。使用者貼入 Shopify payload,server parse 後套 Mapping,再顯示 Ragic payload。這比直接送出好,卻仍有幾個問題:
- 商家不一定知道去哪裡取得正確的 Shopify JSON。
- 貼入內容可能過期、被手動修改,或結構根本不是目前 API 回傳。
- Browser 可以自行提供任意 nested path,預覽結果不代表 App 真正能從 Shopify 取得相同資料。
- JSON 很容易包含不需要的姓名、Email、電話與地址。
- 預覽與真正送出若使用不同資料,商家看到的內容就不是最後寫入內容。
更根本的問題是信任邊界。Dry Run 應該驗證「目前 Server 權限、API version、Mapping 與真實 resource」能否共同產生 payload,而不是只驗證一段使用者提供的 JSON 可以被 JSON.parse()。
問題是怎麼推敲出來的?
來源專案的最新工作樹把 resource selection 與 preview 拆成兩個 server module。
第一個 module 讀取最近的訂單或會員,只回傳 UI 需要的 id 與 label。訂單 label 可以包含訂單顯示編號、金額與日期;會員資料若被 Shopify protected customer data 權限拒絕,會轉成明確的設定提示。
第二個 module 接收 target 與 resource ID。Server 先把 numeric ID 正規化成 Shopify GID,並驗證 Order 流程不能混入 Customer GID。接著使用 Admin GraphQL node 重新取得 resource,交給既有 Dry Run builder。
送出時也不是把 Browser 剛才看到的 payload 原封不動貼回來。Server 會再次 fetch Shopify resource、再次套用 active Mapping,然後才呼叫既有的 explicit send/upsert path。這能縮短 preview 與 write 的資料差異,也避免 payload 被前端竄改。
最後採用的架構
商家操作流程是:
選擇 Order / Customer
→ 列出最近 resources 或輸入 ID
→ Server 驗證 GID 與 target
→ Admin GraphQL 重新讀取 resource
→ 套用 active Mapping
→ 顯示 unique key、payload、warnings
→ 商家按「送出到 Ragic」
→ Server 再次 fetch + build
→ unique key lookup
→ create / update
Preview response 不需要回傳完整 Shopify source。只顯示決策所需的:
- 資料類型與 resource ID。
- 目標 Ragic sheet/API URL 的安全顯示。
- unique key 的 Shopify path、Ragic Field ID 與值。
- 最終 Ragic payload。
- 缺值或 transform 產生的 warnings。
「顯示 payload」本身仍可能含個人資料,因此 UI 只能提供給有設定權限的商家使用,不能送進一般 analytics 或公開 error report。
關鍵實作
Preview 不接受任意 JSON,只接受 resource identity:
const resourceId = normalizeShopifyResourceId({
target,
rawId: formData.get('shopifyResourceId'),
});
const shopifyPayload = await fetchShopifyResource({
shop,
target,
resourceId,
});
const dryRun = await buildDryRunFromActiveMapping({
shop,
target,
shopifyPayload,
});
真正送出是另一個 intent,而且重新建立 preview:
export async function sendSelectedResource(input: SendInput) {
const preview = await previewSelectedResource(input);
return upsertRagicRecordFromDryRun({
shop: input.shop,
dryRun: preview.dryRun,
});
}
Payload builder 遇到缺值時不主動覆寫:
if (value === undefined || value === null || value === '') {
warnings.push(`Shopify path 缺少值:${field.shopifyPath}`);
continue;
}
這裡的「Dry Run」是應用層 preview,不代表 Ragic 提供跨系統 transaction。按下真正送出後,仍要檢查 HTTP status、Ragic response body 與必要的 read-back。
實際驗證
尚未提交的開發分支已有測試覆蓋:
- 最近訂單會整理成商家可選的 option。
- Customer listing 被 protected-data 規則拒絕時,顯示可理解的權限訊息。
- numeric Order ID 可正規化成 Order GID。
- Order preview 會拒絕 Customer GID。
- 選取一筆訂單後,Server fetch Shopify 並產生預期的 Ragic payload。
- send path 不接受使用者提供的 JSON,而是重新 fetch resource。
- Preview 與 send 都使用 active Mapping 與同一套 payload builder。
- 真正送出後可取得 Ragic record ID。
證據邊界也要誠實說明:這些 resource selection/preview 檔案在研究當下仍是來源專案的未提交工作。本文可以描述程式與測試已存在,不能宣稱已合併、已部署或已通過正式商店驗收。
仍然存在的限制
第一個限制是 API version。來源分支目前提供可覆寫的 Admin API version,fallback 仍固定在案例開發時使用的版本;2026-07-26 查閱的 Shopify latest GraphQL reference 已是 2026-07。正式部署時應讓 app config、query fixtures、resource selection 與 backfill 使用同一個明確版本,不能各自漂移。
第二個限制是 protected customer data。Shopify 目前把 Customer、Order 與相關 Webhook 資料列入 protected customer data;姓名、地址、Email 與電話還有更高的直接識別要求。Preview 只應查詢功能必要欄位,權限不足時要能處理 redacted data 或 GraphQL errors。
第三個限制是 TOCTOU,也就是預覽與送出之間資料仍可能改變。重新 fetch 可以縮短差距,但商家看到的 preview 不等於遠端鎖定快照。若金額或狀態變更會造成重大影響,send response 應回傳實際使用的 payload hash/version,必要時要求再次確認。
第四個限制是 warnings 目前只是提醒。若缺少 unique key、必要狀態或法規要求欄位,應升級成 blocking error;不能讓商家忽略警告後仍建立無法對帳的 record。
最後,最近 20 筆清單只是方便選取,不是完整搜尋。大量商店需要 ID 搜尋、日期篩選與 pagination;這些功能仍要遵守 API cost 與最小資料原則。
可以延伸到哪些情境?
受控 Dry Run 可以用在:
- ERP/CRM 初次 Mapping 驗收。
- Webhook 自動同步啟用前的 sample run。
- Schema 變更後的 Mapping repair。
- 歷史資料 backfill 前的單筆預演。
- 狀態 value map 或日期格式調整。
核心原則是「Preview 與 Write 共用同一條 server-side build path」,而不是維護一份只供 UI 模擬的邏輯。只要兩條路分叉,預覽越漂亮,反而越可能讓人誤信。
下一批會繼續處理同步可靠性,從 Webhook job 的 retry、dedup 與 reconciliation 開始。
參考資料
- Shopify:node query
- Shopify:Order object
- Shopify:customers query
- Shopify:Customer object
- Shopify:Work with protected customer data
- Ragic:Creating a New Entry
- Ragic:Modifying an Entry
- Ragic:API Request Error Response
以上平台文件查核日期:2026-07-26。
