Skip to content

Add capability contract interfaces for HTTP and data persistence#339

Merged
hotlong merged 4 commits intomainfrom
copilot/create-core-foundation
Jan 28, 2026
Merged

Add capability contract interfaces for HTTP and data persistence#339
hotlong merged 4 commits intomainfrom
copilot/create-core-foundation

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jan 28, 2026

Implements abstract capability contracts (IHttpServer, IDataEngine) to decouple plugins from concrete HTTP framework and data layer implementations, completing the microkernel architecture foundation.

Changes

New Interfaces

src/interfaces/http-server.ts

  • IHttpServer: Abstract HTTP routing (get, post, put, delete, patch, use, listen)
  • IHttpRequest / IHttpResponse: Generic request/response types
  • Enables plugins to work with Express, Fastify, Hono, or any HTTP framework

src/interfaces/data-engine.ts

  • IDataEngine: Abstract CRUD operations (insert, find, update, delete)
  • QueryOptions: Flexible query filters, sorting, pagination
  • Enables plugins to work with ObjectQL, Prisma, TypeORM, or any data layer

Usage Example

class ApiPlugin implements Plugin {
  name = 'api';
  dependencies = ['http-server', 'data-engine'];
  
  async start(ctx: PluginContext) {
    const server = ctx.getService<IHttpServer>('http-server');
    const db = ctx.getService<IDataEngine>('data-engine');
    
    // Framework-agnostic - works with any HTTP/data implementation
    server.post('/api/users', async (req, res) => {
      const user = await db.insert('user', req.body);
      res.status(201).json(user);
    });
  }
}

Supporting Files

  • src/test-interfaces.ts: Mock implementations demonstrating both interfaces
  • src/index.ts: Export new interfaces from package
  • README.md: API documentation with code examples

Design Notes

Follows Dependency Inversion Principle: plugins depend on stable abstractions rather than volatile concrete implementations. Concrete adapters (Hono, Express, ObjectQL) implement these interfaces and register themselves via the service registry.

Original prompt

全局设定 (System Context)

在开始之前,先给 AI 输入这段上下文,确立架构基调:

Context: I am building "ObjectStack", an enterprise-grade low-code platform. Architecture: Microkernel Architecture (Plugin-based). Package Name: @objectstack/core (The runtime kernel). Design Philosophy: The Core only defines Interfaces (Contracts) and manages the Lifecycle. Actual business logic (like HTTP Servers or Database connections) must be implemented in Plugins. Tech Stack: TypeScript, Node.js.

第一步:构建内核与基础类型 (The Kernel & Types)

目标:建立没有任何业务逻辑的纯净运行时。

📋 Prompt 1

Task: Create the foundation for @objectstack/core.

Requirements:

Define Interfaces (src/types.ts):

Plugin: Must have name, version?, dependencies? (string[]), and lifecycle methods init(ctx) and start(ctx).

PluginContext: The interface for the object passed to plugins. It must support:

registerService(name: string, instance: any): To register a capability.

getService(name: string): To retrieve a capability (throw error if not found).

hook(event: string, handler: Function) and trigger(event: string, data: any).

Implement Kernel (src/kernel.ts):

Create class ObjectKernel.

Implement use(plugin) to add plugins to a registry.

Implement bootstrap():

Phase 1: Validate dependencies (ensure required plugins are present).

Phase 2: Call init() on all plugins sequentially.

Phase 3: Call start() on all plugins.

Constraint: Keep this file generic. Do NOT import Express, Database libraries, or any business logic here.

第二步:定义核心能力契约 (The Contracts)

目标:在 Core 中制定“法律”,规定 HTTP 服务和数据引擎应该长什么样,但不写具体实现。

📋 Prompt 2

Task: Define the standard capabilities contracts in @objectstack/core.

Context: We need standard interfaces so plugins can interact without knowing the underlying implementation (Dependency Inversion).

Requirements:

Create src/interfaces/http-server.ts:

Define interface IHttpServer.

It should define generic methods for routing: get(path, handler), post(path, handler), use(path, handler).

It should define listen(port).

Note: Do not use Express types directly here, keep it abstract if possible, or use generic Request/Response types.

Create src/interfaces/data-engine.ts:

Define interface IDataEngine.

Methods:

insert(objectName: string, data: any): Promise

find(objectName: string, query?: any): Promise<any[]>

update(objectName: string, id: any, data: any): Promise

delete(objectName: string, id: any): Promise

Constraint: Only write TypeScript interface definitions. No class implementations yet.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@vercel
Copy link
Copy Markdown

vercel bot commented Jan 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
spec Ready Ready Preview, Comment Jan 28, 2026 5:19am

Request Review

Copilot AI and others added 3 commits January 28, 2026 05:06
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Establish foundation for @objectstack/core Add capability contract interfaces for HTTP and data persistence Jan 28, 2026
Copilot AI requested a review from hotlong January 28, 2026 05:13
@hotlong hotlong marked this pull request as ready for review January 28, 2026 05:32
Copilot AI review requested due to automatic review settings January 28, 2026 05:32
@github-actions github-actions bot added documentation Improvements or additions to documentation size/l labels Jan 28, 2026
* and IDataEngine interfaces without depending on concrete implementations.
*/

import { IHttpServer, IDataEngine, RouteHandler, IHttpRequest, IHttpResponse, Middleware, QueryOptions } from './index.js';

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note test

Unused imports IHttpRequest, IHttpResponse.

Copilot Autofix

AI 2 months ago

To fix unused imports, the general approach is to remove only the specific imported names that are not referenced anywhere in the file, leaving the rest of the import intact. This avoids changing any runtime or type-checking behavior while eliminating dead code and noise.

Here, the best minimal fix is to edit the import statement on line 8 of packages/runtime/src/test-interfaces.ts and remove IHttpRequest and IHttpResponse from the named imports list, keeping IHttpServer, IDataEngine, RouteHandler, Middleware, and QueryOptions untouched. No additional code changes or imports are needed, and existing functionality is preserved because these two types were not in use.

Concretely: in packages/runtime/src/test-interfaces.ts, update the single import line so it reads import { IHttpServer, IDataEngine, RouteHandler, Middleware, QueryOptions } from './index.js';. No other regions or files need modification.

Suggested changeset 1
packages/runtime/src/test-interfaces.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/runtime/src/test-interfaces.ts b/packages/runtime/src/test-interfaces.ts
--- a/packages/runtime/src/test-interfaces.ts
+++ b/packages/runtime/src/test-interfaces.ts
@@ -5,7 +5,7 @@
  * and IDataEngine interfaces without depending on concrete implementations.
  */
 
-import { IHttpServer, IDataEngine, RouteHandler, IHttpRequest, IHttpResponse, Middleware, QueryOptions } from './index.js';
+import { IHttpServer, IDataEngine, RouteHandler, Middleware, QueryOptions } from './index.js';
 
 /**
  * Example: Mock HTTP Server Plugin
EOF
@@ -5,7 +5,7 @@
* and IDataEngine interfaces without depending on concrete implementations.
*/

import { IHttpServer, IDataEngine, RouteHandler, IHttpRequest, IHttpResponse, Middleware, QueryOptions } from './index.js';
import { IHttpServer, IDataEngine, RouteHandler, Middleware, QueryOptions } from './index.js';

/**
* Example: Mock HTTP Server Plugin
Copilot is powered by AI and may make mistakes. Always verify output.
@hotlong hotlong merged commit 3e161a8 into main Jan 28, 2026
14 of 15 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces abstract capability contract interfaces (IHttpServer and IDataEngine) to enable plugin-based dependency inversion in the ObjectStack microkernel architecture. The interfaces aim to decouple plugins from concrete HTTP framework and data layer implementations.

Changes:

  • Adds IHttpServer interface with generic HTTP routing methods and request/response abstractions
  • Adds IDataEngine interface with CRUD operations and flexible query options
  • Provides mock implementations demonstrating both interfaces in test-interfaces.ts

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/runtime/src/interfaces/http-server.ts Defines IHttpServer, IHttpRequest, IHttpResponse, RouteHandler, and Middleware abstractions
packages/runtime/src/interfaces/data-engine.ts Defines IDataEngine with QueryOptions for database-agnostic CRUD operations
packages/runtime/src/test-interfaces.ts Mock implementations of both interfaces with example usage patterns
packages/runtime/src/index.ts Exports new capability contract interfaces
packages/runtime/README.md Documents capability contracts with API reference and usage examples

Comment on lines +22 to +37
export interface QueryOptions {
/** Filter conditions */
filter?: QueryFilter;
/** Fields to select */
select?: string[];
/** Sort order */
sort?: Record<string, 1 | -1 | 'asc' | 'desc'>;
/** Limit number of results (alternative name for top, used by some drivers) */
limit?: number;
/** Skip number of results (for pagination) */
skip?: number;
/** Maximum number of results (OData-style, takes precedence over limit if both specified) */
top?: number;
}

/**
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

Having both limit and top as separate optional properties is confusing since they serve the same purpose (limiting number of results). The comment states "takes precedence over limit if both specified" but this precedence logic isn't enforced at the type level.

Consider either:

  1. Using a single property (e.g., limit) for consistency
  2. Using a union type to ensure only one can be specified
  3. If you must support both for compatibility with different drivers (MongoDB uses limit, OData uses top), document this more clearly and consider implementing the precedence logic in a helper function or base implementation
Suggested change
export interface QueryOptions {
/** Filter conditions */
filter?: QueryFilter;
/** Fields to select */
select?: string[];
/** Sort order */
sort?: Record<string, 1 | -1 | 'asc' | 'desc'>;
/** Limit number of results (alternative name for top, used by some drivers) */
limit?: number;
/** Skip number of results (for pagination) */
skip?: number;
/** Maximum number of results (OData-style, takes precedence over limit if both specified) */
top?: number;
}
/**
export type QueryOptionsBase = {
/** Filter conditions */
filter?: QueryFilter;
/** Fields to select */
select?: string[];
/** Sort order */
sort?: Record<string, 1 | -1 | 'asc' | 'desc'>;
/** Skip number of results (for pagination) */
skip?: number;
};
/**
* Mutually exclusive limit options:
* - Use `limit` (common across many drivers), or
* - Use `top` (OData-style), or
* - Use neither.
*
* This enforces at the type level that both are not specified together.
*/
export type QueryLimitOptions =
| {
/** Limit number of results (alternative name for top, used by some drivers) */
limit?: number;
top?: never;
}
| {
limit?: never;
/** Maximum number of results (OData-style, takes precedence over limit if both specified) */
top?: number;
}
| {
limit?: undefined;
top?: undefined;
};
/**
* Full query options including mutually exclusive limit behavior.
*/
export type QueryOptions = QueryOptionsBase & QueryLimitOptions;
/**

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +87
const results = Array.from(objectStore.values());
console.log(`✅ Found ${results.length} records in ${objectName}`);
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

The MockDataEngine.find implementation ignores the query parameter and always returns all records. Since this is an example/demonstration file showing how to implement the IDataEngine interface, it would be more educational to show at least basic query handling (e.g., applying filter, limit, or skip).

Consider implementing basic query support to demonstrate how the interface should work, such as:

  • Apply query.filter for basic field matching
  • Apply query.limit or query.top for pagination
  • Apply query.skip for offset pagination
Suggested change
const results = Array.from(objectStore.values());
console.log(`✅ Found ${results.length} records in ${objectName}`);
let results = Array.from(objectStore.values());
// Basic query handling for demonstration purposes:
// - query.filter: exact field matching (AND across fields)
// - query.skip: offset pagination
// - query.limit / query.top: limit number of returned records
if (query?.filter && typeof query.filter === 'object') {
const filter = query.filter as Record<string, any>;
results = results.filter(record =>
Object.entries(filter).every(([key, value]) => record?.[key] === value)
);
}
const skip = typeof query?.skip === 'number' && query.skip > 0 ? query.skip : 0;
const limit =
typeof (query as any)?.limit === 'number'
? (query as any).limit
: typeof (query as any)?.top === 'number'
? (query as any).top
: undefined;
if (skip > 0 || typeof limit === 'number') {
const start = skip;
const end = typeof limit === 'number' ? start + limit : undefined;
results = results.slice(start, end);
}
console.log(`✅ Found ${results.length} records in ${objectName} matching query`);

Copilot uses AI. Check for mistakes.
- `patch(path, handler)` - Register PATCH route
- `use(path, handler?)` - Register middleware
- `listen(port)` - Start server
- `close()` - Stop server (optional)
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

The new IHttpServer interface is not yet implemented by the existing HonoServerPlugin in packages/plugin-hono-server/src/hono-plugin.ts. The HonoServerPlugin currently registers a raw Hono app instance (line 56), but consumers expecting IHttpServer would get a Hono object that has different method signatures.

For example:

  • Hono's methods return Hono for chaining, but IHttpServer methods return void
  • Hono's route handlers use Context (c), not IHttpRequest/IHttpResponse

This creates a breaking change risk. Consider:

  1. Creating an adapter class (e.g., HonoServerAdapter) that wraps Hono and implements IHttpServer
  2. Updating HonoServerPlugin to register the adapter instead of raw Hono
  3. Documenting the migration path for existing plugins that depend on 'http-server' service
Suggested change
- `close()` - Stop server (optional)
- (implementation-specific) method to stop/shutdown the server, if provided by the underlying HTTP framework

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +35
export interface QueryOptions {
/** Filter conditions */
filter?: QueryFilter;
/** Fields to select */
select?: string[];
/** Sort order */
sort?: Record<string, 1 | -1 | 'asc' | 'desc'>;
/** Limit number of results (alternative name for top, used by some drivers) */
limit?: number;
/** Skip number of results (for pagination) */
skip?: number;
/** Maximum number of results (OData-style, takes precedence over limit if both specified) */
top?: number;
}
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

The IDataEngine.find query format doesn't align with ObjectQL's actual query format. ObjectQL uses { where, fields, orderBy, limit } (line 222 in packages/objectql/src/index.ts), while IDataEngine.QueryOptions uses { filter, select, sort, limit, skip, top }.

This creates an impedance mismatch:

  • filter vs where for conditions
  • select vs fields for field selection
  • sort vs orderBy for sorting
  • ObjectQL has no skip parameter for offset pagination
  • IDataEngine has both limit and top (redundant)

Consider either:

  1. Aligning IDataEngine with ObjectQL's established API (use where, fields, orderBy)
  2. Creating an adapter that translates between the two formats
  3. Documenting the mapping between these query formats in the interface documentation

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +24
params: Record<string, string>;
/** Request query parameters */
query: Record<string, string | string[]>;
/** Request body */
body?: any;
/** Request headers */
headers: Record<string, string | string[]>;
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

The headers property type Record<string, string | string[]> implies that all header keys will always have a value, but HTTP requests may not have all headers present. Accessing a non-existent header would return undefined at runtime, but TypeScript won't reflect this.

Consider using one of these alternatives:

  • Partial<Record<string, string | string[]>> - makes all headers optional
  • Record<string, string | string[] | undefined> - explicitly allows undefined
  • A getter method like getHeader(name: string): string | string[] | undefined

This same issue exists for both IHttpRequest.headers (line 24) and IHttpRequest.query (line 20), and IHttpRequest.params (line 18).

Suggested change
params: Record<string, string>;
/** Request query parameters */
query: Record<string, string | string[]>;
/** Request body */
body?: any;
/** Request headers */
headers: Record<string, string | string[]>;
params: Record<string, string | undefined>;
/** Request query parameters */
query: Record<string, string | string[] | undefined>;
/** Request body */
body?: any;
/** Request headers */
headers: Record<string, string | string[] | undefined>;

Copilot uses AI. Check for mistakes.
* and IDataEngine interfaces without depending on concrete implementations.
*/

import { IHttpServer, IDataEngine, RouteHandler, IHttpRequest, IHttpResponse, Middleware, QueryOptions } from './index.js';
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

Unused imports IHttpRequest, IHttpResponse.

Suggested change
import { IHttpServer, IDataEngine, RouteHandler, IHttpRequest, IHttpResponse, Middleware, QueryOptions } from './index.js';
import { IHttpServer, IDataEngine, RouteHandler, Middleware, QueryOptions } from './index.js';

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants