ObjectOS
構建資料

驗證規則

必填欄位、唯一約束與 CEL 驅動的規則,在平臺層攔住壞資料 —— 錯誤訊息由你掌控。

**驗證在每條寫入路徑上都會執行。**REST、Console 表單、ObjectQL —— 同一套規則處處生效,壞資料沒有後門可鑽。自 16.0 起也包括多行更新:批次更新會對每一條匹配的記錄逐行執行規則。驗證規則是作用於單條記錄的確定性、同步、無副作用謂詞:僅憑這次寫入(更新時再加上更新前的記錄)即可判定,不做任何 I/O。

驗證是分層的 —— 能表達規則的最低層就是該用的層:

表達什麼示例
欄位修飾符是否必填、是否唯一required: trueunique: true
欄位約束單欄位形態minmaxmaxLengthformat
條件修飾符依賴上下文的必填requiredWhen: P`record.amount > 10000`
物件級驗證規則跨欄位業務邏輯"折扣不能超過總額"
唯一索引組合 / 限定範圍的唯一性{ fields: ['code', 'organization'], unique: true }
生命週期 Hook任意驗證程式碼、刪除守衛beforeInsert / beforeUpdate / beforeDelete

欄位級:required、unique 與約束

通用修飾符適用於所有欄位型別:

import { ObjectSchema, Field } from '@objectstack/spec/data';

fields: {
  email: Field.email({ label: 'Contact Email', required: true, unique: true }),
  quantity: Field.number({ label: 'Quantity', min: 1, max: 9999 }),
  code: Field.text({ label: 'Code', minLength: 3, maxLength: 20 }),
}
修飾符行為
required: true在執行時拒絕 null / undefined
unique: true資料庫層唯一約束 —— 而不是有競態的應用層檢查
min / max數值範圍(number、currency、percent、rating、slider……)
minLength / maxLength文本型別的字元長度上下限
format內建形態檢查 —— emailurlphone 欄位型別預設自帶
readonly: true更新時由服務端強制 —— 非系統寫入該欄位會被靜默丟棄(插入不受限)

欄位型別本身也自帶免費驗證:email 檢查 local@domain 形態,url 要求帶協議,select 的值必須匹配某個選項,lookup 強制被引用記錄存在,json 必須能解析。每種預設行為見欄位型別參考

條件式必填 / 只讀 / 可見

是否必填可以通過 CEL 謂詞依賴記錄的其餘部分:

import { P } from '@objectstack/spec';

po_number: Field.text({
  label: 'PO Number',
  requiredWhen: P`record.amount > 10000`,
}),

visibleWhenreadonlyWhenrequiredWhen 都接受一個 CEL 謂詞 —— 見公式

物件級驗證規則

跨欄位業務邏輯放在物件的 validations 數組裡:

export const Order = ObjectSchema.create({
  name: 'order',
  fields: {
    amount: Field.currency({ label: 'Amount', required: true }),
    status: Field.select({ label: 'Status', options: [ /* ... */ ] }),
  },

  validations: [
    {
      name: 'amount_positive',
      type: 'script',
      severity: 'error',
      message: 'Amount must be greater than zero',
      // CEL 谓词 —— TRUE 表示记录无效。
      condition: 'record.amount <= 0',
      events: ['insert', 'update'],
    },
  ],
});

警告:script 規則來說,condition失敗謂詞 —— 它求值為 TRUE 時,驗證失敗。要針對狀態來寫判斷:record.amount <= 0 拒絕非正數金額。

公共屬性

每種規則型別都共享這套基礎形態:

屬性必填說明
name唯一的規則名(snake_case
message面向使用者的錯誤訊息
typescriptstate_machineformatcross_fieldjson_schemaconditional
severityerror(阻止儲存,預設)、warning(允許儲存)、info
eventsinsertupdate —— 預設 ['insert', 'update']delete 事件已在 16.0 中移除:求值器從未在刪除路徑上執行(刪除沒有可供驗證的記錄載荷),它一直是靜默空操作 —— 刪除守衛請用 beforeDelete Hook
priority0–9999,數字越小越先執行(預設 100)
active不刪除即可開關(預設 true

六種規則型別

型別檢查什麼關鍵配置
script任意 CEL 謂詞condition(TRUE = 無效)
state_machine允許的狀態遷移fieldtransitions 對映、可選 initialStates
format單欄位匹配正則或內建格式fieldregexformat: 'email' | 'url' | 'phone' | 'json'
cross_field欄位之間的關係fieldscondition
json_schemaJSON 欄位匹配 JSON Schemafieldschema
conditional僅在謂詞成立時應用巢狀規則whenthen、可選 otherwise

兩條你會反覆用到的規則:

// 状态机 —— 强制状态流转
{
  name: 'order_status_transitions',
  type: 'state_machine',
  severity: 'error',
  message: 'Invalid status transition',
  field: 'status',
  transitions: {
    draft: ['submitted', 'cancelled'],
    submitted: ['approved', 'rejected', 'cancelled'],
    approved: ['completed'],
    rejected: ['draft'],
    cancelled: [],
    completed: [],
  },
  initialStates: ['draft'],  // 16.0 新增:记录允许被“创建”为哪些状态
  events: ['insert', 'update'],  // 包含 'insert' 才会检查 initialStates
}

// 跨字段 —— 日期先后有序
{
  name: 'date_range_valid',
  type: 'cross_field',
  severity: 'error',
  message: 'End date must be after start date',
  fields: ['start_date', 'end_date'],
  condition: 'record.end_date <= record.start_date',
  events: ['insert', 'update'],
}

16.0 新增:state_machine 規則可以宣告 initialStates —— 記錄允許被建立為哪些狀態。插入時狀態欄位的值不在列表內會被拒絕(invalid_initial_state)。transitions 只約束更新,而 select 欄位允許任何已宣告的選項作為初始值,所以不加 initialStates 時記錄可能"生在流程中間"(比如一建立就是 approved)。省略它則保持舊行為(插入時不檢查初始狀態)。規則的 events 必須包含 insert,該檢查才會執行。

在更新時的條件裡,previous 持有變更前的快照 —— record.stage != previous.stage 可檢測到變更。

唯一性:用索引,不用規則

沒有 uniqueness 驗證型別 —— 這是有意為之。先 SELECT 再 INSERT 的檢查天然有競態(TOCTOU);資料庫唯一約束沒有。唯一性要在資料層強制:

// 字段级
email: Field.email({ label: 'Contact Email', unique: true }),

// 组合 / 限定范围的唯一性用索引
indexes: [
  { fields: ['code', 'organization'], unique: true },
  // 需要限定范围 / 条件约束时加 `partial`
]

同樣的邏輯也排除了非同步 / 遠端驗證(那是客戶端表單的關注點,放在寫路徑上還是 SSRF 與延遲隱患)和自定義處理器 —— 任意驗證程式碼應該放進 beforeInsert / beforeUpdate 生命週期 Hook,刪除時的守衛則放進 beforeDelete Hook。

自定義錯誤訊息

message 就是規則觸發時使用者看到的內容 —— 出現在 Console 表單和 API 錯誤響應中。要寫得可執行:

"Invalid input""Close date is required for closed deals"
"Validation failed""Phone must match format: +1-XXX-XXX-XXXX"
"Error""Discount cannot exceed total"

severity 來校準:error 阻止儲存,warning 顯示訊息但放行儲存,info 純提示。

**提示:**無法求值的謂詞(解析錯誤、未繫結變數)會被當作損壞的規則處理 —— 記錄日誌後跳過,而不是攔下每一次寫入。

最佳實踐

應該不應該
在儘可能低的層做驗證(欄位 → 規則 → Hook)用 Hook 做欄位修飾符就能表達的事
寫清晰、可執行的訊息在多層重複同一個檢查
priority 讓廉價的格式檢查先跑構造過於複雜的條件
用真實資料測試規則攔住正當的邊界情況

下一步

頁面原因
資料模型這些規則所保護的物件與欄位
公式conditionrequiredWhen 背後的 CEL 語言
關係跨物件的引用完整性
CEL 參考條件可用的完整運算子與標準庫參考
欄位型別各欄位型別的內建驗證預設值
流程用自動化響應(合法的)記錄變更

On this page