mcpField
O
objectstack-data

objectstack-data

github.com/objectstack-ai数据与数据库76222 次安装今天更新
代码仓库

关于 objectstack-data

objectstack-data 是面向 ObjectStack 数据模型设计者的协议参考技能,涵盖对象、字段、关系、权限、生命周期及数据源等定义。它还说明字段条件规则和已有数据库的架构演进约束。

主要功能

  1. 01参考对象与字段定义规范
  2. 02说明关系、索引和验证规则
  3. 03涵盖权限控制与数据生命周期
  4. 04介绍数据源和联邦对象配置
  5. 05说明字段条件规则与架构演进

适用场景

  1. 01设计 ObjectStack 对象及字段模型
  2. 02配置数据访问权限和保留策略
  3. 03为字段定义条件显示或必填规则

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/objectstack-ai/objectstack --skill objectstack-data

安装后在 Claude Code 中输入/objectstack-data确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evals/
hooks-security-seeds-datasources.json
README.md
objects-fields-relationships.json
references/
seeds.md
_index.md
lint-rules.md
examples-objects.md
data-hooks.md
rules/
datasources.md
field-types.md
naming.md
indexing.md
validation.md
security.md
lifecycle.md
relationships.md

详细介绍

数据建模 — ObjectStack 数据协议

技能边界

需求改用
查询、筛选或聚合记录objectstack-query
定义 REST API 端点或身份验证objectstack-api
构建视图、仪表板或应用objectstack-ui
创建插件或注册服务objectstack-platform

快速参考 — 详细规则

如需查看包含错误/正确示例的完整文档:

  • 命名约定 — snake_case 规则、选项值、配置属性
  • 字段类型 — 全部 49 种字段类型及其配置
  • 关系 — lookup 与 master_detail、联结模式、删除行为
  • 验证规则 — 全部验证类型、脚本反转、严重级别
  • 索引策略 — btree/gin/gist/fulltext、复合索引、部分索引
  • 数据生命周期与保留 — lifecycle 类别(record/audit/telemetry/transient/event)、保留/TTL/轮换/归档策略;❗ 仅追加对象必须声明一个类别(与下方的生命周期 钩子 不同)
  • 生命周期钩子 — 8 种生命周期事件、handler 与沙盒化 body(ctx + capability 契约)、注册、规范模式
  • 数据源与联邦 — defineDatasource、外部/联邦对象(remoteName/columnMap)、自动连接门控、凭据;❌ 外部对象上不得设置 field.columnName
  • 安全与访问控制 — 权限集、分配行、RLS 策略、secret / requiredPermissions、tenancy、平台全局安全态势

核心概念

对象定义

对象是 ObjectStack 中的基本数据实体。它映射到数据库表,并自动提供 CRUD API。

必需属性:

属性类型命名约定描述
namestringsnake_case不可变的机器标识符(/^[a-z_][a-z0-9_]*$/)
fieldsmap键使用 snake_case字段定义
sharingModelenum以下四种之一组织范围内的默认记录可见性(OWD)。Zod 将其标记为可选,但若未显式定义 sharingModel,发布将被拒绝,并返回 422 lint 信封 security-owd-unset——未设置不代表做出了决定(维护者裁定,2026-08-13)。每个对象都必须显式定义该属性

sharingModel——四种规范值(ADR-0090 D4;已移除旧别名)。缺少该属性的自定义对象在运行时会解析为 private,但发布入口会先拒绝它:

值读取 / 写入权限
private仅所有者(可通过共享规则、RLS 或 readScope/writeScope 扩大权限)
public_read所有人可读取;所有者可写入
public_read_write所有人可读写
controlled_by_parent继承自主记录(master_detail 子记录)

重要的可选属性:

属性默认值描述
label根据 name 自动生成便于人阅读的单数标签
pluralLabel—复数形式(例如,“Accounts”)——参考应用中的 31/31 个对象都设置了该属性;请与 label 一并定义
icon—用于导航、列表标题和查找选择器的图标名称——参考应用中的 31/31 个对象都设置了该属性
highlightFields推导得出有序字段键,用作记录的精简展示:相关列表在父记录详情页中呈现的列,以及查找选择器显示的内容。在子对象上声明此属性(参见关系)
namespace—不是架构键——ObjectSchema.create() 会拒绝未知键,因此定义此属性会导致构建错误。请改为将前缀直接嵌入 name(例如 name: 'crm_account')
datasource'default'虚拟化数据的目标数据源 ID
nameField推导得出(例如 'name'/'title')规范的记录标题字段——用作记录显示名称的存储字段。使用单个文本 / 电子邮件字段,或用于组合标题的公式字段(returnType: 'text')
displayNameField—nameField 的已弃用别名(仍会作为回退选项使用)
titleFormat—已弃用但未移除(ADR-0079)——仅用于渲染:服务端无法返回或查询该属性。请使用 nameField(优先级更高);如需组合标题,请将 returnType: 'text' 的公式字段指定为 nameField
enable—功能标志(trackHistory、searchable、apiEnabled 等)
fieldGroups—表单 / 详情页的有序逻辑字段组列表(参见字段组)
lifecyclerecord 语义(永久)数据保留 / 轮换 / 归档约定。仅追加、高写入速率的对象必须设置此属性——telemetry/transient/event/audit 类必须声明边界策略,否则解析会失败(参见数据生命周期与保留)
对象能力(enable)

按对象切换系统行为:

标记默认值用途
trackHistoryfalse字段级审计跟踪
searchabletrue为记录编制索引,供全局搜索使用
apiEnabledtrue通过自动生成的 REST + MCP API 暴露
apiMethodsall为六种基本操作(get、list、create、update、delete、bulk)设定白名单;派生动词(search/export/upsert/…)会自动遵循
filesfalse附件和文档管理
feedstrue社交动态、评论、提及 — 选择退出:显式设为 false 会隐藏动态界面并拒绝新评论
activitiestrue活动时间线(sys_activity 中的 CRUD 镜像)— 选择退出:显式设为 false 会停止镜像并隐藏时间线
clonetrue深度克隆记录

搜索字段(searchableFields)

对象上的 searchableFields 是 $search 扫描的规范字段集(ADR-0061)。 若不设置,搜索会自动默认为 nameField 加上对象的短文本和枚举列(text / email / phone / url / autonumber / textarea / markdown / select / status);设置后则会明确固定字段集。视图可以缩小字段集,但不能扩大。

⛔ 搜索关联记录的标题 — 始终镜像该值

$search 扫描被查询对象自身的列。带点号的路径永远不是搜索目标:与 fields / sort / filters 不同,搜索轴不会解析遍历路径;searchableFields(或 $searchFields 覆盖项)中的 project_id.name 会被拒绝,而不是静默丢弃。

这是唯一的规定 — 每次都要给出。 若要按关联记录的标题搜索,请将该标题复制到此对象上的存储字段,并将该字段声明为可搜索。按项目名称搜索任务列表:

// `project_name` is a stored, denormalized mirror of the parent's title.
{
  name: 'task',
  enable: { searchable: true },
  fields: {
    name:         { type: 'text', required: true },
    project_id:   { type: 'lookup', reference: 'project' },
    project_name: { type: 'text', label: 'Project Name' },   // ← the mirror
  },
  searchableFields: ['name', 'project_name'],
}

?search=apollo 会展开为 name $contains 'apollo' OR project_name $contains 'apollo' — 在所有驱动程序上执行单表扫描,无需遍历。(如果对象未声明 searchableFields,text 镜像字段也会自动纳入默认字段集。)

❌ 绝不要镜像到 formula 字段。 公式字段是虚拟的 — 没有驱动程序会为它实体化列,因此针对它的 $contains 谓词无从扫描(SQL 驱动程序会生成针对不存在列的 WHERE 条件)。CEL 也只读取此记录自身的字段(record.<field>),因此公式字段本身无法获取关联标题。此错误会被明确拒绝,而非静默处理:任何 searchableFields 中的 formula 条目(包括对象自身的字段集)都会导致 os validate 报错(searchable-field-unsearchable),请求中指定该字段则会返回 400 INVALID_FIELD。过去会清空这两者,之后却始终匹配不到。

镜像维护是需要权衡的地方 — 镜像是反规范化数据,其新鲜程度取决于写入方式。请覆盖两种写入路径:

时机维护镜像的方式
创建任务,或将任务重新关联到另一个项目在 task 上使用 beforeInsert / beforeUpdate 钩子 — 读取传入 project_id 对应父记录的 name,并写入 project_name
重命名项目在 project 上使用 afterUpdate 钩子 — 为该项目的任务重新写入 project_name

通过绕过钩子的路径写入的行(批量导入、直接 SQL)需要一次性回填。请参阅生命周期钩子。

两种写法都会被明确拒绝:os validate 会报告 searchable-field-unknown,请求中指定带点号的路径则会返回 400 INVALID_FIELD。每条消息都会附带相应的处理指引。

跨对象搜索路径是出于设计而被拒绝,并非尚待实现。不要为此另设按项目划分的约定 — 镜像字段就是解决方案。


→ 原文已移至 references/examples-objects.md § 字段组(MVP)。


条件字段规则

当规则属于数据模型,且应在所有编辑字段的场景中生效时,应将条件 UI/数据录入规则放在字段定义上:默认表单、通过 Studio 编写的表单、内联主从网格、公开表单,以及由 API 支持的写入。

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

export const Invoice = ObjectSchema.create({
  name: 'invoice',
  sharingModel: 'private',
  fields: {
    status: Field.select({
      options: [
        { label: 'Draft', value: 'draft' },
        { label: 'Sent', value: 'sent' },
        { label: 'Paid', value: 'paid' },
        { label: 'Void', value: 'void' },
      ],
    }),
    paid_at: Field.datetime({
      visibleWhen: P`record.status == 'paid'`,
      requiredWhen: P`record.status == 'paid'`,
    }),
    locked_total: Field.currency({
      readonlyWhen: P`record.status == 'paid'`,
    }),
  },
});
  • 使用 visibleWhen 在 ObjectUI 表单中隐藏无关字段。
  • 使用 readonlyWhen 锁定特定状态下的字段;当谓词为 TRUE 时,ObjectQL 写入路径会忽略传入的更改。
  • readonly: true 约束的是终端用户界面,不约束受信任的系统写入方。非系统写入(REST/UI,以及任何 runAs:'user' 流程——默认值)会从所有非系统写入中剥离该字段;写入会报告成功,但该值不会生效。系统上下文写入——runAs:'system' 流程、系统钩子、种子数据、导入、迁移——不受此限制,确实会写入该字段。因此,“用户无法编辑此字段,但自动化可以维护它”这种模式,应通过声明字段为 readonly,并且使用 runAs:'system' 运行维护流程来实现(参见 objectstack-automation),而不是移除 readonly。从 runAs:'user' 的 update_record 节点写入 readonly 字段属于构建时错误(os validate/os build)。
  • 使用 requiredWhen 设置条件必填;ObjectQL 验证器会在提交时强制执行。conditionalRequired 别名已在 protocol 17 中移除——发出该别名会导致解析错误。
  • **根据意图选择——不变量还是转换门槛。**对每条已存储记录都成立的事实属于不变量:使用 validations[] script 规则。违反该规则的行在修复前,任何编辑都会被拒绝。转换条件(“记录达到 paid 状态后必填”)则使用 requiredWhen/字段边界,它们判断的是写入,而不是已存储的行。“在 X 条件下必填”听起来像不变量,但它并不是;将不变量写成门槛,永远无法让它自行生效。
  • 对于内联 master_detail 网格,谓词会针对子行的 record 逐行求值,因此行项目规则应放在子字段上。
  • 对于复杂谓词,加载 objectstack-formula 并通过 P\...`发出 CEL;不要使用 Salesforce 风格的AND、IN (...)或{field}` 语法。

→ 已原样移至 references/examples-objects.md § 快速入门模板。


现有数据库的架构演进

元数据→数据库同步仅支持增量添加:启动时会创建新表和新列,但绝不会修改或删除已有列。对已有数据的对象进行非增量变更,会导致其与物理架构悄然不一致;写入时以数据库列为准:

变更重启时的现有数据库行为
添加对象/字段/索引✅ 自动应用(增量添加)
移除 storage: { notNull: true }(放宽 NOT NULL)⚠️ 绝不自动应用——此差异的 category: 'needs_confirm'(relax_not_null),需通过 os migrate apply 确认。仅放宽 required 不会改变任何列
将 unique 的范围从全局改为按租户开发环境会自动修复;否则使用 os migrate apply(replace_unique_index)
类型/长度变更、删除字段、重命名使用 os migrate apply(删除/收紧约束时使用 --allow-destructive)
移除声明的索引,或更改其列使用 os migrate apply(删除索引或将其重建为 UNIQUE 时使用 --allow-destructive)

required 不是控制 NOT NULL 的开关(ADR-0113)。 required 是写入契约——如果插入时未提供该值,引擎会拒绝操作——它与列本身无关。物理约束需要通过单独的显式选项 storage: { notNull: true } 启用,差异检测也会检查此项。因此,在已部署的对象上收紧 required 是安全的(现有空值行仍可读取);而在存在空值行时声明 storage.notNull 则属于破坏性迁移。两者不能与 requiredWhen 组合使用——条件契约不能成为无条件的列约束。

一个明显的迹象:/meta 报告字段为可选(没有 required,也没有 storage.notNull),但省略该字段的写入会失败,并返回原始驱动错误,而不是明确的 400 验证错误——这说明列的 NOT NULL 约束已过时,而不是验证器有问题。运行 os migrate plan 预览,再运行 os migrate apply 进行协调;或者声明 storage: { notNull: true },以确认该列约束。CLI 详情请参阅 objectstack-platform。


常见模式

命名规则摘要
上下文约定示例
对象 namesnake_caseproject_task
字段键snake_casefirst_name、due_date
架构属性camelCasemaxLength、lookupFilters
选项 value小写in_progress

错误/正确示例请参阅 rules/naming.md。

字段类型选择

共有 49 种类型。快速分类:

  • 文本: text、textarea、email、url、phone、password、markdown、html、richtext — ⚠️ 通用对象上的 password 以明文形式静态存储(读取时会遮蔽,绝不哈希);凭据请优先使用 secret
  • 密钥: secret — 通过已注册的 ICryptoProvider 加密静态存储的凭据(可逆)(数据库密码、API 密钥、令牌);读取时会遮蔽,并采用失败关闭策略(ADR-0100)。凭据的推荐类型
  • 数字: number、currency、percent
  • 日期/时间: date、datetime、time
  • 逻辑: boolean、toggle
  • 选择: select、multiselect、radio、checkboxes
  • 关系: lookup、master_detail、tree、user — user 是人员选择器(专用于 sys_user 的查找字段;存储方式与 lookup 相同)
  • 媒体: image、file、avatar、video、audio
  • 计算: formula、summary、autonumber — formula 字段使用 expression 中的 CEL 表达式(从 @objectstack/spec 使用 F\...``);请参阅 objectstack-formula 技能
  • 嵌入式: composite、repeater、record — 存储在父行上的嵌入式 JSON 子对象(无独立表/外键)
  • 增强: location、address、code、json、color、rating、slider、signature、qrcode、progress、tags、vector

完整参考请参阅 rules/field-types.md。

关系模式
模式实现方式
一对多(独立)子对象上的 lookup 字段
一对多(拥有)子对象上的 master_detail 字段
多对多(简单)多值 lookup(multiple: true)——存储 ID 的数组列
多对多(带属性)使用两个 lookup 字段的关联对象
层级关系tree 字段(自引用)

详细示例请参阅 rules/relationships.md。

multiple: true 的 lookup ≠ 关联对象。 多值 lookup ({ type: 'lookup', reference: 'x', multiple: true })在记录中以 ID 数组的形式存储和读取—— 按位置引用元素(在流程值中使用 {record.tags.0})。它不是关联表。只有当关系本身带有属性(角色、添加时间等)时,才使用关联对象(两个 lookup)。

验证模式

⚠️ 脚本验证的逻辑是反向的:当表达式为 true 时,验证失败。

执行插入时,载荷中省略的可选字段在验证谓词中读取为 null,因此 record.due_date == null 匹配省略字段的情况,效果与显式设为 null 相同。(更新时,会使用先前记录中的值。)

验证类型的完整集合(ValidationRuleSchema 判别字段):

  • script — 公式表达式(逻辑反向)
  • state_machine — 合法的状态转换
  • format — 正则表达式或内置格式
  • cross_field — 比较多个字段的值
  • json_schema — 根据 JSON Schema 验证 JSON 字段
  • conditional — 仅当谓词成立时才应用嵌套规则

没有 unique 验证类型(已从规范中移除)。 使用唯一索引强制唯一性(包括复合唯一性),并注明其范围(ADR-0120): indexes: [{ fields: ['department', 'email'], unique: 'organization' }]。

所有类型和示例请参阅 rules/validation.md。

索引模式

声明中仅有 fields / unique / name。 unique 默认为 false;如果这就是你需要的值,可以省略。

indexes: [
  { fields: ['status', 'created_at'] },                // composite
  { fields: ['email'], unique: 'organization' },       // unique per organization
  { fields: ['hostname'], unique: 'global' },          // unique platform-wide
  { name: 'idx_acct_status', fields: ['status'] },     // custom name
]

type 和 partial 已在协议 17 中弃用:没有任何驱动读取过它们,因此编写 type 不会选择访问方法,而编写 partial 会生成完整索引并丢弃谓词。现在两者都会导致 tsc 错误和解析错误;os migrate meta --from 16 会列出需要修改的内容;--write 会应用能够确认的修改,其余部分需手动处理。

唯一索引必须注明范围——'organization'(每个组织一个持有者,NULL 安全)或 'global'(整个安装环境中一个持有者)。 对于声明的索引,单独写 unique: true 是 'global' 的弃用写法:它看起来像是“每个组织”,实际效果却相反,因此 os lint 会发出警告,协议 18 会拒绝它。对于字段,unique: true 表示 'organization',仍然有效。

有关复合索引、唯一性范围,以及如何在数据库层构建 partial / gin / gist 索引,请参阅 rules/indexing.md。

→ 内容原样移至 references/examples-objects.md § 生命周期钩子(钩子参考文档见 references/data-hooks.md)。



对象扩展模型

要向不归你所有的对象添加字段/验证/索引,请编写 defineObjectExtension({ extend, fields, priority }),并在 defineStack({ objectExtensions: [...] }) 中注册。priority 用于设置合并顺序(默认值 为 200,范围为 0–999);扩展可以添加内容,但不能移除内容。⛔ 这与对象级别的 ownership 不同(记录所有权枚举 'user' | 'business_unit' | 'org' | 'none')。架构: node_modules/@objectstack/spec/src/data/object.zod.ts(ObjectExtensionSchema)。


安全与访问控制

对象级访问控制定义在权限集中,而不是对象架构中。对象级别没有 permissions 键(也没有 hooks 键)—— ObjectSchema.create() 会将这两个键作为未知键拒绝。

完整规则:安全与访问控制。

访问深度(scope-depth)——ERP 中“查看我的单位/我的单位及下级”轴

对于所有者范围(private)对象,权限集中的对象级授权可以携带 readScope / writeScope,以声明式方式扩大所有者匹配范围,而无需手写 RLS 策略(ADR-0057 D1):own(默认) · own_and_reports · unit · unit_and_below · org。它会在请求时解析为 owner_id IN (…) 集合,并像 RLS 一样以 AND 方式注入。⚠️ 开源核心边界(ADR-0016): 开源版仅支持 own 和 org——另外三个基于层级的选项需要付费的 @objectstack/security-enterprise 插件;没有该插件时会默认拒绝并回退到 own,且除非授权声明 requires: ['hierarchy-security'],否则 defineStack 会报错。架构:PermissionSetSchema.objects.*。


→ 已原样移至 references/examples-objects.md § 元数据保护(protection)。


→ 种子数据与固定数据(defineSeed())已原样移至 references/seeds.md(快速入门 · Seed 字段 · 导入模式 · externalId 选择 · 关系引用 · 动态值(CEL)· 种子最佳实践)。


→ lint 与生成质量已原样移至 references/lint-rules.md。


验证你的工作

编写或编辑任何 *.object.ts / *.seed.ts 后,报告完成前请运行作者时校验门禁:

os validate     # Zod schema + CEL predicates (record.<field> existence) + bindings
# or: os build  # the same gates, plus emits dist/

它会捕获那些原本会在运行时静默失败的问题:在 requiredWhen / readonlyWhen / visibleWhen、验证规则、公式或 行级安全/共享谓词中使用裸字段引用(done 而不是 record.done),导致其计算结果为 null,永远不会触发。os lint 是一个单独的 检查步骤,另外还会根据本技能中的约定检查数据模型(关系、主从关系、汇总)——也请运行它,但它不能 替代 os validate。(提醒:连续运行两次 os build 且源代码没有更改时,生成的字节必须完全相同——参见上面的确定性门禁。)在脚手架生成的项目中,门禁命令为 npm run validate。


参考

完整的 Zod 架构列表(附单行说明)请参阅 references/_index.md——其中包含指向 node_modules/@objectstack/spec/src/ 的引用。请务必Read源代码以确认准确的字段结构;不要凭记忆推断属性名称。

评论

支持 Markdown