Skip to content

Commit 080c48e

Browse files
Merge pull request #487 from objectstack-ai/copilot/refactor-plugin-hono-server
2 parents f32f9bd + ee68632 commit 080c48e

3 files changed

Lines changed: 1189 additions & 162 deletions

File tree

packages/plugins/plugin-hono-server/README.md

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,130 @@ interface HonoPluginOptions {
7777
* Path to static files directory (optional)
7878
*/
7979
staticRoot?: string;
80+
81+
/**
82+
* REST server configuration
83+
* Controls automatic endpoint generation and API behavior
84+
*/
85+
restConfig?: RestServerConfig;
86+
87+
/**
88+
* Whether to register standard ObjectStack CRUD endpoints
89+
* @default true
90+
*/
91+
registerStandardEndpoints?: boolean;
92+
93+
/**
94+
* Whether to load endpoints from API Registry
95+
* When enabled, routes are loaded dynamically from the API Registry
96+
* When disabled, uses legacy static route registration
97+
* @default true
98+
*/
99+
useApiRegistry?: boolean;
80100
}
81101
```
82102

103+
### Using API Registry (New in v0.9.0)
104+
105+
The plugin now integrates with the ObjectStack API Registry for centralized endpoint management:
106+
107+
```typescript
108+
import { createApiRegistryPlugin } from '@objectstack/core';
109+
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
110+
111+
const kernel = new ObjectKernel();
112+
113+
// 1. Register API Registry Plugin first
114+
kernel.use(createApiRegistryPlugin({
115+
conflictResolution: 'priority' // Handle route conflicts by priority
116+
}));
117+
118+
// 2. Register Hono Server Plugin
119+
kernel.use(new HonoServerPlugin({
120+
port: 3000,
121+
useApiRegistry: true,
122+
registerStandardEndpoints: true,
123+
restConfig: {
124+
api: {
125+
version: 'v1',
126+
basePath: '/api',
127+
enableCrud: true,
128+
enableMetadata: true,
129+
enableBatch: true
130+
}
131+
}
132+
}));
133+
134+
await kernel.bootstrap();
135+
```
136+
137+
**Benefits of API Registry Integration:**
138+
- 📋 Centralized endpoint registration and discovery
139+
- 🔀 Priority-based route conflict resolution
140+
- 🧩 Support for plugin-registered custom endpoints
141+
- ⚙️ Configurable endpoint generation via `RestServerConfig`
142+
- 🔍 API introspection and documentation generation
143+
144+
### Configuring REST Server Behavior
145+
146+
Use `restConfig` to control which endpoints are automatically generated:
147+
148+
```typescript
149+
new HonoServerPlugin({
150+
restConfig: {
151+
api: {
152+
version: 'v2',
153+
basePath: '/api',
154+
enableCrud: true,
155+
enableMetadata: true,
156+
enableBatch: true,
157+
enableDiscovery: true
158+
},
159+
crud: {
160+
dataPrefix: '/data',
161+
operations: {
162+
create: true,
163+
read: true,
164+
update: true,
165+
delete: true,
166+
list: true
167+
}
168+
},
169+
metadata: {
170+
prefix: '/meta',
171+
enableCache: true,
172+
cacheTtl: 3600
173+
},
174+
batch: {
175+
maxBatchSize: 200,
176+
operations: {
177+
createMany: true,
178+
updateMany: true,
179+
deleteMany: true,
180+
upsertMany: true
181+
}
182+
}
183+
}
184+
})
185+
```
186+
187+
### Legacy Mode (Without API Registry)
188+
189+
If the API Registry plugin is not registered, the server automatically falls back to legacy mode:
190+
191+
```typescript
192+
// No API Registry needed for simple setups
193+
const kernel = new ObjectKernel();
194+
195+
kernel.use(new HonoServerPlugin({
196+
port: 3000,
197+
useApiRegistry: false // Explicitly disable API Registry
198+
}));
199+
200+
await kernel.bootstrap();
201+
// All standard routes registered statically
202+
```
203+
83204
## API Endpoints
84205

85206
The plugin automatically exposes the following ObjectStack REST API endpoints:
@@ -164,6 +285,60 @@ export class MyPlugin implements Plugin {
164285
}
165286
```
166287

288+
### Registering Custom Endpoints via API Registry
289+
290+
Plugins can register their own endpoints through the API Registry:
291+
292+
```typescript
293+
export class MyApiPlugin implements Plugin {
294+
name = 'my-api-plugin';
295+
version = '1.0.0';
296+
297+
async init(ctx: PluginContext) {
298+
const apiRegistry = ctx.getService<ApiRegistry>('api-registry');
299+
300+
apiRegistry.registerApi({
301+
id: 'my_custom_api',
302+
name: 'My Custom API',
303+
type: 'rest',
304+
version: 'v1',
305+
basePath: '/api/v1/custom',
306+
endpoints: [
307+
{
308+
id: 'get_custom_data',
309+
method: 'GET',
310+
path: '/api/v1/custom/data',
311+
summary: 'Get custom data',
312+
priority: 500, // Lower than core endpoints (950)
313+
responses: [{
314+
statusCode: 200,
315+
description: 'Custom data retrieved'
316+
}]
317+
}
318+
],
319+
metadata: {
320+
pluginSource: 'my-api-plugin',
321+
status: 'active',
322+
tags: ['custom']
323+
}
324+
});
325+
326+
ctx.logger.info('Custom API endpoints registered');
327+
}
328+
329+
async start(ctx: PluginContext) {
330+
// Bind the actual handler implementation
331+
const httpServer = ctx.getService<IHttpServer>('http-server');
332+
333+
httpServer.get('/api/v1/custom/data', async (req, res) => {
334+
res.json({ data: 'my custom data' });
335+
});
336+
}
337+
}
338+
```
339+
340+
**Note:** The Hono Server Plugin loads routes from the API Registry sorted by priority (highest first), ensuring core endpoints take precedence over plugin endpoints.
341+
167342
### Extending with Middleware
168343

169344
The plugin provides extension points for adding custom middleware:

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

Lines changed: 137 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
22
import { HonoServerPlugin } from './hono-plugin';
3-
import { PluginContext } from '@objectstack/core';
3+
import { PluginContext, ApiRegistry } from '@objectstack/core';
44

55
describe('HonoServerPlugin', () => {
66
let context: any;
77
let logger: any;
88
let protocol: any;
9+
let apiRegistry: any;
910

1011
beforeEach(() => {
1112
logger = {
@@ -16,14 +17,38 @@ describe('HonoServerPlugin', () => {
1617
};
1718

1819
protocol = {
19-
findData: vi.fn(),
20-
createData: vi.fn()
20+
getDiscovery: vi.fn().mockResolvedValue({ version: 'v1', apiName: 'ObjectStack' }),
21+
getMetaTypes: vi.fn().mockResolvedValue({ types: ['object', 'plugin'] }),
22+
getMetaItems: vi.fn().mockResolvedValue({ type: 'object', items: [] }),
23+
findData: vi.fn().mockResolvedValue({ object: 'test', records: [] }),
24+
getData: vi.fn().mockResolvedValue({ object: 'test', id: '1', record: {} }),
25+
createData: vi.fn().mockResolvedValue({ object: 'test', id: '1', record: {} }),
26+
updateData: vi.fn().mockResolvedValue({ object: 'test', id: '1', record: {} }),
27+
deleteData: vi.fn().mockResolvedValue({ object: 'test', id: '1', success: true }),
28+
batchData: vi.fn().mockResolvedValue({ total: 0, succeeded: 0, failed: 0 }),
29+
createManyData: vi.fn().mockResolvedValue({ object: 'test', records: [], count: 0 }),
30+
updateManyData: vi.fn().mockResolvedValue({ total: 0, succeeded: 0, failed: 0 }),
31+
deleteManyData: vi.fn().mockResolvedValue({ total: 0, succeeded: 0, failed: 0 }),
32+
getMetaItemCached: vi.fn().mockResolvedValue({ data: {}, notModified: false }),
33+
getUiView: vi.fn().mockResolvedValue({ object: 'test', type: 'list' })
34+
};
35+
36+
apiRegistry = {
37+
registerApi: vi.fn(),
38+
getRegistry: vi.fn().mockReturnValue({
39+
version: '1.0.0',
40+
conflictResolution: 'error',
41+
apis: [],
42+
totalApis: 0,
43+
totalEndpoints: 0
44+
})
2145
};
2246

2347
context = {
2448
logger,
2549
getService: vi.fn((service) => {
2650
if (service === 'protocol') return protocol;
51+
if (service === 'api-registry') throw new Error('Not found');
2752
return null;
2853
}),
2954
registerService: vi.fn(),
@@ -47,11 +72,119 @@ describe('HonoServerPlugin', () => {
4772
expect(context.hook).toHaveBeenCalledWith('kernel:ready', expect.any(Function));
4873
});
4974

50-
it('should register CRUD routes', async () => {
75+
it('should register CRUD routes in legacy mode when API Registry not available', async () => {
5176
const plugin = new HonoServerPlugin();
5277
await plugin.init(context as PluginContext);
5378
await plugin.start(context as PluginContext);
5479

5580
expect(context.getService).toHaveBeenCalledWith('protocol');
81+
expect(context.getService).toHaveBeenCalledWith('api-registry');
82+
expect(logger.debug).toHaveBeenCalledWith('API Registry not found, using legacy route registration');
83+
});
84+
85+
it('should use API Registry when available', async () => {
86+
context.getService = vi.fn((service) => {
87+
if (service === 'protocol') return protocol;
88+
if (service === 'api-registry') return apiRegistry;
89+
return null;
90+
});
91+
92+
const plugin = new HonoServerPlugin();
93+
await plugin.init(context as PluginContext);
94+
await plugin.start(context as PluginContext);
95+
96+
expect(context.getService).toHaveBeenCalledWith('api-registry');
97+
expect(apiRegistry.registerApi).toHaveBeenCalled();
98+
});
99+
100+
it('should register standard endpoints to API Registry', async () => {
101+
context.getService = vi.fn((service) => {
102+
if (service === 'protocol') return protocol;
103+
if (service === 'api-registry') return apiRegistry;
104+
return null;
105+
});
106+
107+
const plugin = new HonoServerPlugin();
108+
await plugin.init(context as PluginContext);
109+
await plugin.start(context as PluginContext);
110+
111+
expect(apiRegistry.registerApi).toHaveBeenCalledWith(
112+
expect.objectContaining({
113+
id: 'objectstack_core_api',
114+
name: 'ObjectStack Core API',
115+
type: 'rest',
116+
version: 'v1'
117+
})
118+
);
119+
});
120+
121+
it('should skip standard endpoint registration when disabled', async () => {
122+
context.getService = vi.fn((service) => {
123+
if (service === 'protocol') return protocol;
124+
if (service === 'api-registry') return apiRegistry;
125+
return null;
126+
});
127+
128+
const plugin = new HonoServerPlugin({ registerStandardEndpoints: false });
129+
await plugin.init(context as PluginContext);
130+
await plugin.start(context as PluginContext);
131+
132+
expect(apiRegistry.registerApi).not.toHaveBeenCalled();
133+
});
134+
135+
it('should use legacy routes when useApiRegistry is disabled', async () => {
136+
context.getService = vi.fn((service) => {
137+
if (service === 'protocol') return protocol;
138+
if (service === 'api-registry') return apiRegistry;
139+
return null;
140+
});
141+
142+
const plugin = new HonoServerPlugin({ useApiRegistry: false });
143+
await plugin.init(context as PluginContext);
144+
await plugin.start(context as PluginContext);
145+
146+
expect(apiRegistry.getRegistry).not.toHaveBeenCalled();
147+
expect(logger.debug).toHaveBeenCalledWith('Using legacy route registration');
148+
});
149+
150+
it('should respect REST server configuration', async () => {
151+
context.getService = vi.fn((service) => {
152+
if (service === 'protocol') return protocol;
153+
if (service === 'api-registry') return apiRegistry;
154+
return null;
155+
});
156+
157+
const plugin = new HonoServerPlugin({
158+
restConfig: {
159+
api: {
160+
version: 'v2',
161+
basePath: '/custom',
162+
enableCrud: true,
163+
enableMetadata: true,
164+
enableBatch: true
165+
}
166+
}
167+
});
168+
169+
await plugin.init(context as PluginContext);
170+
await plugin.start(context as PluginContext);
171+
172+
expect(apiRegistry.registerApi).toHaveBeenCalledWith(
173+
expect.objectContaining({
174+
version: 'v2'
175+
})
176+
);
177+
});
178+
179+
it('should handle protocol service not found gracefully', async () => {
180+
context.getService = vi.fn(() => {
181+
throw new Error('Service not found');
182+
});
183+
184+
const plugin = new HonoServerPlugin();
185+
await plugin.init(context as PluginContext);
186+
await plugin.start(context as PluginContext);
187+
188+
expect(logger.warn).toHaveBeenCalledWith('Protocol service not found, skipping protocol routes');
56189
});
57190
});

0 commit comments

Comments
 (0)