Skip to content

Commit b99d9bd

Browse files
xuyushun441-sysos-zhuangclaude
authored
feat(studio): package-scope the metadata editor read (ADR-0048) (#1711)
Two installed packages may ship metadata with the same type/name; the Studio editor now resolves the right one instead of first-match. - MetadataClient: layered()/getDraft() accept { packageId }; get() emits the `package` query param (server prefer-local). - ResourceListPage: each row's edit link carries its owning package (?package=<row._packageId>) so even the unscoped list disambiguates. - ResourceEditPage: reads ?package= and scopes the layered + draft read. The route's :appName is the Studio app, not the edited item's owner, so the scope comes from the URL. Verified live: /apps/com.objectstack.studio/metadata/doc/showcase_index loads each package's own item per ?package= (showcase vs studio); no console errors. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2fd68be commit b99d9bd

4 files changed

Lines changed: 58 additions & 8 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@object-ui/data-objectstack": patch
3+
"@object-ui/app-shell": patch
4+
---
5+
6+
ADR-0048: package-scope the Studio metadata editor read. Two installed packages
7+
may ship metadata with the same `type`/`name`; the editor now resolves the right
8+
one instead of first-match.
9+
10+
- `MetadataClient`: `layered()` and `getDraft()` accept `{ packageId }`, and
11+
`get()` emits the `package` query param (→ server prefer-local, `?package=`).
12+
- `ResourceListPage`: each item's edit link carries its owning package
13+
(`?package=<row._packageId>`), so even the unscoped "all" list disambiguates;
14+
falls back to the workspace suffix for runtime/overlay-only rows.
15+
- `ResourceEditPage`: reads `?package=` and scopes the layered + draft read to
16+
that package. (The route's `:appName` is the Studio app, not the edited item's
17+
owner, so the scope must come from the URL, not the active app.)

packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,12 @@ function MetadataResourceEditPageImpl({
222222
}: MetadataResourceEditPageImplProps) {
223223
const navigate = useNavigate();
224224
const [searchParams, setSearchParams] = useSearchParams();
225+
// ADR-0048 — the owning package of the item being edited, carried on the
226+
// edit URL as `?package=` (emitted by the metadata list links). Scopes the
227+
// layered/draft read so a same-name collision resolves to the right
228+
// package's item. NOT the active Studio app's package — Studio edits items
229+
// across all installed packages.
230+
const ownerPackageId = searchParams.get('package') ?? undefined;
225231
const client = useMetadataClient();
226232
const { entries } = useMetadataTypes(client);
227233
const entry: RichMetadataTypeEntry | undefined = entries.find((t) => t.type === type);
@@ -511,12 +517,13 @@ function MetadataResourceEditPageImpl({
511517
setError(null);
512518
(async () => {
513519
try {
520+
const scope = ownerPackageId ? { packageId: ownerPackageId } : {};
514521
const [lay, draftResp] = await Promise.all([
515-
client.layered<any>(type, name),
522+
client.layered<any>(type, name, scope),
516523
// Draft reads are best-effort — a 404/error must not block
517524
// the page; readers without overlay-write permission still
518525
// see the published item.
519-
client.getDraft<any>(type, name).catch(() => null),
526+
client.getDraft<any>(type, name, scope).catch(() => null),
520527
]);
521528
if (cancelled) return;
522529
setLayered(lay);
@@ -564,7 +571,7 @@ function MetadataResourceEditPageImpl({
564571
return () => {
565572
cancelled = true;
566573
};
567-
}, [client, type, name, createMode, reloadKey]);
574+
}, [client, type, name, ownerPackageId, createMode, reloadKey]);
568575

569576
// Lazy-load references the first time the References sheet opens.
570577
const [refsLoading, setRefsLoading] = React.useState(false);

packages/app-shell/src/views/metadata-admin/ResourceListPage.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,15 @@ function DefaultMetadataList({ type }: { type: string }) {
431431
{filtered.map((row, i) => {
432432
const pk = config.primaryKey ?? 'name';
433433
const name = String(row.item[pk] ?? `(unnamed-${i})`);
434+
// ADR-0048 — link to this row's OWNING package so the editor
435+
// resolves the right item even in the unscoped "all" list
436+
// where two packages may ship the same name. Falls back to the
437+
// workspace suffix for runtime/overlay-only rows (no real
438+
// package, or the `sys_metadata` rehydration sentinel).
439+
const rowPkg = (row.item as any)._packageId as string | undefined;
440+
const rowEditSuffix = rowPkg && rowPkg !== 'sys_metadata'
441+
? `?package=${encodeURIComponent(rowPkg)}`
442+
: pkgSuffix;
434443
const invalid = row.diagnostics?.valid === false;
435444
const errorList = row.diagnostics?.errors ?? [];
436445
const warnList = (row.diagnostics as any)?.warnings ?? [];
@@ -486,7 +495,7 @@ function DefaultMetadataList({ type }: { type: string }) {
486495
</span>
487496
)}
488497
<Link
489-
to={`./${encodeURIComponent(name)}${pkgSuffix}`}
498+
to={`./${encodeURIComponent(name)}${rowEditSuffix}`}
490499
className="text-primary hover:underline font-mono"
491500
>
492501
{cell}

packages/data-objectstack/src/metadata-client.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@ export interface MetadataGetOptions {
113113
* to read the active (published) value.
114114
*/
115115
state?: 'active' | 'draft';
116+
/**
117+
* Software-package id to scope resolution (sent as the `package` query
118+
* param → server prefer-local, ADR-0048). Set when reading one item that
119+
* may collide by name across installed packages (e.g. the Studio editor
120+
* passes the edited item's owning package). Omit for context-free reads.
121+
*/
122+
packageId?: string;
116123
}
117124

118125
export interface MetadataDeleteOptions extends MetadataSaveOptions {
@@ -451,6 +458,7 @@ export class MetadataClient {
451458
// `state=draft` reads the raw draft row explicitly; the overlay flag is
452459
// redundant (and the dispatcher resolves `state` first), so skip it.
453460
else if (this.previewDrafts) params.push('preview=draft');
461+
if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
454462
const qs = params.length ? `?${params.join('&')}` : '';
455463
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
456464
const res = await this.fetchImpl(url, { method: 'GET', headers: this.headers, cache: 'no-store' });
@@ -471,8 +479,12 @@ export class MetadataClient {
471479
* unwrapped body, so this method preserves that asymmetry by
472480
* returning whatever the server sent.
473481
*/
474-
async getDraft<T = unknown>(type: string, name: string): Promise<T | null> {
475-
return this.get<T>(type, name, { state: 'draft' });
482+
async getDraft<T = unknown>(
483+
type: string,
484+
name: string,
485+
options: { packageId?: string } = {},
486+
): Promise<T | null> {
487+
return this.get<T>(type, name, { state: 'draft', ...(options.packageId ? { packageId: options.packageId } : {}) });
476488
}
477489

478490
/**
@@ -545,8 +557,13 @@ export class MetadataClient {
545557
* `code` (the artifact / fallback default), `overlay` (the saved
546558
* customisation, if any), and `effective` (what the runtime sees).
547559
*/
548-
async layered<T = unknown>(type: string, name: string): Promise<MetadataLayered<T>> {
549-
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}?layers=true`;
560+
async layered<T = unknown>(
561+
type: string,
562+
name: string,
563+
options: { packageId?: string } = {},
564+
): Promise<MetadataLayered<T>> {
565+
const pkg = options.packageId ? `&package=${encodeURIComponent(options.packageId)}` : '';
566+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}?layers=true${pkg}`;
550567
const res = await this.fetchImpl(url, { method: 'GET', headers: this.headers, cache: 'no-store' });
551568
if (res.status === 404) {
552569
return { code: null, overlay: null, overlayScope: null, effective: null };

0 commit comments

Comments
 (0)