@@ -285,6 +285,13 @@ export class HttpDispatcher {
285285 return { handled : true , response : this . success ( result ) } ;
286286 }
287287
288+ // POST /analytics/sql (Dry-run or debug)
289+ if ( subPath === 'sql' && m === 'POST' ) {
290+ // Assuming service has generateSql method
291+ const result = await analyticsService . generateSql ( body , { request : context . request } ) ;
292+ return { handled : true , response : this . success ( result ) } ;
293+ }
294+
288295 return { handled : false } ;
289296 }
290297
@@ -297,8 +304,68 @@ export class HttpDispatcher {
297304 if ( ! hubService ) return { handled : false } ;
298305
299306 const m = method . toUpperCase ( ) ;
300- // Dispatch to hub service methods based on convention or explicit mapping
301- // This is a placeholder for Hub Protocol implementation
307+ const parts = path . replace ( / ^ \/ + / , '' ) . split ( '/' ) ;
308+
309+ // Resource-based routing: /hub/:resource/:id
310+ if ( parts . length > 0 ) {
311+ const resource = parts [ 0 ] ; // spaces, plugins, etc.
312+
313+ // Allow mapping "spaces" -> "createSpace", "listSpaces" etc.
314+ // Convention:
315+ // GET /spaces -> listSpaces
316+ // POST /spaces -> createSpace
317+ // GET /spaces/:id -> getSpace
318+ // PATCH /spaces/:id -> updateSpace
319+ // DELETE /spaces/:id -> deleteSpace
320+
321+ const actionBase = resource . endsWith ( 's' ) ? resource . slice ( 0 , - 1 ) : resource ; // space
322+ const id = parts [ 1 ] ;
323+
324+ try {
325+ if ( parts . length === 1 ) {
326+ // Collection Operations
327+ if ( m === 'GET' ) {
328+ const capitalizedAction = 'list' + this . capitalize ( resource ) ; // listSpaces
329+ if ( typeof hubService [ capitalizedAction ] === 'function' ) {
330+ const result = await hubService [ capitalizedAction ] ( query , { request : context . request } ) ;
331+ return { handled : true , response : this . success ( result ) } ;
332+ }
333+ }
334+ if ( m === 'POST' ) {
335+ const capitalizedAction = 'create' + this . capitalize ( actionBase ) ; // createSpace
336+ if ( typeof hubService [ capitalizedAction ] === 'function' ) {
337+ const result = await hubService [ capitalizedAction ] ( body , { request : context . request } ) ;
338+ return { handled : true , response : this . success ( result ) } ;
339+ }
340+ }
341+ } else if ( parts . length === 2 ) {
342+ // Item Operations
343+ if ( m === 'GET' ) {
344+ const capitalizedAction = 'get' + this . capitalize ( actionBase ) ; // getSpace
345+ if ( typeof hubService [ capitalizedAction ] === 'function' ) {
346+ const result = await hubService [ capitalizedAction ] ( id , { request : context . request } ) ;
347+ return { handled : true , response : this . success ( result ) } ;
348+ }
349+ }
350+ if ( m === 'PATCH' || m === 'PUT' ) {
351+ const capitalizedAction = 'update' + this . capitalize ( actionBase ) ; // updateSpace
352+ if ( typeof hubService [ capitalizedAction ] === 'function' ) {
353+ const result = await hubService [ capitalizedAction ] ( id , body , { request : context . request } ) ;
354+ return { handled : true , response : this . success ( result ) } ;
355+ }
356+ }
357+ if ( m === 'DELETE' ) {
358+ const capitalizedAction = 'delete' + this . capitalize ( actionBase ) ; // deleteSpace
359+ if ( typeof hubService [ capitalizedAction ] === 'function' ) {
360+ const result = await hubService [ capitalizedAction ] ( id , { request : context . request } ) ;
361+ return { handled : true , response : this . success ( result ) } ;
362+ }
363+ }
364+ }
365+ } catch ( e : any ) {
366+ return { handled : true , response : this . error ( e . message , 500 ) } ;
367+ }
368+ }
302369
303370 return { handled : false } ;
304371 }
@@ -375,4 +442,131 @@ export class HttpDispatcher {
375442 private capitalize ( s : string ) {
376443 return s . charAt ( 0 ) . toUpperCase ( ) + s . slice ( 1 ) ;
377444 }
445+
446+ /**
447+ * Main Dispatcher Entry Point
448+ * Routes the request to the appropriate handler based on path and precedence
449+ */
450+ async dispatch ( method : string , path : string , body : any , query : any , context : HttpProtocolContext ) : Promise < HttpDispatcherResult > {
451+ const cleanPath = path . replace ( / \/ $ / , '' ) ; // Remove trailing slash if present, but strict on clean paths
452+
453+ // 1. System Protocols (Prefix-based)
454+ if ( cleanPath . startsWith ( '/auth' ) ) {
455+ return this . handleAuth ( cleanPath . substring ( 5 ) , method , body , context ) ;
456+ }
457+
458+ if ( cleanPath . startsWith ( '/metadata' ) ) {
459+ return this . handleMetadata ( cleanPath . substring ( 9 ) , context ) ;
460+ }
461+
462+ if ( cleanPath . startsWith ( '/data' ) ) {
463+ return this . handleData ( cleanPath . substring ( 5 ) , method , body , query , context ) ;
464+ }
465+
466+ if ( cleanPath . startsWith ( '/graphql' ) ) {
467+ if ( method === 'POST' ) return this . handleGraphQL ( body , context ) ;
468+ // GraphQL usually GET for Playground is handled by middleware but we can return 405 or handle it
469+ }
470+
471+ if ( cleanPath . startsWith ( '/storage' ) ) {
472+ return this . handleStorage ( cleanPath . substring ( 8 ) , method , body , context ) ; // body here is file/stream for upload
473+ }
474+
475+ if ( cleanPath . startsWith ( '/analytics' ) ) {
476+ return this . handleAnalytics ( cleanPath . substring ( 10 ) , method , body , context ) ;
477+ }
478+
479+ if ( cleanPath . startsWith ( '/hub' ) ) {
480+ return this . handleHub ( cleanPath . substring ( 4 ) , method , body , query , context ) ;
481+ }
482+
483+ // OpenAPI Specification
484+ if ( cleanPath === '/openapi.json' && method === 'GET' ) {
485+ const broker = this . ensureBroker ( ) ;
486+ try {
487+ const result = await broker . call ( 'metadata.generateOpenApi' , { } , { request : context . request } ) ;
488+ return { handled : true , response : this . success ( result ) } ;
489+ } catch ( e ) {
490+ // If not implemented, fall through or return 404
491+ }
492+ }
493+
494+ // 2. Custom API Endpoints (Registry lookup)
495+ // Check if there is a custom endpoint defined for this path
496+ const result = await this . handleApiEndpoint ( cleanPath , method , body , query , context ) ;
497+ if ( result . handled ) return result ;
498+
499+ // 3. Fallback (404)
500+ return { handled : false } ;
501+ }
502+
503+ /**
504+ * Handles Custom API Endpoints defined in metadata
505+ */
506+ async handleApiEndpoint ( path : string , method : string , body : any , query : any , context : HttpProtocolContext ) : Promise < HttpDispatcherResult > {
507+ const broker = this . ensureBroker ( ) ;
508+ try {
509+ // Attempt to find a matching endpoint in the registry
510+ // This assumes a 'metadata.matchEndpoint' action exists in the kernel/registry
511+ // path should include initial slash e.g. /api/v1/customers
512+ const endpoint = await broker . call ( 'metadata.matchEndpoint' , { path, method } ) ;
513+
514+ if ( endpoint ) {
515+ // Execute the endpoint target logic
516+ if ( endpoint . type === 'flow' ) {
517+ const result = await broker . call ( 'automation.runFlow' , {
518+ flowId : endpoint . target ,
519+ inputs : { ...query , ...body , _request : context . request }
520+ } ) ;
521+ return { handled : true , response : this . success ( result ) } ;
522+ }
523+
524+ if ( endpoint . type === 'script' ) {
525+ const result = await broker . call ( 'automation.runScript' , {
526+ scriptName : endpoint . target ,
527+ context : { ...query , ...body , request : context . request }
528+ } , { request : context . request } ) ;
529+ return { handled : true , response : this . success ( result ) } ;
530+ }
531+
532+ if ( endpoint . type === 'object_operation' ) {
533+ // e.g. Proxy to an object action
534+ if ( endpoint . objectParams ) {
535+ const { object, operation } = endpoint . objectParams ;
536+ // Map standard CRUD operations
537+ if ( operation === 'find' ) {
538+ const result = await broker . call ( 'data.query' , { object, filters : query } , { request : context . request } ) ;
539+ return { handled : true , response : this . success ( result . data , { count : result . count } ) } ;
540+ }
541+ if ( operation === 'get' && query . id ) {
542+ const result = await broker . call ( 'data.get' , { object, id : query . id } , { request : context . request } ) ;
543+ return { handled : true , response : this . success ( result ) } ;
544+ }
545+ if ( operation === 'create' ) {
546+ const result = await broker . call ( 'data.create' , { object, data : body } , { request : context . request } ) ;
547+ return { handled : true , response : this . success ( result ) } ;
548+ }
549+ }
550+ }
551+
552+ if ( endpoint . type === 'proxy' ) {
553+ // Simple proxy implementation (requires a network call, which usually is done by a service but here we can stub return)
554+ // In real implementation this might fetch(endpoint.target)
555+ // For now, return target info
556+ return {
557+ handled : true ,
558+ response : {
559+ status : 200 ,
560+ body : { proxy : true , target : endpoint . target , note : 'Proxy execution requires http-client service' }
561+ }
562+ } ;
563+ }
564+ }
565+ } catch ( e ) {
566+ // If matchEndpoint fails (e.g. not found), we just return not handled
567+ // so we can fallback to 404 or other handlers
568+ }
569+
570+ return { handled : false } ;
571+ }
378572}
0 commit comments