Skip to Content
核心概念規則immutable_when

immutable_when 規則

immutable_when 在列的 pre-image 符合條件時凍結指定欄位。它回答其他寫入規則都答不了的問題:「這列一旦 posted,數量就再也不能改。」

在它之前,這個契約沒有正確的寫法。transition 比對單一 scalar 欄位的前後值,因此一筆讓 Status 維持 posted、只偷改 Qty 的寫入根本不是 transition,什麼都不會觸發。其他規則的 when 前置條件都以 post-image 判斷,於是呼叫者只要在同一筆寫入裡同時解鎖並修改 —— {"Status": "draft", "Qty": 6} —— 條件 Status eq posted 就永遠不會命中那列它本來要守的資料。把鎖定條件改成以寫入之前的列來判斷,這兩個洞同時補上。

Payload

{ "type": "immutable_when", "name": "posted-locks-qty", "prior_when": [ { "column": "狀態", "op": "eq", "value": "posted" } ], "columns": ["數量", "單價"] }

prior_when 是鎖定條件:最多五個 predicate,以 AND 組合,使用與 when 完全相同的詞彙 —— 相同 operators、相同的型別值檢查、同樣接受 col_<hex> 或顯示名稱。columns 是被鎖欄位集合:最多 20 個 ref,存檔時去重。兩者都以 rename-stable 的內部 key 儲存,並由 rules.get 以顯示名稱回吐。

由於 prior_when 沿用 when 的驗證器,超過上限的錯誤訊息仍是 when 的用字:400,detail 為 when exceeds max 5 predicates

沒有 when,這正是重點

immutable_when 不接受一般的 when。帶了非空的 when 會在設定階段回 400

immutable_when rules do not take a when pre-condition — use `prior_when`, which evaluates against the row's PRE-image

以 post-image 判斷的 when,會被它本該擋下的那筆寫入自己打敗。prior_when 讀的是已儲存的列,所以同一個 request 裡把 Status 改成 draft 解不了鎖:pre-image 依然是 posted

被鎖欄位也可以同時出現在 prior_when。在條件 Status eq posted 下鎖住 Status,就讓 posted 成為終端狀態 —— 這是 transition allowlist 表達不出來的單向 latch,因為狀態機終究得列出某條出路。

哪些寫入會被判斷

變更是否判斷
create否。新列沒有 pre-image,所以一列可以一出生就是 posted 且帶任意值
updaterevert
restore否。Restore 原樣重放已儲存的資料
deleteDelete 不執行任何 predicate 規則

被鎖欄位採用與 transition 相同的正規化:null"" 視為同一個值,因此把本來就空的欄位清空不算變更。沒有列進 columns 的欄位,在被鎖的列上仍可自由編輯;pre-image 不符合 prior_when 的列也可自由編輯 —— 包括把自己改鎖定狀態。

這道鎖位於與 comparecheckrequiretransition 相同的 before-insert / before-update 卡口,因此 REST、bulk actions、callback、trigger record action 與 agent toolkit 都會撞上它。唯一會跳過它的,是下面說明的核准套用重放。

違規回什麼

400,detail 是一個純字串:

{ "detail": "Rule 'posted-locks-qty' violated: '數量' cannot change while the row matches the lock condition (prior_when evaluates the row as it was before this write)" }

Rule '<label>' violated: 前綴取規則的 name,其次是伺服器產生的 id —— 每一條存下來的規則都有 id,所以沒命名的規則會顯示成 Rule 'rule_b2c3d4e5' violated,而不是印出型別。請替規則命名。欄位名稱是顯示名稱。

同一筆寫入若改動多個被鎖欄位,它們會被逗號串成一則訊息,而不是分開回報:

Rule 'posted-locks-qty' violated: '數量', '單價' cannot change while the row matches the lock condition (prior_when evaluates the row as it was before this write)

不要靠解析這個字串取出欄位清單來做欄位級的標紅;你自己送出的 request 已經知道碰了哪些被鎖欄位。

設定階段的拒絕

以下每一項都是 rules.set400rules.preview 回同樣的字串),detail 都是純字串。有 hint 的錯誤,伺服器會在訊息後面接一個句點再把 hint 併進去。

原因detail
帶了非空的 whenimmutable_when rules do not take a when pre-condition — use `prior_when`, which evaluates against the row's PRE-image
prior_when 缺少或為空immutable_when requires a non-empty prior_when predicate list (the lock condition, evaluated against the row's pre-image). e.g. [{"column": "status", "op": "eq", "value": "posted"}]
columns 缺少或為空immutable_when requires a non-empty columns list (the fields locked while prior_when matches)
被鎖欄位超過 20 個columns exceeds max 20
columns 內有非字串或空字串each locked column must be a column reference
被鎖欄位不存在column '<ref>' not found in '<Table>' 後面接候選欄位 hint
columns 內是計算欄或 link 欄column '<Name>' is type formula — immutable_when locks only columns stored inline in the record (computed columns are read-only already; link values live in link tables and the lock would never fire)

全表上限不變:10 條規則,immutable_when 也算在內。

核准是官方認可的覆寫途徑

同一張表若有相符的 require_approval 規則,這道鎖根本沒有機會否決該筆寫入。寫入會在規則引擎執行前就被暫存,因此呼叫者收到的是一般的 409 approval_required 加上 process_idstaged_change_id,不是鎖的 400。見 require_approval核准流程指南

Moderator 核准後,套用階段會帶著 session 的 approval bypass 旗標重放該筆寫入,規則評估會跳過 immutable_when —— 而且只跳過 immutable_when。其他 predicate 規則在重放時照常執行。核准後的值會穿過這道鎖,staged change 最終落在 applied

這個跳過是刻意設計,不是漏洞。少了它,重放會拿仍然被鎖的 pre-image 再判一次,違規會逃出套用處理器,staged row 會卡在 applying,且每次 reclaim 都救不回來。

Warning 這道鎖要讀成「沒有人能悄悄改動它」,不是「永遠沒有人能改」。在有相符核准規則的表上,moderator 仍可放行 —— 而留下的軌跡是那筆 staged change,不是一次拒絕。

刪除或轉型被引用的欄位

條件欄位與被鎖欄位都是被依賴的引用。刪除或轉型其中任何一個都會回 409 與結構化的 conflicts 清單:

{ "detail": "Column is referenced by computed columns", "conflicts": [ { "type": "dependent_rule", "field": "posted-locks-qty", "message": "Write rule 'posted-locks-qty' references this column ('狀態'); delete the rule first" } ] }

條件欄位和被鎖欄位一樣重要。若把 prior_when 的欄位刪掉,predicate 會把缺少的 key 讀成 null,條件對每一列都不再成立,鎖就靜靜地停止生效 —— 這正是這個 409 要擋下的「降級成放行」失效模式。

它不做什麼

  • 它不管 create,因此無法約束初始值。那要用 check 或欄位預設值。
  • 它不能鎖計算欄或 link 欄,兩者都在設定階段被拒。Link 成員的變動完全不在這條規則的範圍內。
  • 它不阻止刪除,也不阻止先刪再重建。
  • 它不要求欄位有值。被鎖的值若同時必須存在,請搭配 require
  • 它不約束你沒有列進 columns 的欄位。

用 IaC 撰寫

不需要新的 line kind。rule linespec 是自由 dict,直接交給 REST endpoint 用的同一個驗證器,所以 immutable_when 規則和其他規則一樣寫。prior_when[].columncolumns 內的欄位引用,由 plan、apply、export 共用的同一個通用 ref walker 轉譯,因此一條鎖能通過 export → plan → apply 並在重新 plan 時是 noop。

先用 rules.preview 驗證候選清單,再以 rules.set 發布完整清單。

Last updated on