Skip to content

Commit 9a878b2

Browse files
authored
Merge branch 'main' into claude/issue-6212-batch-a-e-sql-query-signatures
2 parents 5eff983 + d8e8d9c commit 9a878b2

51 files changed

Lines changed: 2741 additions & 360 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): `BulkActionDefSchema` accepts `requiredPermissions` — the capability gate the selection bar already enforces (#6257)
6+
7+
The renderer has filtered selection-bar buttons on `def.requiredPermissions`
8+
since objectui#3492 (`BulkActionBar` runs the same `useCapabilityGate` as the
9+
row kebab and record header), but the `.strict()` `BulkActionDefSchema` did not
10+
declare the key, so no legal metadata could ever reach that filter —
11+
`enforced ≠ declarable`, the mirror image of the "declared ≠ enforced" gap.
12+
The forms with no workaround were the INLINE data-plane defs
13+
(`operation: 'update' | 'delete'`): they dispatch no action, so unlike a def
14+
promoted from `bulkActions: ['<name>']` (or an aggregate def naming a declared
15+
action) they have nothing to inherit a gate from. In practice that meant a
16+
declarative bulk delete — the button that most needs a gate — was visible to
17+
every caller who could open the list, and rejected only per record, server-side,
18+
after the click.
19+
20+
`BulkActionDefSchema` now declares an optional `requiredPermissions: string[]`
21+
with `action.requiredPermissions` semantics verbatim: absent or empty always
22+
passes, several entries AND, a client that cannot resolve the caller's
23+
capabilities fails OPEN (the server stays the authority), and the platform-admin
24+
bit grants no exemption — the gate reads grants. On a data-plane def the key
25+
governs visibility only; the write is still authorized by the data API's object
26+
permissions and server hooks. The `ActionSchema` near-miss aliases
27+
(`permissions`, `capabilities`, `requiresPermissions`, `requiredCapabilities`,
28+
`acl`) rename onto the new key here too. No renderer change: objectui's
29+
`BulkActionDef` type and `BulkActionBar` filter shipped in objectui 11
30+
(objectui#3548).
31+
32+
Specimens: `examples/app-showcase` `showcase_project.default` gains the two
33+
inline gated defs the #6157 action-gating matrix could not pin — `relabel_ops`
34+
(`update` + `patch`, gated on the Ops-held `showcase.export_data`) and
35+
`purge_restricted` (`delete`, gated on the granted-to-nobody
36+
`showcase.restricted_ops`).
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-automation': patch
4+
---
5+
6+
feat(spec)!: `FlowNodeSchema` parses its own ADR-0031 regions — the post-parse pass retires (#4415)
7+
8+
`FlowSchema.parse` normalized a flow's own `nodes[]` / `edges[]` but could not reach a
9+
**region**, because a region lives inside `FlowNodeSchema.config` — a deliberately open
10+
`z.record` (ADR-0018). #4381 closed the resulting gap with a **post-parse pass**,
11+
`normalizeControlFlowRegions`, that every caller had to remember to run:
12+
13+
```ts
14+
const flowShell = FlowSchema.parse(converted);
15+
validateControlFlow(flowShell);
16+
const parsed = normalizeControlFlowRegions(flowShell); // ← had to remember
17+
```
18+
19+
That is an unwritten rule on top of a parse, and it is exactly the condition the #4347
20+
family of defects grows in: a new consumer — a Studio publish path, an MCP tool, a bulk
21+
validation script — takes a `FlowParsed` and uses it, holding a **half-parsed flow that
22+
looks finished**. Nested edge predicates were still bare strings, nested nodes had not been
23+
through `.strict()`, and nothing said so.
24+
25+
Now the schema does it. `FlowNodeSchema` carries a `.transform()` that parses each declared
26+
region slot — `loop.config.body`, `parallel.config.branches[]`, `try_catch.config.try` /
27+
`.catch` — through the schema that slot's value *is*. Nesting needs no manual recursion: a
28+
region's `nodes` are `z.array(FlowNodeSchema)`, so Zod re-enters the transform on the way
29+
down. **"Parsed" now means parsed at every depth** (Prime Directive #1), from any entry
30+
point — including `FlowNodeSchema.parse(node)` on a single node, which the old whole-flow
31+
pass could not serve at all.
32+
33+
## Migration
34+
35+
**`normalizeControlFlowRegions` is removed from `@objectstack/spec/automation`.** Delete the
36+
call; the parse above it already did the work:
37+
38+
```diff
39+
const parsed = FlowSchema.parse(converted);
40+
validateControlFlow(parsed);
41+
- const normalized = normalizeControlFlowRegions(parsed);
42+
```
43+
44+
Its replacement, `parseFlowNodeRegions(node)`, is exported for the same purpose one node at
45+
a time, but you should not normally need it — it is the transform's own body.
46+
47+
**`FlowNodeSchema` is now a `ZodPipe`, not a `ZodObject`,** so it no longer has `.shape` /
48+
`.extend()` / `.pick()`. `z.infer` / `z.input` / `.parse` / `.safeParse` and
49+
`z.toJSONSchema` are unaffected, and the authorable key set is byte-identical (verified by
50+
`check:authorable-surface`). If you were reaching for the object half, read it from the
51+
pipe's input side — `FlowNodeSchema.def.in` — which is also what the repo's own generators
52+
do (`pipeAuthorableSide` in `scripts/lib/zod-graph.ts`).
53+
54+
One visible consequence in the generated reference: `content/docs/references/automation/flow.mdx`
55+
now renders FlowNode's **input** shape, so keys carrying a `.default()` (`boundaryConfig.interrupting`,
56+
`inputSchema[].required`) show as optional. That is what an author actually writes, which is
57+
what an authoring reference should say.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(lint): `object/missing-name-field``nameField`、不再把已退役的 `titleFormat` 当作 name 面(#6108)
6+
7+
`object/missing-name-field` 的谓词从来不读 `obj.nameField`,却仍然采信 `obj.titleFormat`:
8+
9+
```
10+
hasNameField = !!obj.primaryField || !!obj.titleFormat || fields.some(name-like)
11+
```
12+
13+
净效果是同一个包里两条规则互相矛盾。`validate-record-title.ts` 把每一处 `titleFormat`
14+
声明都报成 `title-format-retired`,并按 **ADR-0079** 指示作者迁移到 `nameField`
15+
(`titleFormat` 是 render-only 模板,服务端既不能返回也不能查询);而共享的
16+
`objectTitleCompleteness`(`@objectstack/spec/data`)判定标题面时也从不读它。于是:
17+
**照平台自己的迁移建议把 `titleFormat` 换成 `nameField` 的对象,反而多得一条
18+
"records will display as raw IDs" suggestion;守着已退役的键不动的对象反而干净。**
19+
20+
下游实测(hotcrm main,`@objectstack/* 17.0.0-rc.3`):6 处命中里 4 处是误报,
21+
四个对象——`crm_campaign_member` / `crm_event_attendee` / `crm_contract` /
22+
`crm_forecast`——都显式声明了 `nameField`;只有两个 line-item 对象是真命中。
23+
24+
本次修正:
25+
26+
- 谓词补读 `nameField`(ADR-0079 的规范主标题指针),显式声明它的对象不再被告警;
27+
- 摘掉 `titleFormat` 这一支。**只声明 `titleFormat`、没有 `nameField` 的对象因此会
28+
新得一条本规则的 suggestion** —— 这是刻意的翻转,不是回归:这类对象正是 ADR-0079
29+
要求迁移的那一批,`validate-record-title` 今天已经对它同时报
30+
`title-format-retired``title-unresolvable`。两条规则从此对同一个对象给出一致判断;
31+
- `primaryField` 与 name-like 字段两支行为不变;
32+
- 提示文案改为只点名作者真正能声明的面(`nameField` 与 name-like 字段),并新增 `fix` 提示
33+
说明 `titleFormat` 不算标题面 —— 读到旧文案的作者很容易顺手再写一个 `titleFormat`,
34+
又掉回同一个矛盾里。旧文案里的 `primaryField` 同时不再出现:该键在 `packages/spec`
35+
没有任何声明,`ObjectSchema.create()` 会以 `unrecognized_keys` 拒收它(实测,已立 #6326),
36+
提示不该向作者广告一个会被 schema 硬拒的键。谓词里的这一支保持不动。

.changeset/olive-hounds-repeat.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@objectstack/metadata-protocol": patch
3+
---
4+
5+
fix(metadata-protocol): 删除回执不再对 runtime-only 项谎称"已重置为 artifact 默认值"
6+
7+
`deleteMetaItem` 的四句成功回执(repository 路径两句 + legacy raw-engine 路径两
8+
句)原本无条件把每一次删除都叙述成"摘掉一层 overlay、回落到 artifact 默认值"。
9+
但对一个 **runtime-only** 项 —— 管理员在 Studio 里新建的 `object` / `flow` /
10+
`hook`,没有任何 code package 提供同名 artifact —— 底下根本没有默认值可回落:那
11+
一行就是这个项的全部,删掉之后它在任何层都不复存在。回执却把管理员指向一个从未
12+
存在过的基线。
13+
14+
判据与 #5265 / PR #5926 在 save 侧用的是同一个:`isArtifactBacked` —— 也就是
15+
`intent: 'override-artifact' | 'runtime-only'` 的来源,本方法内早已算出。新增的
16+
方法级绑定**替换**`intent` 原来的那次 inline 调用,所以分句后 registry 读取次
17+
数不增反减。
18+
19+
| | FROM | TO |
20+
|:---|:---|:---|
21+
| 覆盖了 artifact,删除即回落 | `Customization overlay deleted — <t>/<n> reset to artifact default. [seq=N]` | 逐字不变 |
22+
| runtime-only,删除即消失 | 同上 | `Deleted <type> '<name>' — it no longer exists. [seq=N]` |
23+
| 覆盖了 artifact,本就没有 overlay 行 | `No customization overlay found for <t>/<n> — already at artifact default.` | 逐字不变 |
24+
| runtime-only,本就不存在 | 同上 | `No <type> '<name>' found — nothing to delete.` |
25+
26+
`success` / `reset` / `seq` 三个字段一字未动 —— `message` 没有任何消费方解析,仅
27+
作展示。草稿两句(`Draft discarded — …` / `No pending draft for …`)本来就没有声
28+
称过 overlay 或 reset,对两类项都为真,故逐字保留。legacy raw-engine 路径不写
29+
history、不发 watch 事件,两句因此本就不带 `[seq=…]`,该差异为既有设计,分句未
30+
触碰。
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/runtime': minor
3+
---
4+
5+
**`createStandaloneStack` now dispatches `mysql://`, and an unknown `OS_DATABASE_DRIVER` value is refused instead of silently becoming SQLite** (#6265).
6+
7+
Two halves of one defect family: a driver selection this stack could not dispatch.
8+
9+
**`mysql://` — the #5820 split with a different scheme.** The CLI has classified `mysql://` / `mysql2://` as the `mysql` kind since forever (`inferDriverTypeFromUrl`), the shared datasource factory has always been able to build it (`SqlDriver` on the `mysql2` client), and `content/docs/data-modeling/drivers.mdx` lists it in the URL-inference table — only `detectDriverFromUrl()` in this package had no arm. So one `OS_DATABASE_URL=mysql://…` booted under `os start` and hard-failed under `os migrate` (which boots through this stack) with `Unsupported database URL scheme`.
10+
11+
- `mysql://…` and `mysql2://…` resolve to the `mysql` kind, matched by character-for-character the same regex the CLI uses — the two functions answer the same question about the same URL, so a divergence between them *is* the bug.
12+
- The stack declares `{ driver: 'mysql', config: { url } }` and the shared factory builds it, exactly like `postgres`. No optional package and no new dependency: `mysql2` is already an optional peer of `@objectstack/driver-sql`, the same posture `pg` has, so a missing client surfaces at connect like it always did.
13+
- `databaseDriver: 'mysql'` and `OS_DATABASE_DRIVER=mysql` are accepted; `sqliteFile` stays `null` for a MySQL target, so `os migrate`'s occupancy probe does not read a DSN as a file path.
14+
15+
**`OS_DATABASE_DRIVER` is validated now.** `databaseDriver` in config was parsed by a zod enum (loud rejection) while the env var was a bare `as` cast — an assertion that checks nothing at runtime. An unrecognised value matched no dispatch arm and landed in the chain's trailing `else`: SQLite, in silence. `OS_DATABASE_DRIVER=mysql` with no URL therefore created a local `standalone.db` while the operator believed they were talking to MySQL, and a typo (`mysq1`, `postgress`) did the same; with a URL set it surfaced as the doubly-misleading "sqlite driver was selected but the URL does not look like a file path" for someone who never selected sqlite. This is the #3276 class.
16+
17+
- Both paths now read **one** declaration (`StandaloneDatabaseDriverSchema`): the config key parses it, the env value parses it, the `ResolvedDriverKind` union is inferred from it, and the refusal enumerates its options rather than repeating them in a hand-written list.
18+
- An unknown value throws, naming the value and every legal driver: `sqlite, sqlite-wasm, memory, postgres, mysql, mongodb, turso`. The env value is lower-cased first, matching the CLI's reader of the same variable; the accepted vocabulary is the enum and nothing else.
19+
- The dispatch chain's trailing `else` is no longer "sqlite" — it is a `never` guard, so the *next* kind added to the enum without a dispatch arm is a compile error rather than a wrong database.
20+
21+
Unknown URL schemes still throw (the message now lists `mysql://`), and the "unknown driver" and "unknown URL scheme" refusals stay distinguishable.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@objectstack/driver-turso': patch
3+
---
4+
5+
drivers(turso): remote 聚合函数名不再大小写归一化,两面只认协议声明的小写拼写 (#6203)
6+
7+
`TursoDriver` 按连接串 `url` 选面:本地/副本继承 `SqlDriver`,远程委派 `RemoteTransport`
8+
两面此前对聚合函数名的归一化不一致 —— remote 先 `.toLowerCase()` 再查自己的编译表,local
9+
拿到什么查什么。于是同一个驱动、同一条查询,答案取决于连接串:
10+
11+
```
12+
COUNT REMOTE -> RESOLVED "SELECT count(\"stage\") AS \"n\" FROM \"deal\""
13+
LOCAL -> THREW INVALID_QUERY / 400
14+
```
15+
16+
本次删掉 remote 侧的 `.toLowerCase()``AggregationFunction`**大小写敏感**`z.enum`
17+
(`AggregationFunction.parse('COUNT')` 直接抛错),`COUNT` 是协议从未声明的拼写,remote
18+
多认的是一种私有方言;按契约优先(PD#12)收紧消费端,而不是把方言固化成第二套事实契约。
19+
20+
**升级说明(user-visible)**:remote 连接不再接受大写或混合大小写的聚合函数名。
21+
`COUNT` / `Count` / `SUM` 等此前在 remote 能编出 SQL 的拼写,现在与 local 一样统一落
22+
`INVALID_QUERY` / 400(「不是已声明的聚合函数」)。**作者侧修法是改用小写** —— 把
23+
`aggregations[].function` 写成协议声明的 `count` / `sum` / `avg` / `min` / `max`
24+
(以及已声明但本后端未实现的 `count_distinct` / `array_agg` / `string_agg`)。
25+
26+
经 REST/协议门进来的查询不受影响:大写拼写在 `AggregationNodeSchema` 就被拒,到不了驱动;
27+
仓内亦无任何发送大写拼写的调用方。受影响的只有绕过 spec 校验、直接调用远程驱动且依赖该
28+
归一化的进程内调用方。
29+
30+
`#5907` 落地的拒收信封(第 1 类 `INVALID_QUERY`/400、第 2 类 `NOT_IMPLEMENTED`/501、
31+
按调用方原始拼写分类)与默认 alias 的拼法均未改动。

.github/workflows/check-links.yml

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,33 @@
11
name: Check Links
22

3+
# Repo-internal link gate (#6028).
4+
#
5+
# `pull_request` was commented out in a59dc59e (2026-01-28) as a rider on an
6+
# unrelated commit, with no rationale recorded; the gate then sat dormant for
7+
# six months while `lychee.toml` and `fail: true` kept it looking alive.
8+
# Maintainer ruling 2026-08-07: restore the trigger, check REPO-INTERNAL links
9+
# only, and land it advisory-first (NOT in the required set) until it has shown
10+
# a stable green streak.
11+
#
12+
# ⛔ No `merge_group` trigger on purpose: this is an advisory lane, and an
13+
# advisory gate does not get to consume merge-queue capacity. If it is ever
14+
# promoted into the required set, `merge_group` MUST be added in the same
15+
# change or the queue stalls on a required check that never reports (#6121).
316
on:
417
workflow_dispatch:
5-
# push:
6-
# branches:
7-
# - main
8-
# pull_request:
9-
# branches:
10-
# - main
18+
pull_request:
19+
branches:
20+
- main
1121

1222
jobs:
1323
link-checker:
1424
name: Check Documentation Links
1525
runs-on: ubuntu-latest
26+
# Least privilege: the job only reads the tree and runs lychee offline.
27+
# There is no issue-filing step, and `--offline` makes zero network
28+
# requests, so neither `issues: write` nor a GITHUB_TOKEN is needed.
1629
permissions:
1730
contents: read
18-
issues: write
1931

2032
steps:
2133
- name: Checkout repository
@@ -24,14 +36,32 @@ jobs:
2436
- name: Check links with lychee
2537
uses: lycheeverse/lychee-action@v2
2638
with:
27-
# Use configuration file for path remapping and settings
39+
# `--offline` is the internal-only mechanism, and it lives HERE rather
40+
# than in lychee.toml on purpose: the equivalent `offline = true`
41+
# config key is silently ignored by older lychee (measured: ignored on
42+
# 0.19.1, honoured on the 0.24.2 this action pins). A determinism
43+
# guarantee must not depend on which lychee the action happens to
44+
# install, so it is asserted at the invocation site.
45+
#
46+
# Offline means only `file://` targets are resolved -- every http(s)
47+
# link is reported EXCLUDED, never requested. That is what makes this
48+
# gate deterministic and free of external-network flake.
49+
#
50+
# --root-dir is what makes ROOT-RELATIVE links checkable. Most internal
51+
# links in content/** are site routes (`/docs/permissions`), and lychee
52+
# hard-errors on those unless it is told which directory `/` means.
53+
# The Fumadocs content root is `content/`, so `/docs/x` resolves to
54+
# content/docs/x -- and --fallback-extensions supplies the .mdx/.md
55+
# suffix that a site route omits. Without this pair the gate cannot go
56+
# green at all: 1286 root-relative links fail as "Cannot resolve
57+
# root-relative link ... provide a root dir".
2858
args: >-
59+
--offline
60+
--root-dir ${{ github.workspace }}/content
61+
--fallback-extensions mdx,md
2962
--config lychee.toml
3063
'content/**/*.md'
3164
'content/**/*.mdx'
3265
'README.md'
3366
# Fail the job if broken links are found
3467
fail: true
35-
env:
36-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37-

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,6 @@ For the browser, the typed client SDK and React hooks (`useQuery` / `useMutation
321321
| [`@objectstack/service-analytics`](packages/services/service-analytics) | Analytics — aggregations, time series, funnels, dashboards |
322322
| [`@objectstack/service-automation`](packages/services/service-automation) | Automation engine — flows, triggers, and workflow state machines |
323323
| [`@objectstack/service-cache`](packages/services/service-cache) | Cache — in-memory, Redis, multi-tier |
324-
| [`@objectstack/service-feed`](packages/services/service-feed) | Activity feed / chatter |
325324
| [`@objectstack/service-i18n`](packages/services/service-i18n) | Internationalization service |
326325
| [`@objectstack/service-job`](packages/services/service-job) | Cron & interval job scheduler |
327326
| [`@objectstack/service-package`](packages/services/service-package) | Package registry — publish, version, retrieve metadata packages |
@@ -343,7 +342,7 @@ For the browser, the typed client SDK and React hooks (`useQuery` / `useMutation
343342
| [`@objectstack/cli`](packages/cli) | CLI binary (`os` / `objectstack`) — `init`, `dev`, `start`, `serve`, `compile`, `publish`, `validate`, `generate`, `lint`, `doctor` |
344343
| [`create-objectstack`](packages/create-objectstack) | Project scaffolder (`npx create-objectstack`) |
345344
| [`@object-ui/console`](https://github.com/objectstack-ai/objectui/tree/main/apps/console) | Fork-ready runtime console SPA (lives in objectstack-ai/objectui, served via `@object-ui/console` on npm) |
346-
| [`@objectstack/account`](apps/account) | Account & identity portal — sign in, organizations, connected apps |
345+
| [`@objectstack/account`](packages/apps/account) | Account & identity portal — sign in, organizations, connected apps |
347346
| [`@objectstack/docs`](apps/docs) | Documentation site (Fumadocs + Next.js) |
348347

349348
### Examples

content/blog/metadata-driven-architecture.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -583,4 +583,4 @@ ObjectStack is our answer.
583583

584584
---
585585

586-
*Want to dive deeper? Explore our [technical specifications](/docs/specifications) or join the discussion on [GitHub](https://github.com/objectstack-ai/spec/issues).*
586+
*Want to dive deeper? Explore our [technical specifications](/docs/references) or join the discussion on [GitHub](https://github.com/objectstack-ai/spec/issues).*

0 commit comments

Comments
 (0)