@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.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +19 -0
- package/dist/index.cjs +225 -193
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -11
- package/dist/index.d.ts +9 -11
- package/dist/index.js +225 -193
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/app-plugin.ts +9 -0
- package/src/dispatcher-plugin.ts +93 -59
- package/src/http-dispatcher.root.test.ts +0 -3
- package/src/http-dispatcher.test.ts +105 -100
- package/src/http-dispatcher.ts +183 -187
- package/vitest.config.ts +1 -0
package/src/http-dispatcher.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { ObjectKernel, getEnv, resolveLocale } from '@objectstack/core';
|
|
4
4
|
import { CoreServiceName } from '@objectstack/spec/system';
|
|
5
|
+
import { pluralToSingular } from '@objectstack/spec/shared';
|
|
5
6
|
|
|
6
7
|
/** Browser-safe UUID generator — prefers Web Crypto, falls back to RFC 4122 v4 */
|
|
7
8
|
function randomUUID(): string {
|
|
@@ -39,7 +40,7 @@ export interface HttpDispatcherResult {
|
|
|
39
40
|
* ```
|
|
40
41
|
*/
|
|
41
42
|
export class HttpDispatcher {
|
|
42
|
-
private kernel: any; // Casting to any to access dynamic props like
|
|
43
|
+
private kernel: any; // Casting to any to access dynamic props like services, graphql
|
|
43
44
|
|
|
44
45
|
constructor(kernel: ObjectKernel) {
|
|
45
46
|
this.kernel = kernel;
|
|
@@ -78,11 +79,82 @@ export class HttpDispatcher {
|
|
|
78
79
|
};
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Direct data service dispatch — replaces broker.call('data.*').
|
|
84
|
+
* Tries protocol service first (supports expand/populate), falls back to ObjectQL.
|
|
85
|
+
*/
|
|
86
|
+
private async callData(action: string, params: any): Promise<any> {
|
|
87
|
+
const protocol = await this.resolveService('protocol');
|
|
88
|
+
const qlService = await this.getObjectQLService();
|
|
89
|
+
const ql = qlService ?? await this.resolveService('objectql');
|
|
90
|
+
|
|
91
|
+
if (action === 'create') {
|
|
92
|
+
if (ql) {
|
|
93
|
+
const res = await ql.insert(params.object, params.data);
|
|
94
|
+
const record = { ...params.data, ...res };
|
|
95
|
+
return { object: params.object, id: record.id, record };
|
|
96
|
+
}
|
|
97
|
+
throw { statusCode: 503, message: 'Data service not available' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (action === 'get') {
|
|
101
|
+
if (protocol && typeof protocol.getData === 'function') {
|
|
102
|
+
return await protocol.getData({ object: params.object, id: params.id, expand: params.expand, select: params.select });
|
|
103
|
+
}
|
|
104
|
+
if (ql) {
|
|
105
|
+
let all = await ql.find(params.object);
|
|
106
|
+
if (!all) all = [];
|
|
107
|
+
const match = all.find((i: any) => i.id === params.id);
|
|
108
|
+
return match ? { object: params.object, id: params.id, record: match } : null;
|
|
109
|
+
}
|
|
110
|
+
throw { statusCode: 503, message: 'Data service not available' };
|
|
84
111
|
}
|
|
85
|
-
|
|
112
|
+
|
|
113
|
+
if (action === 'update') {
|
|
114
|
+
if (ql && params.id) {
|
|
115
|
+
let all = await ql.find(params.object);
|
|
116
|
+
if (all && (all as any).value) all = (all as any).value;
|
|
117
|
+
if (!all) all = [];
|
|
118
|
+
const existing = all.find((i: any) => i.id === params.id);
|
|
119
|
+
if (!existing) throw new Error('[ObjectStack] Not Found');
|
|
120
|
+
await ql.update(params.object, params.data, { where: { id: params.id } });
|
|
121
|
+
return { object: params.object, id: params.id, record: { ...existing, ...params.data } };
|
|
122
|
+
}
|
|
123
|
+
throw { statusCode: 503, message: 'Data service not available' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (action === 'delete') {
|
|
127
|
+
if (ql) {
|
|
128
|
+
await ql.delete(params.object, { where: { id: params.id } });
|
|
129
|
+
return { object: params.object, id: params.id, deleted: true };
|
|
130
|
+
}
|
|
131
|
+
throw { statusCode: 503, message: 'Data service not available' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (action === 'query' || action === 'find') {
|
|
135
|
+
if (protocol && typeof protocol.findData === 'function') {
|
|
136
|
+
// Build query: use explicit params.query if provided, otherwise extract query fields from params
|
|
137
|
+
const query = params.query || (() => {
|
|
138
|
+
const { object, ...rest } = params;
|
|
139
|
+
return rest;
|
|
140
|
+
})();
|
|
141
|
+
return await protocol.findData({ object: params.object, query });
|
|
142
|
+
}
|
|
143
|
+
if (ql) {
|
|
144
|
+
let all = await ql.find(params.object);
|
|
145
|
+
if (!Array.isArray(all) && all && (all as any).value) all = (all as any).value;
|
|
146
|
+
if (!all) all = [];
|
|
147
|
+
return { object: params.object, records: all, total: all.length };
|
|
148
|
+
}
|
|
149
|
+
throw { statusCode: 503, message: 'Data service not available' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (action === 'batch') {
|
|
153
|
+
// Batch operations — not yet supported via direct service dispatch
|
|
154
|
+
return { object: params.object, results: [] };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
throw { statusCode: 400, message: `Unknown data action: ${action}` };
|
|
86
158
|
}
|
|
87
159
|
|
|
88
160
|
/**
|
|
@@ -248,24 +320,8 @@ export class HttpDispatcher {
|
|
|
248
320
|
return { handled: true, result: response };
|
|
249
321
|
}
|
|
250
322
|
|
|
251
|
-
// 2.
|
|
323
|
+
// 2. Mock fallback for MSW/test environments when no auth service is registered
|
|
252
324
|
const normalizedPath = path.replace(/^\/+/, '');
|
|
253
|
-
if (normalizedPath === 'login' && method.toUpperCase() === 'POST') {
|
|
254
|
-
try {
|
|
255
|
-
const broker = this.ensureBroker();
|
|
256
|
-
const data = await broker.call('auth.login', body, { request: context.request });
|
|
257
|
-
return { handled: true, response: { status: 200, body: data } };
|
|
258
|
-
} catch (error: any) {
|
|
259
|
-
// Only fall through to mock when the broker is truly unavailable
|
|
260
|
-
// (ensureBroker throws statusCode 500 when kernel.broker is null)
|
|
261
|
-
const statusCode = error?.statusCode ?? error?.status;
|
|
262
|
-
if (statusCode !== 500 || !error?.message?.includes('Broker not available')) {
|
|
263
|
-
throw error;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// 3. Mock fallback for MSW/test environments when no auth service is registered
|
|
269
325
|
return this.mockAuthFallback(normalizedPath, method, body);
|
|
270
326
|
}
|
|
271
327
|
|
|
@@ -332,30 +388,29 @@ export class HttpDispatcher {
|
|
|
332
388
|
* Standard: /metadata/:type/:name
|
|
333
389
|
* Fallback for backward compat: /metadata (all objects), /metadata/:objectName (get object)
|
|
334
390
|
*/
|
|
335
|
-
async handleMetadata(path: string,
|
|
336
|
-
// Broker is used as a fallback — not required upfront.
|
|
337
|
-
// This allows metadata to be served when only the protocol service
|
|
338
|
-
// or ObjectQL service is available (e.g. lightweight / serverless setups).
|
|
339
|
-
const broker = this.kernel.broker ?? null;
|
|
391
|
+
async handleMetadata(path: string, _context: HttpProtocolContext, method?: string, body?: any, query?: any): Promise<HttpDispatcherResult> {
|
|
340
392
|
const parts = path.replace(/^\/+/, '').split('/').filter(Boolean);
|
|
341
393
|
|
|
342
394
|
// GET /metadata/types
|
|
343
395
|
if (parts[0] === 'types') {
|
|
344
|
-
// Try
|
|
396
|
+
// PRIORITY 1: Try MetadataService directly (includes both typeRegistry with agent/tool AND runtime-registered types)
|
|
397
|
+
const metadataService = await this.resolveService('metadata');
|
|
398
|
+
|
|
399
|
+
if (metadataService && typeof (metadataService as any).getRegisteredTypes === 'function') {
|
|
400
|
+
try {
|
|
401
|
+
const types = await (metadataService as any).getRegisteredTypes();
|
|
402
|
+
return { handled: true, response: this.success({ types }) };
|
|
403
|
+
} catch (e: any) {
|
|
404
|
+
// Log error but continue to fallbacks
|
|
405
|
+
console.warn('[HttpDispatcher] MetadataService.getRegisteredTypes() failed:', e.message);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// PRIORITY 2: Try protocol service (returns SchemaRegistry types only - missing agent/tool)
|
|
345
409
|
const protocol = await this.resolveService('protocol');
|
|
346
410
|
if (protocol && typeof protocol.getMetaTypes === 'function') {
|
|
347
411
|
const result = await protocol.getMetaTypes({});
|
|
348
412
|
return { handled: true, response: this.success(result) };
|
|
349
413
|
}
|
|
350
|
-
// Fallback: ask broker for registered types
|
|
351
|
-
if (broker) {
|
|
352
|
-
try {
|
|
353
|
-
const data = await broker.call('metadata.types', {}, { request: context.request });
|
|
354
|
-
return { handled: true, response: this.success(data) };
|
|
355
|
-
} catch {
|
|
356
|
-
// fall through to hardcoded defaults
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
414
|
// Last resort: hardcoded defaults
|
|
360
415
|
return { handled: true, response: this.success({ types: ['object', 'app', 'plugin'] }) };
|
|
361
416
|
}
|
|
@@ -369,14 +424,13 @@ export class HttpDispatcher {
|
|
|
369
424
|
if (data === undefined) return { handled: true, response: this.error('Not found', 404) };
|
|
370
425
|
return { handled: true, response: this.success(data) };
|
|
371
426
|
}
|
|
372
|
-
//
|
|
373
|
-
|
|
427
|
+
// Fallback — try MetadataService via resolveService
|
|
428
|
+
const metaSvc = await this.resolveService('metadata');
|
|
429
|
+
if (metaSvc && typeof (metaSvc as any).getPublished === 'function') {
|
|
374
430
|
try {
|
|
375
|
-
const
|
|
376
|
-
return { handled: true, response: this.success(
|
|
377
|
-
} catch
|
|
378
|
-
return { handled: true, response: this.error(e.message, 404) };
|
|
379
|
-
}
|
|
431
|
+
const fallbackData = await (metaSvc as any).getPublished(type, name);
|
|
432
|
+
if (fallbackData !== undefined) return { handled: true, response: this.success(fallbackData) };
|
|
433
|
+
} catch { /* fall through */ }
|
|
380
434
|
}
|
|
381
435
|
return { handled: true, response: this.error('Not found', 404) };
|
|
382
436
|
}
|
|
@@ -401,13 +455,14 @@ export class HttpDispatcher {
|
|
|
401
455
|
}
|
|
402
456
|
}
|
|
403
457
|
|
|
404
|
-
// Fallback
|
|
405
|
-
|
|
458
|
+
// Fallback: try MetadataService directly
|
|
459
|
+
const metaSvc = await this.resolveService('metadata');
|
|
460
|
+
if (metaSvc && typeof (metaSvc as any).saveItem === 'function') {
|
|
406
461
|
try {
|
|
407
|
-
|
|
408
|
-
|
|
462
|
+
const data = await (metaSvc as any).saveItem(type, name, body);
|
|
463
|
+
return { handled: true, response: this.success(data) };
|
|
409
464
|
} catch (e: any) {
|
|
410
|
-
|
|
465
|
+
return { handled: true, response: this.error(e.message || 'Save not supported', 501) };
|
|
411
466
|
}
|
|
412
467
|
}
|
|
413
468
|
return { handled: true, response: this.error('Save not supported', 501) };
|
|
@@ -416,11 +471,7 @@ export class HttpDispatcher {
|
|
|
416
471
|
try {
|
|
417
472
|
// Try specific calls based on type
|
|
418
473
|
if (type === 'objects' || type === 'object') {
|
|
419
|
-
|
|
420
|
-
const data = await broker.call('metadata.getObject', { objectName: name }, { request: context.request });
|
|
421
|
-
return { handled: true, response: this.success(data) };
|
|
422
|
-
}
|
|
423
|
-
// Try ObjectQL service directly when broker is unavailable
|
|
474
|
+
// Try ObjectQL service directly
|
|
424
475
|
const qlService = await this.getObjectQLService();
|
|
425
476
|
if (qlService?.registry) {
|
|
426
477
|
const data = qlService.registry.getObject(name);
|
|
@@ -429,9 +480,8 @@ export class HttpDispatcher {
|
|
|
429
480
|
return { handled: true, response: this.error('Not found', 404) };
|
|
430
481
|
}
|
|
431
482
|
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
const singularType = type.endsWith('s') ? type.slice(0, -1) : type;
|
|
483
|
+
// Normalize plural URL paths to singular registry type names
|
|
484
|
+
const singularType = pluralToSingular(type);
|
|
435
485
|
|
|
436
486
|
// Try Protocol Service First (Preferred)
|
|
437
487
|
const protocol = await this.resolveService('protocol');
|
|
@@ -441,15 +491,16 @@ export class HttpDispatcher {
|
|
|
441
491
|
return { handled: true, response: this.success(data) };
|
|
442
492
|
} catch (e: any) {
|
|
443
493
|
// Protocol might throw if not found or not supported
|
|
444
|
-
// Fallback to broker?
|
|
445
494
|
}
|
|
446
495
|
}
|
|
447
496
|
|
|
448
|
-
//
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
497
|
+
// Try MetadataService for runtime-registered types
|
|
498
|
+
const metaSvc = await this.resolveService('metadata');
|
|
499
|
+
if (metaSvc && typeof (metaSvc as any).getItem === 'function') {
|
|
500
|
+
try {
|
|
501
|
+
const data = await (metaSvc as any).getItem(singularType, name);
|
|
502
|
+
if (data) return { handled: true, response: this.success(data) };
|
|
503
|
+
} catch { /* not found */ }
|
|
453
504
|
}
|
|
454
505
|
return { handled: true, response: this.error('Not found', 404) };
|
|
455
506
|
} catch (e: any) {
|
|
@@ -464,7 +515,7 @@ export class HttpDispatcher {
|
|
|
464
515
|
const typeOrName = parts[0];
|
|
465
516
|
// Extract optional package filter from query string
|
|
466
517
|
const packageId = query?.package || undefined;
|
|
467
|
-
|
|
518
|
+
|
|
468
519
|
// Try protocol service first for any type
|
|
469
520
|
const protocol = await this.resolveService('protocol');
|
|
470
521
|
if (protocol && typeof protocol.getMetaItems === 'function') {
|
|
@@ -479,31 +530,28 @@ export class HttpDispatcher {
|
|
|
479
530
|
}
|
|
480
531
|
}
|
|
481
532
|
|
|
482
|
-
// Try
|
|
483
|
-
|
|
533
|
+
// Try MetadataService directly for runtime-registered metadata (agents, tools, etc.)
|
|
534
|
+
const metadataService = await this.getService(CoreServiceName.enum.metadata);
|
|
535
|
+
if (metadataService && typeof (metadataService as any).list === 'function') {
|
|
484
536
|
try {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
537
|
+
let items = await (metadataService as any).list(typeOrName);
|
|
538
|
+
// Respect package filter: MetadataService.list() returns ALL items,
|
|
539
|
+
// so filter by _packageId when a specific package is requested.
|
|
540
|
+
if (packageId && items && items.length > 0) {
|
|
541
|
+
items = items.filter((item: any) => item?._packageId === packageId);
|
|
488
542
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
return { handled: true, response: this.success(data) };
|
|
543
|
+
if (items && items.length > 0) {
|
|
544
|
+
return { handled: true, response: this.success({ type: typeOrName, items }) };
|
|
492
545
|
}
|
|
493
|
-
} catch {
|
|
494
|
-
// Broker doesn't support this action, fall through
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Legacy: /metadata/:objectName (treat as single object lookup)
|
|
498
|
-
try {
|
|
499
|
-
const data = await broker.call('metadata.getObject', { objectName: typeOrName }, { request: context.request });
|
|
500
|
-
return { handled: true, response: this.success(data) };
|
|
501
546
|
} catch (e: any) {
|
|
502
|
-
|
|
547
|
+
// MetadataService doesn't know this type or failed, continue to other fallbacks
|
|
548
|
+
// Sanitize typeOrName to prevent log injection (CodeQL warning)
|
|
549
|
+
const sanitizedType = String(typeOrName).replace(/[\r\n\t]/g, '');
|
|
550
|
+
console.debug(`[HttpDispatcher] MetadataService.list() failed for type:`, sanitizedType, 'error:', e.message);
|
|
503
551
|
}
|
|
504
552
|
}
|
|
505
553
|
|
|
506
|
-
//
|
|
554
|
+
// Try ObjectQL registry directly for object/type lookups
|
|
507
555
|
const qlService = await this.getObjectQLService();
|
|
508
556
|
if (qlService?.registry) {
|
|
509
557
|
if (typeOrName === 'objects') {
|
|
@@ -524,21 +572,20 @@ export class HttpDispatcher {
|
|
|
524
572
|
|
|
525
573
|
// GET /metadata — return available metadata types
|
|
526
574
|
if (parts.length === 0) {
|
|
575
|
+
// Try MetadataService for registered types
|
|
576
|
+
const metadataService = await this.resolveService('metadata');
|
|
577
|
+
if (metadataService && typeof (metadataService as any).getRegisteredTypes === 'function') {
|
|
578
|
+
try {
|
|
579
|
+
const types = await (metadataService as any).getRegisteredTypes();
|
|
580
|
+
return { handled: true, response: this.success({ types }) };
|
|
581
|
+
} catch { /* fall through */ }
|
|
582
|
+
}
|
|
527
583
|
// Try protocol service for dynamic types
|
|
528
584
|
const protocol = await this.resolveService('protocol');
|
|
529
585
|
if (protocol && typeof protocol.getMetaTypes === 'function') {
|
|
530
586
|
const result = await protocol.getMetaTypes({});
|
|
531
587
|
return { handled: true, response: this.success(result) };
|
|
532
588
|
}
|
|
533
|
-
// Fallback: ask broker for registered types
|
|
534
|
-
if (broker) {
|
|
535
|
-
try {
|
|
536
|
-
const data = await broker.call('metadata.types', {}, { request: context.request });
|
|
537
|
-
return { handled: true, response: this.success(data) };
|
|
538
|
-
} catch {
|
|
539
|
-
// fall through to hardcoded defaults
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
589
|
return { handled: true, response: this.success({ types: ['object', 'app', 'plugin'] }) };
|
|
543
590
|
}
|
|
544
591
|
|
|
@@ -549,8 +596,7 @@ export class HttpDispatcher {
|
|
|
549
596
|
* Handles Data requests
|
|
550
597
|
* path: sub-path after /data/ (e.g. "contacts", "contacts/123", "contacts/query")
|
|
551
598
|
*/
|
|
552
|
-
async handleData(path: string, method: string, body: any, query: any,
|
|
553
|
-
const broker = this.ensureBroker();
|
|
599
|
+
async handleData(path: string, method: string, body: any, query: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult> {
|
|
554
600
|
const parts = path.replace(/^\/+/, '').split('/');
|
|
555
601
|
const objectName = parts[0];
|
|
556
602
|
|
|
@@ -566,14 +612,14 @@ export class HttpDispatcher {
|
|
|
566
612
|
|
|
567
613
|
// POST /data/:object/query
|
|
568
614
|
if (action === 'query' && m === 'POST') {
|
|
569
|
-
// Spec:
|
|
570
|
-
const result = await
|
|
615
|
+
// Spec: returns FindDataResponse = { object, records, total?, hasMore? }
|
|
616
|
+
const result = await this.callData('query', { object: objectName, ...body });
|
|
571
617
|
return { handled: true, response: this.success(result) };
|
|
572
618
|
}
|
|
573
619
|
|
|
574
620
|
// POST /data/:object/batch
|
|
575
621
|
if (action === 'batch' && m === 'POST') {
|
|
576
|
-
const result = await
|
|
622
|
+
const result = await this.callData('batch', { object: objectName, ...body });
|
|
577
623
|
return { handled: true, response: this.success(result) };
|
|
578
624
|
}
|
|
579
625
|
|
|
@@ -586,24 +632,24 @@ export class HttpDispatcher {
|
|
|
586
632
|
const allowedParams: Record<string, unknown> = {};
|
|
587
633
|
if (select != null) allowedParams.select = select;
|
|
588
634
|
if (expand != null) allowedParams.expand = expand;
|
|
589
|
-
// Spec:
|
|
590
|
-
const result = await
|
|
635
|
+
// Spec: returns GetDataResponse = { object, id, record }
|
|
636
|
+
const result = await this.callData('get', { object: objectName, id, ...allowedParams });
|
|
591
637
|
return { handled: true, response: this.success(result) };
|
|
592
638
|
}
|
|
593
639
|
|
|
594
640
|
// PATCH /data/:object/:id
|
|
595
641
|
if (parts.length === 2 && m === 'PATCH') {
|
|
596
642
|
const id = parts[1];
|
|
597
|
-
// Spec:
|
|
598
|
-
const result = await
|
|
643
|
+
// Spec: returns UpdateDataResponse = { object, id, record }
|
|
644
|
+
const result = await this.callData('update', { object: objectName, id, data: body });
|
|
599
645
|
return { handled: true, response: this.success(result) };
|
|
600
646
|
}
|
|
601
647
|
|
|
602
648
|
// DELETE /data/:object/:id
|
|
603
649
|
if (parts.length === 2 && m === 'DELETE') {
|
|
604
650
|
const id = parts[1];
|
|
605
|
-
// Spec:
|
|
606
|
-
const result = await
|
|
651
|
+
// Spec: returns DeleteDataResponse = { object, id, deleted }
|
|
652
|
+
const result = await this.callData('delete', { object: objectName, id });
|
|
607
653
|
return { handled: true, response: this.success(result) };
|
|
608
654
|
}
|
|
609
655
|
} else {
|
|
@@ -613,16 +659,16 @@ export class HttpDispatcher {
|
|
|
613
659
|
// HTTP GET query params use transport-level names (filter, sort, top,
|
|
614
660
|
// skip, select, expand) which are normalized here to canonical
|
|
615
661
|
// QueryAST field names (where, orderBy, limit, offset, fields,
|
|
616
|
-
// expand) before forwarding to the
|
|
662
|
+
// expand) before forwarding to the data service layer.
|
|
617
663
|
// The protocol.ts findData() method performs a deeper normalization
|
|
618
|
-
// pass, but pre-normalizing here ensures the
|
|
664
|
+
// pass, but pre-normalizing here ensures the data service always receives
|
|
619
665
|
// Spec-canonical keys.
|
|
620
666
|
const normalized: Record<string, unknown> = { ...query };
|
|
621
667
|
|
|
622
668
|
// filter/filters → where
|
|
623
669
|
// Note: `filter` is the canonical HTTP *transport* parameter name
|
|
624
670
|
// (see HttpFindQueryParamsSchema). It is normalized here to the
|
|
625
|
-
// canonical *QueryAST* field name `where` before
|
|
671
|
+
// canonical *QueryAST* field name `where` before data dispatch.
|
|
626
672
|
// `filters` (plural) is a deprecated alias for `filter`.
|
|
627
673
|
if (normalized.filter != null || normalized.filters != null) {
|
|
628
674
|
normalized.where = normalized.where ?? normalized.filter ?? normalized.filters;
|
|
@@ -650,15 +696,15 @@ export class HttpDispatcher {
|
|
|
650
696
|
delete normalized.skip;
|
|
651
697
|
}
|
|
652
698
|
|
|
653
|
-
// Spec:
|
|
654
|
-
const result = await
|
|
699
|
+
// Spec: returns FindDataResponse = { object, records, total?, hasMore? }
|
|
700
|
+
const result = await this.callData('query', { object: objectName, query: normalized });
|
|
655
701
|
return { handled: true, response: this.success(result) };
|
|
656
702
|
}
|
|
657
703
|
|
|
658
704
|
// POST /data/:object (Create)
|
|
659
705
|
if (m === 'POST') {
|
|
660
|
-
// Spec:
|
|
661
|
-
const result = await
|
|
706
|
+
// Spec: returns CreateDataResponse = { object, id, record }
|
|
707
|
+
const result = await this.callData('create', { object: objectName, data: body });
|
|
662
708
|
const res = this.success(result);
|
|
663
709
|
res.status = 201;
|
|
664
710
|
return { handled: true, response: res };
|
|
@@ -793,10 +839,9 @@ export class HttpDispatcher {
|
|
|
793
839
|
* - POST /packages/:id/publish → publish a package (metadata snapshot)
|
|
794
840
|
* - POST /packages/:id/revert → revert a package to last published state
|
|
795
841
|
*
|
|
796
|
-
* Uses ObjectQL SchemaRegistry directly (via the 'objectql' service)
|
|
797
|
-
* with broker fallback for backward compatibility.
|
|
842
|
+
* Uses ObjectQL SchemaRegistry directly (via the 'objectql' service).
|
|
798
843
|
*/
|
|
799
|
-
async handlePackages(path: string, method: string, body: any, query: any,
|
|
844
|
+
async handlePackages(path: string, method: string, body: any, query: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult> {
|
|
800
845
|
const m = method.toUpperCase();
|
|
801
846
|
const parts = path.replace(/^\/+/, '').split('/').filter(Boolean);
|
|
802
847
|
|
|
@@ -804,11 +849,8 @@ export class HttpDispatcher {
|
|
|
804
849
|
const qlService = await this.getObjectQLService();
|
|
805
850
|
const registry = qlService?.registry;
|
|
806
851
|
|
|
807
|
-
// If no registry available,
|
|
852
|
+
// If no registry available, return 503
|
|
808
853
|
if (!registry) {
|
|
809
|
-
if (this.kernel.broker) {
|
|
810
|
-
return this.handlePackagesViaBroker(parts, m, body, query, context);
|
|
811
|
-
}
|
|
812
854
|
return { handled: true, response: this.error('Package service not available', 503) };
|
|
813
855
|
}
|
|
814
856
|
|
|
@@ -858,11 +900,6 @@ export class HttpDispatcher {
|
|
|
858
900
|
const result = await (metadataService as any).publishPackage(id, body || {});
|
|
859
901
|
return { handled: true, response: this.success(result) };
|
|
860
902
|
}
|
|
861
|
-
// Broker fallback
|
|
862
|
-
if (this.kernel.broker) {
|
|
863
|
-
const result = await this.kernel.broker.call('metadata.publishPackage', { packageId: id, ...body }, { request: context.request });
|
|
864
|
-
return { handled: true, response: this.success(result) };
|
|
865
|
-
}
|
|
866
903
|
return { handled: true, response: this.error('Metadata service not available', 503) };
|
|
867
904
|
}
|
|
868
905
|
|
|
@@ -874,11 +911,6 @@ export class HttpDispatcher {
|
|
|
874
911
|
await (metadataService as any).revertPackage(id);
|
|
875
912
|
return { handled: true, response: this.success({ success: true }) };
|
|
876
913
|
}
|
|
877
|
-
// Broker fallback
|
|
878
|
-
if (this.kernel.broker) {
|
|
879
|
-
await this.kernel.broker.call('metadata.revertPackage', { packageId: id }, { request: context.request });
|
|
880
|
-
return { handled: true, response: this.success({ success: true }) };
|
|
881
|
-
}
|
|
882
914
|
return { handled: true, response: this.error('Metadata service not available', 503) };
|
|
883
915
|
}
|
|
884
916
|
|
|
@@ -904,47 +936,7 @@ export class HttpDispatcher {
|
|
|
904
936
|
return { handled: false };
|
|
905
937
|
}
|
|
906
938
|
|
|
907
|
-
|
|
908
|
-
* Fallback: handle packages via broker (for backward compatibility)
|
|
909
|
-
*/
|
|
910
|
-
private async handlePackagesViaBroker(parts: string[], m: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
|
|
911
|
-
const broker = this.kernel.broker;
|
|
912
|
-
try {
|
|
913
|
-
if (parts.length === 0 && m === 'GET') {
|
|
914
|
-
const result = await broker.call('package.list', query || {}, { request: context.request });
|
|
915
|
-
return { handled: true, response: this.success(result) };
|
|
916
|
-
}
|
|
917
|
-
if (parts.length === 0 && m === 'POST') {
|
|
918
|
-
const result = await broker.call('package.install', body, { request: context.request });
|
|
919
|
-
const res = this.success(result);
|
|
920
|
-
res.status = 201;
|
|
921
|
-
return { handled: true, response: res };
|
|
922
|
-
}
|
|
923
|
-
if (parts.length === 2 && parts[1] === 'enable' && m === 'PATCH') {
|
|
924
|
-
const id = decodeURIComponent(parts[0]);
|
|
925
|
-
const result = await broker.call('package.enable', { id }, { request: context.request });
|
|
926
|
-
return { handled: true, response: this.success(result) };
|
|
927
|
-
}
|
|
928
|
-
if (parts.length === 2 && parts[1] === 'disable' && m === 'PATCH') {
|
|
929
|
-
const id = decodeURIComponent(parts[0]);
|
|
930
|
-
const result = await broker.call('package.disable', { id }, { request: context.request });
|
|
931
|
-
return { handled: true, response: this.success(result) };
|
|
932
|
-
}
|
|
933
|
-
if (parts.length === 1 && m === 'GET') {
|
|
934
|
-
const id = decodeURIComponent(parts[0]);
|
|
935
|
-
const result = await broker.call('package.get', { id }, { request: context.request });
|
|
936
|
-
return { handled: true, response: this.success(result) };
|
|
937
|
-
}
|
|
938
|
-
if (parts.length === 1 && m === 'DELETE') {
|
|
939
|
-
const id = decodeURIComponent(parts[0]);
|
|
940
|
-
const result = await broker.call('package.uninstall', { id }, { request: context.request });
|
|
941
|
-
return { handled: true, response: this.success(result) };
|
|
942
|
-
}
|
|
943
|
-
} catch (e: any) {
|
|
944
|
-
return { handled: true, response: this.error(e.message, e.statusCode || 500) };
|
|
945
|
-
}
|
|
946
|
-
return { handled: false };
|
|
947
|
-
}
|
|
939
|
+
|
|
948
940
|
|
|
949
941
|
/**
|
|
950
942
|
* Handles Storage requests
|
|
@@ -1208,10 +1200,6 @@ export class HttpDispatcher {
|
|
|
1208
1200
|
return null;
|
|
1209
1201
|
}
|
|
1210
1202
|
|
|
1211
|
-
private capitalize(s: string) {
|
|
1212
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
1203
|
/**
|
|
1216
1204
|
* Handle AI service routes (/ai/chat, /ai/models, /ai/conversations, etc.)
|
|
1217
1205
|
* Resolves the AI service and its built-in route handlers, then dispatches.
|
|
@@ -1393,10 +1381,12 @@ export class HttpDispatcher {
|
|
|
1393
1381
|
|
|
1394
1382
|
// OpenAPI Specification
|
|
1395
1383
|
if (cleanPath === '/openapi.json' && method === 'GET') {
|
|
1396
|
-
const broker = this.ensureBroker();
|
|
1397
1384
|
try {
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1385
|
+
const metaSvc = await this.resolveService('metadata');
|
|
1386
|
+
if (metaSvc && typeof (metaSvc as any).generateOpenApi === 'function') {
|
|
1387
|
+
const result = await (metaSvc as any).generateOpenApi({});
|
|
1388
|
+
return { handled: true, response: this.success(result) };
|
|
1389
|
+
}
|
|
1400
1390
|
} catch (e) {
|
|
1401
1391
|
// If not implemented, fall through or return 404
|
|
1402
1392
|
}
|
|
@@ -1418,17 +1408,22 @@ export class HttpDispatcher {
|
|
|
1418
1408
|
* Handles Custom API Endpoints defined in metadata
|
|
1419
1409
|
*/
|
|
1420
1410
|
async handleApiEndpoint(path: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
|
|
1421
|
-
const broker = this.ensureBroker();
|
|
1422
1411
|
try {
|
|
1423
1412
|
// Attempt to find a matching endpoint in the registry
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1413
|
+
const metaSvc = await this.resolveService('metadata');
|
|
1414
|
+
if (!metaSvc || typeof (metaSvc as any).matchEndpoint !== 'function') {
|
|
1415
|
+
return { handled: false };
|
|
1416
|
+
}
|
|
1417
|
+
const endpoint = await (metaSvc as any).matchEndpoint({ path, method });
|
|
1427
1418
|
|
|
1428
1419
|
if (endpoint) {
|
|
1429
1420
|
// Execute the endpoint target logic
|
|
1430
1421
|
if (endpoint.type === 'flow') {
|
|
1431
|
-
const
|
|
1422
|
+
const automationSvc = await this.resolveService('automation');
|
|
1423
|
+
if (!automationSvc || typeof (automationSvc as any).runFlow !== 'function') {
|
|
1424
|
+
return { handled: true, response: this.error('Automation service not available', 503) };
|
|
1425
|
+
}
|
|
1426
|
+
const result = await (automationSvc as any).runFlow({
|
|
1432
1427
|
flowId: endpoint.target,
|
|
1433
1428
|
inputs: { ...query, ...body, _request: context.request }
|
|
1434
1429
|
});
|
|
@@ -1436,10 +1431,14 @@ export class HttpDispatcher {
|
|
|
1436
1431
|
}
|
|
1437
1432
|
|
|
1438
1433
|
if (endpoint.type === 'script') {
|
|
1439
|
-
|
|
1434
|
+
const automationSvc = await this.resolveService('automation');
|
|
1435
|
+
if (!automationSvc || typeof (automationSvc as any).runScript !== 'function') {
|
|
1436
|
+
return { handled: true, response: this.error('Automation service not available', 503) };
|
|
1437
|
+
}
|
|
1438
|
+
const result = await (automationSvc as any).runScript({
|
|
1440
1439
|
scriptName: endpoint.target,
|
|
1441
1440
|
context: { ...query, ...body, request: context.request }
|
|
1442
|
-
}
|
|
1441
|
+
});
|
|
1443
1442
|
return { handled: true, response: this.success(result) };
|
|
1444
1443
|
}
|
|
1445
1444
|
|
|
@@ -1449,25 +1448,22 @@ export class HttpDispatcher {
|
|
|
1449
1448
|
const { object, operation } = endpoint.objectParams;
|
|
1450
1449
|
// Map standard CRUD operations
|
|
1451
1450
|
if (operation === 'find') {
|
|
1452
|
-
const result = await
|
|
1451
|
+
const result = await this.callData('query', { object, query });
|
|
1453
1452
|
// Spec: FindDataResponse = { object, records, total?, hasMore? }
|
|
1454
1453
|
return { handled: true, response: this.success(result.records, { total: result.total }) };
|
|
1455
1454
|
}
|
|
1456
1455
|
if (operation === 'get' && query.id) {
|
|
1457
|
-
const result = await
|
|
1456
|
+
const result = await this.callData('get', { object, id: query.id });
|
|
1458
1457
|
return { handled: true, response: this.success(result) };
|
|
1459
1458
|
}
|
|
1460
1459
|
if (operation === 'create') {
|
|
1461
|
-
const result = await
|
|
1460
|
+
const result = await this.callData('create', { object, data: body });
|
|
1462
1461
|
return { handled: true, response: this.success(result) };
|
|
1463
1462
|
}
|
|
1464
1463
|
}
|
|
1465
1464
|
}
|
|
1466
1465
|
|
|
1467
1466
|
if (endpoint.type === 'proxy') {
|
|
1468
|
-
// Simple proxy implementation (requires a network call, which usually is done by a service but here we can stub return)
|
|
1469
|
-
// In real implementation this might fetch(endpoint.target)
|
|
1470
|
-
// For now, return target info
|
|
1471
1467
|
return {
|
|
1472
1468
|
handled: true,
|
|
1473
1469
|
response: {
|