@objectstack/runtime 4.0.3 → 4.0.5
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/dist/index.cjs +36768 -1106
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +718 -15
- package/dist/index.d.ts +718 -15
- package/dist/index.js +36780 -1099
- package/dist/index.js.map +1 -1
- package/package.json +42 -9
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -753
- package/src/app-plugin.test.ts +0 -274
- package/src/app-plugin.ts +0 -276
- package/src/dispatcher-plugin.ts +0 -503
- package/src/driver-plugin.ts +0 -76
- package/src/http-dispatcher.root.test.ts +0 -76
- package/src/http-dispatcher.test.ts +0 -1312
- package/src/http-dispatcher.ts +0 -1563
- package/src/http-server.ts +0 -142
- package/src/index.ts +0 -39
- package/src/middleware.ts +0 -222
- package/src/runtime.test.ts +0 -65
- package/src/runtime.ts +0 -69
- package/src/seed-loader.test.ts +0 -1123
- package/src/seed-loader.ts +0 -713
- package/tsconfig.json +0 -10
- package/vitest.config.ts +0 -26
package/src/http-dispatcher.ts
DELETED
|
@@ -1,1563 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import { ObjectKernel, getEnv, resolveLocale } from '@objectstack/core';
|
|
4
|
-
import { CoreServiceName } from '@objectstack/spec/system';
|
|
5
|
-
import { pluralToSingular } from '@objectstack/spec/shared';
|
|
6
|
-
|
|
7
|
-
/** Browser-safe UUID generator — prefers Web Crypto, falls back to RFC 4122 v4 */
|
|
8
|
-
function randomUUID(): string {
|
|
9
|
-
if (globalThis.crypto && typeof globalThis.crypto.randomUUID === 'function') {
|
|
10
|
-
return globalThis.crypto.randomUUID();
|
|
11
|
-
}
|
|
12
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
13
|
-
const r = (Math.random() * 16) | 0;
|
|
14
|
-
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
15
|
-
return v.toString(16);
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface HttpProtocolContext {
|
|
20
|
-
request: any;
|
|
21
|
-
response?: any;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface HttpDispatcherResult {
|
|
25
|
-
handled: boolean;
|
|
26
|
-
response?: {
|
|
27
|
-
status: number;
|
|
28
|
-
body?: any;
|
|
29
|
-
headers?: Record<string, string>;
|
|
30
|
-
};
|
|
31
|
-
result?: any; // For flexible return types or direct response objects (Response/NextResponse)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @deprecated Use `createDispatcherPlugin()` from `@objectstack/runtime` instead.
|
|
36
|
-
* This class will be removed in v2. Prefer the plugin-based approach:
|
|
37
|
-
* ```ts
|
|
38
|
-
* import { createDispatcherPlugin } from '@objectstack/runtime';
|
|
39
|
-
* kernel.use(createDispatcherPlugin({ prefix: '/api/v1' }));
|
|
40
|
-
* ```
|
|
41
|
-
*/
|
|
42
|
-
export class HttpDispatcher {
|
|
43
|
-
private kernel: any; // Casting to any to access dynamic props like broker, services, graphql
|
|
44
|
-
|
|
45
|
-
constructor(kernel: ObjectKernel) {
|
|
46
|
-
this.kernel = kernel;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
private success(data: any, meta?: any) {
|
|
50
|
-
return {
|
|
51
|
-
status: 200,
|
|
52
|
-
body: { success: true, data, meta }
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
private error(message: string, code: number = 500, details?: any) {
|
|
57
|
-
return {
|
|
58
|
-
status: code,
|
|
59
|
-
body: { success: false, error: { message, code, details } }
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* 404 Route Not Found — no route is registered for this path.
|
|
65
|
-
*/
|
|
66
|
-
private routeNotFound(route: string) {
|
|
67
|
-
return {
|
|
68
|
-
status: 404,
|
|
69
|
-
body: {
|
|
70
|
-
success: false,
|
|
71
|
-
error: {
|
|
72
|
-
code: 404,
|
|
73
|
-
message: `Route Not Found: ${route}`,
|
|
74
|
-
type: 'ROUTE_NOT_FOUND' as const,
|
|
75
|
-
route,
|
|
76
|
-
hint: 'No route is registered for this path. Check the API discovery endpoint for available routes.',
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
private ensureBroker() {
|
|
83
|
-
if (!this.kernel.broker) {
|
|
84
|
-
throw { statusCode: 500, message: 'Kernel Broker not available' };
|
|
85
|
-
}
|
|
86
|
-
return this.kernel.broker;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Generates the discovery JSON response for the API root.
|
|
91
|
-
*
|
|
92
|
-
* Uses the same async `resolveService()` fallback chain that request
|
|
93
|
-
* handlers use, so the reported service status is always consistent
|
|
94
|
-
* with the actual runtime availability.
|
|
95
|
-
*/
|
|
96
|
-
async getDiscoveryInfo(prefix: string) {
|
|
97
|
-
// Resolve all services through the same async fallback chain
|
|
98
|
-
// that request handlers (handleI18n, handleAuth, …) use.
|
|
99
|
-
const [
|
|
100
|
-
authSvc, graphqlSvc, searchSvc, realtimeSvc, filesSvc,
|
|
101
|
-
analyticsSvc, workflowSvc, aiSvc, notificationSvc, i18nSvc,
|
|
102
|
-
uiSvc, automationSvc, cacheSvc, queueSvc, jobSvc,
|
|
103
|
-
] = await Promise.all([
|
|
104
|
-
this.resolveService(CoreServiceName.enum.auth),
|
|
105
|
-
this.resolveService(CoreServiceName.enum.graphql),
|
|
106
|
-
this.resolveService(CoreServiceName.enum.search),
|
|
107
|
-
this.resolveService(CoreServiceName.enum.realtime),
|
|
108
|
-
this.resolveService(CoreServiceName.enum['file-storage']),
|
|
109
|
-
this.resolveService(CoreServiceName.enum.analytics),
|
|
110
|
-
this.resolveService(CoreServiceName.enum.workflow),
|
|
111
|
-
this.resolveService(CoreServiceName.enum.ai),
|
|
112
|
-
this.resolveService(CoreServiceName.enum.notification),
|
|
113
|
-
this.resolveService(CoreServiceName.enum.i18n),
|
|
114
|
-
this.resolveService(CoreServiceName.enum.ui),
|
|
115
|
-
this.resolveService(CoreServiceName.enum.automation),
|
|
116
|
-
this.resolveService(CoreServiceName.enum.cache),
|
|
117
|
-
this.resolveService(CoreServiceName.enum.queue),
|
|
118
|
-
this.resolveService(CoreServiceName.enum.job),
|
|
119
|
-
]);
|
|
120
|
-
|
|
121
|
-
const hasAuth = !!authSvc;
|
|
122
|
-
const hasGraphQL = !!(graphqlSvc || this.kernel.graphql);
|
|
123
|
-
const hasSearch = !!searchSvc;
|
|
124
|
-
const hasWebSockets = !!realtimeSvc;
|
|
125
|
-
const hasFiles = !!filesSvc;
|
|
126
|
-
const hasAnalytics = !!analyticsSvc;
|
|
127
|
-
const hasWorkflow = !!workflowSvc;
|
|
128
|
-
const hasAi = !!aiSvc;
|
|
129
|
-
const hasNotification = !!notificationSvc;
|
|
130
|
-
const hasI18n = !!i18nSvc;
|
|
131
|
-
const hasUi = !!uiSvc;
|
|
132
|
-
const hasAutomation = !!automationSvc;
|
|
133
|
-
const hasCache = !!cacheSvc;
|
|
134
|
-
const hasQueue = !!queueSvc;
|
|
135
|
-
const hasJob = !!jobSvc;
|
|
136
|
-
|
|
137
|
-
// Routes are only exposed when a plugin provides the service
|
|
138
|
-
const routes = {
|
|
139
|
-
data: `${prefix}/data`,
|
|
140
|
-
metadata: `${prefix}/meta`,
|
|
141
|
-
packages: `${prefix}/packages`,
|
|
142
|
-
auth: hasAuth ? `${prefix}/auth` : undefined,
|
|
143
|
-
ui: hasUi ? `${prefix}/ui` : undefined,
|
|
144
|
-
graphql: hasGraphQL ? `${prefix}/graphql` : undefined,
|
|
145
|
-
storage: hasFiles ? `${prefix}/storage` : undefined,
|
|
146
|
-
analytics: hasAnalytics ? `${prefix}/analytics` : undefined,
|
|
147
|
-
automation: hasAutomation ? `${prefix}/automation` : undefined,
|
|
148
|
-
workflow: hasWorkflow ? `${prefix}/workflow` : undefined,
|
|
149
|
-
realtime: hasWebSockets ? `${prefix}/realtime` : undefined,
|
|
150
|
-
notifications: hasNotification ? `${prefix}/notifications` : undefined,
|
|
151
|
-
ai: hasAi ? `${prefix}/ai` : undefined,
|
|
152
|
-
i18n: hasI18n ? `${prefix}/i18n` : undefined,
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
// Build per-service status map
|
|
156
|
-
// handlerReady: true means the dispatcher has a real, bound handler for this route.
|
|
157
|
-
// handlerReady: false means the route is present in the discovery table but may not
|
|
158
|
-
// yet have a concrete implementation or may be served by a stub.
|
|
159
|
-
const svcAvailable = (route?: string, provider?: string) => ({
|
|
160
|
-
enabled: true, status: 'available' as const, handlerReady: true, route, provider,
|
|
161
|
-
});
|
|
162
|
-
const svcUnavailable = (name: string) => ({
|
|
163
|
-
enabled: false, status: 'unavailable' as const, handlerReady: false,
|
|
164
|
-
message: `Install a ${name} plugin to enable`,
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// Derive locale info from actual i18n service when available
|
|
168
|
-
let locale = { default: 'en', supported: ['en'], timezone: 'UTC' };
|
|
169
|
-
if (hasI18n && i18nSvc) {
|
|
170
|
-
const defaultLocale = typeof i18nSvc.getDefaultLocale === 'function'
|
|
171
|
-
? i18nSvc.getDefaultLocale() : 'en';
|
|
172
|
-
const locales = typeof i18nSvc.getLocales === 'function'
|
|
173
|
-
? i18nSvc.getLocales() : [];
|
|
174
|
-
locale = {
|
|
175
|
-
default: defaultLocale,
|
|
176
|
-
supported: locales.length > 0 ? locales : [defaultLocale],
|
|
177
|
-
timezone: 'UTC',
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
name: 'ObjectOS',
|
|
183
|
-
version: '1.0.0',
|
|
184
|
-
environment: getEnv('NODE_ENV', 'development'),
|
|
185
|
-
routes,
|
|
186
|
-
endpoints: routes, // Alias for backward compatibility with some clients
|
|
187
|
-
features: {
|
|
188
|
-
graphql: hasGraphQL,
|
|
189
|
-
search: hasSearch,
|
|
190
|
-
websockets: hasWebSockets,
|
|
191
|
-
files: hasFiles,
|
|
192
|
-
analytics: hasAnalytics,
|
|
193
|
-
ai: hasAi,
|
|
194
|
-
workflow: hasWorkflow,
|
|
195
|
-
notifications: hasNotification,
|
|
196
|
-
i18n: hasI18n,
|
|
197
|
-
},
|
|
198
|
-
services: {
|
|
199
|
-
// Kernel-provided (always available via protocol implementation)
|
|
200
|
-
metadata: { enabled: true, status: 'degraded' as const, handlerReady: true, route: routes.metadata, provider: 'kernel', message: 'In-memory registry; DB persistence pending' },
|
|
201
|
-
data: svcAvailable(routes.data, 'kernel'),
|
|
202
|
-
// Plugin-provided — only available when a plugin registers the service
|
|
203
|
-
auth: hasAuth ? svcAvailable(routes.auth) : svcUnavailable('auth'),
|
|
204
|
-
automation: hasAutomation ? svcAvailable(routes.automation) : svcUnavailable('automation'),
|
|
205
|
-
analytics: hasAnalytics ? svcAvailable(routes.analytics) : svcUnavailable('analytics'),
|
|
206
|
-
cache: hasCache ? svcAvailable() : svcUnavailable('cache'),
|
|
207
|
-
queue: hasQueue ? svcAvailable() : svcUnavailable('queue'),
|
|
208
|
-
job: hasJob ? svcAvailable() : svcUnavailable('job'),
|
|
209
|
-
ui: hasUi ? svcAvailable(routes.ui) : svcUnavailable('ui'),
|
|
210
|
-
workflow: hasWorkflow ? svcAvailable(routes.workflow) : svcUnavailable('workflow'),
|
|
211
|
-
realtime: hasWebSockets ? svcAvailable(routes.realtime) : svcUnavailable('realtime'),
|
|
212
|
-
notification: hasNotification ? svcAvailable(routes.notifications) : svcUnavailable('notification'),
|
|
213
|
-
ai: hasAi ? svcAvailable(routes.ai) : svcUnavailable('ai'),
|
|
214
|
-
i18n: hasI18n ? svcAvailable(routes.i18n) : svcUnavailable('i18n'),
|
|
215
|
-
graphql: hasGraphQL ? svcAvailable(routes.graphql) : svcUnavailable('graphql'),
|
|
216
|
-
'file-storage': hasFiles ? svcAvailable(routes.storage) : svcUnavailable('file-storage'),
|
|
217
|
-
search: hasSearch ? svcAvailable() : svcUnavailable('search'),
|
|
218
|
-
},
|
|
219
|
-
locale,
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Handles GraphQL requests
|
|
225
|
-
*/
|
|
226
|
-
async handleGraphQL(body: { query: string; variables?: any }, context: HttpProtocolContext) {
|
|
227
|
-
if (!body || !body.query) {
|
|
228
|
-
throw { statusCode: 400, message: 'Missing query in request body' };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (typeof this.kernel.graphql !== 'function') {
|
|
232
|
-
throw { statusCode: 501, message: 'GraphQL service not available' };
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return this.kernel.graphql(body.query, body.variables, {
|
|
236
|
-
request: context.request
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Handles Auth requests
|
|
242
|
-
* path: sub-path after /auth/
|
|
243
|
-
*/
|
|
244
|
-
async handleAuth(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
|
|
245
|
-
// 1. Try generic Auth Service
|
|
246
|
-
const authService = await this.getService(CoreServiceName.enum.auth);
|
|
247
|
-
if (authService && typeof authService.handler === 'function') {
|
|
248
|
-
const response = await authService.handler(context.request, context.response);
|
|
249
|
-
return { handled: true, result: response };
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// 2. Legacy Login via broker
|
|
253
|
-
const normalizedPath = path.replace(/^\/+/, '');
|
|
254
|
-
if (normalizedPath === 'login' && method.toUpperCase() === 'POST') {
|
|
255
|
-
try {
|
|
256
|
-
const broker = this.ensureBroker();
|
|
257
|
-
const data = await broker.call('auth.login', body, { request: context.request });
|
|
258
|
-
return { handled: true, response: { status: 200, body: data } };
|
|
259
|
-
} catch (error: any) {
|
|
260
|
-
// Only fall through to mock when the broker is truly unavailable
|
|
261
|
-
// (ensureBroker throws statusCode 500 when kernel.broker is null)
|
|
262
|
-
const statusCode = error?.statusCode ?? error?.status;
|
|
263
|
-
if (statusCode !== 500 || !error?.message?.includes('Broker not available')) {
|
|
264
|
-
throw error;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// 3. Mock fallback for MSW/test environments when no auth service is registered
|
|
270
|
-
return this.mockAuthFallback(normalizedPath, method, body);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Provides mock auth responses for core better-auth endpoints when
|
|
275
|
-
* AuthPlugin is not loaded (e.g. MSW/browser-only environments).
|
|
276
|
-
* This ensures registration/sign-in flows do not 404 in mock mode.
|
|
277
|
-
*/
|
|
278
|
-
private mockAuthFallback(path: string, method: string, body: any): HttpDispatcherResult {
|
|
279
|
-
const m = method.toUpperCase();
|
|
280
|
-
const MOCK_SESSION_EXPIRY_MS = 86_400_000; // 24 hours
|
|
281
|
-
|
|
282
|
-
// POST sign-up/email
|
|
283
|
-
if ((path === 'sign-up/email' || path === 'register') && m === 'POST') {
|
|
284
|
-
const id = `mock_${randomUUID()}`;
|
|
285
|
-
return {
|
|
286
|
-
handled: true,
|
|
287
|
-
response: {
|
|
288
|
-
status: 200,
|
|
289
|
-
body: {
|
|
290
|
-
user: { id, name: body?.name || 'Mock User', email: body?.email || 'mock@test.local', emailVerified: false, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
|
|
291
|
-
session: { id: `session_${id}`, userId: id, token: `mock_token_${id}`, expiresAt: new Date(Date.now() + MOCK_SESSION_EXPIRY_MS).toISOString() },
|
|
292
|
-
},
|
|
293
|
-
},
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// POST sign-in/email or login
|
|
298
|
-
if ((path === 'sign-in/email' || path === 'login') && m === 'POST') {
|
|
299
|
-
const id = `mock_${randomUUID()}`;
|
|
300
|
-
return {
|
|
301
|
-
handled: true,
|
|
302
|
-
response: {
|
|
303
|
-
status: 200,
|
|
304
|
-
body: {
|
|
305
|
-
user: { id, name: 'Mock User', email: body?.email || 'mock@test.local', emailVerified: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
|
|
306
|
-
session: { id: `session_${id}`, userId: id, token: `mock_token_${id}`, expiresAt: new Date(Date.now() + MOCK_SESSION_EXPIRY_MS).toISOString() },
|
|
307
|
-
},
|
|
308
|
-
},
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// GET get-session
|
|
313
|
-
if (path === 'get-session' && m === 'GET') {
|
|
314
|
-
return {
|
|
315
|
-
handled: true,
|
|
316
|
-
response: { status: 200, body: { session: null, user: null } },
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// POST sign-out
|
|
321
|
-
if (path === 'sign-out' && m === 'POST') {
|
|
322
|
-
return {
|
|
323
|
-
handled: true,
|
|
324
|
-
response: { status: 200, body: { success: true } },
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
return { handled: false };
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Handles Metadata requests
|
|
333
|
-
* Standard: /metadata/:type/:name
|
|
334
|
-
* Fallback for backward compat: /metadata (all objects), /metadata/:objectName (get object)
|
|
335
|
-
*/
|
|
336
|
-
async handleMetadata(path: string, context: HttpProtocolContext, method?: string, body?: any, query?: any): Promise<HttpDispatcherResult> {
|
|
337
|
-
// Broker is used as a fallback — not required upfront.
|
|
338
|
-
// This allows metadata to be served when only the protocol service
|
|
339
|
-
// or ObjectQL service is available (e.g. lightweight / serverless setups).
|
|
340
|
-
const broker = this.kernel.broker ?? null;
|
|
341
|
-
const parts = path.replace(/^\/+/, '').split('/').filter(Boolean);
|
|
342
|
-
|
|
343
|
-
// GET /metadata/types
|
|
344
|
-
if (parts[0] === 'types') {
|
|
345
|
-
// PRIORITY 1: Try MetadataService directly (includes both typeRegistry with agent/tool AND runtime-registered types)
|
|
346
|
-
console.log('[HttpDispatcher] Attempting to resolve MetadataService...');
|
|
347
|
-
console.log('[HttpDispatcher] Available kernel methods:', {
|
|
348
|
-
hasGetServiceAsync: typeof this.kernel.getServiceAsync === 'function',
|
|
349
|
-
hasGetService: typeof this.kernel.getService === 'function',
|
|
350
|
-
hasContext: !!this.kernel.context,
|
|
351
|
-
hasContextGetService: typeof this.kernel.context?.getService === 'function',
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
// Try all service resolution paths with detailed logging
|
|
355
|
-
let metadataService: any = null;
|
|
356
|
-
|
|
357
|
-
// Path 1: kernel.getServiceAsync
|
|
358
|
-
if (typeof this.kernel.getServiceAsync === 'function') {
|
|
359
|
-
try {
|
|
360
|
-
metadataService = await this.kernel.getServiceAsync('metadata');
|
|
361
|
-
console.log('[HttpDispatcher] kernel.getServiceAsync("metadata") returned:', !!metadataService);
|
|
362
|
-
} catch (e: any) {
|
|
363
|
-
console.log('[HttpDispatcher] kernel.getServiceAsync("metadata") failed:', e.message);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Path 2: kernel.getService (if not found via async)
|
|
368
|
-
if (!metadataService && typeof this.kernel.getService === 'function') {
|
|
369
|
-
try {
|
|
370
|
-
metadataService = await this.kernel.getService('metadata');
|
|
371
|
-
console.log('[HttpDispatcher] kernel.getService("metadata") returned:', !!metadataService);
|
|
372
|
-
} catch (e: any) {
|
|
373
|
-
console.log('[HttpDispatcher] kernel.getService("metadata") failed:', e.message);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Path 3: kernel.context.getService (if not found)
|
|
378
|
-
if (!metadataService && this.kernel.context?.getService) {
|
|
379
|
-
try {
|
|
380
|
-
metadataService = await this.kernel.context.getService('metadata');
|
|
381
|
-
console.log('[HttpDispatcher] kernel.context.getService("metadata") returned:', !!metadataService);
|
|
382
|
-
} catch (e: any) {
|
|
383
|
-
console.log('[HttpDispatcher] kernel.context.getService("metadata") failed:', e.message);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
console.log('[HttpDispatcher] Final metadataService:', !!metadataService, 'has getRegisteredTypes:', typeof (metadataService as any)?.getRegisteredTypes);
|
|
388
|
-
|
|
389
|
-
if (metadataService && typeof (metadataService as any).getRegisteredTypes === 'function') {
|
|
390
|
-
try {
|
|
391
|
-
const types = await (metadataService as any).getRegisteredTypes();
|
|
392
|
-
console.log('[HttpDispatcher] MetadataService.getRegisteredTypes() returned:', types);
|
|
393
|
-
return { handled: true, response: this.success({ types }) };
|
|
394
|
-
} catch (e: any) {
|
|
395
|
-
// Log error but continue to fallbacks
|
|
396
|
-
console.warn('[HttpDispatcher] MetadataService.getRegisteredTypes() failed:', e.message, e.stack);
|
|
397
|
-
}
|
|
398
|
-
} else {
|
|
399
|
-
console.log('[HttpDispatcher] MetadataService not available or missing getRegisteredTypes, falling back to protocol service');
|
|
400
|
-
}
|
|
401
|
-
// PRIORITY 2: Try protocol service (returns SchemaRegistry types only - missing agent/tool)
|
|
402
|
-
const protocol = await this.resolveService('protocol');
|
|
403
|
-
if (protocol && typeof protocol.getMetaTypes === 'function') {
|
|
404
|
-
const result = await protocol.getMetaTypes({});
|
|
405
|
-
console.log('[HttpDispatcher] Protocol service returned types:', result);
|
|
406
|
-
return { handled: true, response: this.success(result) };
|
|
407
|
-
}
|
|
408
|
-
// PRIORITY 3: ask broker for registered types
|
|
409
|
-
if (broker) {
|
|
410
|
-
try {
|
|
411
|
-
const data = await broker.call('metadata.types', {}, { request: context.request });
|
|
412
|
-
console.log('[HttpDispatcher] Broker returned types:', data);
|
|
413
|
-
return { handled: true, response: this.success(data) };
|
|
414
|
-
} catch (e) {
|
|
415
|
-
console.log('[HttpDispatcher] Broker call failed:', e);
|
|
416
|
-
// fall through to hardcoded defaults
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
// Last resort: hardcoded defaults
|
|
420
|
-
console.warn('[HttpDispatcher] Falling back to hardcoded defaults for metadata types');
|
|
421
|
-
return { handled: true, response: this.success({ types: ['object', 'app', 'plugin'] }) };
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// GET /metadata/:type/:name/published → get published version
|
|
425
|
-
if (parts.length === 3 && parts[2] === 'published' && (!method || method === 'GET')) {
|
|
426
|
-
const [type, name] = parts;
|
|
427
|
-
const metadataService = await this.getService(CoreServiceName.enum.metadata);
|
|
428
|
-
if (metadataService && typeof (metadataService as any).getPublished === 'function') {
|
|
429
|
-
const data = await (metadataService as any).getPublished(type, name);
|
|
430
|
-
if (data === undefined) return { handled: true, response: this.error('Not found', 404) };
|
|
431
|
-
return { handled: true, response: this.success(data) };
|
|
432
|
-
}
|
|
433
|
-
// Broker fallback
|
|
434
|
-
if (broker) {
|
|
435
|
-
try {
|
|
436
|
-
const data = await broker.call('metadata.getPublished', { type, name }, { request: context.request });
|
|
437
|
-
return { handled: true, response: this.success(data) };
|
|
438
|
-
} catch (e: any) {
|
|
439
|
-
return { handled: true, response: this.error(e.message, 404) };
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return { handled: true, response: this.error('Not found', 404) };
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// /metadata/:type/:name
|
|
446
|
-
if (parts.length === 2) {
|
|
447
|
-
const [type, name] = parts;
|
|
448
|
-
// Extract optional package filter from query string
|
|
449
|
-
const packageId = query?.package || undefined;
|
|
450
|
-
|
|
451
|
-
// PUT /metadata/:type/:name (Save)
|
|
452
|
-
if (method === 'PUT' && body) {
|
|
453
|
-
// Try to get the protocol service directly
|
|
454
|
-
const protocol = await this.resolveService('protocol');
|
|
455
|
-
|
|
456
|
-
if (protocol && typeof protocol.saveMetaItem === 'function') {
|
|
457
|
-
try {
|
|
458
|
-
const result = await protocol.saveMetaItem({ type, name, item: body });
|
|
459
|
-
return { handled: true, response: this.success(result) };
|
|
460
|
-
} catch (e: any) {
|
|
461
|
-
return { handled: true, response: this.error(e.message, 400) };
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Fallback to broker if protocol not available (legacy)
|
|
466
|
-
if (broker) {
|
|
467
|
-
try {
|
|
468
|
-
const data = await broker.call('metadata.saveItem', { type, name, item: body }, { request: context.request });
|
|
469
|
-
return { handled: true, response: this.success(data) };
|
|
470
|
-
} catch (e: any) {
|
|
471
|
-
return { handled: true, response: this.error(e.message || 'Save not supported', 501) };
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
return { handled: true, response: this.error('Save not supported', 501) };
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
try {
|
|
478
|
-
// Try specific calls based on type
|
|
479
|
-
if (type === 'objects' || type === 'object') {
|
|
480
|
-
if (broker) {
|
|
481
|
-
const data = await broker.call('metadata.getObject', { objectName: name }, { request: context.request });
|
|
482
|
-
return { handled: true, response: this.success(data) };
|
|
483
|
-
}
|
|
484
|
-
// Try ObjectQL service directly when broker is unavailable
|
|
485
|
-
const qlService = await this.getObjectQLService();
|
|
486
|
-
if (qlService?.registry) {
|
|
487
|
-
const data = qlService.registry.getObject(name);
|
|
488
|
-
if (data) return { handled: true, response: this.success(data) };
|
|
489
|
-
}
|
|
490
|
-
return { handled: true, response: this.error('Not found', 404) };
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// Normalize plural URL paths to singular registry type names
|
|
494
|
-
const singularType = pluralToSingular(type);
|
|
495
|
-
|
|
496
|
-
// Try Protocol Service First (Preferred)
|
|
497
|
-
const protocol = await this.resolveService('protocol');
|
|
498
|
-
if (protocol && typeof protocol.getMetaItem === 'function') {
|
|
499
|
-
try {
|
|
500
|
-
const data = await protocol.getMetaItem({ type: singularType, name, packageId });
|
|
501
|
-
return { handled: true, response: this.success(data) };
|
|
502
|
-
} catch (e: any) {
|
|
503
|
-
// Protocol might throw if not found or not supported
|
|
504
|
-
// Fallback to broker?
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Generic call for other types if supported via Broker (Legacy)
|
|
509
|
-
if (broker) {
|
|
510
|
-
const method = `metadata.get${this.capitalize(singularType)}`;
|
|
511
|
-
const data = await broker.call(method, { name }, { request: context.request });
|
|
512
|
-
return { handled: true, response: this.success(data) };
|
|
513
|
-
}
|
|
514
|
-
return { handled: true, response: this.error('Not found', 404) };
|
|
515
|
-
} catch (e: any) {
|
|
516
|
-
// Fallback: treat first part as object name if only 1 part (handled below)
|
|
517
|
-
// But here we are deep in 2 parts. Must be an error.
|
|
518
|
-
return { handled: true, response: this.error(e.message, 404) };
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// GET /metadata/:type (List items of type) OR /metadata/:objectName (Legacy)
|
|
523
|
-
if (parts.length === 1) {
|
|
524
|
-
const typeOrName = parts[0];
|
|
525
|
-
// Extract optional package filter from query string
|
|
526
|
-
const packageId = query?.package || undefined;
|
|
527
|
-
|
|
528
|
-
// Try protocol service first for any type
|
|
529
|
-
const protocol = await this.resolveService('protocol');
|
|
530
|
-
if (protocol && typeof protocol.getMetaItems === 'function') {
|
|
531
|
-
try {
|
|
532
|
-
const data = await protocol.getMetaItems({ type: typeOrName, packageId });
|
|
533
|
-
// Return any valid response from protocol (including empty items arrays)
|
|
534
|
-
if (data && (data.items !== undefined || Array.isArray(data))) {
|
|
535
|
-
return { handled: true, response: this.success(data) };
|
|
536
|
-
}
|
|
537
|
-
} catch {
|
|
538
|
-
// Protocol doesn't know this type, fall through
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// Try MetadataService directly for runtime-registered metadata (agents, tools, etc.)
|
|
543
|
-
const metadataService = await this.getService(CoreServiceName.enum.metadata);
|
|
544
|
-
if (metadataService && typeof (metadataService as any).list === 'function') {
|
|
545
|
-
try {
|
|
546
|
-
const items = await (metadataService as any).list(typeOrName);
|
|
547
|
-
if (items && items.length > 0) {
|
|
548
|
-
return { handled: true, response: this.success({ type: typeOrName, items }) };
|
|
549
|
-
}
|
|
550
|
-
} catch (e: any) {
|
|
551
|
-
// MetadataService doesn't know this type or failed, continue to other fallbacks
|
|
552
|
-
// Sanitize typeOrName to prevent log injection (CodeQL warning)
|
|
553
|
-
const sanitizedType = String(typeOrName).replace(/[\r\n\t]/g, '');
|
|
554
|
-
console.debug(`[HttpDispatcher] MetadataService.list() failed for type:`, sanitizedType, 'error:', e.message);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// Try broker for the type
|
|
559
|
-
if (broker) {
|
|
560
|
-
try {
|
|
561
|
-
if (typeOrName === 'objects') {
|
|
562
|
-
const data = await broker.call('metadata.objects', { packageId }, { request: context.request });
|
|
563
|
-
return { handled: true, response: this.success(data) };
|
|
564
|
-
}
|
|
565
|
-
const data = await broker.call(`metadata.${typeOrName}`, { packageId }, { request: context.request });
|
|
566
|
-
if (data !== null && data !== undefined) {
|
|
567
|
-
return { handled: true, response: this.success(data) };
|
|
568
|
-
}
|
|
569
|
-
} catch {
|
|
570
|
-
// Broker doesn't support this action, fall through
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Legacy: /metadata/:objectName (treat as single object lookup)
|
|
574
|
-
try {
|
|
575
|
-
const data = await broker.call('metadata.getObject', { objectName: typeOrName }, { request: context.request });
|
|
576
|
-
return { handled: true, response: this.success(data) };
|
|
577
|
-
} catch (e: any) {
|
|
578
|
-
return { handled: true, response: this.error(e.message, 404) };
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// No broker — try ObjectQL registry directly for object lookups
|
|
583
|
-
const qlService = await this.getObjectQLService();
|
|
584
|
-
if (qlService?.registry) {
|
|
585
|
-
if (typeOrName === 'objects') {
|
|
586
|
-
const objs = qlService.registry.getAllObjects(packageId);
|
|
587
|
-
return { handled: true, response: this.success({ type: 'object', items: objs }) };
|
|
588
|
-
}
|
|
589
|
-
// Try listing items of the given type
|
|
590
|
-
const items = qlService.registry.listItems?.(typeOrName, packageId);
|
|
591
|
-
if (items && items.length > 0) {
|
|
592
|
-
return { handled: true, response: this.success({ type: typeOrName, items }) };
|
|
593
|
-
}
|
|
594
|
-
// Legacy: treat as object name
|
|
595
|
-
const obj = qlService.registry.getObject(typeOrName);
|
|
596
|
-
if (obj) return { handled: true, response: this.success(obj) };
|
|
597
|
-
}
|
|
598
|
-
return { handled: true, response: this.error('Not found', 404) };
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// GET /metadata — return available metadata types
|
|
602
|
-
if (parts.length === 0) {
|
|
603
|
-
// Try protocol service for dynamic types
|
|
604
|
-
const protocol = await this.resolveService('protocol');
|
|
605
|
-
if (protocol && typeof protocol.getMetaTypes === 'function') {
|
|
606
|
-
const result = await protocol.getMetaTypes({});
|
|
607
|
-
return { handled: true, response: this.success(result) };
|
|
608
|
-
}
|
|
609
|
-
// Fallback: ask broker for registered types
|
|
610
|
-
if (broker) {
|
|
611
|
-
try {
|
|
612
|
-
const data = await broker.call('metadata.types', {}, { request: context.request });
|
|
613
|
-
return { handled: true, response: this.success(data) };
|
|
614
|
-
} catch {
|
|
615
|
-
// fall through to hardcoded defaults
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
return { handled: true, response: this.success({ types: ['object', 'app', 'plugin'] }) };
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
return { handled: false };
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* Handles Data requests
|
|
626
|
-
* path: sub-path after /data/ (e.g. "contacts", "contacts/123", "contacts/query")
|
|
627
|
-
*/
|
|
628
|
-
async handleData(path: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
|
|
629
|
-
const broker = this.ensureBroker();
|
|
630
|
-
const parts = path.replace(/^\/+/, '').split('/');
|
|
631
|
-
const objectName = parts[0];
|
|
632
|
-
|
|
633
|
-
if (!objectName) {
|
|
634
|
-
return { handled: true, response: this.error('Object name required', 400) };
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
const m = method.toUpperCase();
|
|
638
|
-
|
|
639
|
-
// 1. Custom Actions (query, batch)
|
|
640
|
-
if (parts.length > 1) {
|
|
641
|
-
const action = parts[1];
|
|
642
|
-
|
|
643
|
-
// POST /data/:object/query
|
|
644
|
-
if (action === 'query' && m === 'POST') {
|
|
645
|
-
// Spec: broker returns FindDataResponse = { object, records, total?, hasMore? }
|
|
646
|
-
const result = await broker.call('data.query', { object: objectName, ...body }, { request: context.request });
|
|
647
|
-
return { handled: true, response: this.success(result) };
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// POST /data/:object/batch
|
|
651
|
-
if (action === 'batch' && m === 'POST') {
|
|
652
|
-
const result = await broker.call('data.batch', { object: objectName, ...body }, { request: context.request });
|
|
653
|
-
return { handled: true, response: this.success(result) };
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// GET /data/:object/:id
|
|
657
|
-
if (parts.length === 2 && m === 'GET') {
|
|
658
|
-
const id = parts[1];
|
|
659
|
-
// Spec: Only select/expand are allowlisted query params for GET by ID.
|
|
660
|
-
// All other query parameters are discarded to prevent parameter pollution.
|
|
661
|
-
const { select, expand } = query || {};
|
|
662
|
-
const allowedParams: Record<string, unknown> = {};
|
|
663
|
-
if (select != null) allowedParams.select = select;
|
|
664
|
-
if (expand != null) allowedParams.expand = expand;
|
|
665
|
-
// Spec: broker returns GetDataResponse = { object, id, record }
|
|
666
|
-
const result = await broker.call('data.get', { object: objectName, id, ...allowedParams }, { request: context.request });
|
|
667
|
-
return { handled: true, response: this.success(result) };
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// PATCH /data/:object/:id
|
|
671
|
-
if (parts.length === 2 && m === 'PATCH') {
|
|
672
|
-
const id = parts[1];
|
|
673
|
-
// Spec: broker returns UpdateDataResponse = { object, id, record }
|
|
674
|
-
const result = await broker.call('data.update', { object: objectName, id, data: body }, { request: context.request });
|
|
675
|
-
return { handled: true, response: this.success(result) };
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// DELETE /data/:object/:id
|
|
679
|
-
if (parts.length === 2 && m === 'DELETE') {
|
|
680
|
-
const id = parts[1];
|
|
681
|
-
// Spec: broker returns DeleteDataResponse = { object, id, deleted }
|
|
682
|
-
const result = await broker.call('data.delete', { object: objectName, id }, { request: context.request });
|
|
683
|
-
return { handled: true, response: this.success(result) };
|
|
684
|
-
}
|
|
685
|
-
} else {
|
|
686
|
-
// GET /data/:object (List)
|
|
687
|
-
if (m === 'GET') {
|
|
688
|
-
// ── Normalize HTTP transport params → Spec canonical (QueryAST) ──
|
|
689
|
-
// HTTP GET query params use transport-level names (filter, sort, top,
|
|
690
|
-
// skip, select, expand) which are normalized here to canonical
|
|
691
|
-
// QueryAST field names (where, orderBy, limit, offset, fields,
|
|
692
|
-
// expand) before forwarding to the broker layer.
|
|
693
|
-
// The protocol.ts findData() method performs a deeper normalization
|
|
694
|
-
// pass, but pre-normalizing here ensures the broker always receives
|
|
695
|
-
// Spec-canonical keys.
|
|
696
|
-
const normalized: Record<string, unknown> = { ...query };
|
|
697
|
-
|
|
698
|
-
// filter/filters → where
|
|
699
|
-
// Note: `filter` is the canonical HTTP *transport* parameter name
|
|
700
|
-
// (see HttpFindQueryParamsSchema). It is normalized here to the
|
|
701
|
-
// canonical *QueryAST* field name `where` before broker dispatch.
|
|
702
|
-
// `filters` (plural) is a deprecated alias for `filter`.
|
|
703
|
-
if (normalized.filter != null || normalized.filters != null) {
|
|
704
|
-
normalized.where = normalized.where ?? normalized.filter ?? normalized.filters;
|
|
705
|
-
delete normalized.filter;
|
|
706
|
-
delete normalized.filters;
|
|
707
|
-
}
|
|
708
|
-
// select → fields
|
|
709
|
-
if (normalized.select != null && normalized.fields == null) {
|
|
710
|
-
normalized.fields = normalized.select;
|
|
711
|
-
delete normalized.select;
|
|
712
|
-
}
|
|
713
|
-
// sort → orderBy
|
|
714
|
-
if (normalized.sort != null && normalized.orderBy == null) {
|
|
715
|
-
normalized.orderBy = normalized.sort;
|
|
716
|
-
delete normalized.sort;
|
|
717
|
-
}
|
|
718
|
-
// top → limit
|
|
719
|
-
if (normalized.top != null && normalized.limit == null) {
|
|
720
|
-
normalized.limit = normalized.top;
|
|
721
|
-
delete normalized.top;
|
|
722
|
-
}
|
|
723
|
-
// skip → offset
|
|
724
|
-
if (normalized.skip != null && normalized.offset == null) {
|
|
725
|
-
normalized.offset = normalized.skip;
|
|
726
|
-
delete normalized.skip;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// Spec: broker returns FindDataResponse = { object, records, total?, hasMore? }
|
|
730
|
-
const result = await broker.call('data.query', { object: objectName, query: normalized }, { request: context.request });
|
|
731
|
-
return { handled: true, response: this.success(result) };
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// POST /data/:object (Create)
|
|
735
|
-
if (m === 'POST') {
|
|
736
|
-
// Spec: broker returns CreateDataResponse = { object, id, record }
|
|
737
|
-
const result = await broker.call('data.create', { object: objectName, data: body }, { request: context.request });
|
|
738
|
-
const res = this.success(result);
|
|
739
|
-
res.status = 201;
|
|
740
|
-
return { handled: true, response: res };
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
return { handled: false };
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
/**
|
|
748
|
-
* Handles Analytics requests
|
|
749
|
-
* path: sub-path after /analytics/
|
|
750
|
-
*/
|
|
751
|
-
async handleAnalytics(path: string, method: string, body: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult> {
|
|
752
|
-
const analyticsService = await this.getService(CoreServiceName.enum.analytics);
|
|
753
|
-
if (!analyticsService) return { handled: false }; // 404 handled by caller if unhandled
|
|
754
|
-
|
|
755
|
-
const m = method.toUpperCase();
|
|
756
|
-
const subPath = path.replace(/^\/+/, '');
|
|
757
|
-
|
|
758
|
-
// POST /analytics/query
|
|
759
|
-
if (subPath === 'query' && m === 'POST') {
|
|
760
|
-
const result = await analyticsService.query(body);
|
|
761
|
-
return { handled: true, response: this.success(result) };
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// GET /analytics/meta
|
|
765
|
-
if (subPath === 'meta' && m === 'GET') {
|
|
766
|
-
const result = await analyticsService.getMeta();
|
|
767
|
-
return { handled: true, response: this.success(result) };
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
// POST /analytics/sql (Dry-run or debug)
|
|
771
|
-
if (subPath === 'sql' && m === 'POST') {
|
|
772
|
-
// Assuming service has generateSql method
|
|
773
|
-
const result = await analyticsService.generateSql(body);
|
|
774
|
-
return { handled: true, response: this.success(result) };
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
return { handled: false };
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
/**
|
|
781
|
-
* Handles i18n requests
|
|
782
|
-
* path: sub-path after /i18n/
|
|
783
|
-
*
|
|
784
|
-
* Routes:
|
|
785
|
-
* GET /locales → getLocales
|
|
786
|
-
* GET /translations/:locale → getTranslations (locale from path)
|
|
787
|
-
* GET /translations?locale=xx → getTranslations (locale from query)
|
|
788
|
-
* GET /labels/:object/:locale → getFieldLabels (both from path)
|
|
789
|
-
* GET /labels/:object?locale=xx → getFieldLabels (locale from query)
|
|
790
|
-
*/
|
|
791
|
-
async handleI18n(path: string, method: string, query: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult> {
|
|
792
|
-
const i18nService = await this.getService(CoreServiceName.enum.i18n);
|
|
793
|
-
if (!i18nService) return { handled: true, response: this.error('i18n service not available', 501) };
|
|
794
|
-
|
|
795
|
-
const m = method.toUpperCase();
|
|
796
|
-
const parts = path.replace(/^\/+/, '').split('/').filter(Boolean);
|
|
797
|
-
|
|
798
|
-
if (m !== 'GET') return { handled: false };
|
|
799
|
-
|
|
800
|
-
// GET /i18n/locales
|
|
801
|
-
if (parts[0] === 'locales' && parts.length === 1) {
|
|
802
|
-
const locales = i18nService.getLocales();
|
|
803
|
-
return { handled: true, response: this.success({ locales }) };
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// GET /i18n/translations/:locale OR /i18n/translations?locale=xx
|
|
807
|
-
if (parts[0] === 'translations') {
|
|
808
|
-
const locale = parts[1] ? decodeURIComponent(parts[1]) : query?.locale;
|
|
809
|
-
if (!locale) return { handled: true, response: this.error('Missing locale parameter', 400) };
|
|
810
|
-
|
|
811
|
-
let translations = i18nService.getTranslations(locale);
|
|
812
|
-
|
|
813
|
-
// Locale fallback: try resolving to an available locale when
|
|
814
|
-
// the exact code yields empty translations (e.g. zh → zh-CN).
|
|
815
|
-
if (Object.keys(translations).length === 0) {
|
|
816
|
-
const availableLocales = typeof i18nService.getLocales === 'function'
|
|
817
|
-
? i18nService.getLocales() : [];
|
|
818
|
-
const resolved = resolveLocale(locale, availableLocales);
|
|
819
|
-
if (resolved && resolved !== locale) {
|
|
820
|
-
translations = i18nService.getTranslations(resolved);
|
|
821
|
-
return { handled: true, response: this.success({ locale: resolved, requestedLocale: locale, translations }) };
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
return { handled: true, response: this.success({ locale, translations }) };
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
// GET /i18n/labels/:object/:locale OR /i18n/labels/:object?locale=xx
|
|
829
|
-
if (parts[0] === 'labels' && parts.length >= 2) {
|
|
830
|
-
const objectName = decodeURIComponent(parts[1]);
|
|
831
|
-
let locale = parts[2] ? decodeURIComponent(parts[2]) : query?.locale;
|
|
832
|
-
if (!locale) return { handled: true, response: this.error('Missing locale parameter', 400) };
|
|
833
|
-
|
|
834
|
-
// Locale fallback for labels endpoint
|
|
835
|
-
const availableLocales = typeof i18nService.getLocales === 'function'
|
|
836
|
-
? i18nService.getLocales() : [];
|
|
837
|
-
const resolved = resolveLocale(locale, availableLocales);
|
|
838
|
-
if (resolved) locale = resolved;
|
|
839
|
-
|
|
840
|
-
if (typeof i18nService.getFieldLabels === 'function') {
|
|
841
|
-
const labels = i18nService.getFieldLabels(objectName, locale);
|
|
842
|
-
return { handled: true, response: this.success({ object: objectName, locale, labels }) };
|
|
843
|
-
}
|
|
844
|
-
// Fallback: derive field labels from full translation bundle
|
|
845
|
-
const translations = i18nService.getTranslations(locale);
|
|
846
|
-
const prefix = `o.${objectName}.fields.`;
|
|
847
|
-
const labels: Record<string, string> = {};
|
|
848
|
-
for (const [key, value] of Object.entries(translations)) {
|
|
849
|
-
if (key.startsWith(prefix)) {
|
|
850
|
-
labels[key.substring(prefix.length)] = value as string;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
return { handled: true, response: this.success({ object: objectName, locale, labels }) };
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
return { handled: false };
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
/**
|
|
860
|
-
* Handles Package Management requests
|
|
861
|
-
*
|
|
862
|
-
* REST Endpoints:
|
|
863
|
-
* - GET /packages → list all installed packages
|
|
864
|
-
* - GET /packages/:id → get a specific package
|
|
865
|
-
* - POST /packages → install a new package
|
|
866
|
-
* - DELETE /packages/:id → uninstall a package
|
|
867
|
-
* - PATCH /packages/:id/enable → enable a package
|
|
868
|
-
* - PATCH /packages/:id/disable → disable a package
|
|
869
|
-
* - POST /packages/:id/publish → publish a package (metadata snapshot)
|
|
870
|
-
* - POST /packages/:id/revert → revert a package to last published state
|
|
871
|
-
*
|
|
872
|
-
* Uses ObjectQL SchemaRegistry directly (via the 'objectql' service)
|
|
873
|
-
* with broker fallback for backward compatibility.
|
|
874
|
-
*/
|
|
875
|
-
async handlePackages(path: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
|
|
876
|
-
const m = method.toUpperCase();
|
|
877
|
-
const parts = path.replace(/^\/+/, '').split('/').filter(Boolean);
|
|
878
|
-
|
|
879
|
-
// Try to get SchemaRegistry from the ObjectQL service
|
|
880
|
-
const qlService = await this.getObjectQLService();
|
|
881
|
-
const registry = qlService?.registry;
|
|
882
|
-
|
|
883
|
-
// If no registry available, try broker as fallback
|
|
884
|
-
if (!registry) {
|
|
885
|
-
if (this.kernel.broker) {
|
|
886
|
-
return this.handlePackagesViaBroker(parts, m, body, query, context);
|
|
887
|
-
}
|
|
888
|
-
return { handled: true, response: this.error('Package service not available', 503) };
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
try {
|
|
892
|
-
// GET /packages → list packages
|
|
893
|
-
if (parts.length === 0 && m === 'GET') {
|
|
894
|
-
let packages = registry.getAllPackages();
|
|
895
|
-
// Apply optional filters
|
|
896
|
-
if (query?.status) {
|
|
897
|
-
packages = packages.filter((p: any) => p.status === query.status);
|
|
898
|
-
}
|
|
899
|
-
if (query?.type) {
|
|
900
|
-
packages = packages.filter((p: any) => p.manifest?.type === query.type);
|
|
901
|
-
}
|
|
902
|
-
return { handled: true, response: this.success({ packages, total: packages.length }) };
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
// POST /packages → install package
|
|
906
|
-
if (parts.length === 0 && m === 'POST') {
|
|
907
|
-
const pkg = registry.installPackage(body.manifest || body, body.settings);
|
|
908
|
-
const res = this.success(pkg);
|
|
909
|
-
res.status = 201;
|
|
910
|
-
return { handled: true, response: res };
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// PATCH /packages/:id/enable
|
|
914
|
-
if (parts.length === 2 && parts[1] === 'enable' && m === 'PATCH') {
|
|
915
|
-
const id = decodeURIComponent(parts[0]);
|
|
916
|
-
const pkg = registry.enablePackage(id);
|
|
917
|
-
if (!pkg) return { handled: true, response: this.error(`Package '${id}' not found`, 404) };
|
|
918
|
-
return { handled: true, response: this.success(pkg) };
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
// PATCH /packages/:id/disable
|
|
922
|
-
if (parts.length === 2 && parts[1] === 'disable' && m === 'PATCH') {
|
|
923
|
-
const id = decodeURIComponent(parts[0]);
|
|
924
|
-
const pkg = registry.disablePackage(id);
|
|
925
|
-
if (!pkg) return { handled: true, response: this.error(`Package '${id}' not found`, 404) };
|
|
926
|
-
return { handled: true, response: this.success(pkg) };
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
// POST /packages/:id/publish → publish package metadata
|
|
930
|
-
if (parts.length === 2 && parts[1] === 'publish' && m === 'POST') {
|
|
931
|
-
const id = decodeURIComponent(parts[0]);
|
|
932
|
-
const metadataService = await this.getService(CoreServiceName.enum.metadata);
|
|
933
|
-
if (metadataService && typeof (metadataService as any).publishPackage === 'function') {
|
|
934
|
-
const result = await (metadataService as any).publishPackage(id, body || {});
|
|
935
|
-
return { handled: true, response: this.success(result) };
|
|
936
|
-
}
|
|
937
|
-
// Broker fallback
|
|
938
|
-
if (this.kernel.broker) {
|
|
939
|
-
const result = await this.kernel.broker.call('metadata.publishPackage', { packageId: id, ...body }, { request: context.request });
|
|
940
|
-
return { handled: true, response: this.success(result) };
|
|
941
|
-
}
|
|
942
|
-
return { handled: true, response: this.error('Metadata service not available', 503) };
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
// POST /packages/:id/revert → revert package to last published state
|
|
946
|
-
if (parts.length === 2 && parts[1] === 'revert' && m === 'POST') {
|
|
947
|
-
const id = decodeURIComponent(parts[0]);
|
|
948
|
-
const metadataService = await this.getService(CoreServiceName.enum.metadata);
|
|
949
|
-
if (metadataService && typeof (metadataService as any).revertPackage === 'function') {
|
|
950
|
-
await (metadataService as any).revertPackage(id);
|
|
951
|
-
return { handled: true, response: this.success({ success: true }) };
|
|
952
|
-
}
|
|
953
|
-
// Broker fallback
|
|
954
|
-
if (this.kernel.broker) {
|
|
955
|
-
await this.kernel.broker.call('metadata.revertPackage', { packageId: id }, { request: context.request });
|
|
956
|
-
return { handled: true, response: this.success({ success: true }) };
|
|
957
|
-
}
|
|
958
|
-
return { handled: true, response: this.error('Metadata service not available', 503) };
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
// GET /packages/:id → get package
|
|
962
|
-
if (parts.length === 1 && m === 'GET') {
|
|
963
|
-
const id = decodeURIComponent(parts[0]);
|
|
964
|
-
const pkg = registry.getPackage(id);
|
|
965
|
-
if (!pkg) return { handled: true, response: this.error(`Package '${id}' not found`, 404) };
|
|
966
|
-
return { handled: true, response: this.success(pkg) };
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
// DELETE /packages/:id → uninstall package
|
|
970
|
-
if (parts.length === 1 && m === 'DELETE') {
|
|
971
|
-
const id = decodeURIComponent(parts[0]);
|
|
972
|
-
const success = registry.uninstallPackage(id);
|
|
973
|
-
if (!success) return { handled: true, response: this.error(`Package '${id}' not found`, 404) };
|
|
974
|
-
return { handled: true, response: this.success({ success: true }) };
|
|
975
|
-
}
|
|
976
|
-
} catch (e: any) {
|
|
977
|
-
return { handled: true, response: this.error(e.message, e.statusCode || 500) };
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
return { handled: false };
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
/**
|
|
984
|
-
* Fallback: handle packages via broker (for backward compatibility)
|
|
985
|
-
*/
|
|
986
|
-
private async handlePackagesViaBroker(parts: string[], m: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
|
|
987
|
-
const broker = this.kernel.broker;
|
|
988
|
-
try {
|
|
989
|
-
if (parts.length === 0 && m === 'GET') {
|
|
990
|
-
const result = await broker.call('package.list', query || {}, { request: context.request });
|
|
991
|
-
return { handled: true, response: this.success(result) };
|
|
992
|
-
}
|
|
993
|
-
if (parts.length === 0 && m === 'POST') {
|
|
994
|
-
const result = await broker.call('package.install', body, { request: context.request });
|
|
995
|
-
const res = this.success(result);
|
|
996
|
-
res.status = 201;
|
|
997
|
-
return { handled: true, response: res };
|
|
998
|
-
}
|
|
999
|
-
if (parts.length === 2 && parts[1] === 'enable' && m === 'PATCH') {
|
|
1000
|
-
const id = decodeURIComponent(parts[0]);
|
|
1001
|
-
const result = await broker.call('package.enable', { id }, { request: context.request });
|
|
1002
|
-
return { handled: true, response: this.success(result) };
|
|
1003
|
-
}
|
|
1004
|
-
if (parts.length === 2 && parts[1] === 'disable' && m === 'PATCH') {
|
|
1005
|
-
const id = decodeURIComponent(parts[0]);
|
|
1006
|
-
const result = await broker.call('package.disable', { id }, { request: context.request });
|
|
1007
|
-
return { handled: true, response: this.success(result) };
|
|
1008
|
-
}
|
|
1009
|
-
if (parts.length === 1 && m === 'GET') {
|
|
1010
|
-
const id = decodeURIComponent(parts[0]);
|
|
1011
|
-
const result = await broker.call('package.get', { id }, { request: context.request });
|
|
1012
|
-
return { handled: true, response: this.success(result) };
|
|
1013
|
-
}
|
|
1014
|
-
if (parts.length === 1 && m === 'DELETE') {
|
|
1015
|
-
const id = decodeURIComponent(parts[0]);
|
|
1016
|
-
const result = await broker.call('package.uninstall', { id }, { request: context.request });
|
|
1017
|
-
return { handled: true, response: this.success(result) };
|
|
1018
|
-
}
|
|
1019
|
-
} catch (e: any) {
|
|
1020
|
-
return { handled: true, response: this.error(e.message, e.statusCode || 500) };
|
|
1021
|
-
}
|
|
1022
|
-
return { handled: false };
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
/**
|
|
1026
|
-
* Handles Storage requests
|
|
1027
|
-
* path: sub-path after /storage/
|
|
1028
|
-
*/
|
|
1029
|
-
async handleStorage(path: string, method: string, file: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
|
|
1030
|
-
const storageService = await this.getService(CoreServiceName.enum['file-storage']) || this.kernel.services?.['file-storage'];
|
|
1031
|
-
if (!storageService) {
|
|
1032
|
-
return { handled: true, response: this.error('File storage not configured', 501) };
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
const m = method.toUpperCase();
|
|
1036
|
-
const parts = path.replace(/^\/+/, '').split('/');
|
|
1037
|
-
|
|
1038
|
-
// POST /storage/upload
|
|
1039
|
-
if (parts[0] === 'upload' && m === 'POST') {
|
|
1040
|
-
if (!file) {
|
|
1041
|
-
return { handled: true, response: this.error('No file provided', 400) };
|
|
1042
|
-
}
|
|
1043
|
-
const result = await storageService.upload(file, { request: context.request });
|
|
1044
|
-
return { handled: true, response: this.success(result) };
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
// GET /storage/file/:id
|
|
1048
|
-
if (parts[0] === 'file' && parts[1] && m === 'GET') {
|
|
1049
|
-
const id = parts[1];
|
|
1050
|
-
const result = await storageService.download(id, { request: context.request });
|
|
1051
|
-
|
|
1052
|
-
// Result can be URL (redirect), Stream/Blob, or metadata
|
|
1053
|
-
if (result.url && result.redirect) {
|
|
1054
|
-
// Must be handled by adapter to do actual redirect
|
|
1055
|
-
return { handled: true, result: { type: 'redirect', url: result.url } };
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
if (result.stream) {
|
|
1059
|
-
// Must be handled by adapter to pipe stream
|
|
1060
|
-
return {
|
|
1061
|
-
handled: true,
|
|
1062
|
-
result: {
|
|
1063
|
-
type: 'stream',
|
|
1064
|
-
stream: result.stream,
|
|
1065
|
-
headers: {
|
|
1066
|
-
'Content-Type': result.mimeType || 'application/octet-stream',
|
|
1067
|
-
'Content-Length': result.size
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
};
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
return { handled: true, response: this.success(result) };
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
return { handled: false };
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
/**
|
|
1080
|
-
* Handles UI requests
|
|
1081
|
-
* path: sub-path after /ui/
|
|
1082
|
-
*/
|
|
1083
|
-
async handleUi(path: string, query: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult> {
|
|
1084
|
-
const parts = path.replace(/^\/+/, '').split('/').filter(Boolean);
|
|
1085
|
-
|
|
1086
|
-
// GET /ui/view/:object (with optional type param)
|
|
1087
|
-
if (parts[0] === 'view' && parts[1]) {
|
|
1088
|
-
const objectName = parts[1];
|
|
1089
|
-
// Support both path param /view/obj/list AND query param /view/obj?type=list
|
|
1090
|
-
const type = parts[2] || query?.type || 'list';
|
|
1091
|
-
|
|
1092
|
-
const protocol = await this.resolveService('protocol');
|
|
1093
|
-
|
|
1094
|
-
if (protocol && typeof protocol.getUiView === 'function') {
|
|
1095
|
-
try {
|
|
1096
|
-
const result = await protocol.getUiView({ object: objectName, type });
|
|
1097
|
-
return { handled: true, response: this.success(result) };
|
|
1098
|
-
} catch (e: any) {
|
|
1099
|
-
return { handled: true, response: this.error(e.message, 500) };
|
|
1100
|
-
}
|
|
1101
|
-
} else {
|
|
1102
|
-
return { handled: true, response: this.error('Protocol service not available', 503) };
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
return { handled: false };
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
/**
|
|
1110
|
-
* Handles Automation requests
|
|
1111
|
-
* path: sub-path after /automation/
|
|
1112
|
-
*
|
|
1113
|
-
* Routes:
|
|
1114
|
-
* GET / → listFlows
|
|
1115
|
-
* GET /:name → getFlow
|
|
1116
|
-
* POST / → createFlow (registerFlow)
|
|
1117
|
-
* PUT /:name → updateFlow
|
|
1118
|
-
* DELETE /:name → deleteFlow (unregisterFlow)
|
|
1119
|
-
* POST /:name/trigger → execute (legacy: trigger/:name also supported)
|
|
1120
|
-
* POST /:name/toggle → toggleFlow
|
|
1121
|
-
* GET /:name/runs → listRuns
|
|
1122
|
-
* GET /:name/runs/:runId → getRun
|
|
1123
|
-
*/
|
|
1124
|
-
async handleAutomation(path: string, method: string, body: any, context: HttpProtocolContext, query?: any): Promise<HttpDispatcherResult> {
|
|
1125
|
-
const automationService = await this.getService(CoreServiceName.enum.automation);
|
|
1126
|
-
if (!automationService) return { handled: false };
|
|
1127
|
-
|
|
1128
|
-
const m = method.toUpperCase();
|
|
1129
|
-
const parts = path.replace(/^\/+/, '').split('/').filter(Boolean);
|
|
1130
|
-
|
|
1131
|
-
// Legacy: POST /automation/trigger/:name
|
|
1132
|
-
if (parts[0] === 'trigger' && parts[1] && m === 'POST') {
|
|
1133
|
-
const triggerName = parts[1];
|
|
1134
|
-
if (typeof automationService.trigger === 'function') {
|
|
1135
|
-
const result = await automationService.trigger(triggerName, body, { request: context.request });
|
|
1136
|
-
return { handled: true, response: this.success(result) };
|
|
1137
|
-
}
|
|
1138
|
-
// Fallback to execute
|
|
1139
|
-
if (typeof automationService.execute === 'function') {
|
|
1140
|
-
const result = await automationService.execute(triggerName, body);
|
|
1141
|
-
return { handled: true, response: this.success(result) };
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
// GET / → listFlows
|
|
1146
|
-
if (parts.length === 0 && m === 'GET') {
|
|
1147
|
-
if (typeof automationService.listFlows === 'function') {
|
|
1148
|
-
const names = await automationService.listFlows();
|
|
1149
|
-
return { handled: true, response: this.success({ flows: names, total: names.length, hasMore: false }) };
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
// POST / → createFlow
|
|
1154
|
-
if (parts.length === 0 && m === 'POST') {
|
|
1155
|
-
if (typeof automationService.registerFlow === 'function') {
|
|
1156
|
-
automationService.registerFlow(body?.name, body);
|
|
1157
|
-
return { handled: true, response: this.success(body) };
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
// Routes with :name
|
|
1162
|
-
if (parts.length >= 1) {
|
|
1163
|
-
const name = parts[0];
|
|
1164
|
-
|
|
1165
|
-
// POST /:name/trigger → execute
|
|
1166
|
-
if (parts[1] === 'trigger' && m === 'POST') {
|
|
1167
|
-
if (typeof automationService.execute === 'function') {
|
|
1168
|
-
const result = await automationService.execute(name, body);
|
|
1169
|
-
return { handled: true, response: this.success(result) };
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
// POST /:name/toggle → toggleFlow
|
|
1174
|
-
if (parts[1] === 'toggle' && m === 'POST') {
|
|
1175
|
-
if (typeof automationService.toggleFlow === 'function') {
|
|
1176
|
-
await automationService.toggleFlow(name, body?.enabled ?? true);
|
|
1177
|
-
return { handled: true, response: this.success({ name, enabled: body?.enabled ?? true }) };
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
// GET /:name/runs/:runId → getRun
|
|
1182
|
-
if (parts[1] === 'runs' && parts[2] && m === 'GET') {
|
|
1183
|
-
if (typeof automationService.getRun === 'function') {
|
|
1184
|
-
const run = await automationService.getRun(parts[2]);
|
|
1185
|
-
if (!run) return { handled: true, response: this.error('Execution not found', 404) };
|
|
1186
|
-
return { handled: true, response: this.success(run) };
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
// GET /:name/runs → listRuns
|
|
1191
|
-
if (parts[1] === 'runs' && !parts[2] && m === 'GET') {
|
|
1192
|
-
if (typeof automationService.listRuns === 'function') {
|
|
1193
|
-
const options = query ? { limit: query.limit ? Number(query.limit) : undefined, cursor: query.cursor } : undefined;
|
|
1194
|
-
const runs = await automationService.listRuns(name, options);
|
|
1195
|
-
return { handled: true, response: this.success({ runs, hasMore: false }) };
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
// GET /:name → getFlow (no sub-path)
|
|
1200
|
-
if (parts.length === 1 && m === 'GET') {
|
|
1201
|
-
if (typeof automationService.getFlow === 'function') {
|
|
1202
|
-
const flow = await automationService.getFlow(name);
|
|
1203
|
-
if (!flow) return { handled: true, response: this.error('Flow not found', 404) };
|
|
1204
|
-
return { handled: true, response: this.success(flow) };
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
// PUT /:name → updateFlow
|
|
1209
|
-
if (parts.length === 1 && m === 'PUT') {
|
|
1210
|
-
if (typeof automationService.registerFlow === 'function') {
|
|
1211
|
-
automationService.registerFlow(name, body?.definition ?? body);
|
|
1212
|
-
return { handled: true, response: this.success(body?.definition ?? body) };
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
// DELETE /:name → deleteFlow
|
|
1217
|
-
if (parts.length === 1 && m === 'DELETE') {
|
|
1218
|
-
if (typeof automationService.unregisterFlow === 'function') {
|
|
1219
|
-
automationService.unregisterFlow(name);
|
|
1220
|
-
return { handled: true, response: this.success({ name, deleted: true }) };
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
return { handled: false };
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
private getServicesMap(): Record<string, any> {
|
|
1229
|
-
if (this.kernel.services instanceof Map) {
|
|
1230
|
-
return Object.fromEntries(this.kernel.services);
|
|
1231
|
-
}
|
|
1232
|
-
return this.kernel.services || {};
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
private async getService(name: CoreServiceName) {
|
|
1236
|
-
return this.resolveService(name);
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
/**
|
|
1240
|
-
* Resolve any service by name, supporting async factories.
|
|
1241
|
-
* Fallback chain: getServiceAsync → getService (sync) → context.getService → services map.
|
|
1242
|
-
* Only returns when a non-null service is found; otherwise falls through to the next step.
|
|
1243
|
-
*/
|
|
1244
|
-
private async resolveService(name: string) {
|
|
1245
|
-
// Prefer async resolution to support factory-based services (e.g. auth, analytics, protocol)
|
|
1246
|
-
if (typeof this.kernel.getServiceAsync === 'function') {
|
|
1247
|
-
try {
|
|
1248
|
-
const svc = await this.kernel.getServiceAsync(name);
|
|
1249
|
-
if (svc != null) return svc;
|
|
1250
|
-
} catch {
|
|
1251
|
-
// Service not registered or async resolution failed — fall through
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
if (typeof this.kernel.getService === 'function') {
|
|
1255
|
-
try {
|
|
1256
|
-
const svc = await this.kernel.getService(name);
|
|
1257
|
-
if (svc != null) return svc;
|
|
1258
|
-
} catch {
|
|
1259
|
-
// Service not registered or sync resolution threw "is async" — fall through
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
if (this.kernel?.context?.getService) {
|
|
1263
|
-
try {
|
|
1264
|
-
const svc = await this.kernel.context.getService(name);
|
|
1265
|
-
if (svc != null) return svc;
|
|
1266
|
-
} catch {
|
|
1267
|
-
// Service not registered — fall through
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
const services = this.getServicesMap();
|
|
1271
|
-
return services[name];
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
/**
|
|
1275
|
-
* Get the ObjectQL service which provides access to SchemaRegistry.
|
|
1276
|
-
* Tries multiple access patterns since kernel structure varies.
|
|
1277
|
-
*/
|
|
1278
|
-
private async getObjectQLService(): Promise<any> {
|
|
1279
|
-
// 1. Try via resolveService (handles async factories, sync, context, and map)
|
|
1280
|
-
try {
|
|
1281
|
-
const svc = await this.resolveService('objectql');
|
|
1282
|
-
if (svc?.registry) return svc;
|
|
1283
|
-
} catch { /* service not available */ }
|
|
1284
|
-
return null;
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
private capitalize(s: string) {
|
|
1288
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
/**
|
|
1292
|
-
* Handle AI service routes (/ai/chat, /ai/models, /ai/conversations, etc.)
|
|
1293
|
-
* Resolves the AI service and its built-in route handlers, then dispatches.
|
|
1294
|
-
*/
|
|
1295
|
-
async handleAI(subPath: string, method: string, body: any, query: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult> {
|
|
1296
|
-
let aiService: any;
|
|
1297
|
-
try {
|
|
1298
|
-
aiService = await this.resolveService('ai');
|
|
1299
|
-
} catch {
|
|
1300
|
-
// AI service not registered
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
if (!aiService) {
|
|
1304
|
-
return {
|
|
1305
|
-
handled: true,
|
|
1306
|
-
response: {
|
|
1307
|
-
status: 404,
|
|
1308
|
-
body: { success: false, error: { message: 'AI service is not configured', code: 404 } },
|
|
1309
|
-
},
|
|
1310
|
-
};
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
// The AI service exposes route definitions via buildAIRoutes.
|
|
1314
|
-
// We match the request path against known AI route patterns.
|
|
1315
|
-
const fullPath = `/api/v1${subPath}`;
|
|
1316
|
-
|
|
1317
|
-
// Build a simple param-extracting matcher for route patterns like /api/v1/ai/conversations/:id
|
|
1318
|
-
const matchRoute = (pattern: string, path: string): Record<string, string> | null => {
|
|
1319
|
-
const patternParts = pattern.split('/');
|
|
1320
|
-
const pathParts = path.split('/');
|
|
1321
|
-
if (patternParts.length !== pathParts.length) return null;
|
|
1322
|
-
const params: Record<string, string> = {};
|
|
1323
|
-
for (let i = 0; i < patternParts.length; i++) {
|
|
1324
|
-
if (patternParts[i].startsWith(':')) {
|
|
1325
|
-
params[patternParts[i].substring(1)] = pathParts[i];
|
|
1326
|
-
} else if (patternParts[i] !== pathParts[i]) {
|
|
1327
|
-
return null;
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
return params;
|
|
1331
|
-
};
|
|
1332
|
-
|
|
1333
|
-
// Try to get route definitions from the AI service's cached routes
|
|
1334
|
-
const routes = (this.kernel as any).__aiRoutes as Array<{
|
|
1335
|
-
method: string; path: string; handler: (req: any) => Promise<any>;
|
|
1336
|
-
}> | undefined;
|
|
1337
|
-
|
|
1338
|
-
if (!routes) {
|
|
1339
|
-
return {
|
|
1340
|
-
handled: true,
|
|
1341
|
-
response: {
|
|
1342
|
-
status: 503,
|
|
1343
|
-
body: { success: false, error: { message: 'AI service routes not yet initialized', code: 503 } },
|
|
1344
|
-
},
|
|
1345
|
-
};
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
for (const route of routes) {
|
|
1349
|
-
if (route.method !== method) continue;
|
|
1350
|
-
const params = matchRoute(route.path, fullPath);
|
|
1351
|
-
if (params === null) continue;
|
|
1352
|
-
|
|
1353
|
-
const result = await route.handler({ body, params, query });
|
|
1354
|
-
|
|
1355
|
-
if (result.stream && result.events) {
|
|
1356
|
-
// Return a streaming result for the adapter to handle
|
|
1357
|
-
return {
|
|
1358
|
-
handled: true,
|
|
1359
|
-
result: {
|
|
1360
|
-
type: 'stream',
|
|
1361
|
-
contentType: result.vercelDataStream
|
|
1362
|
-
? 'text/plain; charset=utf-8'
|
|
1363
|
-
: 'text/event-stream',
|
|
1364
|
-
events: result.events,
|
|
1365
|
-
vercelDataStream: result.vercelDataStream,
|
|
1366
|
-
headers: {
|
|
1367
|
-
'Content-Type': result.vercelDataStream
|
|
1368
|
-
? 'text/plain; charset=utf-8'
|
|
1369
|
-
: 'text/event-stream',
|
|
1370
|
-
'Cache-Control': 'no-cache',
|
|
1371
|
-
'Connection': 'keep-alive',
|
|
1372
|
-
},
|
|
1373
|
-
},
|
|
1374
|
-
};
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
return {
|
|
1378
|
-
handled: true,
|
|
1379
|
-
response: {
|
|
1380
|
-
status: result.status,
|
|
1381
|
-
body: result.body,
|
|
1382
|
-
},
|
|
1383
|
-
};
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
return {
|
|
1387
|
-
handled: true,
|
|
1388
|
-
response: this.routeNotFound(subPath),
|
|
1389
|
-
};
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
/**
|
|
1393
|
-
* Main Dispatcher Entry Point
|
|
1394
|
-
* Routes the request to the appropriate handler based on path and precedence
|
|
1395
|
-
*/
|
|
1396
|
-
async dispatch(method: string, path: string, body: any, query: any, context: HttpProtocolContext, prefix?: string): Promise<HttpDispatcherResult> {
|
|
1397
|
-
const cleanPath = path.replace(/\/$/, ''); // Remove trailing slash if present, but strict on clean paths
|
|
1398
|
-
|
|
1399
|
-
// 0. Discovery Endpoint (GET /discovery or GET /)
|
|
1400
|
-
// Standard route: /discovery (protocol-compliant)
|
|
1401
|
-
// Legacy route: / (empty path, for backward compatibility — MSW strips base URL)
|
|
1402
|
-
if ((cleanPath === '/discovery' || cleanPath === '') && method === 'GET') {
|
|
1403
|
-
const info = await this.getDiscoveryInfo(prefix ?? '');
|
|
1404
|
-
return {
|
|
1405
|
-
handled: true,
|
|
1406
|
-
response: this.success(info)
|
|
1407
|
-
};
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
// 0b. Health Endpoint (GET /health)
|
|
1411
|
-
if (cleanPath === '/health' && method === 'GET') {
|
|
1412
|
-
return {
|
|
1413
|
-
handled: true,
|
|
1414
|
-
response: this.success({
|
|
1415
|
-
status: 'ok',
|
|
1416
|
-
timestamp: new Date().toISOString(),
|
|
1417
|
-
version: '1.0.0',
|
|
1418
|
-
uptime: typeof process !== 'undefined' ? process.uptime() : undefined,
|
|
1419
|
-
}),
|
|
1420
|
-
};
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
// 1. System Protocols (Prefix-based)
|
|
1424
|
-
if (cleanPath.startsWith('/auth')) {
|
|
1425
|
-
return this.handleAuth(cleanPath.substring(5), method, body, context);
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
if (cleanPath.startsWith('/meta')) {
|
|
1429
|
-
return this.handleMetadata(cleanPath.substring(5), context, method, body, query);
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
if (cleanPath.startsWith('/data')) {
|
|
1433
|
-
return this.handleData(cleanPath.substring(5), method, body, query, context);
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
if (cleanPath.startsWith('/graphql')) {
|
|
1437
|
-
if (method === 'POST') return this.handleGraphQL(body, context);
|
|
1438
|
-
// GraphQL usually GET for Playground is handled by middleware but we can return 405 or handle it
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
if (cleanPath.startsWith('/storage')) {
|
|
1442
|
-
return this.handleStorage(cleanPath.substring(8), method, body, context); // body here is file/stream for upload
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
if (cleanPath.startsWith('/ui')) {
|
|
1446
|
-
return this.handleUi(cleanPath.substring(3), query, context);
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
if (cleanPath.startsWith('/automation')) {
|
|
1450
|
-
return this.handleAutomation(cleanPath.substring(11), method, body, context, query);
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
if (cleanPath.startsWith('/analytics')) {
|
|
1454
|
-
return this.handleAnalytics(cleanPath.substring(10), method, body, context);
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
if (cleanPath.startsWith('/packages')) {
|
|
1458
|
-
return this.handlePackages(cleanPath.substring(9), method, body, query, context);
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
if (cleanPath.startsWith('/i18n')) {
|
|
1462
|
-
return this.handleI18n(cleanPath.substring(5), method, query, context);
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
// AI Service — delegate to the registered AI route handlers
|
|
1466
|
-
if (cleanPath.startsWith('/ai')) {
|
|
1467
|
-
return this.handleAI(cleanPath, method, body, query, context);
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
// OpenAPI Specification
|
|
1471
|
-
if (cleanPath === '/openapi.json' && method === 'GET') {
|
|
1472
|
-
const broker = this.ensureBroker();
|
|
1473
|
-
try {
|
|
1474
|
-
const result = await broker.call('metadata.generateOpenApi', {}, { request: context.request });
|
|
1475
|
-
return { handled: true, response: this.success(result) };
|
|
1476
|
-
} catch (e) {
|
|
1477
|
-
// If not implemented, fall through or return 404
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
// 2. Custom API Endpoints (Registry lookup)
|
|
1482
|
-
// Check if there is a custom endpoint defined for this path
|
|
1483
|
-
const result = await this.handleApiEndpoint(cleanPath, method, body, query, context);
|
|
1484
|
-
if (result.handled) return result;
|
|
1485
|
-
|
|
1486
|
-
// 3. Fallback — return semantic 404 with diagnostic info
|
|
1487
|
-
return {
|
|
1488
|
-
handled: true,
|
|
1489
|
-
response: this.routeNotFound(cleanPath),
|
|
1490
|
-
};
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
/**
|
|
1494
|
-
* Handles Custom API Endpoints defined in metadata
|
|
1495
|
-
*/
|
|
1496
|
-
async handleApiEndpoint(path: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
|
|
1497
|
-
const broker = this.ensureBroker();
|
|
1498
|
-
try {
|
|
1499
|
-
// Attempt to find a matching endpoint in the registry
|
|
1500
|
-
// This assumes a 'metadata.matchEndpoint' action exists in the kernel/registry
|
|
1501
|
-
// path should include initial slash e.g. /api/v1/customers
|
|
1502
|
-
const endpoint = await broker.call('metadata.matchEndpoint', { path, method });
|
|
1503
|
-
|
|
1504
|
-
if (endpoint) {
|
|
1505
|
-
// Execute the endpoint target logic
|
|
1506
|
-
if (endpoint.type === 'flow') {
|
|
1507
|
-
const result = await broker.call('automation.runFlow', {
|
|
1508
|
-
flowId: endpoint.target,
|
|
1509
|
-
inputs: { ...query, ...body, _request: context.request }
|
|
1510
|
-
});
|
|
1511
|
-
return { handled: true, response: this.success(result) };
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
if (endpoint.type === 'script') {
|
|
1515
|
-
const result = await broker.call('automation.runScript', {
|
|
1516
|
-
scriptName: endpoint.target,
|
|
1517
|
-
context: { ...query, ...body, request: context.request }
|
|
1518
|
-
}, { request: context.request });
|
|
1519
|
-
return { handled: true, response: this.success(result) };
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
if (endpoint.type === 'object_operation') {
|
|
1523
|
-
// e.g. Proxy to an object action
|
|
1524
|
-
if (endpoint.objectParams) {
|
|
1525
|
-
const { object, operation } = endpoint.objectParams;
|
|
1526
|
-
// Map standard CRUD operations
|
|
1527
|
-
if (operation === 'find') {
|
|
1528
|
-
const result = await broker.call('data.query', { object, query }, { request: context.request });
|
|
1529
|
-
// Spec: FindDataResponse = { object, records, total?, hasMore? }
|
|
1530
|
-
return { handled: true, response: this.success(result.records, { total: result.total }) };
|
|
1531
|
-
}
|
|
1532
|
-
if (operation === 'get' && query.id) {
|
|
1533
|
-
const result = await broker.call('data.get', { object, id: query.id }, { request: context.request });
|
|
1534
|
-
return { handled: true, response: this.success(result) };
|
|
1535
|
-
}
|
|
1536
|
-
if (operation === 'create') {
|
|
1537
|
-
const result = await broker.call('data.create', { object, data: body }, { request: context.request });
|
|
1538
|
-
return { handled: true, response: this.success(result) };
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
if (endpoint.type === 'proxy') {
|
|
1544
|
-
// Simple proxy implementation (requires a network call, which usually is done by a service but here we can stub return)
|
|
1545
|
-
// In real implementation this might fetch(endpoint.target)
|
|
1546
|
-
// For now, return target info
|
|
1547
|
-
return {
|
|
1548
|
-
handled: true,
|
|
1549
|
-
response: {
|
|
1550
|
-
status: 200,
|
|
1551
|
-
body: { proxy: true, target: endpoint.target, note: 'Proxy execution requires http-client service' }
|
|
1552
|
-
}
|
|
1553
|
-
};
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
|
-
} catch (e) {
|
|
1557
|
-
// If matchEndpoint fails (e.g. not found), we just return not handled
|
|
1558
|
-
// so we can fallback to 404 or other handlers
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
return { handled: false };
|
|
1562
|
-
}
|
|
1563
|
-
}
|