@objectstack/runtime 1.0.0 → 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 +9 -0
- package/dist/http-dispatcher.d.ts +1 -1
- package/dist/http-dispatcher.js +25 -2
- package/dist/http-dispatcher.test.d.ts +1 -0
- package/dist/http-dispatcher.test.js +79 -0
- package/dist/rest-server.js +28 -0
- package/package.json +4 -4
- package/src/http-dispatcher.test.ts +107 -0
- package/src/http-dispatcher.ts +27 -2
- package/src/rest-server.ts +29 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @objectstack/runtime
|
|
2
2
|
|
|
3
|
+
## 1.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fix TypeScript error in http-dispatcher tests to resolve CI build failures.
|
|
8
|
+
- @objectstack/spec@1.0.1
|
|
9
|
+
- @objectstack/core@1.0.1
|
|
10
|
+
- @objectstack/types@1.0.1
|
|
11
|
+
|
|
3
12
|
## 1.0.0
|
|
4
13
|
|
|
5
14
|
### Major Changes
|
|
@@ -65,7 +65,7 @@ export declare class HttpDispatcher {
|
|
|
65
65
|
* Standard: /metadata/:type/:name
|
|
66
66
|
* Fallback for backward compat: /metadata (all objects), /metadata/:objectName (get object)
|
|
67
67
|
*/
|
|
68
|
-
handleMetadata(path: string, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
68
|
+
handleMetadata(path: string, context: HttpProtocolContext, method?: string, body?: any): Promise<HttpDispatcherResult>;
|
|
69
69
|
/**
|
|
70
70
|
* Handles Data requests
|
|
71
71
|
* path: sub-path after /data/ (e.g. "contacts", "contacts/123", "contacts/query")
|
package/dist/http-dispatcher.js
CHANGED
|
@@ -99,7 +99,7 @@ export class HttpDispatcher {
|
|
|
99
99
|
* Standard: /metadata/:type/:name
|
|
100
100
|
* Fallback for backward compat: /metadata (all objects), /metadata/:objectName (get object)
|
|
101
101
|
*/
|
|
102
|
-
async handleMetadata(path, context) {
|
|
102
|
+
async handleMetadata(path, context, method, body) {
|
|
103
103
|
const broker = this.ensureBroker();
|
|
104
104
|
const parts = path.replace(/^\/+/, '').split('/').filter(Boolean);
|
|
105
105
|
// GET /metadata/types
|
|
@@ -108,9 +108,32 @@ export class HttpDispatcher {
|
|
|
108
108
|
// For now we mock the types supported by core
|
|
109
109
|
return { handled: true, response: this.success({ types: ['objects', 'apps', 'plugins'] }) };
|
|
110
110
|
}
|
|
111
|
-
//
|
|
111
|
+
// /metadata/:type/:name
|
|
112
112
|
if (parts.length === 2) {
|
|
113
113
|
const [type, name] = parts;
|
|
114
|
+
// PUT /metadata/:type/:name (Save)
|
|
115
|
+
if (method === 'PUT' && body) {
|
|
116
|
+
// Try to get the protocol service directly
|
|
117
|
+
const protocol = this.kernel?.context?.getService ? this.kernel.context.getService('protocol') : null;
|
|
118
|
+
if (protocol && typeof protocol.saveMetaItem === 'function') {
|
|
119
|
+
try {
|
|
120
|
+
const result = await protocol.saveMetaItem({ type, name, item: body });
|
|
121
|
+
return { handled: true, response: this.success(result) };
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
return { handled: true, response: this.error(e.message, 400) };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Fallback to broker if protocol not available (legacy)
|
|
128
|
+
try {
|
|
129
|
+
const data = await broker.call('metadata.saveItem', { type, name, item: body }, { request: context.request });
|
|
130
|
+
return { handled: true, response: this.success(data) };
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
// If broker doesn't support it either
|
|
134
|
+
return { handled: true, response: this.error(e.message || 'Save not supported', 501) };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
114
137
|
try {
|
|
115
138
|
// Try specific calls based on type
|
|
116
139
|
if (type === 'objects') {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { HttpDispatcher } from './http-dispatcher.js';
|
|
3
|
+
describe('HttpDispatcher', () => {
|
|
4
|
+
let kernel;
|
|
5
|
+
let dispatcher;
|
|
6
|
+
let mockProtocol;
|
|
7
|
+
let mockBroker;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
// Mock Kernel
|
|
10
|
+
mockProtocol = {
|
|
11
|
+
saveMetaItem: vi.fn().mockResolvedValue({ success: true, message: 'Saved' }),
|
|
12
|
+
getMetaItem: vi.fn().mockResolvedValue({ success: true, item: { foo: 'bar' } })
|
|
13
|
+
};
|
|
14
|
+
mockBroker = {
|
|
15
|
+
call: vi.fn(),
|
|
16
|
+
};
|
|
17
|
+
kernel = {
|
|
18
|
+
broker: mockBroker,
|
|
19
|
+
context: {
|
|
20
|
+
getService: (name) => {
|
|
21
|
+
if (name === 'protocol')
|
|
22
|
+
return mockProtocol;
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
dispatcher = new HttpDispatcher(kernel);
|
|
28
|
+
});
|
|
29
|
+
describe('handleMetadata', () => {
|
|
30
|
+
it('should handle PUT /metadata/:type/:name by calling protocol.saveMetaItem', async () => {
|
|
31
|
+
const context = { request: {} };
|
|
32
|
+
const body = { label: 'New Label' };
|
|
33
|
+
const path = '/objects/my_obj';
|
|
34
|
+
const result = await dispatcher.handleMetadata(path, context, 'PUT', body);
|
|
35
|
+
expect(result.handled).toBe(true);
|
|
36
|
+
expect(result.response?.status).toBe(200);
|
|
37
|
+
expect(mockProtocol.saveMetaItem).toHaveBeenCalledWith({
|
|
38
|
+
type: 'objects',
|
|
39
|
+
name: 'my_obj',
|
|
40
|
+
item: body
|
|
41
|
+
});
|
|
42
|
+
expect(result.response?.body).toEqual({
|
|
43
|
+
success: true,
|
|
44
|
+
data: { success: true, message: 'Saved' },
|
|
45
|
+
meta: undefined
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
it('should fallback to broker call if protocol is missing saveMetaItem', async () => {
|
|
49
|
+
// Mock protocol without saveMetaItem
|
|
50
|
+
kernel.context.getService = () => ({});
|
|
51
|
+
// Mock broker success
|
|
52
|
+
mockBroker.call.mockResolvedValue({ success: true, fromBroker: true });
|
|
53
|
+
const context = { request: {} };
|
|
54
|
+
const body = { label: 'Fallback' };
|
|
55
|
+
const path = '/objects/my_obj';
|
|
56
|
+
const result = await dispatcher.handleMetadata(path, context, 'PUT', body);
|
|
57
|
+
expect(result.handled).toBe(true);
|
|
58
|
+
expect(mockBroker.call).toHaveBeenCalledWith('metadata.saveItem', { type: 'objects', name: 'my_obj', item: body }, { request: context.request });
|
|
59
|
+
expect(result.response?.body?.data).toEqual({ success: true, fromBroker: true });
|
|
60
|
+
});
|
|
61
|
+
it('should return error if save fails', async () => {
|
|
62
|
+
mockProtocol.saveMetaItem.mockRejectedValue(new Error('Save failed'));
|
|
63
|
+
const context = { request: {} };
|
|
64
|
+
const body = {};
|
|
65
|
+
const path = '/objects/bad_obj';
|
|
66
|
+
const result = await dispatcher.handleMetadata(path, context, 'PUT', body);
|
|
67
|
+
expect(result.handled).toBe(true);
|
|
68
|
+
expect(result.response?.status).toBe(400);
|
|
69
|
+
expect(result.response?.body?.error?.message).toBe('Save failed');
|
|
70
|
+
});
|
|
71
|
+
it('should handle READ operations as before', async () => {
|
|
72
|
+
mockBroker.call.mockResolvedValue({ name: 'my_obj' });
|
|
73
|
+
const context = { request: {} };
|
|
74
|
+
const result = await dispatcher.handleMetadata('/objects/my_obj', context, 'GET');
|
|
75
|
+
expect(result.handled).toBe(true);
|
|
76
|
+
expect(mockBroker.call).toHaveBeenCalledWith('metadata.getObject', { objectName: 'my_obj' }, { request: context.request });
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
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
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/runtime",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "ObjectStack Core Runtime & Query Engine",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@objectstack/core": "1.0.
|
|
11
|
-
"@objectstack/spec": "1.0.
|
|
12
|
-
"@objectstack/types": "1.0.
|
|
10
|
+
"@objectstack/core": "1.0.1",
|
|
11
|
+
"@objectstack/spec": "1.0.1",
|
|
12
|
+
"@objectstack/types": "1.0.1"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"typescript": "^5.0.0",
|
|
@@ -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
|
+
});
|
package/src/http-dispatcher.ts
CHANGED
|
@@ -131,7 +131,7 @@ export class HttpDispatcher {
|
|
|
131
131
|
* Standard: /metadata/:type/:name
|
|
132
132
|
* Fallback for backward compat: /metadata (all objects), /metadata/:objectName (get object)
|
|
133
133
|
*/
|
|
134
|
-
async handleMetadata(path: string, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
|
|
134
|
+
async handleMetadata(path: string, context: HttpProtocolContext, method?: string, body?: any): Promise<HttpDispatcherResult> {
|
|
135
135
|
const broker = this.ensureBroker();
|
|
136
136
|
const parts = path.replace(/^\/+/, '').split('/').filter(Boolean);
|
|
137
137
|
|
|
@@ -142,9 +142,34 @@ export class HttpDispatcher {
|
|
|
142
142
|
return { handled: true, response: this.success({ types: ['objects', 'apps', 'plugins'] }) };
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
//
|
|
145
|
+
// /metadata/:type/:name
|
|
146
146
|
if (parts.length === 2) {
|
|
147
147
|
const [type, name] = parts;
|
|
148
|
+
|
|
149
|
+
// PUT /metadata/:type/:name (Save)
|
|
150
|
+
if (method === 'PUT' && body) {
|
|
151
|
+
// Try to get the protocol service directly
|
|
152
|
+
const protocol = this.kernel?.context?.getService ? this.kernel.context.getService('protocol') : null;
|
|
153
|
+
|
|
154
|
+
if (protocol && typeof protocol.saveMetaItem === 'function') {
|
|
155
|
+
try {
|
|
156
|
+
const result = await protocol.saveMetaItem({ type, name, item: body });
|
|
157
|
+
return { handled: true, response: this.success(result) };
|
|
158
|
+
} catch (e: any) {
|
|
159
|
+
return { handled: true, response: this.error(e.message, 400) };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Fallback to broker if protocol not available (legacy)
|
|
164
|
+
try {
|
|
165
|
+
const data = await broker.call('metadata.saveItem', { type, name, item: body }, { request: context.request });
|
|
166
|
+
return { handled: true, response: this.success(data) };
|
|
167
|
+
} catch (e: any) {
|
|
168
|
+
// If broker doesn't support it either
|
|
169
|
+
return { handled: true, response: this.error(e.message || 'Save not supported', 501) };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
148
173
|
try {
|
|
149
174
|
// Try specific calls based on type
|
|
150
175
|
if (type === 'objects') {
|
package/src/rest-server.ts
CHANGED
|
@@ -333,6 +333,35 @@ export class RestServer {
|
|
|
333
333
|
},
|
|
334
334
|
});
|
|
335
335
|
}
|
|
336
|
+
|
|
337
|
+
// PUT /meta/:type/:name - Save metadata item
|
|
338
|
+
// We always register this route, but return 501 if protocol doesn't support it
|
|
339
|
+
// This makes it discoverable even if not implemented
|
|
340
|
+
this.routeManager.register({
|
|
341
|
+
method: 'PUT',
|
|
342
|
+
path: `${metaPath}/:type/:name`,
|
|
343
|
+
handler: async (req: any, res: any) => {
|
|
344
|
+
try {
|
|
345
|
+
if (!this.protocol.saveMetaItem) {
|
|
346
|
+
res.status(501).json({ error: 'Save operation not supported by protocol implementation' });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const result = await this.protocol.saveMetaItem({
|
|
351
|
+
type: req.params.type,
|
|
352
|
+
name: req.params.name,
|
|
353
|
+
item: req.body
|
|
354
|
+
});
|
|
355
|
+
res.json(result);
|
|
356
|
+
} catch (error: any) {
|
|
357
|
+
res.status(400).json({ error: error.message });
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
metadata: {
|
|
361
|
+
summary: 'Save specific metadata item',
|
|
362
|
+
tags: ['metadata'],
|
|
363
|
+
},
|
|
364
|
+
});
|
|
336
365
|
}
|
|
337
366
|
|
|
338
367
|
/**
|