前言

Shopify 到 Ragic 的 Mapping 上線後,商家仍可能刪除欄位、改成唯讀、把 Link & Load 欄位換掉,或調整日期與數字型別。若 App 只在第一次設定時驗證,之後仍照舊 Field ID 寫入,最危險的結果不是明確報錯,而是資料進了錯誤欄位。

上一篇談 Shopify 歷史訂單 Backfill。回補大量資料前,必須先確定 Mapping 與目前 Ragic schema 相容;因此案例把「重新匯入 API 文件」變成一次 schema diff 與人工修復流程。

原本的做法與問題症狀

早期做法只保存 Mapping JSON。重新貼入新版 Ragic API 文件時,App 會解析出最新欄位,但既有 Mapping 仍維持 active。商家看得到新 schema,卻不知道哪些同步規則已經失效。

另一個看似方便的方案是重新跑名稱配對,直接覆蓋舊 Mapping。問題是欄位同名不代表業務語意相同;unique key、是否允許覆寫、值域與主表/子表責任都不能靠字面猜測。自動修復一旦選錯,後續 Webhook 與 Backfill 會一起放大錯誤。

問題是怎麼推敲出來的?

來源專案先建立 diffMappingAgainstSchema(),以 Ragic Field ID 比對每個 Mapping field 與 unique key。測試逐步加入四類問題:

  • missing_field:Field ID 已不存在,或整個 sheet 找不到。
  • readonly_field:欄位仍存在,但已不可寫入。
  • loaded_field:欄位由 Link & Load 提供,不應直接寫入。
  • type_changed:Mapping 使用日期或數字 transform,但目前欄位型別可能不相容。

重新儲存連線與 API 文件後,系統會用最新 parsed sheets 檢查該連線下的 active mappings。只要有 issue,就把 Mapping 標成 needs_review,保存 issue details 與一筆 MappingVersion。

同步查詢只取 active Mapping,因此 needs_review 不只是 UI badge,而是停止有風險寫入的 gate。

最後採用的架構

流程分成偵測、阻擋、修復與稽核:

重新匯入 Ragic API 文件
  → parse latest sheets / fields
  → diff active Mapping by Field ID
  → compatible:保持 active
  → incompatible:needs_review + issue version
  → 暫停該 Mapping 的同步
  → 商家查看問題與建議
  → 逐欄選擇替代欄位或重建 Mapping
  → 建立新的 active MappingVersion

已提交版本曾在 Dashboard 顯示 issue code、Ragic 欄位、Shopify path、摘要與建議,讓商家不只看到「Mapping 壞了」,而是知道哪個欄位消失、變唯讀或型別不相容,以及應該重新匯入文件、選擇替代欄位或檢查 Ragic 設定。研究當下的來源工作樹正在重整這段 UI,因此這裡只把 server functions、版本資料與既有測試視為可證明能力,不宣稱目前未提交介面已完成。

關鍵實作

Schema diff 不依賴顯示名稱作為 identity:

const current = fieldsById.get(mappingField.ragicFieldId);

if (!current) {
  issues.push({ code: 'missing_field', shopifyPath: mappingField.shopifyPath });
} else if (!current.writable || current.isReadonly) {
  issues.push({ code: 'readonly_field', shopifyPath: mappingField.shopifyPath });
} else if (current.isLoadedField) {
  issues.push({ code: 'loaded_field', shopifyPath: mappingField.shopifyPath });
}

逐欄修復仍在 server 端驗證替代欄位:

const replacement = latestSheet.fields.find(
  field =>
    field.id === replacementFieldId &&
    field.writable &&
    !field.isReadonly &&
    !field.isLoadedField,
);

if (!replacement) throw new Error('替代欄位不可用');

若修的是 unique key 對應的 Shopify path,unique key 也必須一起更新;同一 Ragic Field ID 已被另一條 Mapping 使用時則拒絕儲存。

實際驗證

來源測試已覆蓋:

  • 相容 schema 不改變 active Mapping。
  • mapped field 或 unique key 消失時標成 needs_review
  • 唯讀、loaded 與不相容型別會產生不同 issue code。
  • issue details 可還原成商家可讀的欄位、Shopify path 與建議。
  • 欄位修復只能選最新 schema 中可寫入的欄位。
  • 修復 unique key field 時同步更新 unique key。
  • 替代 Field ID 已被其他 Mapping 使用時拒絕。
  • 每次建立、封存、needs review、修復與還原都保存版本事件。
  • 還原版本只查詢目前 shop 可存取的歷史,不接受其他 shop 的版本 ID。

這些驗證說明 App 內的 parsed schema 與 Mapping lifecycle;它們不能保證 Ragic 所有 formula、workflow、approval 或 Link & Load 行為都能由目前 parser 表達。

仍然存在的限制

第一,案例使用的是專案定義的 Ragic API 文件模板,不是直接讀取 Ragic 官方 schema endpoint。Ragic 現行 HTTP API 已提供單一 sheet 的 metadata/schema,可回傳 Field ID、型別、唯讀/計算欄位與 subtable 等 metadata;但來源案例尚未串接、測試或遷移到這支 API。文件若沒有同步反映 Ragic 現況,現有 diff 只是在比對兩份過期資料。

第二,目前型別相容判斷是有限規則,不涵蓋所有 write format、selection options、subtable row identity、formula 與 workflow。即使 diff 顯示 compatible,大批同步前仍應做 Dry Run 與少量 read-back。

第三,版本還原只還原 App 保存的 Mapping 設定,不會回滾 Ragic 欄位,也不會撤銷已寫入的資料。現有 restoreMappingVersion() 也不會自動重新執行 diff;若 schema 已刪除舊 Field ID,還原舊版本後仍要再以最新 schema 驗證,不能把 restore 當成無條件安全 rollback。

第四,來源中仍保留以規則建議重新產生 Mapping 的修復路徑;對正式商家流程,更安全的預設仍是逐欄人工確認。名稱打分可以當建議,不能直接取代 unique key 與業務語意決策。

可以延伸到哪些情境?

Schema diff 不只適用 Ragic。只要兩個系統靠欄位 Mapping 串接,都可以保存版本化 contract,來源或目的 schema 變更時先標成 needs review,再用 issue-level repair 恢復。

下一篇會把同一套 App 的資料模型帶到部署層:開發用 SQLite、正式用 PostgreSQL:Prisma 雙 Schema 如何避免部署才爆炸?

參考資料

以上平台文件查核日期:2026-07-27。