Skip to content

Commit 5f47410

Browse files
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
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/objectql': patch
3+
'@objectstack/metadata-protocol': patch
4+
---
5+
6+
fix(objectql, metadata-protocol): a base package's own export carries its objects without its `.custom` pair's extension
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: none
10+
11+
After an administrator added a field to a vendor package's object through Studio's Customize (an extension published in `<base>.custom`, for example `region__c`, or a relabelled base field), `GET /api/v1/packages/<base>/export` carried that field and label inside the base's object, as if the vendor had shipped them. The pair's export carries the same extension as its `objectExtensions` entry, so installing the two exports elsewhere declared the customer's field twice, once as the vendor's. ADR-0146 D8 item 3 says the base's export carries the base unchanged.
12+
13+
- **The base's export** (and its entry in `GET /api/v1/packages/export`) now carries each object as the base declares it, with no field, label or other patch from its pair's extension.
14+
- **The pair's export** is unchanged: it carries the extension in `objectExtensions`.
15+
- **Every served read is unchanged**: `GET /api/v1/meta/object/<name>`, the object list, the console and the data API keep serving the object with the pair's field and labels folded in.
16+
17+
An extension contributed by any other installed package is still folded into the base's object in the base's export, as before.
18+
19+
`SchemaRegistry.listItems` takes a `withoutExtensionsOf` option beside `includeDisabledPackages`, and `getAllObjects` and `foldObjectExtendersOnto` take the same option: each object is resolved with that package's `extend` contributions left out of the fold. Only the metadata protocol's per-package layer (the package export) passes it.

‎packages/metadata-protocol/src/protocol-meta-list-reads.ts‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,20 @@ export async function readFlattenedMetaItems(host: Pick<ProtocolHost, 'artifactL
8989
// and exports every package, enabled or not); the effective and the
9090
// execution reads hide them, at the registry and after the merges below.
9191
const keepDisabledPackages = layer === 'per-package';
92+
// [#29903, ADR-0146 D8 item 3] …and it serves a package's objects as the
93+
// package declares them: its `.custom` pair's extension of one is the
94+
// pair's item, exported as the pair's `objectExtensions` entry, never
95+
// folded into the base's object for the base's export (a field and its
96+
// patches would travel twice, the second time as the base's). Every
97+
// other contributor folds as on the effective read, and the effective
98+
// and execution reads keep the pair's extension in the object they serve.
99+
const pairOfLayer = layer === 'per-package' && packageId ? customizationPackageId(packageId) : undefined;
100+
const withoutExtensionsOf = pairOfLayer?.ok ? pairOfLayer.value : undefined;
92101
const listRegistry = (type: string): unknown[] => (keepDisabledPackages
93-
? host.engine.registry.listItems(type, packageId, { includeDisabledPackages: true })
102+
? host.engine.registry.listItems(type, packageId, {
103+
includeDisabledPackages: true,
104+
...(withoutExtensionsOf !== undefined ? { withoutExtensionsOf } : {}),
105+
})
94106
: host.engine.registry.listItems(type, packageId));
95107
// ── [ADR-0131 D6] The environment → code read ──────────────────────
96108
//
@@ -251,8 +263,10 @@ export async function readFlattenedMetaItems(host: Pick<ProtocolHost, 'artifactL
251263
// object disagree the moment a row exists. Both routes were
252264
// wrong here together, which is why #7556's byName===listed
253265
// pin stayed green through this defect.
266+
// [#29903] …without the package's pair's extension on the
267+
// per-package layer, as its registry items are listed.
254268
return host.foldObjectExtendersFromRegistry(
255-
request.type, (data as { name?: unknown } | null)?.name, data,
269+
request.type, (data as { name?: unknown } | null)?.name, data, withoutExtensionsOf,
256270
);
257271
}, unseated, { servedOverride });
258272

‎packages/metadata-protocol/src/protocol-object-extender-fold.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export function foldObjectExtendersFromRegistry(
4646
type: string,
4747
name: unknown,
4848
body: unknown,
49+
withoutExtensionsOf?: string,
4950
): unknown {
5051
const singular = PLURAL_TO_SINGULAR[type] ?? type;
5152
if (singular !== 'object') return body;
@@ -59,7 +60,11 @@ export function foldObjectExtendersFromRegistry(
5960
// cannot fold answers exactly as it did before.
6061
if (!registry || typeof registry.foldObjectExtendersOnto !== 'function') return body;
6162
try {
62-
return registry.foldObjectExtendersOnto(name, body);
63+
// [#29903] `withoutExtensionsOf`: that package's `extend` contributions
64+
// are left out of the fold (the per-package layer names a base's pair).
65+
return withoutExtensionsOf === undefined
66+
? registry.foldObjectExtendersOnto(name, body)
67+
: registry.foldObjectExtendersOnto(name, body, { withoutExtensionsOf });
6368
} catch {
6469
// The fold is a read over in-memory contributors; a failure here
6570
// must not turn a served schema into a 5xx.

‎packages/metadata-protocol/src/protocol.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3526,13 +3526,13 @@ export class ObjectStackProtocolImplementation implements
35263526
* (see {@link SchemaRegistry.foldObjectExtendersOnto}), so applying it twice
35273527
* would duplicate both.
35283528
*/
3529-
private foldObjectExtendersFromRegistry(type: string, name: unknown, body: unknown): unknown {
3529+
private foldObjectExtendersFromRegistry(type: string, name: unknown, body: unknown, withoutExtensionsOf?: string): unknown {
35303530
// [#25467 cut 30] The body moved to `protocol-object-extender-fold.ts` (see `ObjectExtenderFoldHost`).
35313531
const readEngine = () => this.engine;
35323532
const host: ThisParameterType<typeof foldObjectExtendersFromRegistry> = {
35333533
get engine() { return readEngine(); },
35343534
};
3535-
return foldObjectExtendersFromRegistry.call(host, type, name, body);
3535+
return foldObjectExtendersFromRegistry.call(host, type, name, body, withoutExtensionsOf);
35363536
}
35373537

35383538
/**

‎packages/objectql/src/registry-extension-field-patches.test.ts‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,3 +405,50 @@ describe('ADR-0146 D10 — the manifest loop carries fieldPatches onto the exten
405405
expect(served.options[0]).toEqual({ value: 'tech', label: 'Technology' });
406406
});
407407
});
408+
409+
describe('#29903 — a read can leave one package\'s extensions out of the fold', () => {
410+
const PAIR_PKG = `${OWNER_PKG}.custom`;
411+
/** The vendor's object, a third package's patch, and the pair's field plus its patch. */
412+
function extended(): SchemaRegistry {
413+
const reg = withVendor();
414+
extend(reg, { name: { label: 'Account name' } });
415+
reg.registerObject({
416+
name: OBJ,
417+
fields: { region__c: { name: 'region__c', type: 'text', label: 'Region' } },
418+
fieldPatches: { industry: { label: 'Sector (customized)' } },
419+
} as any, PAIR_PKG, undefined, 'extend', 300);
420+
return reg;
421+
}
422+
const shape = (body: any) => ({
423+
region: !!body?.fields?.region__c,
424+
industry: body?.fields?.industry?.label,
425+
name: body?.fields?.name?.label,
426+
});
427+
const listed = (reg: SchemaRegistry, options?: { withoutExtensionsOf?: string }) =>
428+
(reg.listItems<any>('object', OWNER_PKG, options)).find((o) => o?.name === OBJ);
429+
430+
it('the listed object without the pair\'s extension is the object as no pair had extended it', () => {
431+
const reg = extended();
432+
expect(shape(listed(reg, { withoutExtensionsOf: PAIR_PKG }))).toEqual({ region: false, industry: 'Industry', name: 'Account name' });
433+
expect(listed(reg, { withoutExtensionsOf: PAIR_PKG })?._packageId).toBe(OWNER_PKG);
434+
// CONTROL: every other read of the object still folds the pair in.
435+
expect(shape(listed(reg))).toEqual({ region: true, industry: 'Sector (customized)', name: 'Account name' });
436+
expect(shape(reg.getObject(OBJ))).toEqual({ region: true, industry: 'Sector (customized)', name: 'Account name' });
437+
// A package that extends nothing changes nothing: the cached merged object.
438+
expect(listed(reg, { withoutExtensionsOf: 'com.nobody' })).toBe(reg.getObject(OBJ));
439+
});
440+
441+
it('leaving the pair out is the answer a registry with no pair gives, byte for byte', () => {
442+
const reg = withVendor();
443+
extend(reg, { name: { label: 'Account name' } });
444+
expect(JSON.stringify(listed(extended(), { withoutExtensionsOf: PAIR_PKG }))).toBe(JSON.stringify(listed(reg)));
445+
});
446+
447+
it('the fold onto a stored body leaves the pair\'s extension out the same way', () => {
448+
const reg = extended();
449+
expect(shape(reg.foldObjectExtendersOnto(OBJ, vendorAccount(), { withoutExtensionsOf: PAIR_PKG })))
450+
.toEqual({ region: false, industry: 'Industry', name: 'Account name' });
451+
expect(shape(reg.foldObjectExtendersOnto(OBJ, vendorAccount())))
452+
.toEqual({ region: true, industry: 'Sector (customized)', name: 'Account name' });
453+
});
454+
});

‎packages/objectql/src/registry.ts‎

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,11 @@ function admittedFieldPatches(
721721
*/
722722
type ServedObjectLayer = 'served' | 'code';
723723

724+
/** [#29903] Whether `contributor` is an `extend` contribution of `packageId`. */
725+
function isExtensionOf(contributor: ObjectContributor, packageId: string | undefined): boolean {
726+
return packageId !== undefined && contributor.ownership === 'extend' && contributor.packageId === packageId;
727+
}
728+
724729
/**
725730
* Deep merge two ServiceObject definitions.
726731
*
@@ -4673,6 +4678,29 @@ export class SchemaRegistry {
46734678
return merged;
46744679
}
46754680

4681+
/**
4682+
* [#29903, ADR-0146 D8 item 3] An object as {@link resolveObject} resolves
4683+
* it, but with the `extend` contributions of `excludedPackageId` left out of
4684+
* the fold, as if that package had never extended it: the same base layer
4685+
* (`overlay ?? own`), every other extender folded in the same order, the same
4686+
* tail. With no such contribution it IS {@link resolveObject}'s answer (the
4687+
* cached body). Uncached otherwise, as {@link resolveOwnerLayer} is.
4688+
*
4689+
* The reader is the protocol's per-package layer, which serves a base's
4690+
* objects to the base's own export WITHOUT its `.custom` pair's extension:
4691+
* the base's export carries the base unchanged, and the pair's export carries
4692+
* the extension as its `objectExtensions` entry. Folded in, the pair's field
4693+
* and field patches travel twice, the second time as if the base shipped them.
4694+
*/
4695+
private resolveObjectWithoutExtensionsOf(fqn: string, excludedPackageId: string | undefined): ServiceObject | undefined {
4696+
const contributors = excludedPackageId === undefined ? undefined : this.objectContributors.get(fqn);
4697+
if (!contributors || !contributors.some((c) => isExtensionOf(c, excludedPackageId))) return this.resolveObject(fqn);
4698+
const kept = contributors.filter((c) => !isExtensionOf(c, excludedPackageId));
4699+
const ownerContrib = kept.find(c => c.ownership === 'own');
4700+
if (!ownerContrib) return undefined;
4701+
return this.foldExtenders(kept, kept.find(c => c.ownership === 'overlay') ?? ownerContrib);
4702+
}
4703+
46764704
/**
46774705
* Fold every `extend` contribution over a chosen base layer, in the list's
46784706
* (priority-sorted) order. Extracted from {@link resolveObject} so the
@@ -5477,15 +5505,19 @@ export class SchemaRegistry {
54775505
* matching {@link resolveObject}, so this narrows nothing the fold was doing
54785506
* on an unfolded base.
54795507
*/
5480-
foldObjectExtendersOnto<T>(name: string, base: T): T {
5508+
foldObjectExtendersOnto<T>(name: string, base: T, options?: { withoutExtensionsOf?: string }): T {
54815509
if (base === null || typeof base !== 'object') return base;
54825510
const fqn = this.resolveObjectKey(name);
54835511
// With nothing to fold, the body still gets its picklist-bound fields
54845512
// resolved — the same step the fold below ends with — so a body this
54855513
// registry never saw is served in the one shape every other read uses.
54865514
// By reference when no field names a picklist.
54875515
if (fqn === undefined) return this.finishServedObject(base);
5488-
const contributors = this.objectContributors.get(fqn);
5516+
// [#29903] `options.withoutExtensionsOf`: that package's `extend`
5517+
// contributions are left out, as on {@link getAllObjects}.
5518+
const excluded = options?.withoutExtensionsOf;
5519+
const registered = this.objectContributors.get(fqn);
5520+
const contributors = excluded === undefined ? registered : registered?.filter((c) => !isExtensionOf(c, excluded));
54895521
if (!contributors || !contributors.some((c) => c.ownership === 'extend')) return this.finishServedObject(base);
54905522
return this.foldExtendersOntoDefinition(
54915523
contributors,
@@ -6224,8 +6256,12 @@ export class SchemaRegistry {
62246256
* Get all registered objects (merged).
62256257
*
62266258
* @param packageId - Optional filter: only objects contributed by this package
6259+
* @param options.withoutExtensionsOf - [#29903] each object is resolved with
6260+
* this package's `extend` contributions left out of the fold
6261+
* ({@link resolveObjectWithoutExtensionsOf}). Only the protocol's
6262+
* per-package layer passes it, naming the listed package's `.custom` pair.
62276263
*/
6228-
getAllObjects(packageId?: string): ServiceObject[] {
6264+
getAllObjects(packageId?: string, options?: { withoutExtensionsOf?: string }): ServiceObject[] {
62296265
const results: ServiceObject[] = [];
62306266

62316267
for (const fqn of this.objectContributors.keys()) {
@@ -6236,7 +6272,7 @@ export class SchemaRegistry {
62366272
if (!hasContribution) continue;
62376273
}
62386274

6239-
const merged = this.resolveObject(fqn);
6275+
const merged = this.resolveObjectWithoutExtensionsOf(fqn, options?.withoutExtensionsOf);
62406276
if (merged) {
62416277
// Tag with contributor info for UI
62426278
(merged as any)._packageId = this.getObjectOwner(fqn)?.packageId;
@@ -7594,11 +7630,17 @@ export class SchemaRegistry {
75947630
* items. Only the protocol's per-package layer passes it (the package
75957631
* export, which carries a package's own items enabled or not); every
75967632
* served read leaves it unset and hides them.
7633+
*
7634+
* [#29903] `options.withoutExtensionsOf` resolves each listed object with
7635+
* that package's `extend` contributions left out of the fold
7636+
* ({@link getAllObjects}); no other type reads it. Only the per-package
7637+
* layer passes it, naming the listed package's `.custom` pair, so a base's
7638+
* export carries the base's objects unchanged.
75977639
*/
7598-
listItems<T>(type: string, packageId?: string, options?: { includeDisabledPackages?: boolean }): T[] {
7640+
listItems<T>(type: string, packageId?: string, options?: { includeDisabledPackages?: boolean; withoutExtensionsOf?: string }): T[] {
75997641
// Special handling for 'object' and 'objects' types - use objectContributors
76007642
if (type === 'object' || type === 'objects') {
7601-
return this.getAllObjects(packageId) as unknown as T[];
7643+
return this.getAllObjects(packageId, { withoutExtensionsOf: options?.withoutExtensionsOf }) as unknown as T[];
76027644
}
76037645

76047646
const items = Array.from(this.metadata.get(type)?.values() || []) as T[];

0 commit comments

Comments
 (0)