Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions packages/plugins/plugin-hono-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,130 @@ interface HonoPluginOptions {
* Path to static files directory (optional)
*/
staticRoot?: string;

/**
* REST server configuration
* Controls automatic endpoint generation and API behavior
*/
restConfig?: RestServerConfig;

/**
* Whether to register standard ObjectStack CRUD endpoints
* @default true
*/
registerStandardEndpoints?: boolean;

/**
* Whether to load endpoints from API Registry
* When enabled, routes are loaded dynamically from the API Registry
* When disabled, uses legacy static route registration
* @default true
*/
useApiRegistry?: boolean;
}
```

### Using API Registry (New in v0.9.0)

The plugin now integrates with the ObjectStack API Registry for centralized endpoint management:

```typescript
import { createApiRegistryPlugin } from '@objectstack/core';
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';

const kernel = new ObjectKernel();

// 1. Register API Registry Plugin first
kernel.use(createApiRegistryPlugin({
conflictResolution: 'priority' // Handle route conflicts by priority
}));

// 2. Register Hono Server Plugin
kernel.use(new HonoServerPlugin({
port: 3000,
useApiRegistry: true,
registerStandardEndpoints: true,
restConfig: {
api: {
version: 'v1',
basePath: '/api',
enableCrud: true,
enableMetadata: true,
enableBatch: true
}
}
}));

await kernel.bootstrap();
```

**Benefits of API Registry Integration:**
- ๐Ÿ“‹ Centralized endpoint registration and discovery
- ๐Ÿ”€ Priority-based route conflict resolution
- ๐Ÿงฉ Support for plugin-registered custom endpoints
- โš™๏ธ Configurable endpoint generation via `RestServerConfig`
- ๐Ÿ” API introspection and documentation generation

### Configuring REST Server Behavior

Use `restConfig` to control which endpoints are automatically generated:

```typescript
new HonoServerPlugin({
restConfig: {
api: {
version: 'v2',
basePath: '/api',
enableCrud: true,
enableMetadata: true,
enableBatch: true,
enableDiscovery: true
},
crud: {
dataPrefix: '/data',
operations: {
create: true,
read: true,
update: true,
delete: true,
list: true
}
},
metadata: {
prefix: '/meta',
enableCache: true,
cacheTtl: 3600
},
batch: {
maxBatchSize: 200,
operations: {
createMany: true,
updateMany: true,
deleteMany: true,
upsertMany: true
}
}
}
})
```

### Legacy Mode (Without API Registry)

If the API Registry plugin is not registered, the server automatically falls back to legacy mode:

```typescript
// No API Registry needed for simple setups
const kernel = new ObjectKernel();

kernel.use(new HonoServerPlugin({
port: 3000,
useApiRegistry: false // Explicitly disable API Registry
}));

await kernel.bootstrap();
// All standard routes registered statically
```

## API Endpoints

The plugin automatically exposes the following ObjectStack REST API endpoints:
Expand Down Expand Up @@ -164,6 +285,60 @@ export class MyPlugin implements Plugin {
}
```

### Registering Custom Endpoints via API Registry

Plugins can register their own endpoints through the API Registry:

```typescript
export class MyApiPlugin implements Plugin {
name = 'my-api-plugin';
version = '1.0.0';

async init(ctx: PluginContext) {
const apiRegistry = ctx.getService<ApiRegistry>('api-registry');

apiRegistry.registerApi({
id: 'my_custom_api',
name: 'My Custom API',
type: 'rest',
version: 'v1',
basePath: '/api/v1/custom',
endpoints: [
{
id: 'get_custom_data',
method: 'GET',
path: '/api/v1/custom/data',
summary: 'Get custom data',
priority: 500, // Lower than core endpoints (950)
responses: [{
statusCode: 200,
description: 'Custom data retrieved'
}]
}
],
metadata: {
pluginSource: 'my-api-plugin',
status: 'active',
tags: ['custom']
}
});

ctx.logger.info('Custom API endpoints registered');
}

async start(ctx: PluginContext) {
// Bind the actual handler implementation
const httpServer = ctx.getService<IHttpServer>('http-server');

httpServer.get('/api/v1/custom/data', async (req, res) => {
res.json({ data: 'my custom data' });
});
}
}
```

**Note:** The Hono Server Plugin loads routes from the API Registry sorted by priority (highest first), ensuring core endpoints take precedence over plugin endpoints.
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The note states "The Hono Server Plugin loads routes from the API Registry sorted by priority" but this is misleading. Based on the implementation in bindEndpoint() (hono-plugin.ts:586-589), the plugin only successfully binds routes for which it has handlers defined in createHandlerForEndpoint(). For custom plugin endpoints registered in the API Registry, bindEndpoint() will log a warning "No handler found for endpoint" and skip binding them.

Consider clarifying this note to explain that:

  1. HonoServerPlugin only auto-binds standard ObjectStack endpoints from the registry
  2. Custom plugins must bind their own handlers separately via the http-server service (as shown in the example above at lines 329-336)
  3. The API Registry serves primarily for endpoint discovery and documentation, not automatic handler binding for plugin endpoints

This would help developers understand that registering an endpoint in the API Registry does not automatically make it functional - they must also bind the handler implementation.

Suggested change
**Note:** The Hono Server Plugin loads routes from the API Registry sorted by priority (highest first), ensuring core endpoints take precedence over plugin endpoints.
**Note:** The Hono Server Plugin loads **only the standard ObjectStack endpoints it knows how to handle** from the API Registry, and processes them sorted by priority (highest first) so core endpoints take precedence. Custom plugin endpoints registered in the API Registry are **not** automatically bound; they must still bind their own handlers via the `http-server` service (as shown in the `start()` example above). For plugin-defined APIs, the API Registry primarily serves for endpoint discovery and documentation rather than automatic handler binding.

Copilot uses AI. Check for mistakes.

### Extending with Middleware

The plugin provides extension points for adding custom middleware:
Expand Down
141 changes: 137 additions & 4 deletions packages/plugins/plugin-hono-server/src/hono-plugin.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { HonoServerPlugin } from './hono-plugin';
import { PluginContext } from '@objectstack/core';
import { PluginContext, ApiRegistry } from '@objectstack/core';

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import ApiRegistry.

Copilot Autofix

AI 4 months ago

In general, unused imports should be removed to avoid confusion and keep the codebase clean. Here, only ApiRegistry is unused; PluginContext is still used for type casting, so we must preserve it while removing only the unused symbol.

The best minimal fix is to update the import statement on line 3 to remove ApiRegistry and keep PluginContext. No other lines need to change, and this will not modify any runtime behavior because ApiRegistry is not used. The edit is confined to packages/plugins/plugin-hono-server/src/hono-plugin.test.ts, in the import section at the top of the file.

No new methods, imports, or definitions are required; we are only simplifying an existing import.

Suggested changeset 1
packages/plugins/plugin-hono-server/src/hono-plugin.test.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/plugins/plugin-hono-server/src/hono-plugin.test.ts b/packages/plugins/plugin-hono-server/src/hono-plugin.test.ts
--- a/packages/plugins/plugin-hono-server/src/hono-plugin.test.ts
+++ b/packages/plugins/plugin-hono-server/src/hono-plugin.test.ts
@@ -1,6 +1,6 @@
 import { describe, it, expect, vi, beforeEach } from 'vitest';
 import { HonoServerPlugin } from './hono-plugin';
-import { PluginContext, ApiRegistry } from '@objectstack/core';
+import { PluginContext } from '@objectstack/core';
 
 describe('HonoServerPlugin', () => {
     let context: any;
EOF
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { HonoServerPlugin } from './hono-plugin';
import { PluginContext, ApiRegistry } from '@objectstack/core';
import { PluginContext } from '@objectstack/core';

describe('HonoServerPlugin', () => {
let context: any;
Copilot is powered by AI and may make mistakes. Always verify output.
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import ApiRegistry.

Suggested change
import { PluginContext, ApiRegistry } from '@objectstack/core';
import { PluginContext } from '@objectstack/core';

Copilot uses AI. Check for mistakes.

describe('HonoServerPlugin', () => {
let context: any;
let logger: any;
let protocol: any;
let apiRegistry: any;

beforeEach(() => {
logger = {
Expand All @@ -16,14 +17,38 @@
};

protocol = {
findData: vi.fn(),
createData: vi.fn()
getDiscovery: vi.fn().mockResolvedValue({ version: 'v1', apiName: 'ObjectStack' }),
getMetaTypes: vi.fn().mockResolvedValue({ types: ['object', 'plugin'] }),
getMetaItems: vi.fn().mockResolvedValue({ type: 'object', items: [] }),
findData: vi.fn().mockResolvedValue({ object: 'test', records: [] }),
getData: vi.fn().mockResolvedValue({ object: 'test', id: '1', record: {} }),
createData: vi.fn().mockResolvedValue({ object: 'test', id: '1', record: {} }),
updateData: vi.fn().mockResolvedValue({ object: 'test', id: '1', record: {} }),
deleteData: vi.fn().mockResolvedValue({ object: 'test', id: '1', success: true }),
batchData: vi.fn().mockResolvedValue({ total: 0, succeeded: 0, failed: 0 }),
createManyData: vi.fn().mockResolvedValue({ object: 'test', records: [], count: 0 }),
updateManyData: vi.fn().mockResolvedValue({ total: 0, succeeded: 0, failed: 0 }),
deleteManyData: vi.fn().mockResolvedValue({ total: 0, succeeded: 0, failed: 0 }),
getMetaItemCached: vi.fn().mockResolvedValue({ data: {}, notModified: false }),
getUiView: vi.fn().mockResolvedValue({ object: 'test', type: 'list' })
};

apiRegistry = {
registerApi: vi.fn(),
getRegistry: vi.fn().mockReturnValue({
version: '1.0.0',
conflictResolution: 'error',
apis: [],
totalApis: 0,
totalEndpoints: 0
})
};

context = {
logger,
getService: vi.fn((service) => {
if (service === 'protocol') return protocol;
if (service === 'api-registry') throw new Error('Not found');
return null;
}),
registerService: vi.fn(),
Expand All @@ -47,11 +72,119 @@
expect(context.hook).toHaveBeenCalledWith('kernel:ready', expect.any(Function));
});

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

expect(context.getService).toHaveBeenCalledWith('protocol');
expect(context.getService).toHaveBeenCalledWith('api-registry');
expect(logger.debug).toHaveBeenCalledWith('API Registry not found, using legacy route registration');
});

it('should use API Registry when available', async () => {
context.getService = vi.fn((service) => {
if (service === 'protocol') return protocol;
if (service === 'api-registry') return apiRegistry;
return null;
});

const plugin = new HonoServerPlugin();
await plugin.init(context as PluginContext);
await plugin.start(context as PluginContext);

expect(context.getService).toHaveBeenCalledWith('api-registry');
expect(apiRegistry.registerApi).toHaveBeenCalled();
});

it('should register standard endpoints to API Registry', async () => {
context.getService = vi.fn((service) => {
if (service === 'protocol') return protocol;
if (service === 'api-registry') return apiRegistry;
return null;
});

const plugin = new HonoServerPlugin();
await plugin.init(context as PluginContext);
await plugin.start(context as PluginContext);

expect(apiRegistry.registerApi).toHaveBeenCalledWith(
expect.objectContaining({
id: 'objectstack_core_api',
name: 'ObjectStack Core API',
type: 'rest',
version: 'v1'
})
);
});

it('should skip standard endpoint registration when disabled', async () => {
context.getService = vi.fn((service) => {
if (service === 'protocol') return protocol;
if (service === 'api-registry') return apiRegistry;
return null;
});

const plugin = new HonoServerPlugin({ registerStandardEndpoints: false });
await plugin.init(context as PluginContext);
await plugin.start(context as PluginContext);

expect(apiRegistry.registerApi).not.toHaveBeenCalled();
});

it('should use legacy routes when useApiRegistry is disabled', async () => {
context.getService = vi.fn((service) => {
if (service === 'protocol') return protocol;
if (service === 'api-registry') return apiRegistry;
return null;
});

const plugin = new HonoServerPlugin({ useApiRegistry: false });
await plugin.init(context as PluginContext);
await plugin.start(context as PluginContext);

expect(apiRegistry.getRegistry).not.toHaveBeenCalled();
expect(logger.debug).toHaveBeenCalledWith('Using legacy route registration');
});

it('should respect REST server configuration', async () => {
context.getService = vi.fn((service) => {
if (service === 'protocol') return protocol;
if (service === 'api-registry') return apiRegistry;
return null;
});

const plugin = new HonoServerPlugin({
restConfig: {
api: {
version: 'v2',
basePath: '/custom',
enableCrud: true,
enableMetadata: true,
enableBatch: true
}
}
});

await plugin.init(context as PluginContext);
await plugin.start(context as PluginContext);

expect(apiRegistry.registerApi).toHaveBeenCalledWith(
expect.objectContaining({
version: 'v2'
})
);
});

it('should handle protocol service not found gracefully', async () => {
context.getService = vi.fn(() => {
throw new Error('Service not found');
});

const plugin = new HonoServerPlugin();
await plugin.init(context as PluginContext);
await plugin.start(context as PluginContext);

expect(logger.warn).toHaveBeenCalledWith('Protocol service not found, skipping protocol routes');
});
});
Loading
Loading