Skip to content

feat(console): fetch metadata from API at runtime instead of bundling at build time#516

Merged
hotlong merged 3 commits intomainfrom
copilot/implement-api-driven-metadata
Feb 15, 2026
Merged

feat(console): fetch metadata from API at runtime instead of bundling at build time#516
hotlong merged 3 commits intomainfrom
copilot/implement-api-driven-metadata

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 14, 2026

Console bundles example metadata (CRM, Todo, KitchenSink) via static imports in objectstack.shared.ts. When deployed to another ObjectStack project, it renders the bundled examples instead of that project's metadata.

Changes

New context providers

  • MetadataProvider — fetches all metadata categories (app, object, dashboard, report, page) in parallel via client.meta.getItems() on mount. Exposes useMetadata() hook with refresh() for cache invalidation.
  • AdapterProvider — lifts ObjectStackAdapter creation above routes so both AppContent and RootRedirect share a single connected instance. Exposes useAdapter().

Replaced static imports with API-driven metadata

  • App.tsxConnectedShell wraps routes with AdapterProvider + MetadataProvider
  • DashboardView, ReportView, PageView, AppSidebar, SearchResultsPage — replaced appConfig.X lookups with useMetadata()

objectstack.shared.ts scoped to MSW only

  • Only mocks/browser.ts and mocks/server.ts import it now (to seed the mock server for dev/demo mode)
  • Production builds have zero bundled metadata
// Before: static, bundled at build time
import appConfig from '../../objectstack.shared';
const dashboard = appConfig.dashboards?.find(d => d.name === dashboardName);

// After: fetched from API at runtime
const { dashboards } = useMetadata();
const dashboard = dashboards?.find(d => d.name === dashboardName);

Test updates

  • BrowserSimulation, ConsoleApp, PageView tests mock both MetadataProvider and AdapterProvider to provide test data without requiring a real API connection.

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectui Ready Ready Preview, Comment Feb 14, 2026 7:24pm
objectui-demo Ready Ready Preview, Comment Feb 14, 2026 7:24pm
objectui-storybook Error Error Feb 14, 2026 7:24pm

Request Review

Create MetadataProvider that fetches all metadata (apps, objects, dashboards,
reports, pages) from the ObjectStack API at runtime via ObjectStackAdapter.

- Add MetadataProvider context with useMetadata() hook and refresh()
- Add AdapterProvider context to lift adapter creation above routes
- Replace static appConfig imports in App.tsx, DashboardView, ReportView,
  PageView, AppSidebar, and SearchResultsPage with useMetadata()
- Keep objectstack.shared.ts only for MSW mock server (browser.ts, server.ts)
- Update tests (BrowserSimulation, ConsoleApp, PageView) to mock providers

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor console to use API-driven metadata architecture feat(console): fetch metadata from API at runtime instead of bundling at build time Feb 14, 2026
Copilot AI requested a review from hotlong February 14, 2026 19:19
@hotlong hotlong marked this pull request as ready for review February 15, 2026 01:31
Copilot AI review requested due to automatic review settings February 15, 2026 01:31
@hotlong hotlong merged commit d799e49 into main Feb 15, 2026
7 of 8 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the Console app to load metadata (apps/objects/dashboards/reports/pages) from the ObjectStack API at runtime rather than bundling example metadata via static imports, enabling the Console to correctly render metadata from the deployed project.

Changes:

  • Introduces AdapterProvider and MetadataProvider contexts to share a single connected adapter instance and fetch metadata on mount.
  • Refactors several views/components to use useMetadata() instead of objectstack.shared imports.
  • Updates tests to mock the new providers (and keeps objectstack.shared usage for MSW seeding).

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
apps/console/src/context/MetadataProvider.tsx Adds runtime metadata fetching + useMetadata() hook and refresh().
apps/console/src/context/AdapterProvider.tsx Adds adapter creation/connection + useAdapter() hook.
apps/console/src/App.tsx Wraps app routes with the new providers and replaces static config reads with useMetadata().
apps/console/src/components/DashboardView.tsx Loads dashboards via useMetadata() instead of bundled config.
apps/console/src/components/ReportView.tsx Loads reports via useMetadata() instead of bundled config.
apps/console/src/components/PageView.tsx Loads pages via useMetadata() instead of bundled config.
apps/console/src/components/AppSidebar.tsx Loads apps list via useMetadata() instead of bundled config.
apps/console/src/components/SearchResultsPage.tsx Loads apps/navigation via useMetadata() instead of bundled config.
apps/console/src/tests/PageView.test.tsx Switches from mocking objectstack.shared to mocking useMetadata().
apps/console/src/tests/ConsoleApp.test.tsx Mocks both metadata + adapter providers/hooks for integration test rendering.
apps/console/src/tests/BrowserSimulation.test.tsx Mocks both providers/hooks and updates adapter mock shape for new connection-state subscription usage.
Comments suppressed due to low confidence (1)

apps/console/src/App.tsx:380

  • RootRedirect returns when apps are empty even after loading is false. With API-driven metadata, this can happen on network/auth errors or a genuinely empty tenant, resulting in an infinite loading screen at '/'. Handle the empty/error case explicitly (e.g., show a "No apps configured" empty state or redirect to /login when unauthenticated).
// Redirect root to default app
function RootRedirect() {
    const { apps, loading } = useMetadata();
    const activeApps = apps.filter((a: any) => a.active !== false);
    const defaultApp = activeApps.find((a: any) => a.isDefault === true) || activeApps[0];
    
    if (loading) return <LoadingScreen />;
    if (defaultApp) {
        return <Navigate to={`/apps/${defaultApp.name}`} replace />;
    }
    return <LoadingScreen />;
}

Comment on lines +58 to +62
} catch (err) {
if (!cancelled) {
console.error('[Console] Failed to initialize:', err);
}
}
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

In the init() error path, the adapter remains null and the UI will sit on indefinitely with no user-visible failure state (only console.error). Consider storing an error in state/context and rendering an error screen with a retry action (or propagating the error so AppContent can handle connectionState='error').

Copilot uses AI. Check for mistakes.

/**
* Creates an ObjectStackAdapter, connects to the API, then provides it to children.
* Shows nothing (returns null) until the adapter is ready.
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The JSDoc says AdapterProvider "returns null" until the adapter is ready, but the component always renders the context provider and children (with a possibly null value). Either update the comment to match behavior, or actually gate rendering (e.g., return null / a loading UI) until an adapter exists.

Suggested change
* Shows nothing (returns null) until the adapter is ready.
* Until the adapter is ready, the context value will be null and consumers should handle this state (e.g., by showing a loading UI).

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +66
const [appsRes, objectsRes, dashboardsRes, reportsRes, pagesRes] = await Promise.all([
client.meta.getItems('app').catch(() => ({ items: [] })),
client.meta.getItems('object').catch(() => ({ items: [] })),
client.meta.getItems('dashboard').catch(() => ({ items: [] })),
client.meta.getItems('report').catch(() => ({ items: [] })),
client.meta.getItems('page').catch(() => ({ items: [] })),
]);

return {
apps: extractItems(appsRes),
objects: extractItems(objectsRes),
dashboards: extractItems(dashboardsRes),
reports: extractItems(reportsRes),
pages: extractItems(pagesRes),
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

fetchAllMetadata swallows all request failures via per-call .catch(() => ({ items: [] })), so MetadataState.error will almost never be set and consumers can't distinguish "no metadata" from "failed to load metadata". Use Promise.allSettled (or let rejections bubble) and set an aggregated error (and optionally log) when one or more categories fail.

Suggested change
const [appsRes, objectsRes, dashboardsRes, reportsRes, pagesRes] = await Promise.all([
client.meta.getItems('app').catch(() => ({ items: [] })),
client.meta.getItems('object').catch(() => ({ items: [] })),
client.meta.getItems('dashboard').catch(() => ({ items: [] })),
client.meta.getItems('report').catch(() => ({ items: [] })),
client.meta.getItems('page').catch(() => ({ items: [] })),
]);
return {
apps: extractItems(appsRes),
objects: extractItems(objectsRes),
dashboards: extractItems(dashboardsRes),
reports: extractItems(reportsRes),
pages: extractItems(pagesRes),
type CategoryKey = 'apps' | 'objects' | 'dashboards' | 'reports' | 'pages';
const categories: { key: CategoryKey; type: 'app' | 'object' | 'dashboard' | 'report' | 'page' }[] = [
{ key: 'apps', type: 'app' },
{ key: 'objects', type: 'object' },
{ key: 'dashboards', type: 'dashboard' },
{ key: 'reports', type: 'report' },
{ key: 'pages', type: 'page' },
];
const results = await Promise.allSettled(
categories.map(category => client.meta.getItems(category.type)),
);
const data: Record<CategoryKey, any[]> = {
apps: [],
objects: [],
dashboards: [],
reports: [],
pages: [],
};
const failedCategories: string[] = [];
const errors: unknown[] = [];
results.forEach((result, index) => {
const { key } = categories[index];
if (result.status === 'fulfilled') {
data[key] = extractItems(result.value);
} else {
// Preserve empty array for failed categories but record the failure.
data[key] = [];
failedCategories.push(key);
errors.push(result.reason);
}
});
if (failedCategories.length > 0) {
const message = `Failed to load metadata for categories: ${failedCategories.join(', ')}`;
if (typeof console !== 'undefined' && typeof console.error === 'function') {
console.error(message, errors);
}
throw new Error(message);
}
return {
apps: data.apps,
objects: data.objects,
dashboards: data.dashboards,
reports: data.reports,
pages: data.pages,

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +92
export function MetadataProvider({ children, adapter }: MetadataProviderProps) {
const [state, setState] = useState<MetadataState>({
apps: [],
objects: [],
dashboards: [],
reports: [],
pages: [],
loading: true,
error: null,
});

const adapterRef = useRef(adapter);
adapterRef.current = adapter;

const refresh = useCallback(async () => {
setState(prev => ({ ...prev, loading: true, error: null }));
try {
const data = await fetchAllMetadata(adapterRef.current);
setState({ ...data, loading: false, error: null });
} catch (err) {
setState(prev => ({ ...prev, loading: false, error: err instanceof Error ? err : new Error(String(err)) }));
}
}, []);
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

There are no direct tests for MetadataProvider's real behavior (initial fetch, partial failure handling, and refresh()). Current console tests mock useMetadata(), so regressions in the provider (e.g., loading/error transitions) won't be caught. Add a focused unit/integration test for this provider similar to ExpressionProvider tests.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants