Skip to content

Commit 965cf39

Browse files
Copilothotlong
andcommitted
Implement runtime plugin system with dependency resolution and query pipeline
- Extend type definitions in @objectql/types with BasePlugin, QueryProcessorPlugin - Create @objectql/runtime-core package with PluginManager and QueryPipeline - Implement topological sort for plugin dependency resolution - Implement async series waterfall pattern for query processing - Add createRuntime() factory function - Add comprehensive unit tests (39 tests, all passing) Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 283bc0a commit 965cf39

13 files changed

Lines changed: 1658 additions & 0 deletions

File tree

packages/foundation/types/src/plugin.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,77 @@
77
*/
88

99
import { IObjectQL } from './app';
10+
import { UnifiedQuery } from './query';
1011

1112
export interface ObjectQLPlugin {
1213
name: string;
1314
setup(app: IObjectQL): void | Promise<void>;
1415
}
16+
17+
/**
18+
* Plugin metadata for dependency management and lifecycle
19+
*/
20+
export interface PluginMetadata {
21+
/** Unique plugin name */
22+
name: string;
23+
/** Plugin version (semver format) */
24+
version?: string;
25+
/** Plugin type classification */
26+
type?: 'driver' | 'repository' | 'query_processor' | 'extension';
27+
/** Plugin dependencies (plugin names that must be loaded first) */
28+
dependencies?: string[];
29+
}
30+
31+
/**
32+
* Base plugin interface with lifecycle and dependency support
33+
*/
34+
export interface BasePlugin {
35+
/** Plugin metadata */
36+
readonly metadata: PluginMetadata;
37+
38+
/** Setup hook called during plugin initialization */
39+
setup?(runtime: any): void | Promise<void>;
40+
41+
/** Teardown hook called during plugin shutdown */
42+
teardown?(): void | Promise<void>;
43+
}
44+
45+
/**
46+
* Context provided to query processor plugins
47+
*/
48+
export interface QueryProcessorContext {
49+
/** The object being queried */
50+
objectName: string;
51+
/** Current user/session context */
52+
user?: {
53+
id: string | number;
54+
[key: string]: any;
55+
};
56+
/** Additional runtime context */
57+
[key: string]: any;
58+
}
59+
60+
/**
61+
* Plugin interface for query processing pipeline
62+
*/
63+
export interface QueryProcessorPlugin extends BasePlugin {
64+
metadata: PluginMetadata & { type: 'query_processor' };
65+
66+
/**
67+
* Validate query before execution
68+
* Can throw errors to reject the query
69+
*/
70+
validateQuery?(query: UnifiedQuery, context: QueryProcessorContext): void | Promise<void>;
71+
72+
/**
73+
* Transform query before execution
74+
* Returns modified query (async waterfall pattern)
75+
*/
76+
beforeQuery?(query: UnifiedQuery, context: QueryProcessorContext): UnifiedQuery | Promise<UnifiedQuery>;
77+
78+
/**
79+
* Process results after query execution
80+
* Returns modified results (async waterfall pattern)
81+
*/
82+
afterQuery?(results: any[], context: QueryProcessorContext): any[] | Promise<any[]>;
83+
}

packages/runtime/core/README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# @objectql/runtime-core
2+
3+
Runtime core package for ObjectQL - Plugin system, query pipeline, and runtime orchestration.
4+
5+
## Features
6+
7+
- **PluginManager**: Dependency resolution and plugin lifecycle management
8+
- **QueryPipeline**: Async series waterfall query processing
9+
- **Runtime Factory**: `createRuntime()` for runtime initialization
10+
11+
## Installation
12+
13+
```bash
14+
npm install @objectql/runtime-core @objectql/types
15+
```
16+
17+
## Usage
18+
19+
```typescript
20+
import { createRuntime } from '@objectql/runtime-core';
21+
import { BasePlugin } from '@objectql/types';
22+
23+
// Define a plugin
24+
const myPlugin: BasePlugin = {
25+
metadata: {
26+
name: 'my-plugin',
27+
version: '1.0.0',
28+
dependencies: []
29+
},
30+
async setup(runtime) {
31+
console.log('Plugin initialized');
32+
}
33+
};
34+
35+
// Create runtime instance
36+
const runtime = createRuntime({
37+
plugins: [myPlugin]
38+
});
39+
40+
// Initialize
41+
await runtime.init();
42+
43+
// Execute queries
44+
const results = await runtime.query('project', {
45+
filters: [['status', '=', 'active']]
46+
});
47+
```
48+
49+
## License
50+
51+
MIT
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = {
2+
preset: 'ts-jest',
3+
testEnvironment: 'node',
4+
roots: ['<rootDir>/test'],
5+
testMatch: ['**/*.test.ts'],
6+
collectCoverageFrom: ['src/**/*.ts'],
7+
};

packages/runtime/core/package.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "@objectql/runtime-core",
3+
"version": "3.0.1",
4+
"description": "ObjectQL runtime core - Plugin system, query pipeline, and runtime orchestration",
5+
"keywords": [
6+
"objectql",
7+
"runtime",
8+
"plugin",
9+
"pipeline",
10+
"query-processor",
11+
"dependency-injection",
12+
"orchestration"
13+
],
14+
"license": "MIT",
15+
"main": "dist/index.js",
16+
"types": "dist/index.d.ts",
17+
"scripts": {
18+
"build": "tsc",
19+
"test": "jest"
20+
},
21+
"dependencies": {
22+
"@objectql/types": "workspace:*"
23+
},
24+
"devDependencies": {
25+
"@types/node": "^20.10.0",
26+
"typescript": "^5.3.0"
27+
}
28+
}

packages/runtime/core/src/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* ObjectQL
3+
* Copyright (c) 2026-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
// Export main runtime factory
10+
export { createRuntime } from './runtime';
11+
export type { Runtime, RuntimeConfig, QueryExecutor } from './runtime';
12+
13+
// Export PluginManager
14+
export { PluginManager, PluginError } from './plugin-manager';
15+
16+
// Export QueryPipeline
17+
export { QueryPipeline, PipelineError } from './query-pipeline';
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/**
2+
* ObjectQL
3+
* Copyright (c) 2026-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { BasePlugin } from '@objectql/types';
10+
11+
/**
12+
* Error thrown when plugin operations fail
13+
*/
14+
export class PluginError extends Error {
15+
constructor(
16+
public code: 'DUPLICATE_PLUGIN' | 'MISSING_DEPENDENCY' | 'CIRCULAR_DEPENDENCY' | 'SETUP_FAILED',
17+
message: string,
18+
public pluginName?: string
19+
) {
20+
super(message);
21+
this.name = 'PluginError';
22+
}
23+
}
24+
25+
/**
26+
* PluginManager handles plugin registration, dependency resolution, and lifecycle
27+
*/
28+
export class PluginManager {
29+
private plugins: Map<string, BasePlugin> = new Map();
30+
private setupOrder: string[] = [];
31+
private isBooted = false;
32+
33+
/**
34+
* Register a plugin
35+
* @param plugin Plugin to register
36+
* @throws {PluginError} If plugin name is duplicated
37+
*/
38+
register(plugin: BasePlugin): void {
39+
const name = plugin.metadata.name;
40+
41+
if (this.plugins.has(name)) {
42+
throw new PluginError(
43+
'DUPLICATE_PLUGIN',
44+
`Plugin "${name}" is already registered`,
45+
name
46+
);
47+
}
48+
49+
this.plugins.set(name, plugin);
50+
this.setupOrder = []; // Invalidate cached order
51+
}
52+
53+
/**
54+
* Resolve plugin dependencies using topological sort
55+
* @returns Ordered list of plugin names (dependencies first)
56+
* @throws {PluginError} If there are missing dependencies or circular dependencies
57+
*/
58+
resolveDependencies(): string[] {
59+
if (this.setupOrder.length > 0) {
60+
return this.setupOrder;
61+
}
62+
63+
const visited = new Set<string>();
64+
const visiting = new Set<string>();
65+
const order: string[] = [];
66+
67+
const visit = (pluginName: string, path: string[] = []): void => {
68+
// Check if already processed
69+
if (visited.has(pluginName)) {
70+
return;
71+
}
72+
73+
// Check for circular dependency
74+
if (visiting.has(pluginName)) {
75+
const cycle = [...path, pluginName].join(' -> ');
76+
throw new PluginError(
77+
'CIRCULAR_DEPENDENCY',
78+
`Circular dependency detected: ${cycle}`,
79+
pluginName
80+
);
81+
}
82+
83+
const plugin = this.plugins.get(pluginName);
84+
if (!plugin) {
85+
throw new PluginError(
86+
'MISSING_DEPENDENCY',
87+
`Plugin "${pluginName}" is required but not registered`,
88+
pluginName
89+
);
90+
}
91+
92+
// Mark as being visited
93+
visiting.add(pluginName);
94+
95+
// Visit dependencies first
96+
const dependencies = plugin.metadata.dependencies || [];
97+
for (const dep of dependencies) {
98+
visit(dep, [...path, pluginName]);
99+
}
100+
101+
// Mark as visited and add to order
102+
visiting.delete(pluginName);
103+
visited.add(pluginName);
104+
order.push(pluginName);
105+
};
106+
107+
// Visit all plugins
108+
for (const pluginName of this.plugins.keys()) {
109+
visit(pluginName);
110+
}
111+
112+
this.setupOrder = order;
113+
return order;
114+
}
115+
116+
/**
117+
* Boot all plugins in dependency order
118+
* @param runtime Runtime instance to pass to setup hooks
119+
* @throws {PluginError} If setup fails for any plugin
120+
*/
121+
async boot(runtime: any): Promise<void> {
122+
if (this.isBooted) {
123+
return;
124+
}
125+
126+
const order = this.resolveDependencies();
127+
128+
for (const pluginName of order) {
129+
const plugin = this.plugins.get(pluginName);
130+
if (!plugin) {
131+
continue; // Should never happen after resolveDependencies
132+
}
133+
134+
if (plugin.setup) {
135+
try {
136+
await plugin.setup(runtime);
137+
} catch (error) {
138+
throw new PluginError(
139+
'SETUP_FAILED',
140+
`Failed to setup plugin "${pluginName}": ${error instanceof Error ? error.message : String(error)}`,
141+
pluginName
142+
);
143+
}
144+
}
145+
}
146+
147+
this.isBooted = true;
148+
}
149+
150+
/**
151+
* Shutdown all plugins in reverse dependency order
152+
*/
153+
async shutdown(): Promise<void> {
154+
if (!this.isBooted) {
155+
return;
156+
}
157+
158+
const order = [...this.setupOrder].reverse();
159+
160+
for (const pluginName of order) {
161+
const plugin = this.plugins.get(pluginName);
162+
if (plugin?.teardown) {
163+
try {
164+
await plugin.teardown();
165+
} catch (error) {
166+
// Log but don't throw during shutdown
167+
console.error(`Failed to teardown plugin "${pluginName}":`, error);
168+
}
169+
}
170+
}
171+
172+
this.isBooted = false;
173+
}
174+
175+
/**
176+
* Get a registered plugin by name
177+
*/
178+
get(name: string): BasePlugin | undefined {
179+
return this.plugins.get(name);
180+
}
181+
182+
/**
183+
* Get all registered plugins
184+
*/
185+
getAll(): BasePlugin[] {
186+
return Array.from(this.plugins.values());
187+
}
188+
189+
/**
190+
* Get plugins by type
191+
*/
192+
getByType(type: string): BasePlugin[] {
193+
return this.getAll().filter(p => p.metadata.type === type);
194+
}
195+
196+
/**
197+
* Check if booted
198+
*/
199+
isInitialized(): boolean {
200+
return this.isBooted;
201+
}
202+
}

0 commit comments

Comments
 (0)