Skip to content

Commit 76b5ddc

Browse files
committed
Merge origin/main into claude/issue-4741-package-dependency-dual-source
合并 #4768(C17)/ #4786 / #4780 / #4783(#4634,31 位能力位退役)后重生成。 按 #4535 §7 与「rebase/merge 静默回退」纪律处理三张 ratchet: `dual-source-exports.baseline.json` / `authorable-surface.json` / `json-schema.manifest.json`(+ `api-surface.json`)一律 `git checkout origin/main --` 取 main 版本后全量重跑生成器,只重施本 PR 的一处改动。 其中 json-schema.manifest.json 与 authorable-surface.json 归 os-regen merge driver 管、合并不产生冲突标记,最易静默回退,故以 gen:schema 实跑 输出为准。 逐项确认他人蓄意变更未被回滚(regen 后实测): - #4783 `data/DriverCapabilities:*` 31 行 [RETIRED] + 3 行存活 —— 均在 - C10 `system/EnvironmentArtifact*` 删除 —— 仍为 0 - C17 `studio/ActionLocation` → `studio/ActionContributionLocation` —— 旧 0 新 1 - 本 PR `kernel/PackageDependency` → `kernel/ResolvedPackageDependency` —— 旧 0 新 1 `renamed-defs.ts` 冲突为两条独立改名条目并存(C17 与本簇),按时序保留两条。 dual-source 基线 2 → **0**(entries: []),#4535 第二批收官、双源账目归零。 Claude-Session: https://claude.ai/code/session_0176qgxgCXTJCUv4YFLtusP9 Co-authored-by: Claude <noreply@anthropic.com>
2 parents 464e4f1 + d9fa683 commit 76b5ddc

48 files changed

Lines changed: 1726 additions & 1411 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
BREAKING(spec): `@objectstack/spec/studio` 改名 `ActionLocationSchema``ActionContributionLocationSchema`;裸名 `ActionLocation(Schema)` 现在全包唯一地指 `@objectstack/spec/ui` 的应用 UI 位置词表 (#4737, #4535 C17)
6+
7+
`ActionLocationSchema` 曾由 `./studio``./ui` 各自导出一个声明 —— 同名、词表完全互斥的**两个概念**(#4411 陷阱):
8+
9+
| 入口 | 词表 | 语义 | 处置 |
10+
|:--|:--|:--|:--|
11+
| `./studio`(**改名**) | `toolbar` / `contextMenu` / `commandPalette`(3 值) | Studio IDE 外壳里插件 action contribution 出现的位置(唯一嵌入方 `ActionContributionSchema.location`) |`ActionContributionLocationSchema`,枚举值逐字不变;新增 `ActionContributionLocation` 类型导出(旧 const 从无 type 导出) |
12+
| `./ui`(**一字不动**) | `list_toolbar` / `list_item` / `record_header` / `record_more` / `record_related` / `record_section` / `global_nav`(7 值) | 运行中应用的 UI 上 action 渲染的位置,docblock 自宣全平台唯一真源 | 裸名唯一归属(objectui 按引用钉住 `ACTION_LOCATIONS` 并 re-export 类型族) |
13+
14+
## FROM → TO
15+
16+
```ts
17+
// FROM —— 编译期起以 TS2305 失败(实测 objectstack / cloud / objectui 三仓零外部 importer,预期无人受影响)
18+
import { ActionLocationSchema } from '@objectstack/spec/studio';
19+
20+
// TO —— 同一声明、同一词表,名字点明它唯一的语义
21+
import {
22+
ActionContributionLocationSchema,
23+
type ActionContributionLocation,
24+
} from '@objectstack/spec/studio';
25+
```
26+
27+
**要的是应用 UI 的 action 位置?** `import { ActionLocationSchema, type ActionLocation } from '@objectstack/spec/ui'` —— 本次未动。
28+
29+
不保留旧名别名:在 `./studio` 上 re-export 任何一侧的 `ActionLocationSchema` 都会重开本次关闭的陷阱(要么复活双源,要么把应用 UI 词表谎报成 Studio 清单词表)。
30+
31+
## 零元数据迁移
32+
33+
本次只动 TS 导出名与内部 JSON Schema def 名(`studio/ActionLocation``studio/ActionContributionLocation`,走 `RENAMED_DEFS` 承接表,0-key carry —— 枚举 def 无 authorable properties)。作者在 Studio 插件清单里写的 `contributes.actions[].location` 取值域(`toolbar` / `contextMenu` / `commandPalette`)逐字节不变,已有清单原样解析。无 tombstone(没有 key 退役)、无 ADR-0087 conversion —— `StudioPluginManifestSchema` 是根 schema,不在 stack 树上,conversion walker 到不了它(`converge-activation-event-schema` 先例论证)。发布的 JSON Schema `$id` 随之移动:`…/studio/ActionLocation.json``…/studio/ActionContributionLocation.json`
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
"@objectstack/service-storage": patch
3+
---
4+
5+
fix(service-storage): an UNSCOPED multi-delete of `sys_attachment` is refused instead of authorized (#4757)
6+
7+
`installAttachmentAccessHooks`'s `beforeDelete` gate resolved the rows a delete
8+
matches in two ways — by `input.id`, or by `input.options.where` — and then
9+
short-circuited with `if (!rows.length) return`. A delete carrying **neither**
10+
an id **nor** a `where` took neither branch, so `rows` stayed empty and the gate
11+
returned *allow*. That is not "nothing matched": nothing was ever queried.
12+
13+
The engine reads the same call as a bulk delete over everything — with no
14+
single id it seeds the delete AST as `{ object }` and hands that to
15+
`driver.deleteMany` — so `ql.delete('sys_attachment', { multi: true })` emptied
16+
the whole attachment table with the record-level gate having authorized exactly
17+
zero rows. Neither layer underneath catches it: plugin-sharing composes no
18+
row-scoping predicate for an object with no owner field (`sys_attachment`'s
19+
provenance column is `uploaded_by`), and plugin-security only refuses callers
20+
whose grants lack the delete bit on `sys_attachment` — an app shipping the
21+
domain grant the attachments panel requires passes RBAC and lands here.
22+
23+
The gate now fails **closed** on that shape: no id and no `where` is refused
24+
with 403 `ATTACHMENT_DELETE_DENIED` ("Refusing an unscoped multi-delete of
25+
attachments — scope the delete to the rows you mean"), the posture #4630 gave
26+
`sys_comment` in `resolveTargetRows`. "Nothing to authorize" and "nothing was
27+
ever queried" are different verdicts, and reading the second as the first is
28+
fail-open.
29+
30+
Scoped deletes are unchanged: an id-bound delete, a `where`-bound multi-delete,
31+
and even `where: {}` (which matches every row but is a real query) still resolve
32+
their rows and authorize each one uploader-or-parent-editor as before — a delete
33+
that legitimately matches no row still passes. Only the predicate-less call is
34+
newly refused. If you were relying on `ql.delete('sys_attachment', { multi:
35+
true })` to clear the table, pass a predicate (`{ multi: true, where: {} }`
36+
authorizes row-by-row) or perform the sweep under a system context, which
37+
bypasses the gate as before.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
"@objectstack/spec": major
3+
"@objectstack/driver-memory": major
4+
"@objectstack/driver-mongodb": major
5+
"@objectstack/driver-sql": major
6+
---
7+
8+
refactor(spec)!: retire the 31 inert `DriverCapabilities` bits — declared by every driver, read by nothing (#4634, ADR-0049)
9+
10+
The #4484 findStream close-out left one loose end: `DriverCapabilities.streaming`
11+
described a contract method that no longer exists — and a full liveness audit of
12+
the record (#4634, across objectstack + cloud, objectui confirmed clean) found
13+
`streaming` was not the exception but the rule. Of 34 declared bits, **three**
14+
have a decision-making reader and **thirty-one** were written by every driver
15+
and consulted by no engine, planner, REST layer or renderer:
16+
17+
- Their `.describe()` strings promised engine adaptation that was never built
18+
("If false, ObjectQL will fetch all records and filter in memory" — no such
19+
fallback ever keyed off the bit).
20+
- Zero readers let values go WRONG unnoticed: `SqlDriver` declared
21+
`streaming: false` while implementing `findStream`; `InMemoryDriver` declared
22+
`streaming: true` over a full-table read — the exact inverse of the guarantee.
23+
- The real mechanism everywhere else is **method presence**: transactions gate
24+
on `driver.beginTransaction`, aggregate pushdown on
25+
`typeof driver.aggregate === 'function'`, schema sync on
26+
`typeof driver.syncSchema === 'function'`, and the REQUIRED CRUD/bulk methods
27+
are called unconditionally.
28+
29+
Survivors (each with a named reader — the bits method presence cannot carry):
30+
31+
| bit | reader |
32+
|---|---|
33+
| `queryDateGranularity` | engine aggregate dispatch (`engine.ts`), `checkDateBucketParity` (`@objectstack/verify`) |
34+
| `autonumber` | engine defers autonumber generation to the driver (`engine.ts`) |
35+
| `batchSchemaSync` | engine ANDs it with `syncSchemasBatch` presence (`engine.ts` / `plugin.ts`) |
36+
37+
Migration (FROM → TO):
38+
39+
- Any of the 31 bits (`create`/`read`/`update`/`delete`, `bulkCreate`/
40+
`bulkUpdate`/`bulkDelete`, `transactions`/`savepoints`/`isolationLevels`,
41+
`queryFilters`/`queryAggregations`/`querySorting`/`queryPagination`/
42+
`queryWindowFunctions`/`querySubqueries`/`queryCTE`/`joins`,
43+
`fullTextSearch`/`jsonQuery`/`geospatialQuery`/`streaming`/`jsonFields`/
44+
`arrayFields`/`vectorSearch`, `schemaSync`/`migrations`/`indexes`,
45+
`connectionPooling`/`preparedStatements`/`queryCache`) in a `supports`
46+
literal or a `DriverConfig.capabilities` object → **delete the key**. Each is
47+
tombstoned (`retiredKey()`), not silently stripped: authoring one is a `tsc`
48+
error against `IDataDriver.supports` and a parse error carrying the per-key
49+
prescription, which names the mechanism that actually decides the behaviour.
50+
- `batchSchemaSync` dropped its `.default(false)` for `.optional()` — absence
51+
already meant `false` at both readers, so `supports: {}` is now a valid,
52+
minimal advertisement. If you read `capabilities.batchSchemaSync` from a
53+
*parsed* config and relied on the materialised `false`, treat absence as
54+
`false` (both engine readers always did).
55+
- Driver packages: `InMemoryDriver.supports` is now `{}`,
56+
`MongoDBDriver.supports` is `{ batchSchemaSync: true }`, `SqlDriver.supports`
57+
is `{ queryDateGranularity, autonumber: true, batchSchemaSync: false }`.
58+
Reading a removed bit off these literals no longer type-checks — and no code
59+
in any repository did.
60+
- A future capability (streaming reads, vector search, …) returns **with its
61+
caller and its reader in the same change** — the enforce route of ADR-0049 —
62+
never as a dangling boolean.
63+
64+
The retirement kit: 31 `retiredKey()` tombstones on the non-strict schema
65+
(parse + `tsc` both audible; the schema IS parsed via
66+
`DriverConfigSchema.capabilities` and its SQL/NoSQL extensions); ADR-0087 D3
67+
semantic migration `driver-capabilities-inert-bits-removed` (a driver is CODE,
68+
never stack metadata — `supports` lives in driver classes and `DriverConfig`
69+
is plugin TS configuration, so there is no stored row or stack source for a D2
70+
conversion to rewrite; the stack-tree neighbour `datasource.capabilities` was
71+
retired separately in #4583); baselines (`authorable-surface.json` [RETIRED]
72+
lines, `json-schema.manifest.json`) regenerated deliberately; compiler-API pin
73+
asserting every retired bit is unwritable (`undefined`) and every live bit is
74+
not, sabotage-verified both ways (S1 schema resurrection, S2 driver literal
75+
resurrection).
76+
77+
No runtime behaviour changes — that impossibility is the point: every removed
78+
bit had zero readers, and the three live bits keep theirs.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
'@objectstack/objectql': minor
3+
---
4+
5+
**A declarative hook `condition` is now evaluated against the RECORD — the stored row overlaid with this write's payload — not against the update payload alone (#4770).**
6+
7+
⚠️ **Behaviour change — read this before upgrading.** The condition gate used to evaluate
8+
against `ctx.input.data`: only the fields the current write happened to carry.
9+
`ctx.previous` sat behind it, unreachable, and the two were never merged. So a condition
10+
could reference only a field the update *happened* to touch; referencing anything else
11+
aborted the CEL expression with `No such key` — which the gate swallowed into `false`,
12+
leaving one WARN line as the sole trace.
13+
14+
For a guard-style hook that reads as "let it through"; for an audit-style hook it reads as
15+
"do not record it". `condition: "record.done == true"` on an audit hook therefore did NOT
16+
run on the most ordinary updates there are — change the status, change the assignee —
17+
because `done` was not in the payload.
18+
19+
The record a condition reads is now built the same way a validation predicate's is
20+
(#1871 / #4649, via one shared helper so the two cannot drift):
21+
22+
- **stored ⊕ payload** — the prior record overlaid with this write's data, so a condition
23+
may reference any field of the record, not just the changed ones. The payload still
24+
wins for the fields it carries.
25+
- **total over the object's DECLARED fields**`null` for a declared field present in
26+
neither, so a driver that stores only the columns it wrote no longer decides whether an
27+
expression is evaluable.
28+
- **declared fields only** — an undeclared or typo'd key (`record.stauts`) stays
29+
unevaluable and is still reported, exactly as before.
30+
31+
Materialisation happens only when the persisted state is actually in hand — an insert, or
32+
an update whose prior row was fetched. A predicate (`multi: true`) bulk update carries no
33+
prior row, so its payload is left as it is rather than gaining `null`s that would
34+
contradict the stored rows. No code path fetches a record it did not already load.
35+
36+
**What you may see after upgrading**
37+
38+
- **Conditions that never fired start firing.** A hook gated on a field the payload rarely
39+
carried was silently skipped; it now evaluates. This is the declaration finally being
40+
honoured, but expect hooks to run on writes where they previously did not.
41+
- **A condition is now about the record's STATE, not about this write's diff.**
42+
`record.done == true` fires on every update of a task that *is* done, not only on the
43+
update that set it. A condition cannot express a transition today — the CEL scope binds
44+
`record` only.
45+
- **Conditions guarded with `has(...)` need `!= null`.** `has(x)` asks whether the key is
46+
**present**, and a declared field holding `null` is present — so
47+
`has(a) && has(b) && a > b` still faults on `null > null`. Same lesson as #4649:
48+
49+
```diff
50+
- condition: 'has(record.spent) && has(record.budget) && record.spent > record.budget'
51+
+ condition: 'record.spent != null && record.budget != null && record.spent > record.budget'
52+
```
53+
54+
`has()` remains correct for asking whether an **undeclared** key exists.
55+
56+
**Unchanged, deliberately:** what happens when a condition is *still* unevaluable after
57+
merging — it is logged at WARN and treated as `false`, as before. Whether that fallback
58+
should differ by hook category (a guard fails open, an audit fails silent) is a separate
59+
decision, tracked on its own issue.

content/docs/protocol/objectql/query-syntax.mdx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -749,8 +749,9 @@ form.
749749
The `search` parameter does **not** reach a full-text index. The engine expands it into
750750
an `$or` of `$contains` predicates across the object's server-resolved searchable fields
751751
(ADR-0061) and deletes `search` from the AST before the driver sees it — every driver
752-
already runs `$or`/`$contains`, so no driver support is needed (`SqlDriver` reports
753-
`supports.fullTextSearch: false`).
752+
already runs `$or`/`$contains`, so no driver support is needed (which is also why the
753+
old `supports.fullTextSearch` capability bit had no reader and was retired in 17.0.0,
754+
#4634).
754755

755756
`search` takes the **query text itself** — that is the canonical spelling (ADR-0061 D1:
756757
the client says *what* to search for, the server decides *which fields*), and it is what

content/docs/references/data/driver-nosql.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ const result = AggregationPipelineSchema.parse(data);
123123
| :--- | :--- | :--- | :--- |
124124
| **name** | `string` || Driver instance name |
125125
| **type** | `'nosql'` || Driver type must be "nosql" |
126-
| **capabilities** | `{ create: boolean; read: boolean; update: boolean; delete: boolean; … }` || Driver capability flags |
126+
| **capabilities** | `{ queryDateGranularity?: Record<string, boolean>; autonumber?: boolean; batchSchemaSync?: boolean; create?: any; … }` || Driver capability flags |
127127
| **connectionString** | `string` | optional | Database connection string (driver-specific format) |
128128
| **poolConfig** | `{ min: number; max: number; idleTimeoutMillis: number; connectionTimeoutMillis: number }` | optional | Connection pool configuration |
129129
| **databaseType** | `Enum<'mongodb' \| 'couchdb' \| 'dynamodb' \| 'cassandra' \| 'redis' \| 'elasticsearch' \| 'neo4j' \| 'orientdb'>` || Specific NoSQL database type |

content/docs/references/data/driver-sql.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ const result = DataTypeMappingSchema.parse(data);
6565
| :--- | :--- | :--- | :--- |
6666
| **name** | `string` || Driver instance name |
6767
| **type** | `'sql'` || Driver type must be "sql" |
68-
| **capabilities** | `{ create: boolean; read: boolean; update: boolean; delete: boolean; … }` || Driver capability flags |
68+
| **capabilities** | `{ queryDateGranularity?: Record<string, boolean>; autonumber?: boolean; batchSchemaSync?: boolean; create?: any; … }` || Driver capability flags |
6969
| **connectionString** | `string` | optional | Database connection string (driver-specific format) |
7070
| **poolConfig** | `{ min: number; max: number; idleTimeoutMillis: number; connectionTimeoutMillis: number }` | optional | Connection pool configuration |
7171
| **dialect** | `Enum<'postgresql' \| 'mysql' \| 'sqlite' \| 'mssql' \| 'oracle' \| 'mariadb'>` || SQL database dialect |

0 commit comments

Comments
 (0)