Add capability contract interfaces for HTTP and data persistence#339
Add capability contract interfaces for HTTP and data persistence#339
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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>
| * 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
Show autofix suggestion
Hide autofix suggestion
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.
| @@ -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 |
There was a problem hiding this comment.
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
IHttpServerinterface with generic HTTP routing methods and request/response abstractions - Adds
IDataEngineinterface 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 |
| 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; | ||
| } | ||
|
|
||
| /** |
There was a problem hiding this comment.
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:
- Using a single property (e.g.,
limit) for consistency - Using a union type to ensure only one can be specified
- If you must support both for compatibility with different drivers (MongoDB uses
limit, OData usestop), document this more clearly and consider implementing the precedence logic in a helper function or base implementation
| 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; | |
| /** |
| const results = Array.from(objectStore.values()); | ||
| console.log(`✅ Found ${results.length} records in ${objectName}`); |
There was a problem hiding this comment.
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.filterfor basic field matching - Apply
query.limitorquery.topfor pagination - Apply
query.skipfor offset pagination
| 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`); |
| - `patch(path, handler)` - Register PATCH route | ||
| - `use(path, handler?)` - Register middleware | ||
| - `listen(port)` - Start server | ||
| - `close()` - Stop server (optional) |
There was a problem hiding this comment.
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
Honofor chaining, but IHttpServer methods returnvoid - Hono's route handlers use
Context(c), notIHttpRequest/IHttpResponse
This creates a breaking change risk. Consider:
- Creating an adapter class (e.g., HonoServerAdapter) that wraps Hono and implements IHttpServer
- Updating HonoServerPlugin to register the adapter instead of raw Hono
- Documenting the migration path for existing plugins that depend on 'http-server' service
| - `close()` - Stop server (optional) | |
| - (implementation-specific) method to stop/shutdown the server, if provided by the underlying HTTP framework |
| 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; | ||
| } |
There was a problem hiding this comment.
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:
filtervswherefor conditionsselectvsfieldsfor field selectionsortvsorderByfor sorting- ObjectQL has no
skipparameter for offset pagination - IDataEngine has both
limitandtop(redundant)
Consider either:
- Aligning IDataEngine with ObjectQL's established API (use
where,fields,orderBy) - Creating an adapter that translates between the two formats
- Documenting the mapping between these query formats in the interface documentation
| params: Record<string, string>; | ||
| /** Request query parameters */ | ||
| query: Record<string, string | string[]>; | ||
| /** Request body */ | ||
| body?: any; | ||
| /** Request headers */ | ||
| headers: Record<string, string | string[]>; |
There was a problem hiding this comment.
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 optionalRecord<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).
| 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>; |
| * and IDataEngine interfaces without depending on concrete implementations. | ||
| */ | ||
|
|
||
| import { IHttpServer, IDataEngine, RouteHandler, IHttpRequest, IHttpResponse, Middleware, QueryOptions } from './index.js'; |
There was a problem hiding this comment.
Unused imports IHttpRequest, IHttpResponse.
| import { IHttpServer, IDataEngine, RouteHandler, IHttpRequest, IHttpResponse, Middleware, QueryOptions } from './index.js'; | |
| import { IHttpServer, IDataEngine, RouteHandler, Middleware, QueryOptions } from './index.js'; |
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.tsIHttpServer: Abstract HTTP routing (get, post, put, delete, patch, use, listen)IHttpRequest/IHttpResponse: Generic request/response typessrc/interfaces/data-engine.tsIDataEngine: Abstract CRUD operations (insert, find, update, delete)QueryOptions: Flexible query filters, sorting, paginationUsage Example
Supporting Files
src/test-interfaces.ts: Mock implementations demonstrating both interfacessrc/index.ts: Export new interfaces from packageREADME.md: API documentation with code examplesDesign 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
💡 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.