Repository navigation
Commit 5f47410
fix(objectql, metadata-protocol): a base package's own export carries its objects without its .custom pair's extension (#29930)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBWNVuGPx3cnvbco1VEJpKvp)_
Fixes #29903
Clause-②: yes (widening)
Metadata-protocol: none
## 维护者速读
- **改了什么**:基础包自己的导出(`GET
/packages/BASE/export`,以及整部署导出里基础包那一项)里的对象,不再带上它的 `.custom`
配对包发布的扩展字段和字段改名;扩展仍只在配对包的导出里,作为
`objectExtensions`。`/meta/object`、控制台和数据接口读到的对象不变,仍是合并后的。
- **为什么改**:客户在 Studio「定制」里给厂商对象加了 `region__c`
后,导出的基础包看起来像厂商自己带了这个字段;整部署导出装到新环境时,基础包那一项还会因为 `__c` 字段只许 `.custom` 包使用而被
422 拒装(本 PR 的 dogfood 消融实测)。ADR-0146 D8 第 3 条要求基础包导出保持原样。
- **风险与回滚**:只影响包导出读的 per-package 层;registry 的三个方法各加一个可选参数,不传时行为逐字节不变。回滚即
revert 本 PR。其他已安装包(非配对包)对基础包对象的扩展仍会被并进基础包导出,与改动前相同(见 Acceptance notes,和
#29872 交叠)。
-
**先看**:`packages/objectql/src/registry.ts`(`resolveObjectWithoutExtensionsOf`
与 `foldObjectExtendersOnto`
的过滤)、`packages/metadata-protocol/src/protocol-meta-list-reads.ts`(per-package
层传入配对包 id)。
- **两分钟验证**:`pnpm --filter @objectstack/runtime exec vitest run
--maxWorkers=2
src/domains/packages-export-base-showcase-boot.integration.test.ts -t
29903`,预期 1 passed:发布扩展后基础包导出与发布前逐字节相同,`/meta/object/showcase_task` 仍带
`region__c` 和改后的标签。
- **需要你做的**:无,席位落地。
## What changes
- **`@objectstack/objectql`.** `SchemaRegistry.listItems(type,
packageId, options)` takes `withoutExtensionsOf` beside
`includeDisabledPackages`, and `getAllObjects(packageId, options)` and
`foldObjectExtendersOnto(name, base, options)` take the same option.
Each object is resolved with that package's `extend` contributions left
out of the fold, as if that package had never extended it: the same base
layer (`overlay ?? own`), every other extender folded in the same order,
the same served tail. With no such contribution the answer is the cached
`resolveObject` body (same reference). Without the option every method
answers exactly as before.
- **`@objectstack/metadata-protocol`.** The per-package layer
(`readFlattenedMetaItems` with `layer: 'per-package'`, served by
`getMetaItemsPerPackage`, which the package export reads) names the
listed package's `.custom` pair (`customizationPackageId`) as
`withoutExtensionsOf`, on the registry list and on the stored-row fold
of the overlay merge. The effective and execution reads pass nothing and
keep folding the pair's extension in.
- **Clause-② yes (widening).** The three registry methods are on the
exported `SchemaRegistry` class, so the optional option widens
`@objectstack/objectql`'s public surface. It is not a protocol change
(no authorable key, value or default moves), so both changesets are
`patch`.
Landing site: the order named `assemblePackageManifest` /
`ownDocumentsOf` or the per-package layer it reads. I chose the
per-package layer, reading the base's own document rather than
subtracting the extension after the merge. A field-level subtraction has
no inverse (an extension may replace a base field under the same key,
and `fieldPatches` overwrite labels), while the registry still holds the
base's own contributors, so resolving without the pair's contributor is
exact. `ownDocumentsOf`'s `getArtifactItem('object', …)` was not usable
either: for objects it answers the owner layer with every extender
folded on (`resolveOwnerLayer`), so it carries the pair's field too.
## Why
Measured at `objectstack-ai/objectstack@4e0c5d3` (main after #29914)
through the real door (`HttpDispatcher.handlePackages`), real protocol,
real `ObjectQL` on SQLite and a real `MetadataManager` (the #29649
showcase-shaped harness):
- before the extension: `showcase_task` exports 10 fields, no
`region__c`, `title.label` "Title";
- after the pair publishes `{ extend, fields: { region__c },
fieldPatches: { title: { label } } }`: the base's export carries 11
fields including `region__c` and `title.label` "Title QA7"; the base's
whole export is no longer byte-identical; the pair's export carries the
same extension in `objectExtensions`.
The registry folds every `extend` contributor into the object it lists
(`getAllObjects` → `resolveObject`), and the per-package layer listed
that merged object. `ownDocumentsOf` drops items that declare an
override, but an extension is merged into the base's object rather than
listed as an item, so nothing was there to drop. That confirms the
card's mechanism on current main.
## Tests
-
`packages/runtime/src/domains/packages-export-base-showcase-boot.integration.test.ts`,
new top-level describe `[#29903]` in the existing harness (the harness
gains a `served(path)` helper over `HttpDispatcher.handleMetadata`): the
base's whole export is byte-identical with the extension published; its
object carries no `region__c` and keeps "Title"; it has no
`objectExtensions`; the pair's export carries exactly the extension and
no object; CONTROL: the extension row is published and `GET
/meta/object/showcase_task` and `GET /meta/object` serve the field and
the patched label; after a restart over the same database that hydrates
the extension (`loadMetaFromDb`, 0 errors), the served reads fold it
again and the base's export is still byte-identical. 9/9 in the file.
- `packages/objectql/src/registry-extension-field-patches.test.ts`, new
top-level describe `#29903`: the listed object without the pair's
extension has no pair field and the owner's label, keeps a third
package's patch and the owner's `_packageId`; CONTROL: the plain list
and `getObject` still fold the pair; a package that extends nothing
answers the cached object; the answer is byte-identical to a registry
where no pair ever extended; `foldObjectExtendersOnto` with the option
leaves the pair out the same way. 20/20 in the file.
-
`packages/qa/dogfood/test/package-export-bundle-round-trip.dogfood.test.ts`,
new describe `[#29903]` (two showcase boots, install-local): source base
export unchanged by the extension; the bundle is `[base, pair]` and
parses; the base's entry equals its pre-extension export; the field is
declared exactly once in the bundle, in the pair's `objectExtensions`;
on the target both entries install 200, `GET /meta/object/NAME` serves
the field and the pair's label, and the target base's export carries
neither. 11/11 in the file.
<details><summary>Ablations (each: fix put back to the old behaviour,
red observed, restored, tree proved clean)</summary>
- **Per-package layer** (`protocol-meta-list-reads.ts`:
`withoutExtensionsOf` forced to `undefined` behind a string marker,
metadata-protocol rebuilt, `ablation-dist-preflight` marker present in
`dist/index.cjs` and `dist/index.js`):
- integration pin red: `AssertionError: the base's object with the
extension live: expected [ { name: 'showcase_task', …(4) } ] to deeply
equal [ … ]`;
- dogfood red, 2 of 3: the base's export differs after the extension,
and on the target the base entry is refused: `422 VALIDATION_ERROR …
Field 'region__c' on object 'exportxbase_account' in package
'com.dogfood.exportxbase': the __c suffix is reserved to a base's paired
.custom package (ADR-0146 D10)`. So before this fix the whole-deployment
bundle of a customized base could not be reinstalled at all;
- restored with `git checkout HEAD --`, blob equals HEAD (`72d3fe7c`),
`git diff HEAD` empty, rebuilt, preflight `--absent` clean on all 24
built files.
- **Registry list** (`getAllObjects` passes `undefined`): 2 of the 3
registry pins red (`expected { region: true, … } to deeply equal {
region: false, … }`, and the byte-identity pin). Restored, `git diff
HEAD` empty.
- **Registry stored-row fold** (`foldObjectExtendersOnto` keeps every
contributor): the fold pin red. Restored, `git diff HEAD` empty.
</details>
## Acceptance notes
- **Scope measured, fixed only where ADR-0146 D8 item 3 applies (the
base's `.custom` pair).** On the same harness at `4e0c5d32`:
- an extension contributed by an **installed non-pair package**
(install-local / `registerApp` with `objectExtensions`) is also folded
into the base's object in the base's export (`zone` field, `title`
relabelled). That package's own export omits its `objectExtensions`
today (#29872). If this PR also left that extension out of the base's
export, the whole-deployment bundle would lose the field entirely until
#29872 lands, so it is unchanged here. Overlap with #29872 named: once
that card makes the extender's export carry its `objectExtensions`, the
same `withoutExtensionsOf` mechanism (or a "every non-owner extender"
variant) is the landing for this half;
- a **writable base** cannot have a pair (`ensureCustomizationPackage`
answers 409), so D8 item 3 has no pair to keep out. An installed
extender of a writable base's object is folded into that base's export
the same way as above, also unchanged.
- The round trip's existing describe was not extended: on its shared
fixture an install-local pair's export omits its `objectExtensions` on
the target (#29872), which would red its "same names and counts" pin for
a reason this card does not touch. The new describe compares the
target's served object and base export instead.
- `getMetaItemLayered`'s `code` layer for an object still folds every
extender (it reads the code layer, `resolveOwnerLayer`). Not an export
door, untouched.
## Verification
Measured on `objectstack-ai/objectstack@4e0c5d3` plus this branch, head
`6a0150f1`.
- `pnpm --filter @objectstack/objectql --filter
@objectstack/metadata-protocol --filter @objectstack/runtime --filter
@objectstack/dogfood typecheck`: exit 0.
- `pnpm --filter @objectstack/objectql test`: 546 files, 10819 tests
passed.
- `pnpm --filter @objectstack/metadata-protocol test`: 311 files passed
(4 skipped), 22320 tests passed (26 skipped).
- `pnpm --filter @objectstack/runtime test`: 538 files passed, 8590
tests passed (19 skipped).
- dogfood `test/package-export-bundle-round-trip.dogfood.test.ts`: 11/11
(the full dogfood battery is CI's: no `!` in the title and no
`(narrowing)` arm).
- `node scripts/pm/dispatch-gates.mjs --commands`: 67 families derived,
67 run, every one exit 0; `--ran` reconciliation: "67 run, 0
NOT-MEASURED". Two first exited 3 on prerequisites and were re-run
green: `check-plugin-teardown-shape --self-test` after fetching its
pinned fixture commit into the shallow clone, and
`check:dual-build-cjs-loads` after building the seven packages outside
this closure that had no `dist/`.
- `pnpm lint` (repo-wide eslint) is CI's `Lint & Repo Gates`; eslint on
the touched files: exit 0.
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01WGufzj3sBag4vwbFLaLtQm
<!-- signed: domain:server#6 · os-support-ai ·
session_01WGufzj3sBag4vwbFLaLtQm -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 242665f commit 5f47410
8 files changed
Lines changed: 325 additions & 11 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- objectql/src
- qa/dogfood/test
- 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 | |
|---|---|---|---|
| |||
89 | 89 | | |
90 | 90 | | |
91 | 91 | | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
92 | 101 | | |
93 | | - | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
94 | 106 | | |
95 | 107 | | |
96 | 108 | | |
| |||
251 | 263 | | |
252 | 264 | | |
253 | 265 | | |
| 266 | + | |
| 267 | + | |
254 | 268 | | |
255 | | - | |
| 269 | + | |
256 | 270 | | |
257 | 271 | | |
258 | 272 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
| 49 | + | |
49 | 50 | | |
50 | 51 | | |
51 | 52 | | |
| |||
59 | 60 | | |
60 | 61 | | |
61 | 62 | | |
62 | | - | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
63 | 68 | | |
64 | 69 | | |
65 | 70 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3526 | 3526 | | |
3527 | 3527 | | |
3528 | 3528 | | |
3529 | | - | |
| 3529 | + | |
3530 | 3530 | | |
3531 | 3531 | | |
3532 | 3532 | | |
3533 | 3533 | | |
3534 | 3534 | | |
3535 | | - | |
| 3535 | + | |
3536 | 3536 | | |
3537 | 3537 | | |
3538 | 3538 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
405 | 405 | | |
406 | 406 | | |
407 | 407 | | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
721 | 721 | | |
722 | 722 | | |
723 | 723 | | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
724 | 729 | | |
725 | 730 | | |
726 | 731 | | |
| |||
4673 | 4678 | | |
4674 | 4679 | | |
4675 | 4680 | | |
| 4681 | + | |
| 4682 | + | |
| 4683 | + | |
| 4684 | + | |
| 4685 | + | |
| 4686 | + | |
| 4687 | + | |
| 4688 | + | |
| 4689 | + | |
| 4690 | + | |
| 4691 | + | |
| 4692 | + | |
| 4693 | + | |
| 4694 | + | |
| 4695 | + | |
| 4696 | + | |
| 4697 | + | |
| 4698 | + | |
| 4699 | + | |
| 4700 | + | |
| 4701 | + | |
| 4702 | + | |
| 4703 | + | |
4676 | 4704 | | |
4677 | 4705 | | |
4678 | 4706 | | |
| |||
5477 | 5505 | | |
5478 | 5506 | | |
5479 | 5507 | | |
5480 | | - | |
| 5508 | + | |
5481 | 5509 | | |
5482 | 5510 | | |
5483 | 5511 | | |
5484 | 5512 | | |
5485 | 5513 | | |
5486 | 5514 | | |
5487 | 5515 | | |
5488 | | - | |
| 5516 | + | |
| 5517 | + | |
| 5518 | + | |
| 5519 | + | |
| 5520 | + | |
5489 | 5521 | | |
5490 | 5522 | | |
5491 | 5523 | | |
| |||
6224 | 6256 | | |
6225 | 6257 | | |
6226 | 6258 | | |
| 6259 | + | |
| 6260 | + | |
| 6261 | + | |
| 6262 | + | |
6227 | 6263 | | |
6228 | | - | |
| 6264 | + | |
6229 | 6265 | | |
6230 | 6266 | | |
6231 | 6267 | | |
| |||
6236 | 6272 | | |
6237 | 6273 | | |
6238 | 6274 | | |
6239 | | - | |
| 6275 | + | |
6240 | 6276 | | |
6241 | 6277 | | |
6242 | 6278 | | |
| |||
7594 | 7630 | | |
7595 | 7631 | | |
7596 | 7632 | | |
| 7633 | + | |
| 7634 | + | |
| 7635 | + | |
| 7636 | + | |
| 7637 | + | |
| 7638 | + | |
7597 | 7639 | | |
7598 | | - | |
| 7640 | + | |
7599 | 7641 | | |
7600 | 7642 | | |
7601 | | - | |
| 7643 | + | |
7602 | 7644 | | |
7603 | 7645 | | |
7604 | 7646 | | |
| |||
0 commit comments