@objectstack/runtime 0.9.2 → 1.0.1
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 +25 -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 +538 -0
- package/dist/http-dispatcher.test.d.ts +1 -0
- package/dist/http-dispatcher.test.js +79 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/rest-server.js +28 -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.test.ts +107 -0
- package/src/http-dispatcher.ts +625 -0
- package/src/index.ts +8 -0
- package/src/rest-server.ts +29 -0
- package/src/runtime.test.ts +65 -0
- package/src/runtime.ts +78 -0
package/dist/rest-server.js
CHANGED
|
@@ -250,6 +250,34 @@ export class RestServer {
|
|
|
250
250
|
},
|
|
251
251
|
});
|
|
252
252
|
}
|
|
253
|
+
// PUT /meta/:type/:name - Save metadata item
|
|
254
|
+
// We always register this route, but return 501 if protocol doesn't support it
|
|
255
|
+
// This makes it discoverable even if not implemented
|
|
256
|
+
this.routeManager.register({
|
|
257
|
+
method: 'PUT',
|
|
258
|
+
path: `${metaPath}/:type/:name`,
|
|
259
|
+
handler: async (req, res) => {
|
|
260
|
+
try {
|
|
261
|
+
if (!this.protocol.saveMetaItem) {
|
|
262
|
+
res.status(501).json({ error: 'Save operation not supported by protocol implementation' });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const result = await this.protocol.saveMetaItem({
|
|
266
|
+
type: req.params.type,
|
|
267
|
+
name: req.params.name,
|
|
268
|
+
item: req.body
|
|
269
|
+
});
|
|
270
|
+
res.json(result);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
res.status(400).json({ error: error.message });
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
metadata: {
|
|
277
|
+
summary: 'Save specific metadata item',
|
|
278
|
+
tags: ['metadata'],
|
|
279
|
+
},
|
|
280
|
+
});
|
|
253
281
|
}
|
|
254
282
|
/**
|
|
255
283
|
* Register CRUD endpoints for data operations
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ObjectKernel, Plugin, IHttpServer, ObjectKernelConfig } from '@objectstack/core';
|
|
2
|
+
import { ApiRegistryConfig } from './api-registry-plugin.js';
|
|
3
|
+
export interface RuntimeConfig {
|
|
4
|
+
/**
|
|
5
|
+
* Optional existing server instance (e.g. Hono, Express app)
|
|
6
|
+
* If provided, Runtime will use it as the 'http.server' service.
|
|
7
|
+
* If not provided, Runtime expects a server plugin (like HonoServerPlugin) to be registered manually.
|
|
8
|
+
*/
|
|
9
|
+
server?: IHttpServer;
|
|
10
|
+
/**
|
|
11
|
+
* API Registry Configuration
|
|
12
|
+
*/
|
|
13
|
+
api?: ApiRegistryConfig;
|
|
14
|
+
/**
|
|
15
|
+
* Kernel Configuration
|
|
16
|
+
*/
|
|
17
|
+
kernel?: ObjectKernelConfig;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* ObjectStack Runtime
|
|
21
|
+
*
|
|
22
|
+
* High-level entry point for bootstrapping an ObjectStack application.
|
|
23
|
+
* Wraps ObjectKernel and provides standard orchestration for:
|
|
24
|
+
* - HTTP Server binding
|
|
25
|
+
* - API Registry (REST Routes)
|
|
26
|
+
* - Plugin Management
|
|
27
|
+
*/
|
|
28
|
+
export declare class Runtime {
|
|
29
|
+
readonly kernel: ObjectKernel;
|
|
30
|
+
constructor(config?: RuntimeConfig);
|
|
31
|
+
/**
|
|
32
|
+
* Register a plugin
|
|
33
|
+
*/
|
|
34
|
+
use(plugin: Plugin): this;
|
|
35
|
+
/**
|
|
36
|
+
* Start the runtime
|
|
37
|
+
* 1. Initializes all plugins (init phase)
|
|
38
|
+
* 2. Starts all plugins (start phase)
|
|
39
|
+
*/
|
|
40
|
+
start(): Promise<this>;
|
|
41
|
+
/**
|
|
42
|
+
* Get the kernel instance
|
|
43
|
+
*/
|
|
44
|
+
getKernel(): ObjectKernel;
|
|
45
|
+
}
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ObjectKernel } from '@objectstack/core';
|
|
2
|
+
import { createApiRegistryPlugin } from './api-registry-plugin.js';
|
|
3
|
+
/**
|
|
4
|
+
* ObjectStack Runtime
|
|
5
|
+
*
|
|
6
|
+
* High-level entry point for bootstrapping an ObjectStack application.
|
|
7
|
+
* Wraps ObjectKernel and provides standard orchestration for:
|
|
8
|
+
* - HTTP Server binding
|
|
9
|
+
* - API Registry (REST Routes)
|
|
10
|
+
* - Plugin Management
|
|
11
|
+
*/
|
|
12
|
+
export class Runtime {
|
|
13
|
+
constructor(config = {}) {
|
|
14
|
+
this.kernel = new ObjectKernel(config.kernel);
|
|
15
|
+
// If external server provided, register it immediately
|
|
16
|
+
if (config.server) {
|
|
17
|
+
// If the provided server is not already an HttpServer wrapper, wrap it?
|
|
18
|
+
// Since IHttpServer is the interface, we assume it complies.
|
|
19
|
+
// But HttpServer class in runtime is an adapter.
|
|
20
|
+
// If user passes raw Hono, it won't work unless they wrapped it.
|
|
21
|
+
// We'll assume they pass a compliant IHttpServer.
|
|
22
|
+
this.kernel.registerService('http.server', config.server);
|
|
23
|
+
}
|
|
24
|
+
// Register API Registry by default
|
|
25
|
+
// This plugin is passive (wait for services) so it's safe to add early.
|
|
26
|
+
this.kernel.use(createApiRegistryPlugin(config.api));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Register a plugin
|
|
30
|
+
*/
|
|
31
|
+
use(plugin) {
|
|
32
|
+
this.kernel.use(plugin);
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Start the runtime
|
|
37
|
+
* 1. Initializes all plugins (init phase)
|
|
38
|
+
* 2. Starts all plugins (start phase)
|
|
39
|
+
*/
|
|
40
|
+
async start() {
|
|
41
|
+
await this.kernel.bootstrap();
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get the kernel instance
|
|
46
|
+
*/
|
|
47
|
+
getKernel() {
|
|
48
|
+
return this.kernel;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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.1",
|
|
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.1",
|
|
11
|
+
"@objectstack/spec": "1.0.1",
|
|
12
|
+
"@objectstack/types": "1.0.1"
|
|
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
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { HttpDispatcher } from './http-dispatcher.js';
|
|
4
|
+
import { ObjectKernel } from '@objectstack/core';
|
|
5
|
+
|
|
6
|
+
describe('HttpDispatcher', () => {
|
|
7
|
+
let kernel: ObjectKernel;
|
|
8
|
+
let dispatcher: HttpDispatcher;
|
|
9
|
+
let mockProtocol: any;
|
|
10
|
+
let mockBroker: any;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// Mock Kernel
|
|
14
|
+
mockProtocol = {
|
|
15
|
+
saveMetaItem: vi.fn().mockResolvedValue({ success: true, message: 'Saved' }),
|
|
16
|
+
getMetaItem: vi.fn().mockResolvedValue({ success: true, item: { foo: 'bar' } })
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
mockBroker = {
|
|
20
|
+
call: vi.fn(),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
kernel = {
|
|
24
|
+
broker: mockBroker,
|
|
25
|
+
context: {
|
|
26
|
+
getService: (name: string) => {
|
|
27
|
+
if (name === 'protocol') return mockProtocol;
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} as any;
|
|
32
|
+
|
|
33
|
+
dispatcher = new HttpDispatcher(kernel);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('handleMetadata', () => {
|
|
37
|
+
it('should handle PUT /metadata/:type/:name by calling protocol.saveMetaItem', async () => {
|
|
38
|
+
const context = { request: {} };
|
|
39
|
+
const body = { label: 'New Label' };
|
|
40
|
+
const path = '/objects/my_obj';
|
|
41
|
+
|
|
42
|
+
const result = await dispatcher.handleMetadata(path, context, 'PUT', body);
|
|
43
|
+
|
|
44
|
+
expect(result.handled).toBe(true);
|
|
45
|
+
expect(result.response?.status).toBe(200);
|
|
46
|
+
expect(mockProtocol.saveMetaItem).toHaveBeenCalledWith({
|
|
47
|
+
type: 'objects',
|
|
48
|
+
name: 'my_obj',
|
|
49
|
+
item: body
|
|
50
|
+
});
|
|
51
|
+
expect(result.response?.body).toEqual({
|
|
52
|
+
success: true,
|
|
53
|
+
data: { success: true, message: 'Saved' },
|
|
54
|
+
meta: undefined
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should fallback to broker call if protocol is missing saveMetaItem', async () => {
|
|
59
|
+
// Mock protocol without saveMetaItem
|
|
60
|
+
(kernel as any).context.getService = () => ({});
|
|
61
|
+
// Mock broker success
|
|
62
|
+
mockBroker.call.mockResolvedValue({ success: true, fromBroker: true });
|
|
63
|
+
|
|
64
|
+
const context = { request: {} };
|
|
65
|
+
const body = { label: 'Fallback' };
|
|
66
|
+
const path = '/objects/my_obj';
|
|
67
|
+
|
|
68
|
+
const result = await dispatcher.handleMetadata(path, context, 'PUT', body);
|
|
69
|
+
|
|
70
|
+
expect(result.handled).toBe(true);
|
|
71
|
+
expect(mockBroker.call).toHaveBeenCalledWith(
|
|
72
|
+
'metadata.saveItem',
|
|
73
|
+
{ type: 'objects', name: 'my_obj', item: body },
|
|
74
|
+
{ request: context.request }
|
|
75
|
+
);
|
|
76
|
+
expect(result.response?.body?.data).toEqual({ success: true, fromBroker: true });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should return error if save fails', async () => {
|
|
80
|
+
mockProtocol.saveMetaItem.mockRejectedValue(new Error('Save failed'));
|
|
81
|
+
|
|
82
|
+
const context = { request: {} };
|
|
83
|
+
const body = {};
|
|
84
|
+
const path = '/objects/bad_obj';
|
|
85
|
+
|
|
86
|
+
const result = await dispatcher.handleMetadata(path, context, 'PUT', body);
|
|
87
|
+
|
|
88
|
+
expect(result.handled).toBe(true);
|
|
89
|
+
expect(result.response?.status).toBe(400);
|
|
90
|
+
expect(result.response?.body?.error?.message).toBe('Save failed');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should handle READ operations as before', async () => {
|
|
94
|
+
mockBroker.call.mockResolvedValue({ name: 'my_obj' });
|
|
95
|
+
|
|
96
|
+
const context = { request: {} };
|
|
97
|
+
const result = await dispatcher.handleMetadata('/objects/my_obj', context, 'GET');
|
|
98
|
+
|
|
99
|
+
expect(result.handled).toBe(true);
|
|
100
|
+
expect(mockBroker.call).toHaveBeenCalledWith(
|
|
101
|
+
'metadata.getObject',
|
|
102
|
+
{ objectName: 'my_obj' },
|
|
103
|
+
{ request: context.request }
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|