Skip to content

Commit a3a133f

Browse files
committed
feat(hono): add analytics, hub, and automation endpoints with request handling
1 parent e4fd290 commit a3a133f

5 files changed

Lines changed: 281 additions & 1 deletion

File tree

packages/adapters/hono/src/index.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,62 @@ export function createHonoApp(options: ObjectStackHonoOptions) {
102102
}
103103
});
104104

105-
// --- 5. Storage Endpoints ---
105+
// --- 5. Analytics Endpoints ---
106+
app.all(`${prefix}/analytics*`, async (c) => {
107+
try {
108+
const path = c.req.path.substring(c.req.path.indexOf('/analytics') + 10);
109+
const method = c.req.method;
110+
111+
let body = {};
112+
if (method === 'POST') {
113+
body = await c.req.json().catch(() => ({}));
114+
}
115+
116+
const result = await dispatcher.handleAnalytics(path, method, body, { request: c.req.raw });
117+
return normalizeResponse(c, result);
118+
} catch (err: any) {
119+
return c.json({ success: false, error: { message: err.message, code: err.statusCode || 500 } }, err.statusCode || 500);
120+
}
121+
});
122+
123+
// --- 6. Hub Endpoints ---
124+
app.all(`${prefix}/hub*`, async (c) => {
125+
try {
126+
const path = c.req.path.substring(c.req.path.indexOf('/hub') + 4);
127+
const method = c.req.method;
128+
129+
let body = {};
130+
if (method === 'POST' || method === 'PATCH' || method === 'PUT') {
131+
body = await c.req.json().catch(() => ({}));
132+
}
133+
const query = c.req.query();
134+
135+
const result = await dispatcher.handleHub(path, method, body, query, { request: c.req.raw });
136+
return normalizeResponse(c, result);
137+
} catch (err: any) {
138+
return c.json({ success: false, error: { message: err.message, code: err.statusCode || 500 } }, err.statusCode || 500);
139+
}
140+
});
141+
142+
// --- 7. Automation Endpoints ---
143+
app.all(`${prefix}/automation*`, async (c) => {
144+
try {
145+
const path = c.req.path.substring(c.req.path.indexOf('/automation') + 11);
146+
const method = c.req.method;
147+
148+
let body = {};
149+
if (method === 'POST') {
150+
body = await c.req.json().catch(() => ({}));
151+
}
152+
153+
const result = await dispatcher.handleAutomation(path, method, body, { request: c.req.raw });
154+
return normalizeResponse(c, result);
155+
} catch (err: any) {
156+
return c.json({ success: false, error: { message: err.message, code: err.statusCode || 500 } }, err.statusCode || 500);
157+
}
158+
});
159+
160+
// --- 8. Storage Endpoints ---
106161
app.all(`${prefix}/storage*`, async (c) => {
107162
try {
108163
const path = c.req.path.substring(c.req.path.indexOf('/storage') + 8);

packages/plugins/plugin-hono-server/src/hono-plugin.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,72 @@ export class HonoServerPlugin implements Plugin {
507507
priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
508508
});
509509

510+
// Analytics Endpoints
511+
endpoints.push(
512+
{
513+
id: 'analytics_query',
514+
method: 'POST',
515+
path: `${apiPath}/analytics/query`,
516+
summary: 'Analytics Query',
517+
description: 'Execute analytics query',
518+
priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
519+
},
520+
{
521+
id: 'get_analytics_meta',
522+
method: 'GET',
523+
path: `${apiPath}/analytics/meta`,
524+
summary: 'Analytics Metadata',
525+
description: 'Get analytics cubes definitions',
526+
priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
527+
}
528+
);
529+
530+
// Automation Endpoints
531+
endpoints.push(
532+
{
533+
id: 'automation_trigger',
534+
method: 'POST',
535+
path: `${apiPath}/automation/trigger/:trigger`,
536+
summary: 'Trigger Automation',
537+
description: 'Trigger a named automation',
538+
parameters: [{
539+
name: 'trigger',
540+
in: 'path',
541+
required: true,
542+
schema: { type: 'string' }
543+
}],
544+
priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
545+
}
546+
);
547+
548+
// Hub Endpoints
549+
endpoints.push(
550+
{
551+
id: 'hub_list_spaces',
552+
method: 'GET',
553+
path: `${apiPath}/hub/spaces`,
554+
summary: 'List Spaces',
555+
description: 'List all Hub spaces',
556+
priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
557+
},
558+
{
559+
id: 'hub_create_space',
560+
method: 'POST',
561+
path: `${apiPath}/hub/spaces`,
562+
summary: 'Create Space',
563+
description: 'Create a new Hub space',
564+
priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
565+
},
566+
{
567+
id: 'hub_install_plugin',
568+
method: 'POST',
569+
path: `${apiPath}/hub/plugins/install`,
570+
summary: 'Install Plugin',
571+
description: 'Install a plugin from marketplace',
572+
priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
573+
}
574+
);
575+
510576
// Register the API in the registry
511577
const apiEntry: ApiRegistryEntryInput = {
512578
id: 'objectstack_core_api',
@@ -798,6 +864,65 @@ export class HonoServerPlugin implements Plugin {
798864
ctx.logger.warn('UI view not found', { object: req.params.object });
799865
res.status(404).json({ error: e.message });
800866
}
867+
},
868+
// Analytics
869+
'analytics_query': async (req: any, res: any) => {
870+
ctx.logger.info('Analytics query request');
871+
try {
872+
const result = await p.analyticsQuery(req.body);
873+
res.json(result);
874+
} catch (e: any) {
875+
ctx.logger.error('Analytics query failed', e);
876+
res.status(400).json({ error: e.message });
877+
}
878+
},
879+
'get_analytics_meta': async (req: any, res: any) => {
880+
ctx.logger.info('Analytics meta request');
881+
try {
882+
const result = await p.getAnalyticsMeta({});
883+
res.json(result);
884+
} catch (e: any) {
885+
ctx.logger.error('Analytics meta failed', e);
886+
res.status(500).json({ error: e.message });
887+
}
888+
},
889+
// Automation
890+
'automation_trigger': async (req: any, res: any) => {
891+
const trigger = req.params.trigger;
892+
ctx.logger.info('Automation trigger request', { trigger });
893+
try {
894+
const result = await p.triggerAutomation({ trigger, payload: req.body });
895+
res.json(result);
896+
} catch (e: any) {
897+
ctx.logger.error('Automation trigger failed', e, { trigger });
898+
res.status(500).json({ error: e.message });
899+
}
900+
},
901+
// Hub
902+
'hub_list_spaces': async (req: any, res: any) => {
903+
try {
904+
const result = await p.listSpaces({ ...req.query });
905+
res.json(result);
906+
} catch (e: any) {
907+
res.status(500).json({ error: e.message });
908+
}
909+
},
910+
'hub_create_space': async (req: any, res: any) => {
911+
try {
912+
const result = await p.createSpace(req.body);
913+
res.status(201).json(result);
914+
} catch (e: any) {
915+
res.status(500).json({ error: e.message });
916+
}
917+
},
918+
'hub_install_plugin': async (req: any, res: any) => {
919+
const spaceId = req.params.space_id;
920+
try {
921+
const result = await p.installPlugin({ spaceId, ...req.body });
922+
res.json(result);
923+
} catch (e: any) {
924+
res.status(500).json({ error: e.message });
925+
}
801926
}
802927
};
803928

packages/runtime/src/http-dispatcher.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,29 @@ export class HttpDispatcher {
425425
return { handled: false };
426426
}
427427

428+
/**
429+
* Handles Automation requests
430+
* path: sub-path after /automation/
431+
*/
432+
async handleAutomation(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
433+
const automationService = this.getService(CoreServiceName.enum.automation);
434+
if (!automationService) return { handled: false };
435+
436+
const m = method.toUpperCase();
437+
const parts = path.replace(/^\/+/, '').split('/');
438+
439+
// POST /automation/trigger/:name
440+
if (parts[0] === 'trigger' && parts[1] && m === 'POST') {
441+
const triggerName = parts[1];
442+
if (typeof automationService.trigger === 'function') {
443+
const result = await automationService.trigger(triggerName, body, { request: context.request });
444+
return { handled: true, response: this.success(result) };
445+
}
446+
}
447+
448+
return { handled: false };
449+
}
450+
428451
private getServicesMap(): Record<string, any> {
429452
if (this.kernel.services instanceof Map) {
430453
return Object.fromEntries(this.kernel.services);
@@ -473,6 +496,10 @@ export class HttpDispatcher {
473496
return this.handleStorage(cleanPath.substring(8), method, body, context); // body here is file/stream for upload
474497
}
475498

499+
if (cleanPath.startsWith('/automation')) {
500+
return this.handleAutomation(cleanPath.substring(11), method, body, context);
501+
}
502+
476503
if (cleanPath.startsWith('/analytics')) {
477504
return this.handleAnalytics(cleanPath.substring(10), method, body, context);
478505
}

packages/spec/src/api/hub.zod.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -930,3 +930,20 @@ export const HubAPIContract = {
930930
},
931931
},
932932
} as const;
933+
934+
/**
935+
* Install Plugin Request
936+
*/
937+
export const InstallPluginRequestSchema = z.object({
938+
spaceId: z.string().describe('Target Space ID'),
939+
pluginId: z.string().describe('Plugin Package ID'),
940+
version: z.string().optional().describe('Version requirement'),
941+
config: z.record(z.any()).optional().describe('Plugin configuration'),
942+
});
943+
944+
/**
945+
* Install Plugin Response
946+
*/
947+
export const InstallPluginResponseSchema = BaseResponseSchema.extend({
948+
data: z.any() // Returns installation status or installed instance
949+
});

packages/spec/src/api/protocol.zod.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,25 @@ import {
1010
GetAnalyticsMetaRequestSchema,
1111
AnalyticsMetadataResponseSchema
1212
} from './analytics.zod';
13+
import {
14+
ListSpacesRequestSchema,
15+
SpaceResponseSchema,
16+
CreateSpaceRequestSchema,
17+
UpdateSpaceRequestSchema,
18+
InstallPluginRequestSchema,
19+
InstallPluginResponseSchema
20+
} from './hub.zod';
21+
22+
export const AutomationTriggerRequestSchema = z.object({
23+
trigger: z.string(),
24+
payload: z.record(z.any())
25+
});
26+
27+
export const AutomationTriggerResponseSchema = z.object({
28+
success: z.boolean(),
29+
jobId: z.string().optional(),
30+
result: z.any().optional()
31+
});
1332

1433
/**
1534
* ObjectStack Protocol - Zod Schema Definitions
@@ -387,6 +406,28 @@ export const ObjectStackProtocolSchema = z.object({
387406
output: z.promise(AnalyticsMetadataResponseSchema)
388407
}).describe('Get analytics metadata (cubes)'),
389408

409+
// Automation Operations
410+
triggerAutomation: z.function({
411+
input: z.tuple([AutomationTriggerRequestSchema]),
412+
output: z.promise(AutomationTriggerResponseSchema)
413+
}).describe('Trigger an automation flow or script'),
414+
415+
// Hub Operations
416+
listSpaces: z.function({
417+
input: z.tuple([ListSpacesRequestSchema]),
418+
output: z.promise(z.any()) // TODO: Use ListSpacesResponseSchema when available/exported
419+
}).describe('List Hub Spaces'),
420+
421+
createSpace: z.function({
422+
input: z.tuple([CreateSpaceRequestSchema]),
423+
output: z.promise(SpaceResponseSchema)
424+
}).describe('Create Hub Space'),
425+
426+
installPlugin: z.function({
427+
input: z.tuple([InstallPluginRequestSchema]),
428+
output: z.promise(InstallPluginResponseSchema)
429+
}).describe('Install Plugin into Space'),
430+
390431
// Data Operations
391432
findData: z.function({
392433
input: z.tuple([FindDataRequestSchema]),
@@ -457,6 +498,15 @@ export type AnalyticsResultResponse = z.infer<typeof AnalyticsResultResponseSche
457498
export type GetAnalyticsMetaRequest = z.infer<typeof GetAnalyticsMetaRequestSchema>;
458499
export type GetAnalyticsMetaResponse = z.infer<typeof AnalyticsMetadataResponseSchema>;
459500

501+
export type AutomationTriggerRequest = z.infer<typeof AutomationTriggerRequestSchema>;
502+
export type AutomationTriggerResponse = z.infer<typeof AutomationTriggerResponseSchema>;
503+
504+
export type ListSpacesRequest = z.infer<typeof ListSpacesRequestSchema>;
505+
export type CreateSpaceRequest = z.infer<typeof CreateSpaceRequestSchema>;
506+
export type SpaceResponse = z.infer<typeof SpaceResponseSchema>;
507+
export type InstallPluginRequest = z.infer<typeof InstallPluginRequestSchema>;
508+
export type InstallPluginResponse = z.infer<typeof InstallPluginResponseSchema>;
509+
460510
export type FindDataRequest = z.input<typeof FindDataRequestSchema>;
461511
export type FindDataResponse = z.infer<typeof FindDataResponseSchema>;
462512
export type GetDataRequest = z.input<typeof GetDataRequestSchema>;
@@ -495,6 +545,12 @@ export interface IObjectStackProtocolLegacy {
495545
analyticsQuery(request: AnalyticsQueryRequest): Promise<AnalyticsResultResponse>;
496546
getAnalyticsMeta(request: GetAnalyticsMetaRequest): Promise<GetAnalyticsMetaResponse>;
497547

548+
triggerAutomation(request: AutomationTriggerRequest): Promise<AutomationTriggerResponse>;
549+
550+
listSpaces(request: ListSpacesRequest): Promise<any>;
551+
createSpace(request: CreateSpaceRequest): Promise<SpaceResponse>;
552+
installPlugin(request: InstallPluginRequest): Promise<InstallPluginResponse>;
553+
498554
findData(request: FindDataRequest): Promise<FindDataResponse>;
499555
getData(request: GetDataRequest): Promise<GetDataResponse>;
500556
createData(request: CreateDataRequest): Promise<CreateDataResponse>;

0 commit comments

Comments
 (0)