@objectstack/runtime 3.2.7 → 3.2.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/runtime",
3
- "version": "3.2.7",
3
+ "version": "3.2.8",
4
4
  "license": "Apache-2.0",
5
5
  "description": "ObjectStack Core Runtime & Query Engine",
6
6
  "type": "module",
@@ -15,14 +15,14 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "zod": "^4.3.6",
18
- "@objectstack/core": "3.2.7",
19
- "@objectstack/rest": "3.2.7",
20
- "@objectstack/spec": "3.2.7",
21
- "@objectstack/types": "3.2.7"
18
+ "@objectstack/core": "3.2.8",
19
+ "@objectstack/rest": "3.2.8",
20
+ "@objectstack/spec": "3.2.8",
21
+ "@objectstack/types": "3.2.8"
22
22
  },
23
23
  "devDependencies": {
24
24
  "typescript": "^5.0.0",
25
- "vitest": "^4.0.18"
25
+ "vitest": "^4.1.0"
26
26
  },
27
27
  "scripts": {
28
28
  "build": "tsup --config ../../tsup.config.ts",
@@ -1208,4 +1208,104 @@ describe('HttpDispatcher', () => {
1208
1208
  expect(result.response?.status).toBe(200);
1209
1209
  });
1210
1210
  });
1211
+
1212
+ describe('handleMetadata without broker (serverless degradation)', () => {
1213
+ let brokerlessKernel: any;
1214
+ let brokerlessDispatcher: HttpDispatcher;
1215
+
1216
+ beforeEach(() => {
1217
+ // Kernel with NO broker — simulates a lightweight/serverless setup
1218
+ // where only the protocol service and/or ObjectQL registry are available.
1219
+ brokerlessKernel = {
1220
+ broker: null,
1221
+ context: {
1222
+ getService: vi.fn().mockReturnValue(null),
1223
+ },
1224
+ };
1225
+ brokerlessDispatcher = new HttpDispatcher(brokerlessKernel);
1226
+ });
1227
+
1228
+ it('GET /meta should return default types when broker is missing', async () => {
1229
+ const context = { request: {} };
1230
+ const result = await brokerlessDispatcher.handleMetadata('', context, 'GET');
1231
+ expect(result.handled).toBe(true);
1232
+ expect(result.response?.status).toBe(200);
1233
+ expect(result.response?.body?.data?.types).toContain('object');
1234
+ });
1235
+
1236
+ it('GET /meta/types should return default types when broker is missing', async () => {
1237
+ const context = { request: {} };
1238
+ const result = await brokerlessDispatcher.handleMetadata('/types', context, 'GET');
1239
+ expect(result.handled).toBe(true);
1240
+ expect(result.response?.status).toBe(200);
1241
+ expect(result.response?.body?.data?.types).toContain('object');
1242
+ });
1243
+
1244
+ it('GET /meta/objects should use ObjectQL registry when broker is missing', async () => {
1245
+ const mockRegistry = {
1246
+ getAllObjects: vi.fn().mockReturnValue([{ name: 'account' }]),
1247
+ getObject: vi.fn(),
1248
+ };
1249
+ brokerlessKernel.context.getService = vi.fn().mockImplementation((name: string) => {
1250
+ if (name === 'objectql') return { registry: mockRegistry };
1251
+ return null;
1252
+ });
1253
+
1254
+ const context = { request: {} };
1255
+ const result = await brokerlessDispatcher.handleMetadata('/objects', context, 'GET');
1256
+ expect(result.handled).toBe(true);
1257
+ expect(result.response?.status).toBe(200);
1258
+ expect(mockRegistry.getAllObjects).toHaveBeenCalled();
1259
+ });
1260
+
1261
+ it('GET /meta/objects/:name should use ObjectQL registry when broker is missing', async () => {
1262
+ const mockRegistry = {
1263
+ registry: {
1264
+ getObject: vi.fn().mockReturnValue({ name: 'account', fields: {} }),
1265
+ },
1266
+ };
1267
+ brokerlessKernel.context.getService = vi.fn().mockImplementation((name: string) => {
1268
+ if (name === 'objectql') return mockRegistry;
1269
+ return null;
1270
+ });
1271
+
1272
+ const context = { request: {} };
1273
+ const result = await brokerlessDispatcher.handleMetadata('/objects/account', context, 'GET');
1274
+ expect(result.handled).toBe(true);
1275
+ expect(result.response?.status).toBe(200);
1276
+ expect(mockRegistry.registry.getObject).toHaveBeenCalledWith('account');
1277
+ });
1278
+
1279
+ it('GET /meta/:type/:name/published should return 404 when broker is missing and metadata service is unavailable', async () => {
1280
+ const context = { request: {} };
1281
+ const result = await brokerlessDispatcher.handleMetadata('/object/my_obj/published', context, 'GET');
1282
+ expect(result.handled).toBe(true);
1283
+ expect(result.response?.status).toBe(404);
1284
+ });
1285
+
1286
+ it('PUT /meta/:type/:name should return 501 when broker is missing and protocol is unavailable', async () => {
1287
+ const context = { request: {} };
1288
+ const body = { label: 'Test' };
1289
+ const result = await brokerlessDispatcher.handleMetadata('/objects/my_obj', context, 'PUT', body);
1290
+ expect(result.handled).toBe(true);
1291
+ expect(result.response?.status).toBe(501);
1292
+ });
1293
+
1294
+ it('should use protocol service even when broker is missing', async () => {
1295
+ const mockProtocolLocal = {
1296
+ getMetaTypes: vi.fn().mockResolvedValue({ types: ['custom_type'] }),
1297
+ };
1298
+ brokerlessKernel.context.getService = vi.fn().mockImplementation((name: string) => {
1299
+ if (name === 'protocol') return mockProtocolLocal;
1300
+ return null;
1301
+ });
1302
+
1303
+ const context = { request: {} };
1304
+ const result = await brokerlessDispatcher.handleMetadata('/types', context, 'GET');
1305
+ expect(result.handled).toBe(true);
1306
+ expect(result.response?.status).toBe(200);
1307
+ expect(mockProtocolLocal.getMetaTypes).toHaveBeenCalled();
1308
+ expect(result.response?.body?.data?.types).toContain('custom_type');
1309
+ });
1310
+ });
1211
1311
  });
@@ -311,7 +311,10 @@ export class HttpDispatcher {
311
311
  * Fallback for backward compat: /metadata (all objects), /metadata/:objectName (get object)
312
312
  */
313
313
  async handleMetadata(path: string, context: HttpProtocolContext, method?: string, body?: any, query?: any): Promise<HttpDispatcherResult> {
314
- const broker = this.ensureBroker();
314
+ // Broker is used as a fallback — not required upfront.
315
+ // This allows metadata to be served when only the protocol service
316
+ // or ObjectQL service is available (e.g. lightweight / serverless setups).
317
+ const broker = this.kernel.broker ?? null;
315
318
  const parts = path.replace(/^\/+/, '').split('/').filter(Boolean);
316
319
 
317
320
  // GET /metadata/types
@@ -323,13 +326,16 @@ export class HttpDispatcher {
323
326
  return { handled: true, response: this.success(result) };
324
327
  }
325
328
  // Fallback: ask broker for registered types
326
- try {
327
- const data = await broker.call('metadata.types', {}, { request: context.request });
328
- return { handled: true, response: this.success(data) };
329
- } catch {
330
- // Last resort: hardcoded defaults
331
- return { handled: true, response: this.success({ types: ['object', 'app', 'plugin'] }) };
329
+ if (broker) {
330
+ try {
331
+ const data = await broker.call('metadata.types', {}, { request: context.request });
332
+ return { handled: true, response: this.success(data) };
333
+ } catch {
334
+ // fall through to hardcoded defaults
335
+ }
332
336
  }
337
+ // Last resort: hardcoded defaults
338
+ return { handled: true, response: this.success({ types: ['object', 'app', 'plugin'] }) };
333
339
  }
334
340
 
335
341
  // GET /metadata/:type/:name/published → get published version
@@ -342,12 +348,15 @@ export class HttpDispatcher {
342
348
  return { handled: true, response: this.success(data) };
343
349
  }
344
350
  // Broker fallback
345
- try {
346
- const data = await broker.call('metadata.getPublished', { type, name }, { request: context.request });
347
- return { handled: true, response: this.success(data) };
348
- } catch (e: any) {
349
- return { handled: true, response: this.error(e.message, 404) };
351
+ if (broker) {
352
+ try {
353
+ const data = await broker.call('metadata.getPublished', { type, name }, { request: context.request });
354
+ return { handled: true, response: this.success(data) };
355
+ } catch (e: any) {
356
+ return { handled: true, response: this.error(e.message, 404) };
357
+ }
350
358
  }
359
+ return { handled: true, response: this.error('Not found', 404) };
351
360
  }
352
361
 
353
362
  // /metadata/:type/:name
@@ -369,20 +378,31 @@ export class HttpDispatcher {
369
378
  }
370
379
 
371
380
  // Fallback to broker if protocol not available (legacy)
372
- try {
373
- const data = await broker.call('metadata.saveItem', { type, name, item: body }, { request: context.request });
374
- return { handled: true, response: this.success(data) };
375
- } catch (e: any) {
376
- // If broker doesn't support it either
377
- return { handled: true, response: this.error(e.message || 'Save not supported', 501) };
381
+ if (broker) {
382
+ try {
383
+ const data = await broker.call('metadata.saveItem', { type, name, item: body }, { request: context.request });
384
+ return { handled: true, response: this.success(data) };
385
+ } catch (e: any) {
386
+ return { handled: true, response: this.error(e.message || 'Save not supported', 501) };
387
+ }
378
388
  }
389
+ return { handled: true, response: this.error('Save not supported', 501) };
379
390
  }
380
391
 
381
392
  try {
382
393
  // Try specific calls based on type
383
394
  if (type === 'objects' || type === 'object') {
384
- const data = await broker.call('metadata.getObject', { objectName: name }, { request: context.request });
385
- return { handled: true, response: this.success(data) };
395
+ if (broker) {
396
+ const data = await broker.call('metadata.getObject', { objectName: name }, { request: context.request });
397
+ return { handled: true, response: this.success(data) };
398
+ }
399
+ // Try ObjectQL service directly when broker is unavailable
400
+ const qlService = await this.getObjectQLService();
401
+ if (qlService?.registry) {
402
+ const data = qlService.registry.getObject(name);
403
+ if (data) return { handled: true, response: this.success(data) };
404
+ }
405
+ return { handled: true, response: this.error('Not found', 404) };
386
406
  }
387
407
 
388
408
  // If type is singular (e.g. 'app'), use it directly
@@ -402,9 +422,12 @@ export class HttpDispatcher {
402
422
  }
403
423
 
404
424
  // Generic call for other types if supported via Broker (Legacy)
405
- const method = `metadata.get${this.capitalize(singularType)}`;
406
- const data = await broker.call(method, { name }, { request: context.request });
407
- return { handled: true, response: this.success(data) };
425
+ if (broker) {
426
+ const method = `metadata.get${this.capitalize(singularType)}`;
427
+ const data = await broker.call(method, { name }, { request: context.request });
428
+ return { handled: true, response: this.success(data) };
429
+ }
430
+ return { handled: true, response: this.error('Not found', 404) };
408
431
  } catch (e: any) {
409
432
  // Fallback: treat first part as object name if only 1 part (handled below)
410
433
  // But here we are deep in 2 parts. Must be an error.
@@ -433,26 +456,46 @@ export class HttpDispatcher {
433
456
  }
434
457
 
435
458
  // Try broker for the type
436
- try {
437
- if (typeOrName === 'objects') {
438
- const data = await broker.call('metadata.objects', { packageId }, { request: context.request });
439
- return { handled: true, response: this.success(data) };
459
+ if (broker) {
460
+ try {
461
+ if (typeOrName === 'objects') {
462
+ const data = await broker.call('metadata.objects', { packageId }, { request: context.request });
463
+ return { handled: true, response: this.success(data) };
464
+ }
465
+ const data = await broker.call(`metadata.${typeOrName}`, { packageId }, { request: context.request });
466
+ if (data !== null && data !== undefined) {
467
+ return { handled: true, response: this.success(data) };
468
+ }
469
+ } catch {
470
+ // Broker doesn't support this action, fall through
440
471
  }
441
- const data = await broker.call(`metadata.${typeOrName}`, { packageId }, { request: context.request });
442
- if (data !== null && data !== undefined) {
472
+
473
+ // Legacy: /metadata/:objectName (treat as single object lookup)
474
+ try {
475
+ const data = await broker.call('metadata.getObject', { objectName: typeOrName }, { request: context.request });
443
476
  return { handled: true, response: this.success(data) };
477
+ } catch (e: any) {
478
+ return { handled: true, response: this.error(e.message, 404) };
444
479
  }
445
- } catch {
446
- // Broker doesn't support this action, fall through
447
480
  }
448
481
 
449
- // Legacy: /metadata/:objectName (treat as single object lookup)
450
- try {
451
- const data = await broker.call('metadata.getObject', { objectName: typeOrName }, { request: context.request });
452
- return { handled: true, response: this.success(data) };
453
- } catch (e: any) {
454
- return { handled: true, response: this.error(e.message, 404) };
482
+ // No broker try ObjectQL registry directly for object lookups
483
+ const qlService = await this.getObjectQLService();
484
+ if (qlService?.registry) {
485
+ if (typeOrName === 'objects') {
486
+ const objs = qlService.registry.getAllObjects(packageId);
487
+ return { handled: true, response: this.success({ type: 'object', items: objs }) };
488
+ }
489
+ // Try listing items of the given type
490
+ const items = qlService.registry.listItems?.(typeOrName, packageId);
491
+ if (items && items.length > 0) {
492
+ return { handled: true, response: this.success({ type: typeOrName, items }) };
493
+ }
494
+ // Legacy: treat as object name
495
+ const obj = qlService.registry.getObject(typeOrName);
496
+ if (obj) return { handled: true, response: this.success(obj) };
455
497
  }
498
+ return { handled: true, response: this.error('Not found', 404) };
456
499
  }
457
500
 
458
501
  // GET /metadata — return available metadata types
@@ -464,12 +507,15 @@ export class HttpDispatcher {
464
507
  return { handled: true, response: this.success(result) };
465
508
  }
466
509
  // Fallback: ask broker for registered types
467
- try {
468
- const data = await broker.call('metadata.types', {}, { request: context.request });
469
- return { handled: true, response: this.success(data) };
470
- } catch {
471
- return { handled: true, response: this.success({ types: ['object', 'app', 'plugin'] }) };
510
+ if (broker) {
511
+ try {
512
+ const data = await broker.call('metadata.types', {}, { request: context.request });
513
+ return { handled: true, response: this.success(data) };
514
+ } catch {
515
+ // fall through to hardcoded defaults
516
+ }
472
517
  }
518
+ return { handled: true, response: this.success({ types: ['object', 'app', 'plugin'] }) };
473
519
  }
474
520
 
475
521
  return { handled: false };