Repository navigation
Commit 8cc709d
fix(objectql)!: withhold a disabled package's objectExtensions fields from the object they extend, and keep their columns (#30362)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgVBgbYzDpPvJK5AeiYUmnKk)_
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF
## 维护者速读
1. 客户在设置里停用一个模块后,这个模块加到别的对象上的字段(例如合同上的 O2C
汇总字段)现在也一起隐藏了,和模块自己的对象、菜单、动作一样。
2. 隐藏只管"展示和读写":对象元数据、数据接口的读写、记录页都看不到这些字段;数据库里的列和已存的值一个不动。
3. 重新启用后字段和原来的值原样回来;一个从"已停用"状态启动的环境也照样建好这些列,启用后马上能写。
4. 停用期间再写这些字段,回答和写一个对象上不存在的字段完全一样(400 INVALID_FIELD),没有新增拒绝码。
5. 需要你知道的一点:如果另一个仍启用的模块有校验规则读了被停用模块的字段,停用期间这个对象的写入会被该规则拦下(400
VALIDATION_FAILED,点名规则);公式读到的是空值,不会 500。
6. 这是一次对外对象形状的收窄(停用状态下少了字段),按 BREAKING 申报,合并前需要合同审查。
Fixes #30304
Clause-②: yes (narrowing) — while a package is disabled, the served
object it extends (`GET /api/v1/meta/object/:name`, the record page, the
`/data` field map) no longer carries the fields, validation rules and
indexes the package's `objectExtensions` add, so a `/data` write,
filter, sort or `fields` projection naming one is refused as a field the
object does not have, and `GET /api/v1/packages/:id/customizations` no
longer lists them among the base's `contributions`.
Metadata-protocol: semantic
## What changed
- **`packages/objectql/src/registry.ts`** — every served fold leaves out
the `extend` contributions of a disabled package (`isPackageDisabled`,
the predicate objects, flows, navigation and actions already use):
- `resolveObject` (behind `getObject` / `getAllObjects`), the
per-package fold (`withoutExtensionsOf` / `onlyExtensionsOf`) and
`foldObjectExtendersOnto` (the protocol's by-name read over a
metadata-service body);
- a base layer that already carries a withheld field, rule or index (a
tenant overlay saved from a served read, a metadata-service copy seeded
from a resolved body) has it taken off (indexes compared by the fold's
own structural key); a member the owner declares, or another enabled
extension also adds, stays;
- new `getStorageObject(name)` / `getAllStorageObjects()`: the fold with
every contribution, disabled or not. With nothing disabled they are the
served answer (same cache, same walk);
- `installPackage` drops cached folds when the row lands disabled (a
fold cached before the row existed read the package as enabled).
- **Storage readers move to the full fold**, so a disable never drops,
skips or re-shapes a column:
- `ObjectQLPlugin` boot sync, `registerSchemasWithoutDdl` and the
federated reconciliation (`readRegisteredObjects(seam, 'storage')`); the
governance inventory keeps the served read;
- `ObjectQL.syncSchemas` / `syncObjectSchema`;
- the lifecycle sweep (`getAllStorageObjects` when the registry has it),
so the archive datasource's schema keeps every column the hot rows
carry;
- `os migrate`'s composed coverage (`measureComposedCoverage`,
`packages/cli/src/utils/schema-migration-plugins.ts`): its DDL-free
branch (`OS_SKIP_SCHEMA_SYNC`) installs each object as the driver's
managed field set with `registerObjectMetadata`, so on the served fold
the drift report proposed dropping every column a disable only hides. It
reads `getAllStorageObjects` (falling back to `getAllObjects`).
- **`packages/runtime/src/domains/packages.ts`** —
`readBaseContributions`' `field` loop skips a disabled package's
contributor, as its `nav` loop does. This makes the spec docblock #30086
names (`package-lifecycle.zod.ts` "a disabled package's contributions
MUST NOT be listed, as the fold drops them") true for fields. The
array-level describe there still names only flows and navigation; that
text is the spec lane's (#30086), not edited here.
## Measured, not assumed
- **Disable scope.** Disable is per environment
(`setPackageDisabled(environmentId, …)`, one registry row per kernel);
there is no separate org-level disable to keep in step. Pinned through
the real door, including a boot that replays the state file.
- **Schema sync.** It read the served fold (`getAllObjects`). With the
served fold narrowed and sync left on it, a boot that starts with the
module disabled creates the table without the module's columns, and a
write after enable answers 500 (measured in the ablation below). Hence
the storage fold.
- **Summary (`o2c_probe_total` shape).** The roll-up index is built from
the served fold, so a withheld summary is not recomputed while disabled;
its child object is refused (`OBJECT_PACKAGE_DISABLED`) anyway.
Re-enable serves the stored value (pinned: 2000 back).
- **Formula** on the object reading a withheld field answers `null`
(200, no 5xx).
- **Filter / sort / `fields`** naming a withheld field: byte-identical
to naming an unknown field (`400 INVALID_FIELD` / `INVALID_SORT`),
pinned by comparison.
- **Write naming a withheld field**: byte-identical to an unknown field
(`400 INVALID_FIELD`, `Unknown field '…' on object '…'`), create and
update. No new code.
## Tests
-
`packages/runtime/src/domains/packages-disable-module-extension-fields.test.ts`
— real doors (`HttpDispatcher` → `/packages/:id/disable|enable`,
`/meta/object`, `/data`, `/packages/:id/customizations`) over a
file-backed SQLite: control, disable, unknown-field parity, other-field
writes, contributions list, re-enable with values, disabled-at-boot
column creation, and the cross-module formula / rule / query
measurements.
- `packages/objectql/src/registry-disabled-package-extensions.test.ts` —
the four served readers, the storage readers, a folded base and a tenant
overlay (fields, rules and indexes), a row that lands disabled after a
cached fold.
- `packages/cli/src/utils/schema-migration-plugins.test.ts` — a
top-level describe over a real SQLite `SqlDriver` and a real
`SchemaRegistry`: a table created with the module enabled, the module
disabled, `measureComposedCoverage(…, { ddlFree: true })`, then
`detectManagedDrift()` proposes no drop of the hidden column and the
stored value stays.
Ablations (one-time, via `scripts/ablation-replace.mjs`, restored to
`HEAD` with `git diff HEAD` empty; dist restore checked with
`ablation-dist-preflight --absent`):
| Mutation | Result |
| --- | --- |
| `withholdDisabledExtensions` always answers `undefined` | registry
suite 7 failed / 17 |
| boot sync and `syncSchemas` read the served fold (objectql rebuilt) |
runtime suite 1 failed / 11: write after enable `500` |
| `readBaseContributions` field loop without the disabled skip | runtime
suite 1 failed / 11 |
| `measureComposedCoverage` reads `getAllObjects` (served) | cli pin
red: `expected [ 'drop_column:qm_credit' ] to deeply equal []` |
| withheld indexes not taken off a folded base | registry suite 2 failed
/ 17 (folded base, tenant overlay) |
## Gates (at `f2ce2c0f7`)
- `node scripts/pm/dispatch-gates.mjs --commands` on this diff: 65
families, all run, all exit 0. `--ran`: 65 derived, 65 run, 0
NOT-MEASURED.
- `pnpm --filter @objectstack/cli typecheck` / `@objectstack/objectql
typecheck` / `@objectstack/runtime typecheck`: exit 0 (cli's test layer
holds its ledger: 28 errors / 6 pinned signatures, unchanged).
- `pnpm lint`: exit 0 (at `12d54cf31`, before the review fixes).
- `@objectstack/objectql` full suite before the merge of `origin/main`:
552 files, the one red file (`plugin-registry-read-failure.test.ts`, 5
cases) fixed by scoping the storage read to the DDL seams. At
`f2ce2c0f7`: the registry pins and
`plugin-registry-read-failure.test.ts` green.
- `@objectstack/cli` `schema-migration-plugins.test.ts` and the runtime
route pin: green at `f2ce2c0f7`.
- Not run locally (CI's): `test:repo`, dogfood, the whole-tree families
the derivation leaves to CI.
## Contract review
Round 1 FAIL (6653307077): M1 (`measureComposedCoverage` on the served
fold) and S1 (a withheld extension's indexes kept on a folded base) are
fixed in `024ea96e5`, the pin's type in `f2ce2c0f7`. S2 (the roll-up
index reads the served fold) and S3 (another package's rule reading a
withheld field) are filed by the seat, not fixed here.
## Acceptance notes
- **Rule of another enabled package reading a withheld field**
(measured, kept as is): it cannot be evaluated, so every write to the
object is refused `400 VALIDATION_FAILED` naming the rule until the
field's package is enabled again. This is the answer any rule reading an
undeclared field already gets (fail closed). A suite whose modules read
each other's fields should declare that dependency; whether a disable
should then be refused, or such a rule skipped, is a ruling, not this
card.
- **UI residue:** none required by the served object: the record page
draws from `/meta/object`. A page or form layout that names a withheld
field explicitly is not measured here (ui lane paused).
- **Layered read** `GET /meta/object/:name?layers=true` measured while
disabled: `code` and `effective` carry none of the module's fields (the
code layer is the metadata-service copy folded by
`foldObjectExtendersOnto`). The registry's own code-layer lookup
(`resolveOwnerLayer` → `getArtifactItem`, artifact identity for the save
gates and the fallback when the service holds no copy) still folds every
contribution: it answers "what does the code ship".
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Utp6CbdPwGwxWkDNgrE4LE
<!-- signed: domain:server#4 · os-bill ·
session_01Utp6CZMkHvuDeVYgTD73qF -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 22f0d65 commit 8cc709d
10 files changed
Lines changed: 857 additions & 15 deletions
File tree
- .changeset
- packages
- cli/src/utils
- objectql/src
- lifecycle
- runtime/src/domains
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
| 9 | + | |
8 | 10 | | |
9 | 11 | | |
10 | 12 | | |
| |||
803 | 805 | | |
804 | 806 | | |
805 | 807 | | |
| 808 | + | |
| 809 | + | |
| 810 | + | |
| 811 | + | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
| 817 | + | |
| 818 | + | |
| 819 | + | |
| 820 | + | |
| 821 | + | |
| 822 | + | |
| 823 | + | |
| 824 | + | |
| 825 | + | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
| 829 | + | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
| 834 | + | |
| 835 | + | |
| 836 | + | |
| 837 | + | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
| 846 | + | |
| 847 | + | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
806 | 868 | | |
807 | 869 | | |
808 | 870 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2399 | 2399 | | |
2400 | 2400 | | |
2401 | 2401 | | |
2402 | | - | |
| 2402 | + | |
| 2403 | + | |
| 2404 | + | |
| 2405 | + | |
| 2406 | + | |
| 2407 | + | |
| 2408 | + | |
2403 | 2409 | | |
2404 | 2410 | | |
2405 | 2411 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
117 | 117 | | |
118 | 118 | | |
119 | 119 | | |
120 | | - | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
121 | 123 | | |
122 | 124 | | |
123 | 125 | | |
| |||
187 | 189 | | |
188 | 190 | | |
189 | 191 | | |
190 | | - | |
| 192 | + | |
| 193 | + | |
191 | 194 | | |
192 | 195 | | |
193 | 196 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
100 | 100 | | |
101 | 101 | | |
102 | 102 | | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
103 | 111 | | |
104 | 112 | | |
105 | 113 | | |
| |||
733 | 741 | | |
734 | 742 | | |
735 | 743 | | |
736 | | - | |
737 | | - | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
738 | 747 | | |
739 | 748 | | |
740 | 749 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2099 | 2099 | | |
2100 | 2100 | | |
2101 | 2101 | | |
| 2102 | + | |
2102 | 2103 | | |
2103 | | - | |
| 2104 | + | |
2104 | 2105 | | |
2105 | 2106 | | |
2106 | 2107 | | |
| |||
2114 | 2115 | | |
2115 | 2116 | | |
2116 | 2117 | | |
| 2118 | + | |
| 2119 | + | |
| 2120 | + | |
| 2121 | + | |
| 2122 | + | |
| 2123 | + | |
| 2124 | + | |
2117 | 2125 | | |
2118 | 2126 | | |
2119 | 2127 | | |
| |||
2186 | 2194 | | |
2187 | 2195 | | |
2188 | 2196 | | |
2189 | | - | |
| 2197 | + | |
2190 | 2198 | | |
2191 | 2199 | | |
2192 | 2200 | | |
| |||
2338 | 2346 | | |
2339 | 2347 | | |
2340 | 2348 | | |
2341 | | - | |
| 2349 | + | |
2342 | 2350 | | |
2343 | 2351 | | |
2344 | 2352 | | |
| |||
2423 | 2431 | | |
2424 | 2432 | | |
2425 | 2433 | | |
2426 | | - | |
| 2434 | + | |
2427 | 2435 | | |
2428 | 2436 | | |
2429 | 2437 | | |
| |||
0 commit comments