diff --git a/packages/runtime/README.md b/packages/runtime/README.md index 91be75893..9136c94a4 100644 --- a/packages/runtime/README.md +++ b/packages/runtime/README.md @@ -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 @@ -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('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) + +#### 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('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 @@ -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 diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 20b376614..6030b874a 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -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'; + diff --git a/packages/runtime/src/interfaces/data-engine.ts b/packages/runtime/src/interfaces/data-engine.ts new file mode 100644 index 000000000..afe3c4e02 --- /dev/null +++ b/packages/runtime/src/interfaces/data-engine.ts @@ -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; + /** 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; +} + +/** + * 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; + + /** + * 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; + + /** + * 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; + + /** + * 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; +} diff --git a/packages/runtime/src/interfaces/http-server.ts b/packages/runtime/src/interfaces/http-server.ts new file mode 100644 index 000000000..e3f384912 --- /dev/null +++ b/packages/runtime/src/interfaces/http-server.ts @@ -0,0 +1,140 @@ +/** + * IHttpServer - Standard HTTP Server Interface + * + * Abstract interface for HTTP server capabilities. + * This allows plugins to interact with HTTP servers without knowing + * the underlying implementation (Express, Fastify, Hono, etc.). + * + * Follows Dependency Inversion Principle - plugins depend on this interface, + * not on concrete HTTP framework implementations. + */ + +/** + * Generic HTTP Request type + * Abstraction over framework-specific request objects + */ +export interface IHttpRequest { + /** Request path parameters */ + params: Record; + /** Request query parameters */ + query: Record; + /** Request body */ + body?: any; + /** Request headers */ + headers: Record; + /** HTTP method */ + method: string; + /** Request path */ + path: string; +} + +/** + * Generic HTTP Response type + * Abstraction over framework-specific response objects + */ +export interface IHttpResponse { + /** + * Send a JSON response + * @param data - Data to send + */ + json(data: any): void | Promise; + + /** + * Send a text/html response + * @param data - Data to send + */ + send(data: string): void | Promise; + + /** + * Set HTTP status code + * @param code - HTTP status code + */ + status(code: number): IHttpResponse; + + /** + * Set response header + * @param name - Header name + * @param value - Header value (string or array of strings for multi-value headers) + */ + header(name: string, value: string | string[]): IHttpResponse; +} + +/** + * Route handler function + */ +export type RouteHandler = ( + req: IHttpRequest, + res: IHttpResponse +) => void | Promise; + +/** + * Middleware function + */ +export type Middleware = ( + req: IHttpRequest, + res: IHttpResponse, + next: () => void | Promise +) => void | Promise; + +/** + * IHttpServer - HTTP Server capability interface + * + * Defines the contract for HTTP server implementations. + * Concrete implementations (Express, Fastify, Hono) should implement this interface. + */ +export interface IHttpServer { + /** + * Register a GET route handler + * @param path - Route path (e.g., '/api/users/:id') + * @param handler - Route handler function + */ + get(path: string, handler: RouteHandler): void; + + /** + * Register a POST route handler + * @param path - Route path + * @param handler - Route handler function + */ + post(path: string, handler: RouteHandler): void; + + /** + * Register a PUT route handler + * @param path - Route path + * @param handler - Route handler function + */ + put(path: string, handler: RouteHandler): void; + + /** + * Register a DELETE route handler + * @param path - Route path + * @param handler - Route handler function + */ + delete(path: string, handler: RouteHandler): void; + + /** + * Register a PATCH route handler + * @param path - Route path + * @param handler - Route handler function + */ + patch(path: string, handler: RouteHandler): void; + + /** + * Register middleware + * @param path - Optional path to apply middleware to (if omitted, applies globally) + * @param handler - Middleware function + */ + use(path: string | Middleware, handler?: Middleware): void; + + /** + * Start the HTTP server + * @param port - Port number to listen on + * @returns Promise that resolves when server is ready + */ + listen(port: number): Promise; + + /** + * Stop the HTTP server + * @returns Promise that resolves when server is stopped + */ + close?(): Promise; +} diff --git a/packages/runtime/src/test-interfaces.ts b/packages/runtime/src/test-interfaces.ts new file mode 100644 index 000000000..47e3f280f --- /dev/null +++ b/packages/runtime/src/test-interfaces.ts @@ -0,0 +1,170 @@ +/** + * Test file to verify capability contract interfaces + * + * This file demonstrates how plugins can implement the IHttpServer + * and IDataEngine interfaces without depending on concrete implementations. + */ + +import { IHttpServer, IDataEngine, RouteHandler, IHttpRequest, IHttpResponse, Middleware, QueryOptions } from './index.js'; + +/** + * Example: Mock HTTP Server Plugin + * + * Shows how a plugin can implement the IHttpServer interface + * without depending on Express, Fastify, or any specific framework. + */ +class MockHttpServer implements IHttpServer { + private routes: Map = new Map(); + + get(path: string, handler: RouteHandler): void { + this.routes.set(`GET:${path}`, { method: 'GET', handler }); + console.log(`✅ Registered GET ${path}`); + } + + post(path: string, handler: RouteHandler): void { + this.routes.set(`POST:${path}`, { method: 'POST', handler }); + console.log(`✅ Registered POST ${path}`); + } + + put(path: string, handler: RouteHandler): void { + this.routes.set(`PUT:${path}`, { method: 'PUT', handler }); + console.log(`✅ Registered PUT ${path}`); + } + + delete(path: string, handler: RouteHandler): void { + this.routes.set(`DELETE:${path}`, { method: 'DELETE', handler }); + console.log(`✅ Registered DELETE ${path}`); + } + + patch(path: string, handler: RouteHandler): void { + this.routes.set(`PATCH:${path}`, { method: 'PATCH', handler }); + console.log(`✅ Registered PATCH ${path}`); + } + + use(path: string | Middleware, handler?: Middleware): void { + console.log(`✅ Registered middleware`); + } + + async listen(port: number): Promise { + console.log(`✅ Mock HTTP server listening on port ${port}`); + } + + async close(): Promise { + console.log(`✅ Mock HTTP server closed`); + } +} + +/** + * Example: Mock Data Engine Plugin + * + * Shows how a plugin can implement the IDataEngine interface + * without depending on ObjectQL, Prisma, or any specific database. + */ +class MockDataEngine implements IDataEngine { + private store: Map> = new Map(); + private idCounter = 0; + + async insert(objectName: string, data: any): Promise { + if (!this.store.has(objectName)) { + this.store.set(objectName, new Map()); + } + + const id = `${objectName}_${++this.idCounter}`; + const record = { id, ...data }; + this.store.get(objectName)!.set(id, record); + + console.log(`✅ Inserted into ${objectName}:`, record); + return record; + } + + async find(objectName: string, query?: QueryOptions): Promise { + const objectStore = this.store.get(objectName); + if (!objectStore) { + return []; + } + + const results = Array.from(objectStore.values()); + console.log(`✅ Found ${results.length} records in ${objectName}`); + return results; + } + + async update(objectName: string, id: any, data: any): Promise { + const objectStore = this.store.get(objectName); + if (!objectStore || !objectStore.has(id)) { + throw new Error(`Record ${id} not found in ${objectName}`); + } + + const existing = objectStore.get(id); + const updated = { ...existing, ...data }; + objectStore.set(id, updated); + + console.log(`✅ Updated ${objectName}/${id}:`, updated); + return updated; + } + + async delete(objectName: string, id: any): Promise { + const objectStore = this.store.get(objectName); + if (!objectStore) { + return false; + } + + const deleted = objectStore.delete(id); + console.log(`✅ Deleted ${objectName}/${id}: ${deleted}`); + return deleted; + } +} + +/** + * Test the interfaces + */ +async function testInterfaces() { + console.log('\n=== Testing IHttpServer Interface ===\n'); + + const httpServer: IHttpServer = new MockHttpServer(); + + // Register routes using the interface + httpServer.get('/api/users', async (req, res) => { + res.json({ users: [] }); + }); + + httpServer.post('/api/users', async (req, res) => { + res.status(201).json({ id: 1, ...req.body }); + }); + + await httpServer.listen(3000); + + console.log('\n=== Testing IDataEngine Interface ===\n'); + + const dataEngine: IDataEngine = new MockDataEngine(); + + // Use the data engine interface + const user1 = await dataEngine.insert('user', { + name: 'John Doe', + email: 'john@example.com' + }); + + const user2 = await dataEngine.insert('user', { + name: 'Jane Smith', + email: 'jane@example.com' + }); + + const users = await dataEngine.find('user'); + console.log(`Found ${users.length} users after inserts`); + + const updatedUser = await dataEngine.update('user', user1.id, { + name: 'John Updated' + }); + console.log(`Updated user:`, updatedUser); + + const deleted = await dataEngine.delete('user', user2.id); + console.log(`Delete result: ${deleted}`); + + console.log('\n✅ All interface tests passed!\n'); + + if (httpServer.close) { + await httpServer.close(); + } +} + +// Run tests +testInterfaces().catch(console.error);