@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 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")
@@ -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
- // GET /metadata/:type/:name
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
+ });
@@ -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.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.0",
11
- "@objectstack/spec": "1.0.0",
12
- "@objectstack/types": "1.0.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
+ });
@@ -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
- // GET /metadata/:type/:name
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') {
@@ -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
  /**