Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions apps/studio/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export default function App() {
const handleSelectPackage = useCallback((pkg: InstalledPackage) => {
setSelectedPackage(pkg);
setSelectedObject(null);
setSelectedMeta(null);
setSelectedView('overview');
}, []);

Expand Down Expand Up @@ -126,9 +127,17 @@ export default function App() {
/>
<div className="flex flex-1 flex-col overflow-hidden">
{selectedView === 'object' && selectedObject ? (
<PluginHost metadataType="object" metadataName={selectedObject} />
<PluginHost
metadataType="object"
metadataName={selectedObject}
packageId={selectedPackage?.manifest?.id}
/>
) : selectedView === 'metadata' && selectedMeta ? (
<PluginHost metadataType={selectedMeta.type} metadataName={selectedMeta.name} />
<PluginHost
metadataType={selectedMeta.type}
metadataName={selectedMeta.name}
packageId={selectedPackage?.manifest?.id}
/>
) : selectedView === 'packages' ? (
<PackageManager />
) : selectedView === 'api-console' ? (
Expand Down
7 changes: 4 additions & 3 deletions apps/studio/src/components/MetadataInspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp
interface MetadataInspectorProps {
metaType: string;
metaName: string;
packageId?: string;
}

const TYPE_ICONS: Record<string, LucideIcon> = {
Expand Down Expand Up @@ -173,7 +174,7 @@ function JsonTree({ data, depth = 0 }: { data: any; depth?: number }) {
}


export function MetadataInspector({ metaType, metaName }: MetadataInspectorProps) {
export function MetadataInspector({ metaType, metaName, packageId }: MetadataInspectorProps) {
const client = useClient();
const [item, setItem] = useState<any>(null);
const [loading, setLoading] = useState(true);
Expand All @@ -189,7 +190,7 @@ export function MetadataInspector({ metaType, metaName }: MetadataInspectorProps

async function load() {
try {
const result: any = await client.meta.getItem(metaType, metaName);
const result: any = await client.meta.getItem(metaType, metaName, packageId ? { packageId } : undefined);
if (mounted) {
setItem(result?.item || result);
}
Expand All @@ -201,7 +202,7 @@ export function MetadataInspector({ metaType, metaName }: MetadataInspectorProps
}
load();
return () => { mounted = false; };
}, [client, metaType, metaName]);
}, [client, metaType, metaName, packageId]);

if (loading) {
return (
Expand Down
4 changes: 2 additions & 2 deletions apps/studio/src/plugins/built-in/default-plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import type { StudioPlugin, MetadataViewerProps } from '../types';

// ─── Viewer Component (adapts MetadataInspector to plugin interface) ─

function DefaultViewerComponent({ metadataType, metadataName }: MetadataViewerProps) {
return <MetadataInspector metaType={metadataType} metaName={metadataName} />;
function DefaultViewerComponent({ metadataType, metadataName, packageId }: MetadataViewerProps) {
return <MetadataInspector metaType={metadataType} metaName={metadataName} packageId={packageId} />;
}

// ─── Plugin Definition ───────────────────────────────────────────────
Expand Down
5 changes: 4 additions & 1 deletion apps/studio/src/plugins/plugin-host.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,13 @@ interface PluginHostProps {
metadataName: string;
/** Pre-loaded metadata data (optional) */
data?: any;
/** Package ID to filter metadata by (optional) */
packageId?: string;
}

// ─── Component ───────────────────────────────────────────────────────

export function PluginHost({ metadataType, metadataName, data }: PluginHostProps) {
export function PluginHost({ metadataType, metadataName, data, packageId }: PluginHostProps) {
const [activeMode, setActiveMode] = useState<ViewMode>('preview');

// Get available modes and viewers for this metadata type
Expand Down Expand Up @@ -176,6 +178,7 @@ export function PluginHost({ metadataType, metadataName, data }: PluginHostProps
metadataName={metadataName}
data={data}
mode={effectiveMode}
packageId={packageId}
/>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions apps/studio/src/plugins/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export interface MetadataViewerProps {
data?: any;
/** Current view mode */
mode: ViewMode;
/** Package ID to filter metadata by (optional) */
packageId?: string;
}

/** Context passed to action handlers */
Expand Down
9 changes: 7 additions & 2 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,10 +359,15 @@ export class ObjectStackClient {
* Get a specific metadata item by type and name
* @param type - Metadata type (e.g., 'object', 'plugin')
* @param name - Item name (snake_case identifier)
* @param options - Optional filters (e.g., packageId to scope by package)
*/
getItem: async (type: string, name: string) => {
getItem: async (type: string, name: string, options?: { packageId?: string }) => {
const route = this.getRoute('metadata');
const res = await this.fetch(`${this.baseUrl}${route}/${type}/${name}`);
const params = new URLSearchParams();
if (options?.packageId) params.set('package', options.packageId);
const qs = params.toString();
const url = `${this.baseUrl}${route}/${type}/${name}${qs ? `?${qs}` : ''}`;
const res = await this.fetch(url);
return this.unwrapResponse(res);
},

Expand Down
10 changes: 6 additions & 4 deletions packages/runtime/src/http-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,12 +384,14 @@ export class HttpDispatcher {
// /metadata/:type/:name
if (parts.length === 2) {
const [type, name] = parts;
// Extract optional package filter from query string
const packageId = query?.package || undefined;
Comment on lines +387 to +388

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are existing unit tests for handleMetadata, but none assert the new query.packagepackageId behavior for GET /meta/:type/:name. Add a test that passes a query object with package set and verifies the protocol is invoked with that packageId (and/or that the scoped item is returned).

Copilot uses AI. Check for mistakes.

// PUT /metadata/:type/:name (Save)
if (method === 'PUT' && body) {
// Try to get the protocol service directly
const protocol = await this.resolveService('protocol');

if (protocol && typeof protocol.saveMetaItem === 'function') {
try {
const result = await protocol.saveMetaItem({ type, name, item: body });
Expand All @@ -398,7 +400,7 @@ export class HttpDispatcher {
return { handled: true, response: this.error(e.message, 400) };
}
}

// Fallback to broker if protocol not available (legacy)
if (broker) {
try {
Expand Down Expand Up @@ -430,12 +432,12 @@ export class HttpDispatcher {
// If type is singular (e.g. 'app'), use it directly
// If plural (e.g. 'apps'), slice it
const singularType = type.endsWith('s') ? type.slice(0, -1) : type;

// Try Protocol Service First (Preferred)
const protocol = await this.resolveService('protocol');
if (protocol && typeof protocol.getMetaItem === 'function') {
try {
const data = await protocol.getMetaItem({ type: singularType, name });
const data = await protocol.getMetaItem({ type: singularType, name, packageId });
return { handled: true, response: this.success(data) };
Comment on lines 436 to 441

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

packageId is being passed into protocol.getMetaItem(...), but the current protocol implementation in this repo (packages/objectql/src/protocol.ts) defines getMetaItem(request: { type, name }) and ignores any packageId field, so this change won’t actually scope lookups by package. To make package filtering effective, update the protocol contract + implementation to accept packageId and use it when resolving from SchemaRegistry (prefer ${packageId}:${name} / _packageId-tagged items) and in any DB fallback query (e.g., include packageId in the where clause when provided).

Copilot uses AI. Check for mistakes.
} catch (e: any) {
// Protocol might throw if not found or not supported
Comment on lines 441 to 443

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If protocol.getMetaItem(...) throws, this handler falls through to the broker-based lookup below, which currently does not receive packageId. That means package scoping can be lost depending on which backend implementation is active. Consider ensuring the fallback path also receives the package filter (or explicitly returning a scoped-not-found) to keep behavior consistent.

Copilot uses AI. Check for mistakes.
Expand Down
Loading