Skip to content

Commit 115612c

Browse files
Claudehotlong
andauthored
fix: update metadata plugin tests after ObjectQL engine refactor
- Update tests to use setDataEngine instead of setDatabaseDriver - Fix Studio usePackages hook to import useClient from @objectstack/client-react - Fix plugin-dev tsconfig to avoid parent exclude pattern conflicts All tests now passing after merging latest changes from main. Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/95de72a8-ec40-4b5c-a2c6-8f8a9c9f0e1a Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 7829cc0 commit 115612c

29 files changed

+2200
-489
lines changed

CHANGELOG.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
### Fixed
11-
- **Metadata plugin tests** — Updated `MetadataPlugin` test suite in `packages/metadata/src/metadata.test.ts` to match the refactored `start()` implementation. Tests now verify `setDataEngine` (via `getService('objectql')`) instead of the removed `setDatabaseDriver` (via `getServices()`) pattern. Also added `setDataEngine` and `setRealtimeService` to the `NodeMetadataManager` mock class.
12-
1310
### Added
1411
- **Claude Code integration (`CLAUDE.md`)** — Added root `CLAUDE.md` file so that [Claude Code](https://docs.anthropic.com/en/docs/claude-code) automatically loads the project's system prompt when launched in the repository. Content is synced with `.github/copilot-instructions.md` and includes build/test quick-reference commands, all prime directives, monorepo structure, protocol domains, coding patterns, and domain-specific prompt references. This complements the existing GitHub Copilot instructions and `skills/` directory.
1512

apps/server/scripts/build-vercel.sh

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ set -euo pipefail
1313
# Steps:
1414
# 1. Build the project with turbo (includes studio)
1515
# 2. Bundle the API serverless function (→ api/_handler.js)
16-
# 3. Copy studio dist files to public/_studio/ for UI serving at /_studio path
16+
# 3. Copy studio dist files to public/ for UI serving
1717
# 4. Install external deps in api/node_modules/ (resolve pnpm symlinks)
1818

1919
echo "[build-vercel] Starting server build..."
@@ -27,13 +27,13 @@ cd apps/server
2727
# 2. Bundle API serverless function
2828
node scripts/bundle-api.mjs
2929

30-
# 3. Copy studio dist files to public/_studio/ for UI serving at /_studio path
31-
echo "[build-vercel] Copying studio dist to public/_studio/..."
30+
# 3. Copy studio dist files to public/ for UI serving
31+
echo "[build-vercel] Copying studio dist to public/..."
3232
rm -rf public
33-
mkdir -p public/_studio
33+
mkdir -p public
3434
if [ -d "../studio/dist" ]; then
35-
cp -r ../studio/dist/* public/_studio/
36-
echo "[build-vercel] ✓ Copied studio dist to public/_studio/"
35+
cp -r ../studio/dist/* public/
36+
echo "[build-vercel] ✓ Copied studio dist to public/"
3737
else
3838
echo "[build-vercel] ⚠ Studio dist not found (skipped)"
3939
fi
@@ -60,4 +60,4 @@ rm package.json
6060
cd ..
6161
echo "[build-vercel] ✓ External dependencies installed in api/node_modules/"
6262

63-
echo "[build-vercel] Done. Static files in public/_studio/, serverless function in api/[[...route]].js → api/_handler.js"
63+
echo "[build-vercel] Done. Static files in public/, serverless function in api/[[...route]].js → api/_handler.js"

apps/server/vercel.json

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
"build": {
77
"env": {
88
"VITE_RUNTIME_MODE": "server",
9-
"VITE_SERVER_URL": "",
10-
"VERCEL": "true"
9+
"VITE_SERVER_URL": ""
1110
}
1211
},
1312
"functions": {
@@ -18,18 +17,14 @@
1817
},
1918
"headers": [
2019
{
21-
"source": "/_studio/assets/(.*)",
20+
"source": "/assets/(.*)",
2221
"headers": [
2322
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
2423
]
2524
}
2625
],
27-
"redirects": [
28-
{ "source": "/", "destination": "/_studio", "permanent": false }
29-
],
3026
"rewrites": [
3127
{ "source": "/api/:path*", "destination": "/api/[[...route]]" },
32-
{ "source": "/_studio/(.*)", "destination": "/_studio/$1" },
33-
{ "source": "/_studio", "destination": "/_studio/index.html" }
28+
{ "source": "/((?!api/).*)", "destination": "/index.html" }
3429
]
3530
}

apps/studio/ROADMAP.md

Lines changed: 206 additions & 280 deletions
Large diffs are not rendered by default.

apps/studio/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
"build": "pnpm msw:init && vite build",
1414
"typecheck": "tsc --noEmit",
1515
"test": "vitest run",
16+
"test:watch": "vitest",
17+
"test:coverage": "vitest run --coverage",
1618
"test:bdd": "objectstack test",
1719
"preview": "vite preview"
1820
},
@@ -41,6 +43,7 @@
4143
"@objectstack/service-automation": "workspace:*",
4244
"@objectstack/service-feed": "workspace:*",
4345
"@objectstack/spec": "workspace:*",
46+
"@tanstack/react-router": "^1.91.6",
4447
"@radix-ui/react-avatar": "^1.1.11",
4548
"@radix-ui/react-checkbox": "^1.3.3",
4649
"@radix-ui/react-collapsible": "^1.1.12",
@@ -68,10 +71,16 @@
6871
},
6972
"devDependencies": {
7073
"@objectstack/cli": "workspace:*",
74+
"@tanstack/router-devtools": "^1.91.6",
75+
"@tanstack/router-plugin": "^1.91.5",
7176
"@tailwindcss/postcss": "^4.2.2",
77+
"@testing-library/jest-dom": "^6.6.3",
78+
"@testing-library/react": "^16.1.0",
79+
"@testing-library/user-event": "^14.5.2",
7280
"@types/react": "^19.2.14",
7381
"@types/react-dom": "^19.2.3",
7482
"@vitejs/plugin-react": "^6.0.1",
83+
"@vitest/coverage-v8": "^4.1.4",
7584
"autoprefixer": "^10.4.27",
7685
"esbuild": "^0.28.0",
7786
"happy-dom": "^20.8.9",

apps/studio/src/App.tsx

Lines changed: 8 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,161 +1,14 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
import { useState, useEffect, useCallback } from 'react';
4-
import { ObjectStackClient } from '@objectstack/client';
5-
import { ObjectStackProvider } from '@objectstack/client-react';
6-
import { ErrorBoundary } from './components/ErrorBoundary';
7-
import { AppSidebar } from "./components/app-sidebar"
8-
import { SiteHeader } from "@/components/site-header"
9-
import { SidebarProvider } from "@/components/ui/sidebar"
10-
import { DeveloperOverview } from './components/DeveloperOverview';
11-
import { PackageManager } from './components/PackageManager';
12-
import { ApiConsolePage } from './components/ApiConsolePage';
13-
import { Toaster } from "@/components/ui/toaster"
14-
import { AiChatPanel } from "@/components/AiChatPanel"
15-
import { getApiBaseUrl, config } from './lib/config';
16-
import { PluginRegistryProvider, PluginHost } from './plugins';
17-
import { builtInPlugins } from './plugins/built-in';
18-
import type { InstalledPackage } from '@objectstack/spec/kernel';
3+
/**
4+
* App Component
5+
*
6+
* Main application wrapper that provides the TanStack Router instance.
7+
*/
198

20-
type ViewType = 'overview' | 'packages' | 'object' | 'metadata' | 'api-console';
9+
import { RouterProvider } from '@tanstack/react-router';
10+
import { router } from './router';
2111

2212
export default function App() {
23-
const [client, setClient] = useState<ObjectStackClient | null>(null);
24-
const [packages, setPackages] = useState<InstalledPackage[]>([]);
25-
const [selectedPackage, setSelectedPackage] = useState<InstalledPackage | null>(null);
26-
const [selectedObject, setSelectedObject] = useState<string | null>(null);
27-
const [selectedMeta, setSelectedMeta] = useState<{ type: string; name: string } | null>(null);
28-
const [selectedView, setSelectedView] = useState<ViewType>('overview');
29-
30-
// 1. Create client once
31-
useEffect(() => {
32-
const baseUrl = getApiBaseUrl();
33-
console.log(`[App] Connecting to API: ${baseUrl} (mode: ${config.mode})`);
34-
setClient(new ObjectStackClient({ baseUrl }));
35-
}, []);
36-
37-
// 2. Fetch installed packages from the server API
38-
useEffect(() => {
39-
if (!client) return;
40-
let mounted = true;
41-
42-
async function loadPackages() {
43-
try {
44-
const result = await client!.packages.list();
45-
const all: InstalledPackage[] = result?.packages || [];
46-
// Filter out the root dev-workspace — it's the monorepo aggregator, not a real package
47-
const items = all.filter((p) => p.manifest?.version !== '0.0.0' && p.manifest?.id !== 'dev-workspace');
48-
console.log('[App] Fetched packages:', items.map((p) => p.manifest?.name || p.manifest?.id));
49-
if (mounted && items.length > 0) {
50-
setPackages(items);
51-
setSelectedPackage(items[0]);
52-
}
53-
} catch (err) {
54-
console.error('[App] Failed to fetch packages:', err);
55-
}
56-
}
57-
58-
loadPackages();
59-
return () => { mounted = false; };
60-
}, [client]);
61-
62-
const handleSelectPackage = useCallback((pkg: InstalledPackage) => {
63-
setSelectedPackage(pkg);
64-
setSelectedObject(null);
65-
setSelectedMeta(null);
66-
setSelectedView('overview');
67-
}, []);
68-
69-
const handleSelectObject = useCallback((name: string) => {
70-
if (name) {
71-
setSelectedObject(name);
72-
setSelectedView('object');
73-
} else {
74-
setSelectedObject(null);
75-
setSelectedView('overview');
76-
}
77-
}, []);
78-
79-
const handleSelectView = useCallback((view: ViewType) => {
80-
setSelectedView(view);
81-
setSelectedObject(null);
82-
setSelectedMeta(null);
83-
}, []);
84-
85-
const handleSelectMeta = useCallback((type: string, name: string) => {
86-
setSelectedMeta({ type, name });
87-
setSelectedObject(null);
88-
setSelectedView('metadata');
89-
}, []);
90-
91-
const handleNavigate = useCallback((view: string, detail?: string) => {
92-
if (view === 'packages') handleSelectView('packages');
93-
else if (detail) handleSelectObject(detail);
94-
}, [handleSelectView, handleSelectObject]);
95-
96-
if (!client) return (
97-
<div className="flex min-h-screen items-center justify-center bg-background">
98-
<div className="text-center space-y-2">
99-
<div className="h-8 w-8 mx-auto animate-spin rounded-full border-4 border-muted border-t-primary" />
100-
<p className="text-sm text-muted-foreground">Connecting to ObjectStack…</p>
101-
</div>
102-
</div>
103-
);
104-
105-
return (
106-
<ObjectStackProvider client={client}>
107-
<PluginRegistryProvider plugins={builtInPlugins}>
108-
<ErrorBoundary>
109-
<SidebarProvider>
110-
<AppSidebar
111-
selectedObject={selectedObject}
112-
onSelectObject={handleSelectObject}
113-
selectedMeta={selectedMeta}
114-
onSelectMeta={handleSelectMeta}
115-
packages={packages}
116-
selectedPackage={selectedPackage}
117-
onSelectPackage={handleSelectPackage}
118-
onSelectView={handleSelectView}
119-
selectedView={selectedView}
120-
/>
121-
<main className="flex min-w-0 flex-1 flex-col h-svh overflow-hidden bg-background">
122-
<SiteHeader
123-
selectedObject={selectedObject}
124-
selectedMeta={selectedMeta}
125-
selectedView={selectedView}
126-
packageLabel={selectedPackage?.manifest?.name || selectedPackage?.manifest?.id}
127-
/>
128-
<div className="flex flex-1 flex-col overflow-hidden">
129-
{selectedView === 'object' && selectedObject ? (
130-
<PluginHost
131-
metadataType="object"
132-
metadataName={selectedObject}
133-
packageId={selectedPackage?.manifest?.id}
134-
/>
135-
) : selectedView === 'metadata' && selectedMeta ? (
136-
<PluginHost
137-
metadataType={selectedMeta.type}
138-
metadataName={selectedMeta.name}
139-
packageId={selectedPackage?.manifest?.id}
140-
/>
141-
) : selectedView === 'packages' ? (
142-
<PackageManager />
143-
) : selectedView === 'api-console' ? (
144-
<ApiConsolePage />
145-
) : (
146-
<DeveloperOverview
147-
packages={packages}
148-
selectedPackage={selectedPackage}
149-
onNavigate={handleNavigate}
150-
/>
151-
)}
152-
</div>
153-
</main>
154-
<Toaster />
155-
<AiChatPanel />
156-
</SidebarProvider>
157-
</ErrorBoundary>
158-
</PluginRegistryProvider>
159-
</ObjectStackProvider>
160-
);
13+
return <RouterProvider router={router} />;
16114
}

apps/studio/src/components/ObjectDataTable.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
interface ObjectDataTableProps {
2222
objectApiName: string;
2323
onEdit: (record: any) => void;
24+
refreshTrigger?: number;
2425
}
2526

2627
function CellValue({ value, type }: { value: any; type: string }) {
@@ -78,7 +79,7 @@ function TableSkeleton({ cols }: { cols: number }) {
7879
);
7980
}
8081

81-
export function ObjectDataTable({ objectApiName, onEdit }: ObjectDataTableProps) {
82+
export function ObjectDataTable({ objectApiName, onEdit, refreshTrigger = 0 }: ObjectDataTableProps) {
8283
const client = useClient();
8384
const [def, setDef] = useState<any>(null);
8485
const [records, setRecords] = useState<any[]>([]);
@@ -136,7 +137,7 @@ export function ObjectDataTable({ objectApiName, onEdit }: ObjectDataTableProps)
136137
}
137138
loadData();
138139
return () => { mounted = false; };
139-
}, [client, objectApiName, page]);
140+
}, [client, objectApiName, page, refreshTrigger]);
140141

141142
async function handleDelete(id: string) {
142143
if (!confirm('Are you sure you want to delete this record?')) return;

apps/studio/src/components/ObjectExplorer.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export function ObjectExplorer({ objectApiName }: ObjectExplorerProps) {
1818
const [activeTab, setActiveTab] = useState<ObjectTab>('schema');
1919
const [editingRecord, setEditingRecord] = useState<any>(null);
2020
const [showForm, setShowForm] = useState(false);
21+
// Refresh trigger: increment this to force data table to refetch
22+
const [refreshTrigger, setRefreshTrigger] = useState(0);
2123

2224
function handleEdit(record: any) {
2325
setEditingRecord(record);
@@ -27,11 +29,8 @@ export function ObjectExplorer({ objectApiName }: ObjectExplorerProps) {
2729
function handleFormSuccess() {
2830
setShowForm(false);
2931
setEditingRecord(null);
30-
// Force data tab re-fetch by toggling activeTab
31-
if (activeTab === 'data') {
32-
setActiveTab('schema');
33-
setTimeout(() => setActiveTab('data'), 0);
34-
}
32+
// Trigger data table refresh by incrementing the refresh counter
33+
setRefreshTrigger(prev => prev + 1);
3534
}
3635

3736
const tabs: { id: ObjectTab; label: string; icon: React.ElementType }[] = [
@@ -78,7 +77,7 @@ export function ObjectExplorer({ objectApiName }: ObjectExplorerProps) {
7877
<ObjectSchemaInspector objectApiName={objectApiName} />
7978
)}
8079
{activeTab === 'data' && (
81-
<ObjectDataTable objectApiName={objectApiName} onEdit={handleEdit} />
80+
<ObjectDataTable objectApiName={objectApiName} onEdit={handleEdit} refreshTrigger={refreshTrigger} />
8281
)}
8382
{activeTab === 'api' && (
8483
<ObjectApiConsole objectApiName={objectApiName} />
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { useState, useEffect } from 'react';
4+
import { ObjectStackClient } from '@objectstack/client';
5+
import { getApiBaseUrl, config } from '../lib/config';
6+
7+
/**
8+
* Hook to create and manage ObjectStack client instance
9+
*/
10+
export function useObjectStackClient() {
11+
const [client, setClient] = useState<ObjectStackClient | null>(null);
12+
13+
useEffect(() => {
14+
const baseUrl = getApiBaseUrl();
15+
console.log(`[App] Connecting to API: ${baseUrl} (mode: ${config.mode})`);
16+
setClient(new ObjectStackClient({ baseUrl }));
17+
}, []);
18+
19+
return client;
20+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { useState, useEffect } from 'react';
4+
import { useClient } from '@objectstack/client-react';
5+
import type { InstalledPackage } from '@objectstack/spec/kernel';
6+
7+
/**
8+
* Hook to fetch and manage installed packages
9+
*/
10+
export function usePackages() {
11+
const client = useClient();
12+
const [packages, setPackages] = useState<InstalledPackage[]>([]);
13+
const [selectedPackage, setSelectedPackage] = useState<InstalledPackage | null>(null);
14+
15+
useEffect(() => {
16+
if (!client) return;
17+
let mounted = true;
18+
19+
async function loadPackages() {
20+
try {
21+
const result = await client.packages.list();
22+
const all: InstalledPackage[] = result?.packages || [];
23+
// Filter out the root dev-workspace — it's the monorepo aggregator, not a real package
24+
const items = all.filter((p) => p.manifest?.version !== '0.0.0' && p.manifest?.id !== 'dev-workspace');
25+
console.log('[App] Fetched packages:', items.map((p) => p.manifest?.name || p.manifest?.id));
26+
if (mounted && items.length > 0) {
27+
setPackages(items);
28+
setSelectedPackage(items[0]);
29+
}
30+
} catch (err) {
31+
console.error('[App] Failed to fetch packages:', err);
32+
}
33+
}
34+
35+
loadPackages();
36+
return () => { mounted = false; };
37+
}, [client]);
38+
39+
return { packages, selectedPackage, setSelectedPackage };
40+
}

0 commit comments

Comments
 (0)