@objectstack/runtime 0.9.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/api-registry-plugin.d.ts +16 -0
- package/dist/api-registry-plugin.js +42 -0
- package/dist/app-plugin.d.ts +2 -2
- package/dist/app-plugin.js +61 -61
- package/dist/app-plugin.test.d.ts +1 -0
- package/dist/app-plugin.test.js +80 -0
- package/dist/driver-plugin.d.ts +2 -2
- package/dist/driver-plugin.js +14 -14
- package/dist/http-dispatcher.d.ts +106 -0
- package/dist/http-dispatcher.js +515 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/runtime.d.ts +45 -0
- package/dist/runtime.js +50 -0
- package/dist/runtime.test.d.ts +1 -0
- package/dist/runtime.test.js +57 -0
- package/package.json +9 -6
- package/src/api-registry-plugin.ts +58 -0
- package/src/app-plugin.test.ts +102 -0
- package/src/app-plugin.ts +2 -2
- package/src/driver-plugin.ts +2 -2
- package/src/http-dispatcher.ts +600 -0
- package/src/index.ts +8 -0
- package/src/runtime.test.ts +65 -0
- package/src/runtime.ts +78 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { Runtime } from './runtime';
|
|
3
|
+
// Mock ObjectKernel to isolate Runtime logic
|
|
4
|
+
vi.mock('@objectstack/core', async () => {
|
|
5
|
+
const actual = await vi.importActual('@objectstack/core');
|
|
6
|
+
return {
|
|
7
|
+
...actual,
|
|
8
|
+
ObjectKernel: class {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.use = vi.fn();
|
|
11
|
+
this.registerService = vi.fn();
|
|
12
|
+
this.bootstrap = vi.fn().mockResolvedValue(undefined);
|
|
13
|
+
this.getServices = vi.fn().mockReturnValue(new Map());
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
describe('Runtime', () => {
|
|
19
|
+
it('should initialize successfully', () => {
|
|
20
|
+
const runtime = new Runtime();
|
|
21
|
+
expect(runtime).toBeDefined();
|
|
22
|
+
// Should create a kernel
|
|
23
|
+
expect(runtime.getKernel()).toBeDefined();
|
|
24
|
+
});
|
|
25
|
+
it('should register api registry plugin by default', () => {
|
|
26
|
+
const runtime = new Runtime();
|
|
27
|
+
const kernel = runtime.getKernel();
|
|
28
|
+
// Check if use was called (at least once for api registry)
|
|
29
|
+
expect(kernel.use).toHaveBeenCalled();
|
|
30
|
+
});
|
|
31
|
+
it('should register external http server if provided', () => {
|
|
32
|
+
const mockServer = {
|
|
33
|
+
listen: vi.fn(),
|
|
34
|
+
close: vi.fn(),
|
|
35
|
+
get: vi.fn(),
|
|
36
|
+
post: vi.fn(),
|
|
37
|
+
put: vi.fn(),
|
|
38
|
+
delete: vi.fn(),
|
|
39
|
+
patch: vi.fn(),
|
|
40
|
+
use: vi.fn(),
|
|
41
|
+
};
|
|
42
|
+
const runtime = new Runtime({ server: mockServer });
|
|
43
|
+
const kernel = runtime.getKernel();
|
|
44
|
+
expect(kernel.registerService).toHaveBeenCalledWith('http.server', mockServer);
|
|
45
|
+
});
|
|
46
|
+
it('should delegate use() to kernel', () => {
|
|
47
|
+
const runtime = new Runtime();
|
|
48
|
+
const mockPlugin = { name: 'test', init: vi.fn() };
|
|
49
|
+
runtime.use(mockPlugin);
|
|
50
|
+
expect(runtime.getKernel().use).toHaveBeenCalledWith(mockPlugin);
|
|
51
|
+
});
|
|
52
|
+
it('should delegate start() to kernel.bootstrap()', async () => {
|
|
53
|
+
const runtime = new Runtime();
|
|
54
|
+
await runtime.start();
|
|
55
|
+
expect(runtime.getKernel().bootstrap).toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
});
|
package/package.json
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
4
5
|
"description": "ObjectStack Core Runtime & Query Engine",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "dist/index.js",
|
|
7
8
|
"types": "dist/index.d.ts",
|
|
8
9
|
"dependencies": {
|
|
9
|
-
"@objectstack/core": "0.
|
|
10
|
-
"@objectstack/spec": "0.
|
|
11
|
-
"@objectstack/types": "0.
|
|
10
|
+
"@objectstack/core": "1.0.0",
|
|
11
|
+
"@objectstack/spec": "1.0.0",
|
|
12
|
+
"@objectstack/types": "1.0.0"
|
|
12
13
|
},
|
|
13
14
|
"devDependencies": {
|
|
14
|
-
"typescript": "^5.0.0"
|
|
15
|
+
"typescript": "^5.0.0",
|
|
16
|
+
"vitest": "^4.0.18"
|
|
15
17
|
},
|
|
16
18
|
"scripts": {
|
|
17
19
|
"build": "tsc",
|
|
18
|
-
"dev": "tsc -w"
|
|
20
|
+
"dev": "tsc -w",
|
|
21
|
+
"test": "vitest run"
|
|
19
22
|
}
|
|
20
23
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Plugin, PluginContext, IHttpServer } from '@objectstack/core';
|
|
2
|
+
import { RestServer } from './rest-server.js';
|
|
3
|
+
import { ObjectStackProtocol, RestServerConfig } from '@objectstack/spec/api';
|
|
4
|
+
|
|
5
|
+
export interface ApiRegistryConfig {
|
|
6
|
+
serverServiceName?: string;
|
|
7
|
+
protocolServiceName?: string;
|
|
8
|
+
api?: RestServerConfig;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ApiRegistryPlugin
|
|
13
|
+
*
|
|
14
|
+
* Responsibilities:
|
|
15
|
+
* 1. Consumes 'http.server' (or configured service)
|
|
16
|
+
* 2. Consumes 'protocol' (ObjectStackProtocol)
|
|
17
|
+
* 3. Instantiates RestServer to auto-generate routes
|
|
18
|
+
*/
|
|
19
|
+
export function createApiRegistryPlugin(config: ApiRegistryConfig = {}): Plugin {
|
|
20
|
+
return {
|
|
21
|
+
name: 'com.objectstack.runtime.api-registry',
|
|
22
|
+
version: '1.0.0',
|
|
23
|
+
|
|
24
|
+
init: async (ctx: PluginContext) => {
|
|
25
|
+
// No service registration, this is a consumer plugin
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
start: async (ctx: PluginContext) => {
|
|
29
|
+
const serverService = config.serverServiceName || 'http.server';
|
|
30
|
+
const protocolService = config.protocolServiceName || 'protocol';
|
|
31
|
+
|
|
32
|
+
const server = ctx.getService<IHttpServer>(serverService);
|
|
33
|
+
const protocol = ctx.getService<ObjectStackProtocol>(protocolService);
|
|
34
|
+
|
|
35
|
+
if (!server) {
|
|
36
|
+
ctx.logger.warn(`ApiRegistryPlugin: HTTP Server service '${serverService}' not found. REST routes skipped.`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!protocol) {
|
|
41
|
+
ctx.logger.warn(`ApiRegistryPlugin: Protocol service '${protocolService}' not found. REST routes skipped.`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
ctx.logger.info('Hydrating REST API from Protocol...');
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const restServer = new RestServer(server, protocol, config.api as any);
|
|
49
|
+
restServer.registerRoutes();
|
|
50
|
+
|
|
51
|
+
ctx.logger.info('REST API successfully registered');
|
|
52
|
+
} catch (err: any) {
|
|
53
|
+
ctx.logger.error('Failed to register REST API routes', { error: err.message } as any);
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { AppPlugin } from './app-plugin';
|
|
3
|
+
import { PluginContext } from '@objectstack/core';
|
|
4
|
+
|
|
5
|
+
describe('AppPlugin', () => {
|
|
6
|
+
let mockContext: PluginContext;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
mockContext = {
|
|
10
|
+
logger: {
|
|
11
|
+
info: vi.fn(),
|
|
12
|
+
error: vi.fn(),
|
|
13
|
+
warn: vi.fn(),
|
|
14
|
+
debug: vi.fn()
|
|
15
|
+
},
|
|
16
|
+
registerService: vi.fn(),
|
|
17
|
+
getService: vi.fn(),
|
|
18
|
+
getServices: vi.fn()
|
|
19
|
+
} as unknown as PluginContext;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should initialize with manifest info', () => {
|
|
23
|
+
const bundle = {
|
|
24
|
+
id: 'com.test.app',
|
|
25
|
+
name: 'Test App',
|
|
26
|
+
version: '1.0.0'
|
|
27
|
+
};
|
|
28
|
+
const plugin = new AppPlugin(bundle);
|
|
29
|
+
expect(plugin.name).toBe('plugin.app.com.test.app');
|
|
30
|
+
expect(plugin.version).toBe('1.0.0');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should handle nested stack definition manifest', () => {
|
|
34
|
+
const bundle = {
|
|
35
|
+
manifest: {
|
|
36
|
+
id: 'com.test.stack',
|
|
37
|
+
version: '2.0.0'
|
|
38
|
+
},
|
|
39
|
+
objects: []
|
|
40
|
+
};
|
|
41
|
+
const plugin = new AppPlugin(bundle);
|
|
42
|
+
expect(plugin.name).toBe('plugin.app.com.test.stack');
|
|
43
|
+
expect(plugin.version).toBe('2.0.0');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('registerService should register raw manifest in init phase', async () => {
|
|
47
|
+
const bundle = {
|
|
48
|
+
id: 'com.test.simple',
|
|
49
|
+
objects: []
|
|
50
|
+
};
|
|
51
|
+
const plugin = new AppPlugin(bundle);
|
|
52
|
+
|
|
53
|
+
await plugin.init(mockContext);
|
|
54
|
+
|
|
55
|
+
expect(mockContext.registerService).toHaveBeenCalledWith('app.com.test.simple', bundle);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('start should do nothing if no runtime hooks', async () => {
|
|
59
|
+
const bundle = { id: 'com.test.static' };
|
|
60
|
+
const plugin = new AppPlugin(bundle);
|
|
61
|
+
|
|
62
|
+
vi.mocked(mockContext.getService).mockReturnValue({}); // Mock ObjectQL exists
|
|
63
|
+
|
|
64
|
+
await plugin.start!(mockContext);
|
|
65
|
+
// Only logs, no errors
|
|
66
|
+
expect(mockContext.logger.debug).toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('start should invoke onEnable if present', async () => {
|
|
70
|
+
const onEnableSpy = vi.fn();
|
|
71
|
+
const bundle = {
|
|
72
|
+
id: 'com.test.code',
|
|
73
|
+
onEnable: onEnableSpy
|
|
74
|
+
};
|
|
75
|
+
const plugin = new AppPlugin(bundle);
|
|
76
|
+
|
|
77
|
+
// Mock ObjectQL engine
|
|
78
|
+
const mockQL = { registry: {} };
|
|
79
|
+
vi.mocked(mockContext.getService).mockReturnValue(mockQL);
|
|
80
|
+
|
|
81
|
+
await plugin.start!(mockContext);
|
|
82
|
+
|
|
83
|
+
expect(onEnableSpy).toHaveBeenCalled();
|
|
84
|
+
// Check context passed to onEnable
|
|
85
|
+
const callArg = onEnableSpy.mock.calls[0][0];
|
|
86
|
+
expect(callArg.ql).toBe(mockQL);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('start should warn if objectql not found', async () => {
|
|
90
|
+
const bundle = { id: 'com.test.warn' };
|
|
91
|
+
const plugin = new AppPlugin(bundle);
|
|
92
|
+
|
|
93
|
+
vi.mocked(mockContext.getService).mockReturnValue(undefined); // No ObjectQL
|
|
94
|
+
|
|
95
|
+
await plugin.start!(mockContext);
|
|
96
|
+
|
|
97
|
+
expect(mockContext.logger.warn).toHaveBeenCalledWith(
|
|
98
|
+
expect.stringContaining('ObjectQL engine service not found'),
|
|
99
|
+
expect.any(Object)
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
});
|
package/src/app-plugin.ts
CHANGED
|
@@ -25,7 +25,7 @@ export class AppPlugin implements Plugin {
|
|
|
25
25
|
this.version = sys.version;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
async
|
|
28
|
+
init = async (ctx: PluginContext) => {
|
|
29
29
|
const sys = this.bundle.manifest || this.bundle;
|
|
30
30
|
const appId = sys.id || sys.name;
|
|
31
31
|
|
|
@@ -48,7 +48,7 @@ export class AppPlugin implements Plugin {
|
|
|
48
48
|
ctx.registerService(serviceName, servicePayload);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
async
|
|
51
|
+
start = async (ctx: PluginContext) => {
|
|
52
52
|
const sys = this.bundle.manifest || this.bundle;
|
|
53
53
|
const appId = sys.id || sys.name;
|
|
54
54
|
|
package/src/driver-plugin.ts
CHANGED
|
@@ -26,7 +26,7 @@ export class DriverPlugin implements Plugin {
|
|
|
26
26
|
this.name = `com.objectstack.driver.${driverName || driver.name || 'unknown'}`;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
async
|
|
29
|
+
init = async (ctx: PluginContext) => {
|
|
30
30
|
// Register driver as a service instead of directly to objectql
|
|
31
31
|
const serviceName = `driver.${this.driver.name || 'unknown'}`;
|
|
32
32
|
ctx.registerService(serviceName, this.driver);
|
|
@@ -37,7 +37,7 @@ export class DriverPlugin implements Plugin {
|
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
async
|
|
40
|
+
start = async (ctx: PluginContext) => {
|
|
41
41
|
// Drivers don't need start phase, initialization happens in init
|
|
42
42
|
ctx.logger.debug('Driver plugin started', { driverName: this.driver.name || 'unknown' });
|
|
43
43
|
}
|