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
103 changes: 96 additions & 7 deletions packages/runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ ObjectStack Core Runtime & Query Engine

The runtime package provides the `ObjectKernel` (MiniKernel) - a highly modular, plugin-based microkernel that orchestrates ObjectStack applications. It manages the application lifecycle through a standardized plugin system with dependency injection and event hooks.

The package also defines **capability contract interfaces** (`IHttpServer`, `IDataEngine`) that enable plugins to interact with common services without depending on concrete implementations, following the **Dependency Inversion Principle**.

### Architecture Highlights

- **MiniKernel Design**: Business logic is completely separated into plugins
- **Capability Contracts**: Abstract interfaces for HTTP server and data persistence
- **Dependency Injection**: Service registry for inter-plugin communication
- **Event/Hook System**: Publish-subscribe mechanism for loose coupling
- **Lifecycle Management**: Standardized init/start/destroy phases
Expand Down Expand Up @@ -119,6 +122,91 @@ new AppManifestPlugin(appConfig)

## API Reference

### Capability Contract Interfaces

#### IHttpServer

Abstract interface for HTTP server capabilities. Allows plugins to work with any HTTP framework (Express, Fastify, Hono, etc.) without tight coupling.

```typescript
import { IHttpServer, IHttpRequest, IHttpResponse } from '@objectstack/runtime';

// In your HTTP server plugin
class MyHttpServerPlugin implements Plugin {
name = 'http-server';

async init(ctx: PluginContext) {
const server: IHttpServer = createMyServer(); // Express, Hono, etc.
ctx.registerService('http-server', server);
}
}

// In your API plugin
class MyApiPlugin implements Plugin {
name = 'api';
dependencies = ['http-server'];

async start(ctx: PluginContext) {
const server = ctx.getService<IHttpServer>('http-server');

// Register routes - works with any HTTP framework
server.get('/api/users', async (req, res) => {
res.json({ users: [] });
});
}
}
```

**Interface Methods:**
- `get(path, handler)` - Register GET route
- `post(path, handler)` - Register POST route
- `put(path, handler)` - Register PUT route
- `delete(path, handler)` - Register DELETE route
- `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.

#### IDataEngine

Abstract interface for data persistence. Allows plugins to work with any data layer (ObjectQL, Prisma, TypeORM, etc.) without tight coupling.

```typescript
import { IDataEngine } from '@objectstack/runtime';

// In your data plugin
class MyDataPlugin implements Plugin {
name = 'data';

async init(ctx: PluginContext) {
const engine: IDataEngine = createMyDataEngine(); // ObjectQL, Prisma, etc.
ctx.registerService('data-engine', engine);
}
}

// In your business logic plugin
class MyBusinessPlugin implements Plugin {
name = 'business';
dependencies = ['data'];

async start(ctx: PluginContext) {
const engine = ctx.getService<IDataEngine>('data-engine');

// CRUD operations - works with any data layer
const user = await engine.insert('user', { name: 'John' });
const users = await engine.find('user', { filter: { active: true } });
await engine.update('user', user.id, { name: 'Jane' });
await engine.delete('user', user.id);
}
}
```

**Interface Methods:**
- `insert(objectName, data)` - Create a record
- `find(objectName, query?)` - Query records
- `update(objectName, id, data)` - Update a record
- `delete(objectName, id)` - Delete a record

### ObjectKernel

#### Methods
Expand Down Expand Up @@ -161,17 +249,18 @@ interface PluginContext {
See the `examples/` directory for complete examples:
- `examples/host/` - Full server setup with Hono
- `examples/msw-react-crud/` - Browser-based setup with MSW
- `examples/custom-objectql-example.ts` - Custom ObjectQL instance
- `test-mini-kernel.ts` - Comprehensive test suite
- `test-mini-kernel.ts` - Comprehensive kernel test suite
- `packages/runtime/src/test-interfaces.ts` - Capability contract interface examples

## Benefits of MiniKernel

1. **True Modularity**: Each plugin is independent and reusable
2. **Testability**: Mock services easily in tests
3. **Flexibility**: Load plugins conditionally
4. **Extensibility**: Add new plugins without modifying kernel
5. **Clear Dependencies**: Explicit dependency declarations
6. **Better Architecture**: Separation of concerns
2. **Capability Contracts**: Plugins depend on interfaces, not implementations
3. **Testability**: Mock services easily in tests
4. **Flexibility**: Load plugins conditionally, swap implementations
5. **Extensibility**: Add new plugins without modifying kernel
6. **Clear Dependencies**: Explicit dependency declarations
7. **Better Architecture**: Separation of concerns with Dependency Inversion

## Best Practices

Expand Down
4 changes: 4 additions & 0 deletions packages/runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ export { ObjectStackRuntimeProtocol } from './protocol.js';
// Export Types
export * from './types.js';

// Export Interfaces (Capability Contracts)
export * from './interfaces/http-server.js';
export * from './interfaces/data-engine.js';

122 changes: 122 additions & 0 deletions packages/runtime/src/interfaces/data-engine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* IDataEngine - Standard Data Engine Interface
*
* Abstract interface for data persistence capabilities.
* This allows plugins to interact with data engines without knowing
* the underlying implementation (SQL, MongoDB, Memory, etc.).
*
* Follows Dependency Inversion Principle - plugins depend on this interface,
* not on concrete database implementations.
*/

/**
* Query filter conditions
*/
export interface QueryFilter {
[field: string]: any;
}

/**
* Query options for find operations
*/
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;
}
Comment on lines +22 to +35
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 +22 to +37
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.
* IDataEngine - Data persistence capability interface
*
* Defines the contract for data engine implementations.
* Concrete implementations (ObjectQL, Prisma, TypeORM) should implement this interface.
*/
export interface IDataEngine {
/**
* Insert a new record
*
* @param objectName - Name of the object/table (e.g., 'user', 'order')
* @param data - Data to insert
* @returns Promise resolving to the created record (including generated ID)
*
* @example
* ```ts
* const user = await engine.insert('user', {
* name: 'John Doe',
* email: 'john@example.com'
* });
* console.log(user.id); // Auto-generated ID
* ```
*/
insert(objectName: string, data: any): Promise<any>;

/**
* Find records matching a query
*
* @param objectName - Name of the object/table
* @param query - Query conditions (optional)
* @returns Promise resolving to an array of matching records
*
* @example
* ```ts
* // Find all users
* const allUsers = await engine.find('user');
*
* // Find with filter
* const activeUsers = await engine.find('user', {
* filter: { status: 'active' }
* });
*
* // Find with limit and sort
* const recentUsers = await engine.find('user', {
* sort: { createdAt: -1 },
* limit: 10
* });
* ```
*/
find(objectName: string, query?: QueryOptions): Promise<any[]>;

/**
* Update a record by ID
*
* @param objectName - Name of the object/table
* @param id - Record ID
* @param data - Updated data (partial update)
* @returns Promise resolving to the updated record
*
* @example
* ```ts
* const updatedUser = await engine.update('user', '123', {
* name: 'Jane Doe',
* email: 'jane@example.com'
* });
* ```
*/
update(objectName: string, id: any, data: any): Promise<any>;

/**
* Delete a record by ID
*
* @param objectName - Name of the object/table
* @param id - Record ID
* @returns Promise resolving to true if deleted, false otherwise
*
* @example
* ```ts
* const deleted = await engine.delete('user', '123');
* if (deleted) {
* console.log('User deleted successfully');
* }
* ```
*/
delete(objectName: string, id: any): Promise<boolean>;
}
Loading
Loading