Skip to content

Commit 426cf52

Browse files
committed
feat(objectql,metadata-protocol)!: register a tenant object overlay as its own contributor layer (ADR-0029 D9) (#7277)
租户对 `object` 的定制此前以默认的 `own` 身份进入 SchemaRegistry;当行的 `package_id` 与代码包所有者相同时,`registerObject` 会把打包的 contributor 直接摘掉 —— 打包定义在写入时被销毁,注册表里没有第二份副本, `loadMetaFromDb` 每次启动无声重放这次销毁。 ADR-0029 D9 把层次关系显式化:第三种非拥有的 `overlay` 种类,对基础层是 替换语义,解析变成 `base = overlay ?? own`,extender 照旧叠在上面。解析 结果逐字节不变(含 `_provenance: 'org'`),变的只是注册表记得什么。 - `assertSingleOwnerPerObject` 一字未改,新增孤儿 overlay 违规类 - 基础层选择问「种类」,永不问优先级(150 只用于列举顺序) - artifact 身份改读 owner contributor 的层,`isArtifactBacked` 不再说谎 - `provisionPrimary` 的门从「是不是 own」改成「是不是基础层」 - 行上的 `package_id` 是层的来源标记:同包正常、无包哨兵接受、 异包在生产者侧以 `OBJECT_OVERLAY_PACKAGE_MISMATCH` / 422 拒绝 - 迟到安装:代码层取得所有权,租户贡献重新归类为它的 overlay 层 - 删除退化为减法:`removeObjectOverlay` 只摘层,打包 owner 原地不动 - #7012 的包绑定守卫随之退役(实测:唯一变红的是它自己那条已接受成本) Fixes #7277 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6bLax4KMrSfnE1ydFU8Dw
1 parent 7084313 commit 426cf52

23 files changed

Lines changed: 1746 additions & 690 deletions
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"@objectstack/objectql": minor
3+
"@objectstack/metadata-protocol": minor
4+
"@objectstack/spec": minor
5+
---
6+
7+
ADR-0029 D9: a tenant object overlay registers as its own contributor LAYER instead of splicing the packaged owner out
8+
9+
租户对 `object` 的定制(`sys_metadata` 行)此前以默认的 `own` 身份进入 `SchemaRegistry`。当该行的 `package_id` 与代码包所有者相同时,`registerObject` 会走"重复注册"分支把**打包的 contributor 直接摘掉**——打包定义不是被遮蔽,而是在写入时被销毁,注册表里不存在第二份副本;`loadMetaFromDb` 每次启动都无声重放这次销毁。
10+
11+
D9 把这个层次关系显式化:
12+
13+
- **第三种非拥有的 contributor 种类 `overlay`**,对基础层是替换语义。解析变成 `base = overlay ?? own`,extender 照旧叠在上面。**解析结果逐字节不变**(含 `_provenance: 'org'`)——变的只是注册表"记得"什么:打包的 owner 依然在下面。
14+
- `assertSingleOwnerPerObject` **一字未改**(overlay 不是 owner),新增一类违规:孤儿 overlay(有 overlay 没有 owner)。
15+
- **基础层的选择问"种类",永远不问优先级**`DEFAULT_OVERLAY_PRIORITY = 150` 只用于列举顺序:extender 的优先级是作者声明的,不能让某个包用 `priority: 140` 把租户的 overlay 挤出基础层。
16+
- **artifact 身份改为读 owner contributor 的层**,而不是合并后的文档。这一条不是层次化改动的自然推论:合并结果按设计仍带 `_provenance: 'org'`,所以只有从 owner 层读,`isArtifactBacked` 才不再说谎。
17+
- `provisionPrimary` / `provisionSearchCompanion` 的门从"是不是 `own`"改成"**是不是基础层**",否则每个被 overlay 的对象的 `nameField` 都会变。
18+
- 行上的 `package_id` 是层的**来源标记**,从来不是所有权主张:同包正常;**无包(`sys_metadata` 哨兵)予以接受**(此前的抛错是借用 `own` 槽位的副产品);绑定到**其他包**的行在生产者侧被明确拒绝,新错误码 `OBJECT_OVERLAY_PACKAGE_MISMATCH`(422),启动时计入 `loadMetaFromDb``errors`
19+
- **迟到安装**:代码包为一个租户行已占据的对象名注册时,代码层成为 owner,租户的贡献被重新归类为它的 overlay 层——不再抛 "already owned by",也不再把租户的定制吞掉。
20+
- 删除退化为**减法**`SchemaRegistry.removeObjectOverlay(name)` 只摘掉 overlay 层,打包 owner 原地不动,因此"恢复"根本不是一次重新注册。
21+
22+
**行为变化(记录在案的成本)**:谓词诚实之后,`object` 声明的 `allowOrgOverride: false` 会被**一致地**执行——对打包对象的 overlay 写入**每次**都以 `NOT_OVERRIDABLE` 拒绝,而不是只拒第一次(此前第一次被拒、并因销毁证据而让后续每次都从 `allowRuntimeCreate` 那一档混过去)。同一谓词也喂给 `deleteMetaItem` 的两档鉴权与仓库的 `assertAllowed`,所以重置该定制同样需要那道文档化的运维口子 `OS_METADATA_WRITABLE=object`——现在它必须在定制的**整个生命周期**内保持打开,而不只是第一次保存时。
23+
24+
`ObjectContributor.ownership``ObjectOwnershipEnum` 的联合类型因此加宽(loader 设定,永不可由作者书写),这是 `objectui` / `cloud` 消费方可见的公开类型变化。

content/docs/references/api/analytics.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const result = AnalyticsEndpoint.parse(data);
4444
| Property | Type | Required | Description |
4545
| :--- | :--- | :--- | :--- |
4646
| **success** | `boolean` || Operation success status |
47-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +258 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
47+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
4848
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
4949
| **data** | `{ name: string; title?: string; measures: object[]; dimensions: object[] }[]` || Available cubes, each as the `CubeMeta` discovery projection — the cube name, its title, and the measures/dimensions a client may name in a query. A bare array: there is no `cubes` wrapper object, and no cube `sql` is published. |
5050

@@ -79,7 +79,7 @@ const result = AnalyticsEndpoint.parse(data);
7979
| Property | Type | Required | Description |
8080
| :--- | :--- | :--- | :--- |
8181
| **success** | `boolean` || Operation success status |
82-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +258 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
82+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
8383
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
8484
| **data** | `{ rows: Record<string, any>[]; fields: object[]; sql?: string }` || |
8585

@@ -93,7 +93,7 @@ const result = AnalyticsEndpoint.parse(data);
9393
| Property | Type | Required | Description |
9494
| :--- | :--- | :--- | :--- |
9595
| **success** | `boolean` || Operation success status |
96-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +258 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
96+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
9797
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
9898
| **data** | `{ sql: string; params: any[] }` || |
9999

content/docs/references/api/auth.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ const result = AuthProvider.parse(data);
117117
| Property | Type | Required | Description |
118118
| :--- | :--- | :--- | :--- |
119119
| **success** | `boolean` || Operation success status |
120-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +258 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
120+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
121121
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
122122
| **data** | `{ session: object; user: object; token?: string }` || |
123123

@@ -153,7 +153,7 @@ const result = AuthProvider.parse(data);
153153
| Property | Type | Required | Description |
154154
| :--- | :--- | :--- | :--- |
155155
| **success** | `boolean` || Operation success status |
156-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +258 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
156+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
157157
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
158158
| **data** | `{ id: string; email: string; emailVerified: boolean; name: string; … }` || |
159159

content/docs/references/api/automation-api.mdx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ const result = AutomationApiErrorCode.parse(data);
119119
| Property | Type | Required | Description |
120120
| :--- | :--- | :--- | :--- |
121121
| **success** | `boolean` || Operation success status |
122-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +258 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
122+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
123123
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
124124
| **data** | `{ name: string; label: string; description?: string; successMessage?: string; … }` || The created flow definition |
125125

@@ -144,7 +144,7 @@ const result = AutomationApiErrorCode.parse(data);
144144
| Property | Type | Required | Description |
145145
| :--- | :--- | :--- | :--- |
146146
| **success** | `boolean` || Operation success status |
147-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +258 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
147+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
148148
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
149149
| **data** | `{ name: string; deleted: boolean }` || |
150150

@@ -187,7 +187,7 @@ const result = AutomationApiErrorCode.parse(data);
187187
| Property | Type | Required | Description |
188188
| :--- | :--- | :--- | :--- |
189189
| **success** | `boolean` || Operation success status |
190-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +258 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
190+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
191191
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
192192
| **data** | `{ name: string; label: string; description?: string; successMessage?: string; … }` || Full flow definition |
193193

@@ -213,7 +213,7 @@ const result = AutomationApiErrorCode.parse(data);
213213
| Property | Type | Required | Description |
214214
| :--- | :--- | :--- | :--- |
215215
| **success** | `boolean` || Operation success status |
216-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +258 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
216+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
217217
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
218218
| **data** | `{ id: string; flowName: string; flowVersion?: integer; status: Enum<'pending' \| 'running' \| 'paused' \| 'completed' \| 'failed' \| 'cancelled' \| … +2 more>; … }` || Full execution log with step details |
219219

@@ -241,7 +241,7 @@ const result = AutomationApiErrorCode.parse(data);
241241
| Property | Type | Required | Description |
242242
| :--- | :--- | :--- | :--- |
243243
| **success** | `boolean` || Operation success status |
244-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +258 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
244+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
245245
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
246246
| **data** | `{ flows: object[]; total?: integer; nextCursor?: string; hasMore: boolean }` || |
247247

@@ -269,7 +269,7 @@ const result = AutomationApiErrorCode.parse(data);
269269
| Property | Type | Required | Description |
270270
| :--- | :--- | :--- | :--- |
271271
| **success** | `boolean` || Operation success status |
272-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +258 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
272+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
273273
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
274274
| **data** | `{ runs: object[]; total?: integer; nextCursor?: string; hasMore: boolean }` || |
275275

@@ -295,7 +295,7 @@ const result = AutomationApiErrorCode.parse(data);
295295
| Property | Type | Required | Description |
296296
| :--- | :--- | :--- | :--- |
297297
| **success** | `boolean` || Operation success status |
298-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +258 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
298+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
299299
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
300300
| **data** | `{ name: string; enabled: boolean }` || |
301301

@@ -325,7 +325,7 @@ const result = AutomationApiErrorCode.parse(data);
325325
| Property | Type | Required | Description |
326326
| :--- | :--- | :--- | :--- |
327327
| **success** | `boolean` || Operation success status |
328-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +258 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
328+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
329329
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
330330
| **data** | `{ success: boolean; output?: any; error?: string; durationMs?: number }` || |
331331

@@ -351,7 +351,7 @@ const result = AutomationApiErrorCode.parse(data);
351351
| Property | Type | Required | Description |
352352
| :--- | :--- | :--- | :--- |
353353
| **success** | `boolean` || Operation success status |
354-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +258 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
354+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
355355
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
356356
| **data** | `{ name: string; label: string; description?: string; successMessage?: string; … }` || The updated flow definition |
357357

content/docs/references/api/batch.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const result = BatchConfigSchema.parse(data);
5555
| :--- | :--- | :--- | :--- |
5656
| **id** | `string` | optional | Record ID if operation succeeded |
5757
| **success** | `boolean` || Whether this record was processed successfully |
58-
| **errors** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +258 more>; message: string; category?: string; httpStatus?: integer; … }[]` | optional | Array of errors if operation failed. Branch on `errors[0].code` — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error (#4793). |
58+
| **errors** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }[]` | optional | Array of errors if operation failed. Branch on `errors[0].code` — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error (#4793). |
5959
| **data** | `Record<string, any>` | optional | Full record data (if returnRecords=true) |
6060
| **index** | `number` | optional | Index of the record in the request array |
6161
| **droppedFields** | `{ object: string; fields: string[]; reason: Enum<'readonly' \| 'readonly_when' \| 'primary_key'> }[]` | optional | Write-observability (#3407/#3431/#3455): caller-supplied fields LEGALLY stripped from THIS row before it was written — static `readonly` (#2948) / TRUE `readonlyWhen` (#3042) on update, or the #3043 create-ingress strip. Per-row because a batch can drop different fields on different rows (`readonlyWhen` is record-state-dependent). Present ONLY when ≥1 field was dropped for this row; the row still succeeded (success unchanged). A single response header cannot express per-row drops, so this body field is the canonical bulk channel — REST does not emit `X-ObjectStack-Dropped-Fields` for batches. Optional — omit-when-empty keeps the shape backward-compatible. |
@@ -122,7 +122,7 @@ const result = BatchConfigSchema.parse(data);
122122
| Property | Type | Required | Description |
123123
| :--- | :--- | :--- | :--- |
124124
| **success** | `boolean` || Operation success status |
125-
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +258 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
125+
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
126126
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
127127
| **operation** | `Enum<'create' \| 'update' \| 'upsert' \| 'delete'>` | optional | Operation type that was performed |
128128
| **total** | `number` || Total number of records in the batch |

0 commit comments

Comments
 (0)