@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/runtime",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.4",
|
|
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": "4.0.
|
|
19
|
-
"@objectstack/rest": "4.0.
|
|
20
|
-
"@objectstack/spec": "4.0.
|
|
21
|
-
"@objectstack/types": "4.0.
|
|
18
|
+
"@objectstack/core": "4.0.4",
|
|
19
|
+
"@objectstack/rest": "4.0.4",
|
|
20
|
+
"@objectstack/spec": "4.0.4",
|
|
21
|
+
"@objectstack/types": "4.0.4"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"typescript": "^6.0.2",
|
|
25
|
-
"vitest": "^4.1.
|
|
25
|
+
"vitest": "^4.1.4"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "tsup --config ../../tsup.config.ts",
|
package/src/app-plugin.ts
CHANGED
|
@@ -75,6 +75,15 @@ export class AppPlugin implements Plugin {
|
|
|
75
75
|
|
|
76
76
|
ctx.logger.debug('Retrieved ObjectQL engine service', { appId });
|
|
77
77
|
|
|
78
|
+
// Configure datasourceMapping if provided in the stack definition
|
|
79
|
+
if (this.bundle.datasourceMapping && Array.isArray(this.bundle.datasourceMapping)) {
|
|
80
|
+
ctx.logger.info('Configuring datasource mapping rules', {
|
|
81
|
+
appId,
|
|
82
|
+
ruleCount: this.bundle.datasourceMapping.length
|
|
83
|
+
});
|
|
84
|
+
ql.setDatasourceMapping(this.bundle.datasourceMapping);
|
|
85
|
+
}
|
|
86
|
+
|
|
78
87
|
const runtime = this.bundle.default || this.bundle;
|
|
79
88
|
|
|
80
89
|
if (runtime && typeof runtime.onEnable === 'function') {
|
package/src/dispatcher-plugin.ts
CHANGED
|
@@ -22,6 +22,75 @@ interface RouteDefinition {
|
|
|
22
22
|
handler: (req: any) => Promise<any>;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Register a single RouteDefinition on the HTTP server.
|
|
27
|
+
* Returns true if the route was successfully registered.
|
|
28
|
+
*/
|
|
29
|
+
function mountRouteOnServer(route: RouteDefinition, server: IHttpServer, routePath: string): boolean {
|
|
30
|
+
const handler = async (req: any, res: any) => {
|
|
31
|
+
try {
|
|
32
|
+
const result = await route.handler({
|
|
33
|
+
body: req.body,
|
|
34
|
+
params: req.params,
|
|
35
|
+
query: req.query,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (result.stream && result.events) {
|
|
39
|
+
// SSE streaming response
|
|
40
|
+
res.status(result.status);
|
|
41
|
+
|
|
42
|
+
// Apply headers from the route result if available
|
|
43
|
+
if (result.headers) {
|
|
44
|
+
for (const [k, v] of Object.entries(result.headers)) {
|
|
45
|
+
res.header(k, String(v));
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
res.header('Content-Type', 'text/event-stream');
|
|
49
|
+
res.header('Cache-Control', 'no-cache');
|
|
50
|
+
res.header('Connection', 'keep-alive');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Write the stream — events are pre-encoded SSE strings
|
|
54
|
+
if (typeof res.write === 'function' && typeof res.end === 'function') {
|
|
55
|
+
for await (const event of result.events) {
|
|
56
|
+
res.write(typeof event === 'string' ? event : `data: ${JSON.stringify(event)}\n\n`);
|
|
57
|
+
}
|
|
58
|
+
res.end();
|
|
59
|
+
} else {
|
|
60
|
+
// Fallback: collect events into array
|
|
61
|
+
const events = [];
|
|
62
|
+
for await (const event of result.events) {
|
|
63
|
+
events.push(event);
|
|
64
|
+
}
|
|
65
|
+
res.json({ events });
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
res.status(result.status);
|
|
69
|
+
if (result.body !== undefined) {
|
|
70
|
+
res.json(result.body);
|
|
71
|
+
} else {
|
|
72
|
+
res.end();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch (err: any) {
|
|
76
|
+
errorResponse(err, res);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const m = route.method.toLowerCase();
|
|
81
|
+
if (m === 'get' && typeof server.get === 'function') {
|
|
82
|
+
server.get(routePath, handler);
|
|
83
|
+
return true;
|
|
84
|
+
} else if (m === 'post' && typeof server.post === 'function') {
|
|
85
|
+
server.post(routePath, handler);
|
|
86
|
+
return true;
|
|
87
|
+
} else if (m === 'delete' && typeof server.delete === 'function') {
|
|
88
|
+
server.delete(routePath, handler);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
25
94
|
/**
|
|
26
95
|
* Send an HttpDispatcherResult through IHttpResponse.
|
|
27
96
|
* Differentiates between handled, unhandled (404), and special results.
|
|
@@ -402,68 +471,33 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
|
|
|
402
471
|
const routePath = route.path.startsWith('/api/v1')
|
|
403
472
|
? route.path
|
|
404
473
|
: `${prefix}${route.path}`;
|
|
405
|
-
|
|
406
|
-
const handler = async (req: any, res: any) => {
|
|
407
|
-
try {
|
|
408
|
-
const result = await route.handler({
|
|
409
|
-
body: req.body,
|
|
410
|
-
params: req.params,
|
|
411
|
-
query: req.query,
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
if (result.stream && result.events) {
|
|
415
|
-
// SSE streaming response
|
|
416
|
-
res.status(result.status);
|
|
417
|
-
|
|
418
|
-
// Apply headers from the route result if available
|
|
419
|
-
if (result.headers) {
|
|
420
|
-
for (const [k, v] of Object.entries(result.headers)) {
|
|
421
|
-
res.header(k, v);
|
|
422
|
-
}
|
|
423
|
-
} else {
|
|
424
|
-
res.header('Content-Type', 'text/event-stream');
|
|
425
|
-
res.header('Cache-Control', 'no-cache');
|
|
426
|
-
res.header('Connection', 'keep-alive');
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Write the stream — events are pre-encoded SSE strings
|
|
430
|
-
if (typeof res.write === 'function' && typeof res.end === 'function') {
|
|
431
|
-
for await (const event of result.events) {
|
|
432
|
-
res.write(typeof event === 'string' ? event : `data: ${JSON.stringify(event)}\n\n`);
|
|
433
|
-
}
|
|
434
|
-
res.end();
|
|
435
|
-
} else {
|
|
436
|
-
// Fallback: collect events into array
|
|
437
|
-
const events = [];
|
|
438
|
-
for await (const event of result.events) {
|
|
439
|
-
events.push(event);
|
|
440
|
-
}
|
|
441
|
-
res.json({ events });
|
|
442
|
-
}
|
|
443
|
-
} else {
|
|
444
|
-
res.status(result.status);
|
|
445
|
-
if (result.body !== undefined) {
|
|
446
|
-
res.json(result.body);
|
|
447
|
-
} else {
|
|
448
|
-
res.end();
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
} catch (err: any) {
|
|
452
|
-
errorResponse(err, res);
|
|
453
|
-
}
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
const m = route.method.toLowerCase();
|
|
457
|
-
if (m === 'get' && typeof server.get === 'function') {
|
|
458
|
-
server.get(routePath, handler);
|
|
459
|
-
} else if (m === 'post' && typeof server.post === 'function') {
|
|
460
|
-
server.post(routePath, handler);
|
|
461
|
-
} else if (m === 'delete' && typeof server.delete === 'function') {
|
|
462
|
-
server.delete(routePath, handler);
|
|
463
|
-
}
|
|
474
|
+
mountRouteOnServer(route, server, routePath);
|
|
464
475
|
}
|
|
465
476
|
ctx.logger.info(`[Dispatcher] Registered ${routes.length} AI routes`);
|
|
466
477
|
});
|
|
478
|
+
|
|
479
|
+
// ── Fallback: recover routes cached before hook was registered ──
|
|
480
|
+
// If AIServicePlugin.start() ran before DispatcherPlugin.start()
|
|
481
|
+
// (possible when plugin start order differs from registration order),
|
|
482
|
+
// the 'ai:routes' trigger fires with no listener. The AIServicePlugin
|
|
483
|
+
// caches the routes on the kernel as __aiRoutes (see AIServicePlugin.start())
|
|
484
|
+
// as an internal cross-plugin protocol so we can recover them here.
|
|
485
|
+
// TODO: replace with a formal kernel.getCachedRoutes('ai') API in a future release.
|
|
486
|
+
const cachedRoutes = (kernel as any).__aiRoutes as RouteDefinition[] | undefined;
|
|
487
|
+
if (cachedRoutes && Array.isArray(cachedRoutes) && cachedRoutes.length > 0) {
|
|
488
|
+
let registered = 0;
|
|
489
|
+
for (const route of cachedRoutes) {
|
|
490
|
+
const routePath = route.path.startsWith('/api/v1')
|
|
491
|
+
? route.path
|
|
492
|
+
: `${prefix}${route.path}`;
|
|
493
|
+
if (mountRouteOnServer(route, server, routePath)) {
|
|
494
|
+
registered++;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (registered > 0) {
|
|
498
|
+
ctx.logger.info(`[Dispatcher] Recovered ${registered} cached AI routes (hook timing fallback)`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
467
501
|
},
|
|
468
502
|
};
|
|
469
503
|
}
|