关于 objectstack-query
objectstack-query 是面向 ObjectStack 开发者的查询 DSL 设计技能,说明如何筛选、排序、分页、展开关联记录及聚合数据。内容也涵盖查询上下文、搜索规则和已弃用选项的替代写法。
主要功能
- 01使用比较、集合和逻辑运算符筛选
- 02排序并采用偏移或键集分页
- 03展开关联记录并控制字段投影
- 04按字段分组并执行聚合
- 05配置全文搜索与执行上下文
适用场景
- 01筛选并分页浏览业务对象记录
- 02读取记录及其关联对象字段
- 03汇总销售或运营数据
- 04对文本字段执行关键词检索
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ objectstack-ai/ objectstack --skill objectstack-query安装后在 Claude Code 中输入/objectstack-query确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/filters-pagination-search.jsonreferences/_index.mdrules/filters.mdaggregation.mdpagination.md详细介绍
查询设计 — ObjectStack 查询 DSL
调用约定 — object 是第一个参数
| 接口 | 形式 | 合法的选项键 |
|---|---|---|
引擎 find / findOne | engine.find('task', {…}, { context }) | context、where、fields、orderBy、limit、offset、search、searchFields、expand — 以及 六个驱动透传键 transaction、tenantId、tenantIds、timezone、bypassTenantAudit、preserveAudit |
引擎 aggregate | engine.aggregate('deal', {…}) | context、where、groupBy、aggregations、having、timezone、search、searchFields — 两个搜索键会在分组前筛选输入行,并与 where 组合为 AND 条件,与 find 中的行为完全相同 |
引擎 count | engine.count('task', {…}) | context、where |
| 协议 / REST | findData({ object: 'task', query: {…} }) | object 位于查询之外 |
嵌套的 expand 值 | 一个 QueryAST — { object, fields, where } | (见 Expand) |
ENGINE_FIND_OPTION_KEYS / ENGINE_AGGREGATE_OPTION_KEYS 是封闭集合:
集合之外的键会按名称拒绝(find('task') does not recognise option 'bogus'),绝不会忽略。因此,独立的 { object: 'account', limit: 20 } 字面量是一个 QueryAST,可用作 findData 的 query 或 expand 值,而不是引擎选项包。在进行上述检查之前,top 会折叠为 limit,filter 会折叠为 where。
这六个透传键会随 find / findOne(以及 update / delete)一同传递,因为这些方法的选项包就是驱动选项的基础,这也是显式 tenantId 能传到驱动的方式。count 和 aggregate 从不转发选项包,因此这两个方法会刻意将相同的键视为非法——如果接受它们,就会产生此检查要杜绝的静默忽略选项。唯一例外是 timezone:aggregate 会自行读取它来进行日期分桶,因此它在这里合法,并列于上表;count 则会像处理其余键一样拒绝它。
应使用哪种筛选方言?
| 编写内容…… | 方言 | 归属 |
|---|---|---|
ObjectQL where | 下文的 $ 运算符 | 本技能 |
列表视图 / nav-item filter | 使用 20 个运算符的 VIEW_FILTER_OPERATORS 枚举(equals、icontains、is_null、before、between、…)进行筛选的 [{ field, operator, value }] — 解析时会拒绝未知运算符 | objectstack-ui |
数据集度量 filter | 度量自身的筛选器 | objectstack-ui |
执行上下文(context)
这是 RLS / 系统读取的逃逸通道。钩子、作业或端点若未携带上下文进行读取,就会以调用方携带的身份运行——组织范围钩子因此会返回更少的行,让人无法分辨究竟是权限所致,还是“根本没有数据”。
const SYS = { isSystem: true } as const;
const [row] = await engine.find('project', { where: { name }, limit: 1, context: SYS });
可以传入执行信封(身份、租户、事务)的任意子集:系统读取使用 { isSystem: true };否则使用调用方自身的上下文(用户、职位或权限集),否则会返回 403 PERMISSION_DENIED(ADR-0096 D5)。对于读取方法,上下文可以放在查询包(上文)中,也可以放在末尾的选项参数中:engine.find(obj, query, { context });两者都提供时,以末尾参数为准。写入方法只接受末尾参数。
已移除的键 → 当前替代方案
| 已移除的键 | 当前替代方案 |
|---|---|
query.cursor | 键集分页 — 对排序键使用 where,并配合 orderBy 和 limit |
query.joins | expand(用于展示),或在 where 中使用 { relation: { field: value } }(用于筛选) |
query.distinct | 对字段使用 groupBy — 每种唯一组合对应一行 |
query.windowFunctions | 报告 / 仪表板元数据(objectstack-ui),或在应用代码中进行排名 / 累积 |
aggregation distinct: true | count_distinct |
aggregation array_agg / string_agg | 无 — 使用 fields 读取行并在调用方整理,或将汇总结果物化为存储字段 |
这六项在 @objectstack/spec 17 中都已列入墓碑清单:tsc 会将它们的类型设为 never,携带这些键的查询会在解析时失败,并给出升级指引。退役流程和完整的墓碑清单见 objectstack-upgrade。
快速参考 — 详细规则
- 筛选器 — 所有运算符、逻辑组合、按相关记录筛选、日期宏和会话令牌
- 聚合 — groupBy、日期分桶、函数、
having、每个度量的filter - 分页 — offset 与 keyset、最佳实践、性能
筛选运算符
隐式相等(简写)
最简单的筛选器 — 字段等于某个值:
{ where: { status: 'active' } }
// SQL: WHERE status = 'active'
比较运算符
| 运算符 | 用途 | SQL 等价项 | 类型 |
|---|---|---|---|
$eq | 等于 | = | 任意 |
$ne | 不等于 | <> | 任意 |
$gt | 大于 | > | 数字、日期 |
$gte | 大于或等于 | >= | 数字、日期 |
$lt | 小于 | < | 数字、日期 |
$lte | 小于或等于 | <= | 数字、日期 |
{ where: { age: { $gte: 18 } } }
// SQL: WHERE age >= 18
集合与范围运算符
| 运算符 | 用途 | SQL 等价项 |
|---|---|---|
$in | 在列表中 | IN (...) |
$nin | 不在列表中 | NOT IN (...) |
$between | 闭区间 | BETWEEN ? AND ? |
{ where: { status: { $in: ['active', 'pending'] } } }
{ where: { amount: { $between: [100, 500] } } }
字符串运算符
| 运算符 | 用途 | SQL 等价项 |
|---|---|---|
$contains | 包含子字符串 | LIKE '%?%' |
$notContains | 不包含 | NOT LIKE '%?%' |
$startsWith | 以前缀开头 | LIKE '?%' |
$endsWith | 以后缀结尾 | LIKE '%?' |
$icontains | 包含,不区分大小写 | LIKE '%?%' 折叠大小写 |
$like | 完整值模式,由调用方绑定 % / _ | LIKE ? |
$ilike | $like,不区分大小写 | ILIKE ? |
$contains / $notContains / $startsWith / $endsWith 执行
区分大小写比较;$icontains 是不区分大小写的对应运算符(仅折叠 ASCII
大小写)。因此,面向用户的场景应使用 $icontains:
{ where: { email: { $icontains: '@company.com' } } }
完整表格、$like 的可移植性和 $ilike 的适用范围:筛选规则。
Null 与存在性运算符
| 运算符 | 用途 | SQL / NoSQL |
|---|---|---|
$null | 检查是否为 null | IS NULL / IS NOT NULL |
$exists | 有值 | IS NOT NULL / IS NULL |
{ where: { deleted_at: { $null: true } } }
逻辑运算符
使用 $and、$or 和 $not 组合条件:
// OR: active accounts OR accounts with high revenue
{ where: { $or: [{ status: 'active' }, { revenue: { $gt: 1000000 } }] } }
// AND + OR combined
{
where: {
$and: [
{ type: 'enterprise' },
{ $or: [{ region: 'us' }, { region: 'eu' }] },
]
}
}
// NOT: exclude closed accounts
{ where: { $not: { status: 'closed' } } }
按相关记录筛选
查找下方的 { customer: { country: 'US' } } 会通过 where 处理:引擎以调用方的身份读取相关对象,并匹配其 ID。限制条件(仅一级、仅正向、仅 where、1000 个 ID、调用方的权限)以及绕过这些限制的两步路径:筛选规则 → 关系筛选器。
跨字段比较
{ $field: '...' } 会比较同一行的两列,且只能用于比较位置 — 参见**筛选规则 → 字段引用**。
排序
使用 orderBy 排序 — 它是一个排序节点数组:
{
object: 'account',
orderBy: [
{ field: 'priority', order: 'desc' },
{ field: 'name', order: 'asc' }, // Secondary sort
]
}
规则:
- 数组元素的顺序决定排序优先级
- 默认
order为'asc'— 升序排序时可以省略 - 为提升性能,排序字段应建立索引(参见 objectstack-data 索引规则)
分页
// Offset paging — page 3
{ object: 'account', limit: 20, offset: 40 }
适用场景: UI 页面、小型数据集、“跳转到第 N 页”。对于较大的偏移量,性能会下降 — 数据库仍需扫描被跳过的行。
对于 keyset 分页(无限滚动、API、大型数据集、实时信息流),应使用排序键上的 where 筛选出你看到的最后一行之后的记录,并始终使用同一字段、同一方向的 orderBy — 具体模式、方向规则和注意事项请参见**分页规则**。
聚合
六个函数(count、sum、avg、min、max、count_distinct)、日期分桶、having 和每个度量的 filter 都属于**聚合规则**。调用形式如下:
// Total revenue per region
const rows = await engine.aggregate('deal', {
groupBy: ['region'],
aggregations: [
{ function: 'sum', field: 'amount', alias: 'total_revenue' },
{ function: 'count', alias: 'deal_count' },
],
});
// SQL: SELECT region, SUM(amount) AS total_revenue, COUNT(*) AS deal_count
// FROM deal GROUP BY region
fields 和 orderBy 不在 ENGINE_AGGREGATE_OPTION_KEYS 中——不要将它们放入 aggregate 参数包。分组字段会自动选入结果行;按各度量的 alias 读取,并在 having 中引用相同的名称。groupBy 条目可以是用于日期分桶的对象——{ field: 'closed_at', dateGranularity: 'quarter' }。
展开(相关记录)
通过 lookup / master_detail 字段加载相关记录。请在 fields 中保留外键——关系由该列承载:
const tasks = await engine.find('task', {
fields: ['title', 'status', 'assignee', 'project'], // the FK columns stay
expand: {
assignee: { object: 'user', fields: ['name', 'email'] },
project: {
object: 'project',
fields: ['name'],
expand: { org: { object: 'org', fields: ['name'] } }, // nested expand
},
},
});
规则:
- 投影必须保留外键列。 使用
fields: ['title']并搭配expand: { project: … }不会解析出任何内容:引擎会从每条记录中读取外键,若外键缺失,就会跳过该关系,因此调用会返回没有相关数据的行,且不会报错。 - 默认最大展开深度为 3
- 引擎通过批量
$in查询解析展开项(而非 N+1 查询) expand中的键必须是 lookup 或 master_detail 字段名- 每个展开值都是嵌套的
QueryAST,但引擎只应用选择(fields)和筛选(where)——此路径不会应用每个父项的limit/offset/orderBy。如需对相关记录分页或排序,请直接查询相关对象。
全文搜索
标准形式是不带键的字符串,并在同级设置 searchFields:
const rows = await engine.find('article', {
search: 'machine learning',
searchFields: ['title', 'content'],
limit: 10,
});
// Executes as:
// { $and: [
// { $or: [{ title: { $icontains: 'machine' } }, { content: { $icontains: 'machine' } }] },
// { $or: [{ title: { $icontains: 'learning' } }, { content: { $icontains: 'learning' } }] },
// ]}
每个词都会在解析后的可搜索字段中生成一组 $icontains 谓词,并通过 $or 连接;以空格分隔的词之间通过 AND 连接(每个词都必须匹配某个字段)。select / status 字段按选项标签匹配,并映射到存储值。
**一个选项,三种写法:**输出 searchFields(引擎选项)。协议会将 $searchFields 规范化为该选项;对象形式 search: { query, fields } 中的 fields 也表示同样的范围缩小。
省略它时,将搜索对象声明的 searchableFields(或自动默认的 name/title + 短文本字段),并由服务端解析。它只能缩小该范围,不能扩大:通过 REST / 协议入口提交范围之外的字段名,会返回 400 INVALID_FIELD,而不会静默退回到全表扫描。对象形式 search: { query, fields } 仍可用于下面的 Tier-2 选项。
⚠️ 会验证,但随后静默忽略——切勿输出这些选项。 全部选项为
fuzzy、boost、operator、minScore、language和highlight;其.describe()标记对此有明确说明。词项始终通过 AND 连接;不进行相关性评分或高亮。
search 不会遍历关系。 带点号的路径会被拒绝——searchFields: ['project_id.name'] 指定了 task 未声明的列。请将相关记录的标题镜像到被查询对象的某个存储字段中,并对其进行搜索;该字段、写入钩子和 lint 提示文字见 objectstack-data → Search Fields (searchableFields)。如需按相关记录的列筛选,使用 where: { relation: { column: value } };如需显示相关记录,使用 expand。
常见模式
跨对象查询:应使用哪个工具?
| 场景 | 使用方式 |
|---|---|
| 加载 lookup 字段用于显示 | expand |
| 按 lookup 目标的列筛选行 | 在 where 中使用 { lookup: { column: value } },最多处理 1000 个相关 ID;超过上限时,先查询目标对象,再使用 { lookup: { $in: ids } }——当 multiple 时,需对每个 ID 使用 $contains |
| 按子记录条件筛选父记录 | 使用 fields: [lookup] 查询子记录,然后在父记录上使用 { id: { $in: those ids } } |
| 按相关记录的标题进行关键词搜索 | 将标题镜像到此对象的某个存储字段中,并搜索该字段——search 不会遍历关系 |
| 对父记录的相关记录分页 / 排序 | 直接查询相关对象 |
| 跨对象分析查询 | 使用报表 / 仪表板元数据,或分别查询后在应用代码中合并 |
API 分页模式
const page = await engine.find('account', {
where: { status: 'active' },
fields: ['id', 'name', 'email'],
orderBy: [{ field: 'name', order: 'asc' }],
limit: 20,
offset: (pageNumber - 1) * 20,
});
仪表板聚合模式
仪表板上的所有 KPI 共用一次聚合调用——无条件指标直接使用,条件指标则带上各自的 filter。where 会限定整个调用的范围,因此只有当所有指标都需要相同范围时才使用它:
const [kpis] = await engine.aggregate('deal', {
aggregations: [
{ function: 'count', alias: 'total_deals' },
{ function: 'sum', field: 'amount', alias: 'pipeline_value' },
{ function: 'avg', field: 'amount', alias: 'avg_deal_size' },
{ function: 'count', alias: 'won_deals', filter: { stage: 'closed_won' } },
],
});
仪表板和报表本身——KPI 小组件、compareTo、dateGranularity 分桶、矩阵行/列——都是元数据,而非手写查询:在 objectstack-ui 中对其建模,渲染器会发起查询。
验证你的工作
大多数查询会在运行时执行(使用 os data query 或 vitest 测试进行冒烟测试),但查询元数据——列表视图筛选器规范以及报表/仪表板数据集——会进行静态验证。编辑这些内容后,请运行:
os validate # schema + CEL predicates + widget/dataset bindings (no artifact)
# or: os build # the same gates, plus emits dist/
如果仪表板小组件的 dataset / dimensions / values 无法解析,验证会在此处失败,而不是渲染出空图表(ADR-0021)。在脚手架生成的项目中,验证命令为 npm run validate。请参阅 objectstack-platform → 验证你的工作。
参考资料
完整的 Zod 架构列表(附一行说明)请参阅 references/_index.md——其中包含指向 node_modules/@objectstack/spec/src/ 的索引。请务必 Read 源码以确认准确的字段结构;不要依赖对属性名称的记忆。