22
33import { useState , useEffect , useCallback } from 'react' ;
44import { useClient } from '@objectstack/client-react' ;
5+ import { useScopedClient } from './useObjectStackClient' ;
56
67// ─── Types ──────────────────────────────────────────────────────────
78
@@ -35,13 +36,6 @@ interface ServiceEndpointEntry {
3536/** Metadata types that should be excluded from the endpoint tree */
3637const EXCLUDED_META_TYPES = [ 'plugin' , 'plugins' , 'kind' , 'package' ] ;
3738
38- const SYSTEM_ENDPOINTS : EndpointDef [ ] = [
39- { method : 'GET' , path : '/api/v1/discovery' , desc : 'API Discovery' , group : 'System' } ,
40- { method : 'GET' , path : '/api/v1/meta/types' , desc : 'List metadata types' , group : 'Metadata' } ,
41- { method : 'GET' , path : '/api/v1/packages' , desc : 'List packages' , group : 'System' } ,
42- { method : 'GET' , path : '/api/v1/health' , desc : 'Health check' , group : 'System' } ,
43- ] ;
44-
4539/** Build auth endpoints from the discovered auth base path. */
4640function buildAuthEndpoints ( authBase : string ) : EndpointDef [ ] {
4741 return [
@@ -194,25 +188,41 @@ export function buildServiceEndpoints(serviceName: string, routePrefix: string):
194188
195189// ─── Hook ───────────────────────────────────────────────────────────
196190
197- export function useApiDiscovery ( ) {
198- const client = useClient ( ) ;
191+ export function useApiDiscovery ( projectId ?: string ) {
192+ const unscopedClient = useClient ( ) ;
193+ const scopedClient = useScopedClient ( projectId ) ;
194+ const client : any = projectId ? scopedClient : unscopedClient ;
199195 const [ groups , setGroups ] = useState < EndpointGroup [ ] > ( [ ] ) ;
200196 const [ allEndpoints , setAllEndpoints ] = useState < EndpointDef [ ] > ( [ ] ) ;
201197 const [ loading , setLoading ] = useState ( true ) ;
202198 const [ error , setError ] = useState < string | null > ( null ) ;
203199
204200 const discover = useCallback ( async ( ) => {
201+ // When projectId is provided but the scoped client hasn't resolved yet,
202+ // defer until the next render.
203+ if ( projectId && ! scopedClient ) return ;
204+
205205 setLoading ( true ) ;
206206 setError ( null ) ;
207207
208+ // Scope prefix for system endpoints; discovery already handles its own routes.
209+ const scopePrefix = projectId ? `/api/v1/projects/${ projectId } ` : '/api/v1' ;
210+ const discoveryUrl = `${ scopePrefix } /discovery` ;
211+ const systemEndpoints : EndpointDef [ ] = [
212+ { method : 'GET' , path : discoveryUrl , desc : 'API Discovery' , group : 'System' } ,
213+ { method : 'GET' , path : `${ scopePrefix } /meta/types` , desc : 'List metadata types' , group : 'Metadata' } ,
214+ { method : 'GET' , path : `${ scopePrefix } /packages` , desc : 'List packages' , group : 'System' } ,
215+ { method : 'GET' , path : '/api/v1/health' , desc : 'Health check' , group : 'System' } ,
216+ ] ;
217+
208218 try {
209219 // 1. Fetch discovery response — the source of truth for available services
210220 let authBase = '/api/v1/auth' ;
211221 let discoveredServices : Record < string , { enabled : boolean ; route ?: string } > = { } ;
212222 let discoveredRoutes : Record < string , string > = { } ;
213223
214224 try {
215- const discRes = await fetch ( '/api/v1/discovery' ) ;
225+ const discRes = await fetch ( discoveryUrl ) ;
216226 if ( discRes . ok ) {
217227 const discData = await discRes . json ( ) ;
218228 const data = discData ?. data ?? discData ;
@@ -238,10 +248,15 @@ export function useApiDiscovery() {
238248 const hasHandler = serviceInfo ?. handlerReady
239249 ?? ( serviceInfo ?. status === 'available' || serviceInfo ?. status === 'degraded' ) ;
240250
241- // Use route from discovery services, discovery routes map, or catalog default
242- const routePrefix = serviceInfo ?. route
251+ // Use route from discovery services, discovery routes map, or catalog default.
252+ // When in a project scope, rewrite unscoped catalog defaults so they include
253+ // the /projects/:projectId segment.
254+ const rawRoute = serviceInfo ?. route
243255 ?? discoveredRoutes [ serviceName ]
244256 ?? catalog . defaultRoute ;
257+ const routePrefix = projectId && rawRoute . startsWith ( '/api/v1/' ) && ! rawRoute . includes ( '/projects/' )
258+ ? rawRoute . replace ( '/api/v1/' , `/api/v1/projects/${ projectId } /` )
259+ : rawRoute ;
245260
246261 if ( isEnabled && hasHandler ) {
247262 serviceEndpoints . push ( ...buildServiceEndpoints ( serviceName , routePrefix ) ) ;
@@ -279,34 +294,34 @@ export function useApiDiscovery() {
279294
280295 // 5. Build dynamic data endpoints for each object
281296 const dataEndpoints : EndpointDef [ ] = objectNames . flatMap ( name => [
282- { method : 'GET' as HttpMethod , path : `/api/v1 /data/${ name } ` , desc : `List ${ name } ` , group : `Data: ${ name } ` } ,
283- { method : 'POST' as HttpMethod , path : `/api/v1 /data/${ name } ` , desc : `Create ${ name } ` , group : `Data: ${ name } ` , bodyTemplate : { name : 'example' } } ,
284- { method : 'GET' as HttpMethod , path : `/api/v1 /data/${ name } /:id` , desc : `Get ${ name } by ID` , group : `Data: ${ name } ` } ,
285- { method : 'PATCH' as HttpMethod , path : `/api/v1 /data/${ name } /:id` , desc : `Update ${ name } ` , group : `Data: ${ name } ` , bodyTemplate : { name : 'updated' } } ,
286- { method : 'DELETE' as HttpMethod , path : `/api/v1 /data/${ name } /:id` , desc : `Delete ${ name } ` , group : `Data: ${ name } ` } ,
297+ { method : 'GET' as HttpMethod , path : `${ scopePrefix } /data/${ name } ` , desc : `List ${ name } ` , group : `Data: ${ name } ` } ,
298+ { method : 'POST' as HttpMethod , path : `${ scopePrefix } /data/${ name } ` , desc : `Create ${ name } ` , group : `Data: ${ name } ` , bodyTemplate : { name : 'example' } } ,
299+ { method : 'GET' as HttpMethod , path : `${ scopePrefix } /data/${ name } /:id` , desc : `Get ${ name } by ID` , group : `Data: ${ name } ` } ,
300+ { method : 'PATCH' as HttpMethod , path : `${ scopePrefix } /data/${ name } /:id` , desc : `Update ${ name } ` , group : `Data: ${ name } ` , bodyTemplate : { name : 'updated' } } ,
301+ { method : 'DELETE' as HttpMethod , path : `${ scopePrefix } /data/${ name } /:id` , desc : `Delete ${ name } ` , group : `Data: ${ name } ` } ,
287302 ] ) ;
288303
289304 // 6. Build metadata endpoints for each type
290305 const metaEndpoints : EndpointDef [ ] = metaTypes
291306 . filter ( t => ! EXCLUDED_META_TYPES . includes ( t ) )
292307 . map ( type => ( {
293308 method : 'GET' as HttpMethod ,
294- path : `/api/v1 /meta/${ type } ` ,
309+ path : `${ scopePrefix } /meta/${ type } ` ,
295310 desc : `List ${ type } metadata` ,
296311 group : 'Metadata' ,
297312 } ) ) ;
298313
299314 // 7. Build per-object schema endpoints
300315 const schemaEndpoints : EndpointDef [ ] = objectNames . map ( name => ( {
301316 method : 'GET' as HttpMethod ,
302- path : `/api/v1 /meta/object/${ name } ` ,
317+ path : `${ scopePrefix } /meta/object/${ name } ` ,
303318 desc : `${ name } schema` ,
304319 group : 'Metadata' ,
305320 } ) ) ;
306321
307322 // 8. Combine all endpoints
308323 const all = [
309- ...SYSTEM_ENDPOINTS ,
324+ ...systemEndpoints ,
310325 ...buildAuthEndpoints ( authBase ) ,
311326 ...serviceEndpoints ,
312327 ...metaEndpoints ,
@@ -346,7 +361,7 @@ export function useApiDiscovery() {
346361 } finally {
347362 setLoading ( false ) ;
348363 }
349- } , [ client ] ) ;
364+ } , [ client , projectId , scopedClient ] ) ;
350365
351366 useEffect ( ( ) => { discover ( ) ; } , [ discover ] ) ;
352367
0 commit comments