@objectstack/runtime 3.0.9 → 3.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +20 -0
- package/dist/index.cjs +24 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +24 -20
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/http-dispatcher.test.ts +265 -0
- package/src/http-dispatcher.ts +27 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/runtime",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.11",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "ObjectStack Core Runtime & Query Engine",
|
|
6
6
|
"type": "module",
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"zod": "^4.3.6",
|
|
18
|
-
"@objectstack/
|
|
19
|
-
"@objectstack/
|
|
20
|
-
"@objectstack/spec": "3.0.
|
|
21
|
-
"@objectstack/types": "3.0.
|
|
18
|
+
"@objectstack/core": "3.0.11",
|
|
19
|
+
"@objectstack/rest": "3.0.11",
|
|
20
|
+
"@objectstack/spec": "3.0.11",
|
|
21
|
+
"@objectstack/types": "3.0.11"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"typescript": "^5.0.0",
|
|
@@ -204,4 +204,269 @@ describe('HttpDispatcher', () => {
|
|
|
204
204
|
expect(mockAutomationService.trigger).toHaveBeenCalledWith('flow_a', { data: 1 }, { request: {} });
|
|
205
205
|
});
|
|
206
206
|
});
|
|
207
|
+
|
|
208
|
+
// ═══════════════════════════════════════════════════════════════
|
|
209
|
+
// Async Service Resolution Tests
|
|
210
|
+
// Covers: getService awaits Promise-based (async factory) services
|
|
211
|
+
// ═══════════════════════════════════════════════════════════════
|
|
212
|
+
|
|
213
|
+
describe('Async service resolution (Promise-based injection)', () => {
|
|
214
|
+
|
|
215
|
+
describe('handleAnalytics with async service', () => {
|
|
216
|
+
it('should resolve analytics service from Promise (async factory)', async () => {
|
|
217
|
+
const mockAnalytics = {
|
|
218
|
+
query: vi.fn().mockResolvedValue({ rows: [{ id: 1 }], total: 1 }),
|
|
219
|
+
getMetadata: vi.fn().mockResolvedValue({ tables: ['t1'] }),
|
|
220
|
+
generateSql: vi.fn().mockResolvedValue({ sql: 'SELECT 1' }),
|
|
221
|
+
};
|
|
222
|
+
// Inject as Promise (simulates async factory registration)
|
|
223
|
+
(kernel as any).getService = vi.fn().mockImplementation((name: string) => {
|
|
224
|
+
if (name === 'analytics') return Promise.resolve(mockAnalytics);
|
|
225
|
+
return null;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const result = await dispatcher.handleAnalytics('query', 'POST', { sql: 'SELECT 1' }, { request: {} });
|
|
229
|
+
expect(result.handled).toBe(true);
|
|
230
|
+
expect(result.response?.status).toBe(200);
|
|
231
|
+
expect(mockAnalytics.query).toHaveBeenCalled();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should handle POST /analytics/sql with async service', async () => {
|
|
235
|
+
const mockAnalytics = {
|
|
236
|
+
generateSql: vi.fn().mockResolvedValue({ sql: 'SELECT * FROM t' }),
|
|
237
|
+
};
|
|
238
|
+
(kernel as any).getService = vi.fn().mockResolvedValue(mockAnalytics);
|
|
239
|
+
|
|
240
|
+
const result = await dispatcher.handleAnalytics('sql', 'POST', { object: 'test' }, { request: {} });
|
|
241
|
+
expect(result.handled).toBe(true);
|
|
242
|
+
expect(result.response?.status).toBe(200);
|
|
243
|
+
expect(mockAnalytics.generateSql).toHaveBeenCalled();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should handle GET /analytics/meta with async service', async () => {
|
|
247
|
+
const mockAnalytics = {
|
|
248
|
+
getMetadata: vi.fn().mockResolvedValue({ tables: ['users', 'orders'] }),
|
|
249
|
+
};
|
|
250
|
+
(kernel as any).getService = vi.fn().mockResolvedValue(mockAnalytics);
|
|
251
|
+
|
|
252
|
+
const result = await dispatcher.handleAnalytics('meta', 'GET', {}, { request: {} });
|
|
253
|
+
expect(result.handled).toBe(true);
|
|
254
|
+
expect(result.response?.status).toBe(200);
|
|
255
|
+
expect(result.response?.body?.data?.tables).toEqual(['users', 'orders']);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should return unhandled when analytics service is not registered', async () => {
|
|
259
|
+
(kernel as any).getService = vi.fn().mockResolvedValue(null);
|
|
260
|
+
(kernel as any).services = new Map();
|
|
261
|
+
|
|
262
|
+
const result = await dispatcher.handleAnalytics('query', 'POST', {}, { request: {} });
|
|
263
|
+
expect(result.handled).toBe(false);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should return unhandled for unknown analytics sub-path', async () => {
|
|
267
|
+
const mockAnalytics = { query: vi.fn() };
|
|
268
|
+
(kernel as any).getService = vi.fn().mockResolvedValue(mockAnalytics);
|
|
269
|
+
|
|
270
|
+
const result = await dispatcher.handleAnalytics('unknown', 'POST', {}, { request: {} });
|
|
271
|
+
expect(result.handled).toBe(false);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('handleAuth with async service', () => {
|
|
276
|
+
it('should resolve auth service from Promise', async () => {
|
|
277
|
+
const mockAuth = {
|
|
278
|
+
handler: vi.fn().mockResolvedValue({ user: { id: '1' } }),
|
|
279
|
+
};
|
|
280
|
+
(kernel as any).getService = vi.fn().mockImplementation((name: string) => {
|
|
281
|
+
if (name === 'auth') return Promise.resolve(mockAuth);
|
|
282
|
+
return null;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const result = await dispatcher.handleAuth('', 'POST', {}, { request: {}, response: {} });
|
|
286
|
+
expect(result.handled).toBe(true);
|
|
287
|
+
expect(mockAuth.handler).toHaveBeenCalled();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should fallback to legacy login when async auth service has no handler', async () => {
|
|
291
|
+
(kernel as any).getService = vi.fn().mockResolvedValue({});
|
|
292
|
+
mockBroker.call.mockResolvedValue({ token: 'abc' });
|
|
293
|
+
|
|
294
|
+
const result = await dispatcher.handleAuth('/login', 'POST', { user: 'a' }, { request: {} });
|
|
295
|
+
expect(result.handled).toBe(true);
|
|
296
|
+
expect(mockBroker.call).toHaveBeenCalledWith('auth.login', { user: 'a' }, { request: {} });
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should return unhandled when auth service not registered and no legacy match', async () => {
|
|
300
|
+
(kernel as any).getService = vi.fn().mockResolvedValue(null);
|
|
301
|
+
(kernel as any).services = new Map();
|
|
302
|
+
|
|
303
|
+
const result = await dispatcher.handleAuth('/profile', 'GET', {}, { request: {} });
|
|
304
|
+
expect(result.handled).toBe(false);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe('handleStorage with async service', () => {
|
|
309
|
+
it('should resolve storage service from Promise', async () => {
|
|
310
|
+
const mockStorage = {
|
|
311
|
+
upload: vi.fn().mockResolvedValue({ id: 'file_1', url: '/files/1' }),
|
|
312
|
+
};
|
|
313
|
+
(kernel as any).getService = vi.fn().mockImplementation((name: string) => {
|
|
314
|
+
if (name === 'file-storage') return Promise.resolve(mockStorage);
|
|
315
|
+
return null;
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const result = await dispatcher.handleStorage('/upload', 'POST', { name: 'test.txt' }, { request: {} });
|
|
319
|
+
expect(result.handled).toBe(true);
|
|
320
|
+
expect(result.response?.status).toBe(200);
|
|
321
|
+
expect(mockStorage.upload).toHaveBeenCalled();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should return 501 when storage service is not registered (async null)', async () => {
|
|
325
|
+
(kernel as any).getService = vi.fn().mockResolvedValue(null);
|
|
326
|
+
(kernel as any).services = new Map();
|
|
327
|
+
|
|
328
|
+
const result = await dispatcher.handleStorage('/upload', 'POST', {}, { request: {} });
|
|
329
|
+
expect(result.handled).toBe(true);
|
|
330
|
+
expect(result.response?.status).toBe(501);
|
|
331
|
+
expect(result.response?.body?.error?.message).toBe('File storage not configured');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should handle GET /storage/file/:id with async service', async () => {
|
|
335
|
+
const mockStorage = {
|
|
336
|
+
download: vi.fn().mockResolvedValue({ data: 'content', mimeType: 'text/plain' }),
|
|
337
|
+
};
|
|
338
|
+
(kernel as any).getService = vi.fn().mockImplementation((name: string) => {
|
|
339
|
+
if (name === 'file-storage') return Promise.resolve(mockStorage);
|
|
340
|
+
return null;
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const result = await dispatcher.handleStorage('/file/abc123', 'GET', null, { request: {} });
|
|
344
|
+
expect(result.handled).toBe(true);
|
|
345
|
+
expect(mockStorage.download).toHaveBeenCalledWith('abc123', { request: {} });
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should return 400 when upload has no file', async () => {
|
|
349
|
+
const mockStorage = { upload: vi.fn() };
|
|
350
|
+
(kernel as any).getService = vi.fn().mockResolvedValue(mockStorage);
|
|
351
|
+
|
|
352
|
+
const result = await dispatcher.handleStorage('/upload', 'POST', null, { request: {} });
|
|
353
|
+
expect(result.handled).toBe(true);
|
|
354
|
+
expect(result.response?.status).toBe(400);
|
|
355
|
+
expect(result.response?.body?.error?.message).toBe('No file provided');
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('handleAutomation with async service', () => {
|
|
360
|
+
it('should resolve automation service from Promise (async factory)', async () => {
|
|
361
|
+
const mockAuto = {
|
|
362
|
+
listFlows: vi.fn().mockResolvedValue(['f1']),
|
|
363
|
+
};
|
|
364
|
+
(kernel as any).getService = vi.fn().mockImplementation((name: string) => {
|
|
365
|
+
if (name === 'automation') return Promise.resolve(mockAuto);
|
|
366
|
+
return null;
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const result = await dispatcher.handleAutomation('', 'GET', {}, { request: {} });
|
|
370
|
+
expect(result.handled).toBe(true);
|
|
371
|
+
expect(result.response?.body?.data?.flows).toEqual(['f1']);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('should return unhandled when automation service not registered', async () => {
|
|
375
|
+
(kernel as any).getService = vi.fn().mockResolvedValue(null);
|
|
376
|
+
(kernel as any).services = new Map();
|
|
377
|
+
|
|
378
|
+
const result = await dispatcher.handleAutomation('', 'GET', {}, { request: {} });
|
|
379
|
+
expect(result.handled).toBe(false);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe('handleMetadata with async protocol service', () => {
|
|
384
|
+
it('should resolve protocol service from async getService', async () => {
|
|
385
|
+
const asyncProtocol = {
|
|
386
|
+
saveMetaItem: vi.fn().mockResolvedValue({ success: true }),
|
|
387
|
+
};
|
|
388
|
+
(kernel as any).context.getService = vi.fn().mockImplementation((name: string) => {
|
|
389
|
+
if (name === 'protocol') return Promise.resolve(asyncProtocol);
|
|
390
|
+
return null;
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const result = await dispatcher.handleMetadata('/objects/my_obj', { request: {} }, 'PUT', { label: 'Test' });
|
|
394
|
+
expect(result.handled).toBe(true);
|
|
395
|
+
expect(result.response?.status).toBe(200);
|
|
396
|
+
expect(asyncProtocol.saveMetaItem).toHaveBeenCalled();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('should fallback to broker when async protocol returns null', async () => {
|
|
400
|
+
(kernel as any).context.getService = vi.fn().mockResolvedValue(null);
|
|
401
|
+
mockBroker.call.mockResolvedValue({ name: 'my_obj' });
|
|
402
|
+
|
|
403
|
+
const result = await dispatcher.handleMetadata('/objects/my_obj', { request: {} }, 'GET');
|
|
404
|
+
expect(result.handled).toBe(true);
|
|
405
|
+
expect(mockBroker.call).toHaveBeenCalledWith(
|
|
406
|
+
'metadata.getObject',
|
|
407
|
+
{ objectName: 'my_obj' },
|
|
408
|
+
{ request: {} }
|
|
409
|
+
);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// ═══════════════════════════════════════════════════════════════
|
|
415
|
+
// Synchronous service resolution (backward compatibility)
|
|
416
|
+
// ═══════════════════════════════════════════════════════════════
|
|
417
|
+
|
|
418
|
+
describe('Synchronous service resolution (backward compat)', () => {
|
|
419
|
+
it('should work with synchronous service from services Map', async () => {
|
|
420
|
+
const syncAnalytics = {
|
|
421
|
+
query: vi.fn().mockResolvedValue({ rows: [], total: 0 }),
|
|
422
|
+
};
|
|
423
|
+
(kernel as any).services = new Map([['analytics', syncAnalytics]]);
|
|
424
|
+
|
|
425
|
+
const result = await dispatcher.handleAnalytics('query', 'POST', {}, { request: {} });
|
|
426
|
+
expect(result.handled).toBe(true);
|
|
427
|
+
expect(syncAnalytics.query).toHaveBeenCalled();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should work with synchronous getService returning service directly', async () => {
|
|
431
|
+
const syncAuto = {
|
|
432
|
+
listFlows: vi.fn().mockResolvedValue(['flow_x']),
|
|
433
|
+
};
|
|
434
|
+
(kernel as any).getService = vi.fn().mockReturnValue(syncAuto);
|
|
435
|
+
|
|
436
|
+
const result = await dispatcher.handleAutomation('', 'GET', {}, { request: {} });
|
|
437
|
+
expect(result.handled).toBe(true);
|
|
438
|
+
expect(result.response?.body?.data?.flows).toEqual(['flow_x']);
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// ═══════════════════════════════════════════════════════════════
|
|
443
|
+
// Error handling for service method failures
|
|
444
|
+
// ═══════════════════════════════════════════════════════════════
|
|
445
|
+
|
|
446
|
+
describe('Service method error handling', () => {
|
|
447
|
+
it('should propagate analytics query error', async () => {
|
|
448
|
+
const badAnalytics = {
|
|
449
|
+
query: vi.fn().mockRejectedValue(new Error('Query timeout')),
|
|
450
|
+
};
|
|
451
|
+
(kernel as any).getService = vi.fn().mockResolvedValue(badAnalytics);
|
|
452
|
+
|
|
453
|
+
await expect(
|
|
454
|
+
dispatcher.handleAnalytics('query', 'POST', {}, { request: {} })
|
|
455
|
+
).rejects.toThrow('Query timeout');
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('should propagate storage upload error', async () => {
|
|
459
|
+
const badStorage = {
|
|
460
|
+
upload: vi.fn().mockRejectedValue(new Error('Disk full')),
|
|
461
|
+
};
|
|
462
|
+
(kernel as any).getService = vi.fn().mockImplementation((name: string) => {
|
|
463
|
+
if (name === 'file-storage') return Promise.resolve(badStorage);
|
|
464
|
+
return null;
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
await expect(
|
|
468
|
+
dispatcher.handleStorage('/upload', 'POST', { data: 'file' }, { request: {} })
|
|
469
|
+
).rejects.toThrow('Disk full');
|
|
470
|
+
});
|
|
471
|
+
});
|
|
207
472
|
});
|
package/src/http-dispatcher.ts
CHANGED
|
@@ -173,7 +173,7 @@ export class HttpDispatcher {
|
|
|
173
173
|
*/
|
|
174
174
|
async handleAuth(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
|
|
175
175
|
// 1. Try generic Auth Service
|
|
176
|
-
const authService = this.getService(CoreServiceName.enum.auth);
|
|
176
|
+
const authService = await this.getService(CoreServiceName.enum.auth);
|
|
177
177
|
if (authService && typeof authService.handler === 'function') {
|
|
178
178
|
const response = await authService.handler(context.request, context.response);
|
|
179
179
|
return { handled: true, result: response };
|
|
@@ -202,7 +202,7 @@ export class HttpDispatcher {
|
|
|
202
202
|
// GET /metadata/types
|
|
203
203
|
if (parts[0] === 'types') {
|
|
204
204
|
// Try protocol service for dynamic types
|
|
205
|
-
const protocol = this.kernel?.context?.getService ? this.kernel.context.getService('protocol') : null;
|
|
205
|
+
const protocol = this.kernel?.context?.getService ? await this.kernel.context.getService('protocol') : null;
|
|
206
206
|
if (protocol && typeof protocol.getMetaTypes === 'function') {
|
|
207
207
|
const result = await protocol.getMetaTypes({});
|
|
208
208
|
return { handled: true, response: this.success(result) };
|
|
@@ -224,7 +224,7 @@ export class HttpDispatcher {
|
|
|
224
224
|
// PUT /metadata/:type/:name (Save)
|
|
225
225
|
if (method === 'PUT' && body) {
|
|
226
226
|
// Try to get the protocol service directly
|
|
227
|
-
const protocol = this.kernel?.context?.getService ? this.kernel.context.getService('protocol') : null;
|
|
227
|
+
const protocol = this.kernel?.context?.getService ? await this.kernel.context.getService('protocol') : null;
|
|
228
228
|
|
|
229
229
|
if (protocol && typeof protocol.saveMetaItem === 'function') {
|
|
230
230
|
try {
|
|
@@ -257,7 +257,7 @@ export class HttpDispatcher {
|
|
|
257
257
|
const singularType = type.endsWith('s') ? type.slice(0, -1) : type;
|
|
258
258
|
|
|
259
259
|
// Try Protocol Service First (Preferred)
|
|
260
|
-
const protocol = this.kernel?.context?.getService ? this.kernel.context.getService('protocol') : null;
|
|
260
|
+
const protocol = this.kernel?.context?.getService ? await this.kernel.context.getService('protocol') : null;
|
|
261
261
|
if (protocol && typeof protocol.getMetaItem === 'function') {
|
|
262
262
|
try {
|
|
263
263
|
const data = await protocol.getMetaItem({ type: singularType, name });
|
|
@@ -286,7 +286,7 @@ export class HttpDispatcher {
|
|
|
286
286
|
const packageId = query?.package || undefined;
|
|
287
287
|
|
|
288
288
|
// Try protocol service first for any type
|
|
289
|
-
const protocol = this.kernel?.context?.getService ? this.kernel.context.getService('protocol') : null;
|
|
289
|
+
const protocol = this.kernel?.context?.getService ? await this.kernel.context.getService('protocol') : null;
|
|
290
290
|
if (protocol && typeof protocol.getMetaItems === 'function') {
|
|
291
291
|
try {
|
|
292
292
|
const data = await protocol.getMetaItems({ type: typeOrName, packageId });
|
|
@@ -325,7 +325,7 @@ export class HttpDispatcher {
|
|
|
325
325
|
// GET /metadata — return available metadata types
|
|
326
326
|
if (parts.length === 0) {
|
|
327
327
|
// Try protocol service for dynamic types
|
|
328
|
-
const protocol = this.kernel?.context?.getService ? this.kernel.context.getService('protocol') : null;
|
|
328
|
+
const protocol = this.kernel?.context?.getService ? await this.kernel.context.getService('protocol') : null;
|
|
329
329
|
if (protocol && typeof protocol.getMetaTypes === 'function') {
|
|
330
330
|
const result = await protocol.getMetaTypes({});
|
|
331
331
|
return { handled: true, response: this.success(result) };
|
|
@@ -377,8 +377,14 @@ export class HttpDispatcher {
|
|
|
377
377
|
// GET /data/:object/:id
|
|
378
378
|
if (parts.length === 2 && m === 'GET') {
|
|
379
379
|
const id = parts[1];
|
|
380
|
+
// Spec: Only select/expand are allowlisted query params for GET by ID.
|
|
381
|
+
// All other query parameters are discarded to prevent parameter pollution.
|
|
382
|
+
const { select, expand } = query || {};
|
|
383
|
+
const allowedParams: Record<string, unknown> = {};
|
|
384
|
+
if (select != null) allowedParams.select = select;
|
|
385
|
+
if (expand != null) allowedParams.expand = expand;
|
|
380
386
|
// Spec: broker returns GetDataResponse = { object, id, record }
|
|
381
|
-
const result = await broker.call('data.get', { object: objectName, id, ...
|
|
387
|
+
const result = await broker.call('data.get', { object: objectName, id, ...allowedParams }, { request: context.request });
|
|
382
388
|
return { handled: true, response: this.success(result) };
|
|
383
389
|
}
|
|
384
390
|
|
|
@@ -401,7 +407,7 @@ export class HttpDispatcher {
|
|
|
401
407
|
// GET /data/:object (List)
|
|
402
408
|
if (m === 'GET') {
|
|
403
409
|
// Spec: broker returns FindDataResponse = { object, records, total?, hasMore? }
|
|
404
|
-
const result = await broker.call('data.query', { object: objectName,
|
|
410
|
+
const result = await broker.call('data.query', { object: objectName, query }, { request: context.request });
|
|
405
411
|
return { handled: true, response: this.success(result) };
|
|
406
412
|
}
|
|
407
413
|
|
|
@@ -423,7 +429,7 @@ export class HttpDispatcher {
|
|
|
423
429
|
* path: sub-path after /analytics/
|
|
424
430
|
*/
|
|
425
431
|
async handleAnalytics(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
|
|
426
|
-
const analyticsService = this.getService(CoreServiceName.enum.analytics);
|
|
432
|
+
const analyticsService = await this.getService(CoreServiceName.enum.analytics);
|
|
427
433
|
if (!analyticsService) return { handled: false }; // 404 handled by caller if unhandled
|
|
428
434
|
|
|
429
435
|
const m = method.toUpperCase();
|
|
@@ -470,7 +476,7 @@ export class HttpDispatcher {
|
|
|
470
476
|
const parts = path.replace(/^\/+/, '').split('/').filter(Boolean);
|
|
471
477
|
|
|
472
478
|
// Try to get SchemaRegistry from the ObjectQL service
|
|
473
|
-
const qlService = this.getObjectQLService();
|
|
479
|
+
const qlService = await this.getObjectQLService();
|
|
474
480
|
const registry = qlService?.registry;
|
|
475
481
|
|
|
476
482
|
// If no registry available, try broker as fallback
|
|
@@ -588,7 +594,7 @@ export class HttpDispatcher {
|
|
|
588
594
|
* path: sub-path after /storage/
|
|
589
595
|
*/
|
|
590
596
|
async handleStorage(path: string, method: string, file: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
|
|
591
|
-
const storageService = this.getService(CoreServiceName.enum['file-storage']) || this.kernel.services?.['file-storage'];
|
|
597
|
+
const storageService = await this.getService(CoreServiceName.enum['file-storage']) || this.kernel.services?.['file-storage'];
|
|
592
598
|
if (!storageService) {
|
|
593
599
|
return { handled: true, response: this.error('File storage not configured', 501) };
|
|
594
600
|
}
|
|
@@ -650,7 +656,7 @@ export class HttpDispatcher {
|
|
|
650
656
|
// Support both path param /view/obj/list AND query param /view/obj?type=list
|
|
651
657
|
const type = parts[2] || query?.type || 'list';
|
|
652
658
|
|
|
653
|
-
const protocol = this.kernel?.context?.getService ? this.kernel.context.getService('protocol') : null;
|
|
659
|
+
const protocol = this.kernel?.context?.getService ? await this.kernel.context.getService('protocol') : null;
|
|
654
660
|
|
|
655
661
|
if (protocol && typeof protocol.getUiView === 'function') {
|
|
656
662
|
try {
|
|
@@ -683,7 +689,7 @@ export class HttpDispatcher {
|
|
|
683
689
|
* GET /:name/runs/:runId → getRun
|
|
684
690
|
*/
|
|
685
691
|
async handleAutomation(path: string, method: string, body: any, context: HttpProtocolContext, query?: any): Promise<HttpDispatcherResult> {
|
|
686
|
-
const automationService = this.getService(CoreServiceName.enum.automation);
|
|
692
|
+
const automationService = await this.getService(CoreServiceName.enum.automation);
|
|
687
693
|
if (!automationService) return { handled: false };
|
|
688
694
|
|
|
689
695
|
const m = method.toUpperCase();
|
|
@@ -793,9 +799,9 @@ export class HttpDispatcher {
|
|
|
793
799
|
return this.kernel.services || {};
|
|
794
800
|
}
|
|
795
801
|
|
|
796
|
-
private getService(name: CoreServiceName) {
|
|
802
|
+
private async getService(name: CoreServiceName) {
|
|
797
803
|
if (typeof this.kernel.getService === 'function') {
|
|
798
|
-
return this.kernel.getService(name);
|
|
804
|
+
return await this.kernel.getService(name);
|
|
799
805
|
}
|
|
800
806
|
const services = this.getServicesMap();
|
|
801
807
|
return services[name];
|
|
@@ -805,18 +811,18 @@ export class HttpDispatcher {
|
|
|
805
811
|
* Get the ObjectQL service which provides access to SchemaRegistry.
|
|
806
812
|
* Tries multiple access patterns since kernel structure varies.
|
|
807
813
|
*/
|
|
808
|
-
private getObjectQLService(): any {
|
|
814
|
+
private async getObjectQLService(): Promise<any> {
|
|
809
815
|
// 1. Try via kernel.getService
|
|
810
816
|
if (typeof this.kernel.getService === 'function') {
|
|
811
817
|
try {
|
|
812
|
-
const svc = this.kernel.getService('objectql');
|
|
818
|
+
const svc = await this.kernel.getService('objectql');
|
|
813
819
|
if (svc?.registry) return svc;
|
|
814
820
|
} catch { /* ignore */ }
|
|
815
821
|
}
|
|
816
822
|
// 2. Try via kernel context
|
|
817
823
|
if (this.kernel?.context?.getService) {
|
|
818
824
|
try {
|
|
819
|
-
const svc = this.kernel.context.getService('objectql');
|
|
825
|
+
const svc = await this.kernel.context.getService('objectql');
|
|
820
826
|
if (svc?.registry) return svc;
|
|
821
827
|
} catch { /* ignore */ }
|
|
822
828
|
}
|
|
@@ -941,8 +947,9 @@ export class HttpDispatcher {
|
|
|
941
947
|
const { object, operation } = endpoint.objectParams;
|
|
942
948
|
// Map standard CRUD operations
|
|
943
949
|
if (operation === 'find') {
|
|
944
|
-
const result = await broker.call('data.query', { object,
|
|
945
|
-
|
|
950
|
+
const result = await broker.call('data.query', { object, query }, { request: context.request });
|
|
951
|
+
// Spec: FindDataResponse = { object, records, total?, hasMore? }
|
|
952
|
+
return { handled: true, response: this.success(result.records, { total: result.total }) };
|
|
946
953
|
}
|
|
947
954
|
if (operation === 'get' && query.id) {
|
|
948
955
|
const result = await broker.call('data.get', { object, id: query.id }, { request: context.request });
|