@objectstack/runtime 4.0.2 → 4.0.4

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.
@@ -7,24 +7,35 @@ describe('HttpDispatcher', () => {
7
7
  let kernel: ObjectKernel;
8
8
  let dispatcher: HttpDispatcher;
9
9
  let mockProtocol: any;
10
- let mockBroker: any;
10
+ let mockObjectQL: any;
11
11
 
12
12
  beforeEach(() => {
13
13
  // Mock Kernel
14
14
  mockProtocol = {
15
15
  saveMetaItem: vi.fn().mockResolvedValue({ success: true, message: 'Saved' }),
16
- getMetaItem: vi.fn().mockResolvedValue({ success: true, item: { foo: 'bar' } })
16
+ getMetaItem: vi.fn().mockResolvedValue({ success: true, item: { foo: 'bar' } }),
17
+ findData: vi.fn().mockResolvedValue({ object: 'test', records: [], total: 0 }),
18
+ getData: vi.fn().mockResolvedValue({ object: 'test', id: '1', record: {} }),
17
19
  };
18
-
19
- mockBroker = {
20
- call: vi.fn(),
20
+
21
+ mockObjectQL = {
22
+ insert: vi.fn().mockResolvedValue({ id: 'new_1' }),
23
+ find: vi.fn().mockResolvedValue([]),
24
+ update: vi.fn().mockResolvedValue({}),
25
+ delete: vi.fn().mockResolvedValue({}),
26
+ getObjects: vi.fn().mockReturnValue({}),
27
+ registry: {
28
+ getObject: vi.fn().mockReturnValue({ name: 'test_obj' }),
29
+ getRegisteredTypes: vi.fn().mockReturnValue([]),
30
+ getAllPackages: vi.fn().mockReturnValue([]),
31
+ },
21
32
  };
22
33
 
23
34
  kernel = {
24
- broker: mockBroker,
25
35
  context: {
26
36
  getService: (name: string) => {
27
37
  if (name === 'protocol') return mockProtocol;
38
+ if (name === 'objectql') return mockObjectQL;
28
39
  return null;
29
40
  }
30
41
  }
@@ -55,11 +66,17 @@ describe('HttpDispatcher', () => {
55
66
  });
56
67
  });
57
68
 
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 });
69
+ it('should fallback to MetadataService when protocol is missing saveMetaItem', async () => {
70
+ // Mock protocol without saveMetaItem, but MetadataService with saveItem
71
+ const mockMetaSvc = {
72
+ saveItem: vi.fn().mockResolvedValue({ success: true, fromMetaSvc: true }),
73
+ };
74
+ (kernel as any).context.getService = (name: string) => {
75
+ if (name === 'protocol') return {};
76
+ if (name === 'metadata') return mockMetaSvc;
77
+ if (name === 'objectql') return mockObjectQL;
78
+ return null;
79
+ };
63
80
 
64
81
  const context = { request: {} };
65
82
  const body = { label: 'Fallback' };
@@ -68,12 +85,8 @@ describe('HttpDispatcher', () => {
68
85
  const result = await dispatcher.handleMetadata(path, context, 'PUT', body);
69
86
 
70
87
  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 });
88
+ expect(mockMetaSvc.saveItem).toHaveBeenCalledWith('objects', 'my_obj', body);
89
+ expect(result.response?.body?.data).toEqual({ success: true, fromMetaSvc: true });
77
90
  });
78
91
 
79
92
  it('should return error if save fails', async () => {
@@ -90,18 +103,14 @@ describe('HttpDispatcher', () => {
90
103
  expect(result.response?.body?.error?.message).toBe('Save failed');
91
104
  });
92
105
 
93
- it('should handle READ operations as before', async () => {
94
- mockBroker.call.mockResolvedValue({ name: 'my_obj' });
106
+ it('should handle READ operations via ObjectQL registry', async () => {
107
+ mockObjectQL.registry.getObject.mockReturnValue({ name: 'my_obj', fields: {} });
95
108
 
96
109
  const context = { request: {} };
97
110
  const result = await dispatcher.handleMetadata('/objects/my_obj', context, 'GET');
98
111
 
99
112
  expect(result.handled).toBe(true);
100
- expect(mockBroker.call).toHaveBeenCalledWith(
101
- 'metadata.getObject',
102
- { objectName: 'my_obj' },
103
- { request: context.request }
104
- );
113
+ expect(mockObjectQL.registry.getObject).toHaveBeenCalledWith('my_obj');
105
114
  });
106
115
  });
107
116
 
@@ -287,13 +296,14 @@ describe('HttpDispatcher', () => {
287
296
  expect(mockAuth.handler).toHaveBeenCalled();
288
297
  });
289
298
 
290
- it('should fallback to legacy login when async auth service has no handler', async () => {
299
+ it('should fallback to mock auth when async auth service has no handler', async () => {
291
300
  (kernel as any).getService = vi.fn().mockResolvedValue({});
292
- mockBroker.call.mockResolvedValue({ token: 'abc' });
293
301
 
294
- const result = await dispatcher.handleAuth('/login', 'POST', { user: 'a' }, { request: {} });
302
+ const result = await dispatcher.handleAuth('/login', 'POST', { email: 'test@example.com' }, { request: {} });
295
303
  expect(result.handled).toBe(true);
296
- expect(mockBroker.call).toHaveBeenCalledWith('auth.login', { user: 'a' }, { request: {} });
304
+ // Falls through to mock auth fallback (sign-in behavior)
305
+ expect(result.response?.status).toBe(200);
306
+ expect(result.response?.body?.user).toBeDefined();
297
307
  });
298
308
 
299
309
  it('should return unhandled when auth service not registered and no legacy match', async () => {
@@ -307,10 +317,9 @@ describe('HttpDispatcher', () => {
307
317
 
308
318
  describe('handleAuth mock fallback (MSW/test mode)', () => {
309
319
  beforeEach(() => {
310
- // No auth service, no broker — simulates MSW/mock mode
320
+ // No auth service — simulates MSW/mock mode
311
321
  (kernel as any).getService = vi.fn().mockResolvedValue(null);
312
322
  (kernel as any).services = new Map();
313
- (kernel as any).broker = null;
314
323
  });
315
324
 
316
325
  it('should mock sign-up/email endpoint', async () => {
@@ -344,7 +353,7 @@ describe('HttpDispatcher', () => {
344
353
  expect(result.response?.body).toEqual({ success: true });
345
354
  });
346
355
 
347
- it('should mock login fallback when broker unavailable', async () => {
356
+ it('should mock login fallback when no auth service registered', async () => {
348
357
  const result = await dispatcher.handleAuth('/login', 'POST', { email: 'test@example.com' }, { request: {} });
349
358
  expect(result.handled).toBe(true);
350
359
  expect(result.response?.status).toBe(200);
@@ -449,17 +458,16 @@ describe('HttpDispatcher', () => {
449
458
  expect(asyncProtocol.saveMetaItem).toHaveBeenCalled();
450
459
  });
451
460
 
452
- it('should fallback to broker when async protocol returns null', async () => {
453
- (kernel as any).context.getService = vi.fn().mockResolvedValue(null);
454
- mockBroker.call.mockResolvedValue({ name: 'my_obj' });
461
+ it('should fallback to ObjectQL registry when async protocol returns null', async () => {
462
+ (kernel as any).context.getService = vi.fn().mockImplementation((name: string) => {
463
+ if (name === 'objectql') return mockObjectQL;
464
+ return null;
465
+ });
466
+ mockObjectQL.registry.getObject.mockReturnValue({ name: 'my_obj', fields: {} });
455
467
 
456
468
  const result = await dispatcher.handleMetadata('/objects/my_obj', { request: {} }, 'GET');
457
469
  expect(result.handled).toBe(true);
458
- expect(mockBroker.call).toHaveBeenCalledWith(
459
- 'metadata.getObject',
460
- { objectName: 'my_obj' },
461
- { request: {} }
462
- );
470
+ expect(mockObjectQL.registry.getObject).toHaveBeenCalledWith('my_obj');
463
471
  });
464
472
  });
465
473
  });
@@ -599,8 +607,8 @@ describe('HttpDispatcher', () => {
599
607
  // ═══════════════════════════════════════════════════════════════
600
608
 
601
609
  describe('handleData', () => {
602
- it('should pass expand and select to broker for GET /data/:object/:id', async () => {
603
- mockBroker.call.mockResolvedValue({ object: 'order_item', id: 'oi_1', record: { id: 'oi_1' } });
610
+ it('should pass expand and select to protocol for GET /data/:object/:id', async () => {
611
+ mockProtocol.getData.mockResolvedValue({ object: 'order_item', id: 'oi_1', record: { id: 'oi_1' } });
604
612
 
605
613
  const result = await dispatcher.handleData(
606
614
  '/order_item/oi_1', 'GET', {},
@@ -610,15 +618,13 @@ describe('HttpDispatcher', () => {
610
618
 
611
619
  expect(result.handled).toBe(true);
612
620
  expect(result.response?.status).toBe(200);
613
- expect(mockBroker.call).toHaveBeenCalledWith(
614
- 'data.get',
615
- { object: 'order_item', id: 'oi_1', expand: 'order,product', select: 'name,total' },
616
- { request: {} }
621
+ expect(mockProtocol.getData).toHaveBeenCalledWith(
622
+ { object: 'order_item', id: 'oi_1', expand: 'order,product', select: 'name,total' }
617
623
  );
618
624
  });
619
625
 
620
626
  it('should NOT pass non-allowlisted params for GET /data/:object/:id', async () => {
621
- mockBroker.call.mockResolvedValue({ object: 'task', id: 't1', record: {} });
627
+ mockProtocol.getData.mockResolvedValue({ object: 'task', id: 't1', record: {} });
622
628
 
623
629
  await dispatcher.handleData(
624
630
  '/task/t1', 'GET', {},
@@ -627,15 +633,13 @@ describe('HttpDispatcher', () => {
627
633
  );
628
634
 
629
635
  // Only expand is passed; malicious and filter are dropped
630
- expect(mockBroker.call).toHaveBeenCalledWith(
631
- 'data.get',
632
- { object: 'task', id: 't1', expand: 'assignee' },
633
- { request: {} }
636
+ expect(mockProtocol.getData).toHaveBeenCalledWith(
637
+ { object: 'task', id: 't1', expand: 'assignee' }
634
638
  );
635
639
  });
636
640
 
637
641
  it('should pass full query (with expand/populate) for GET /data/:object list', async () => {
638
- mockBroker.call.mockResolvedValue({ object: 'task', records: [], total: 0 });
642
+ mockProtocol.findData.mockResolvedValue({ object: 'task', records: [], total: 0 });
639
643
 
640
644
  const query = { populate: 'assignee,project', top: '10', skip: '0' };
641
645
  const result = await dispatcher.handleData(
@@ -646,23 +650,19 @@ describe('HttpDispatcher', () => {
646
650
 
647
651
  expect(result.handled).toBe(true);
648
652
  // top → limit and skip → offset are normalized by the dispatcher
649
- expect(mockBroker.call).toHaveBeenCalledWith(
650
- 'data.query',
651
- { object: 'task', query: { populate: 'assignee,project', limit: '10', offset: '0' } },
652
- { request: {} }
653
+ expect(mockProtocol.findData).toHaveBeenCalledWith(
654
+ { object: 'task', query: { populate: 'assignee,project', limit: '10', offset: '0' } }
653
655
  );
654
656
  });
655
657
 
656
658
  it('should pass expand in query for GET /data/:object list', async () => {
657
- mockBroker.call.mockResolvedValue({ object: 'order', records: [], total: 0 });
659
+ mockProtocol.findData.mockResolvedValue({ object: 'order', records: [], total: 0 });
658
660
 
659
661
  const query = { expand: 'customer,products' };
660
662
  await dispatcher.handleData('/order', 'GET', {}, query, { request: {} });
661
663
 
662
- expect(mockBroker.call).toHaveBeenCalledWith(
663
- 'data.query',
664
- { object: 'order', query: { expand: 'customer,products' } },
665
- { request: {} }
664
+ expect(mockProtocol.findData).toHaveBeenCalledWith(
665
+ { object: 'order', query: { expand: 'customer,products' } }
666
666
  );
667
667
  });
668
668
 
@@ -673,7 +673,7 @@ describe('HttpDispatcher', () => {
673
673
  });
674
674
 
675
675
  it('should handle POST /data/:object/query with body containing expand', async () => {
676
- mockBroker.call.mockResolvedValue({ object: 'task', records: [] });
676
+ mockProtocol.findData.mockResolvedValue({ object: 'task', records: [] });
677
677
 
678
678
  await dispatcher.handleData(
679
679
  '/task/query', 'POST',
@@ -682,10 +682,8 @@ describe('HttpDispatcher', () => {
682
682
  { request: {} }
683
683
  );
684
684
 
685
- expect(mockBroker.call).toHaveBeenCalledWith(
686
- 'data.query',
687
- { object: 'task', filter: { status: 'active' }, populate: ['assignee'] },
688
- { request: {} }
685
+ expect(mockProtocol.findData).toHaveBeenCalledWith(
686
+ { object: 'task', query: { filter: { status: 'active' }, populate: ['assignee'] } }
689
687
  );
690
688
  });
691
689
  });
@@ -774,7 +772,7 @@ describe('HttpDispatcher', () => {
774
772
  expect(mockMetadata.revertPackage).toHaveBeenCalledWith('com.acme.crm');
775
773
  });
776
774
 
777
- it('should fallback to broker for publish when metadata service unavailable', async () => {
775
+ it('should return 503 for publish when metadata service unavailable', async () => {
778
776
  const mockRegistry = {
779
777
  getAllPackages: vi.fn().mockReturnValue([]),
780
778
  };
@@ -783,11 +781,10 @@ describe('HttpDispatcher', () => {
783
781
  if (name === 'objectql') return Promise.resolve({ registry: mockRegistry });
784
782
  return null;
785
783
  });
786
- mockBroker.call.mockResolvedValue({ success: true, packageId: 'crm', version: 1, publishedAt: '2025-01-01T00:00:00Z', itemsPublished: 2 });
787
784
 
788
785
  const result = await dispatcher.handlePackages('/crm/publish', 'POST', {}, {}, { request: {} });
789
786
  expect(result.handled).toBe(true);
790
- expect(mockBroker.call).toHaveBeenCalledWith('metadata.publishPackage', { packageId: 'crm' }, { request: {} });
787
+ expect(result.response?.status).toBe(503);
791
788
  });
792
789
  });
793
790
 
@@ -826,18 +823,27 @@ describe('HttpDispatcher', () => {
826
823
  expect(result.response?.status).toBe(404);
827
824
  });
828
825
 
829
- it('should fallback to broker for getPublished when metadata service unavailable', async () => {
830
- (kernel as any).getService = vi.fn().mockResolvedValue(null);
831
- mockBroker.call.mockResolvedValue({ name: 'account', fields: ['name'] });
826
+ it('should fallback to resolveService for getPublished when metadata service unavailable', async () => {
827
+ const metaSvc = {
828
+ getPublished: vi.fn().mockResolvedValue({ name: 'account', fields: ['name'] }),
829
+ };
830
+ (kernel as any).getService = vi.fn().mockImplementation((name: string) => {
831
+ if (name === 'metadata') return Promise.resolve(metaSvc);
832
+ if (name === 'objectql') return Promise.resolve(mockObjectQL);
833
+ return null;
834
+ });
835
+ (kernel as any).context = {
836
+ getService: (name: string) => {
837
+ if (name === 'metadata') return metaSvc;
838
+ if (name === 'objectql') return mockObjectQL;
839
+ return null;
840
+ }
841
+ };
832
842
 
833
843
  const result = await dispatcher.handleMetadata('/object/account/published', { request: {} }, 'GET');
834
844
  expect(result.handled).toBe(true);
835
845
  expect(result.response?.status).toBe(200);
836
- expect(mockBroker.call).toHaveBeenCalledWith(
837
- 'metadata.getPublished',
838
- { type: 'object', name: 'account' },
839
- { request: {} }
840
- );
846
+ expect(metaSvc.getPublished).toHaveBeenCalledWith('object', 'account');
841
847
  });
842
848
  });
843
849
 
@@ -1210,99 +1216,98 @@ describe('HttpDispatcher', () => {
1210
1216
  });
1211
1217
  });
1212
1218
 
1213
- describe('handleMetadata without broker (serverless degradation)', () => {
1214
- let brokerlessKernel: any;
1215
- let brokerlessDispatcher: HttpDispatcher;
1219
+ describe('handleMetadata with minimal kernel (serverless/lightweight)', () => {
1220
+ let minimalKernel: any;
1221
+ let minimalDispatcher: HttpDispatcher;
1216
1222
 
1217
1223
  beforeEach(() => {
1218
- // Kernel with NO broker — simulates a lightweight/serverless setup
1224
+ // Minimal kernel — simulates a lightweight/serverless setup
1219
1225
  // where only the protocol service and/or ObjectQL registry are available.
1220
- brokerlessKernel = {
1221
- broker: null,
1226
+ minimalKernel = {
1222
1227
  context: {
1223
1228
  getService: vi.fn().mockReturnValue(null),
1224
1229
  },
1225
1230
  };
1226
- brokerlessDispatcher = new HttpDispatcher(brokerlessKernel);
1231
+ minimalDispatcher = new HttpDispatcher(minimalKernel);
1227
1232
  });
1228
1233
 
1229
- it('GET /meta should return default types when broker is missing', async () => {
1234
+ it('GET /meta should return default types with minimal kernel', async () => {
1230
1235
  const context = { request: {} };
1231
- const result = await brokerlessDispatcher.handleMetadata('', context, 'GET');
1236
+ const result = await minimalDispatcher.handleMetadata('', context, 'GET');
1232
1237
  expect(result.handled).toBe(true);
1233
1238
  expect(result.response?.status).toBe(200);
1234
1239
  expect(result.response?.body?.data?.types).toContain('object');
1235
1240
  });
1236
1241
 
1237
- it('GET /meta/types should return default types when broker is missing', async () => {
1242
+ it('GET /meta/types should return default types with minimal kernel', async () => {
1238
1243
  const context = { request: {} };
1239
- const result = await brokerlessDispatcher.handleMetadata('/types', context, 'GET');
1244
+ const result = await minimalDispatcher.handleMetadata('/types', context, 'GET');
1240
1245
  expect(result.handled).toBe(true);
1241
1246
  expect(result.response?.status).toBe(200);
1242
1247
  expect(result.response?.body?.data?.types).toContain('object');
1243
1248
  });
1244
1249
 
1245
- it('GET /meta/objects should use ObjectQL registry when broker is missing', async () => {
1250
+ it('GET /meta/objects should use ObjectQL registry', async () => {
1246
1251
  const mockRegistry = {
1247
1252
  getAllObjects: vi.fn().mockReturnValue([{ name: 'account' }]),
1248
1253
  getObject: vi.fn(),
1249
1254
  };
1250
- brokerlessKernel.context.getService = vi.fn().mockImplementation((name: string) => {
1255
+ minimalKernel.context.getService = vi.fn().mockImplementation((name: string) => {
1251
1256
  if (name === 'objectql') return { registry: mockRegistry };
1252
1257
  return null;
1253
1258
  });
1254
1259
 
1255
1260
  const context = { request: {} };
1256
- const result = await brokerlessDispatcher.handleMetadata('/objects', context, 'GET');
1261
+ const result = await minimalDispatcher.handleMetadata('/objects', context, 'GET');
1257
1262
  expect(result.handled).toBe(true);
1258
1263
  expect(result.response?.status).toBe(200);
1259
1264
  expect(mockRegistry.getAllObjects).toHaveBeenCalled();
1260
1265
  });
1261
1266
 
1262
- it('GET /meta/objects/:name should use ObjectQL registry when broker is missing', async () => {
1267
+ it('GET /meta/objects/:name should use ObjectQL registry', async () => {
1263
1268
  const mockRegistry = {
1264
1269
  registry: {
1265
1270
  getObject: vi.fn().mockReturnValue({ name: 'account', fields: {} }),
1266
1271
  },
1267
1272
  };
1268
- brokerlessKernel.context.getService = vi.fn().mockImplementation((name: string) => {
1273
+ minimalKernel.context.getService = vi.fn().mockImplementation((name: string) => {
1269
1274
  if (name === 'objectql') return mockRegistry;
1270
1275
  return null;
1271
1276
  });
1272
1277
 
1273
1278
  const context = { request: {} };
1274
- const result = await brokerlessDispatcher.handleMetadata('/objects/account', context, 'GET');
1279
+ const result = await minimalDispatcher.handleMetadata('/objects/account', context, 'GET');
1275
1280
  expect(result.handled).toBe(true);
1276
1281
  expect(result.response?.status).toBe(200);
1277
1282
  expect(mockRegistry.registry.getObject).toHaveBeenCalledWith('account');
1278
1283
  });
1279
1284
 
1280
- it('GET /meta/:type/:name/published should return 404 when broker is missing and metadata service is unavailable', async () => {
1285
+ it('GET /meta/:type/:name/published should return 404 when metadata service is unavailable', async () => {
1281
1286
  const context = { request: {} };
1282
- const result = await brokerlessDispatcher.handleMetadata('/object/my_obj/published', context, 'GET');
1287
+ const result = await minimalDispatcher.handleMetadata('/object/my_obj/published', context, 'GET');
1283
1288
  expect(result.handled).toBe(true);
1284
1289
  expect(result.response?.status).toBe(404);
1285
1290
  });
1286
1291
 
1287
- it('PUT /meta/:type/:name should return 501 when broker is missing and protocol is unavailable', async () => {
1292
+ it('PUT /meta/:type/:name should return 501 when protocol is unavailable', async () => {
1288
1293
  const context = { request: {} };
1289
1294
  const body = { label: 'Test' };
1290
- const result = await brokerlessDispatcher.handleMetadata('/objects/my_obj', context, 'PUT', body);
1295
+ const result = await minimalDispatcher.handleMetadata('/objects/my_obj', context, 'PUT', body);
1291
1296
  expect(result.handled).toBe(true);
1292
1297
  expect(result.response?.status).toBe(501);
1293
1298
  });
1294
1299
 
1295
- it('should use protocol service even when broker is missing', async () => {
1300
+ it('should use protocol service with minimal kernel', async () => {
1296
1301
  const mockProtocolLocal = {
1297
1302
  getMetaTypes: vi.fn().mockResolvedValue({ types: ['custom_type'] }),
1298
1303
  };
1299
- brokerlessKernel.context.getService = vi.fn().mockImplementation((name: string) => {
1304
+ minimalKernel.context.getService = vi.fn().mockImplementation((name: string) => {
1300
1305
  if (name === 'protocol') return mockProtocolLocal;
1301
1306
  return null;
1302
1307
  });
1303
1308
 
1304
1309
  const context = { request: {} };
1305
- const result = await brokerlessDispatcher.handleMetadata('/types', context, 'GET');
1310
+ const result = await minimalDispatcher.handleMetadata('/types', context, 'GET');
1306
1311
  expect(result.handled).toBe(true);
1307
1312
  expect(result.response?.status).toBe(200);
1308
1313
  expect(mockProtocolLocal.getMetaTypes).toHaveBeenCalled();