Skip to content

Commit af6bc74

Browse files
committed
feat(http-dispatcher): enhance analytics and hub request handling with dynamic routing and custom API endpoint support
1 parent a9f11ca commit af6bc74

1 file changed

Lines changed: 196 additions & 2 deletions

File tree

packages/runtime/src/http-dispatcher.ts

Lines changed: 196 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)