@objectstack/runtime 3.2.7 → 3.2.9
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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +18 -0
- package/dist/index.cjs +77 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +77 -40
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/http-dispatcher.test.ts +100 -0
- package/src/http-dispatcher.ts +88 -42
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/runtime",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.9",
|
|
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.
|
|
19
|
-
"@objectstack/rest": "3.2.
|
|
20
|
-
"@objectstack/spec": "3.2.
|
|
21
|
-
"@objectstack/types": "3.2.
|
|
18
|
+
"@objectstack/core": "3.2.9",
|
|
19
|
+
"@objectstack/rest": "3.2.9",
|
|
20
|
+
"@objectstack/spec": "3.2.9",
|
|
21
|
+
"@objectstack/types": "3.2.9"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"typescript": "^5.0.0",
|
|
25
|
-
"vitest": "^4.0
|
|
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
|
});
|
package/src/http-dispatcher.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
385
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
442
|
-
|
|
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
|
-
//
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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 };
|