Skip to content

Commit 35e10fd

Browse files
committed
feat: add logging for intercepted requests in MSWPlugin and implement root path handling in HttpDispatcher
1 parent 3c01503 commit 35e10fd

3 files changed

Lines changed: 95 additions & 3 deletions

File tree

packages/plugins/plugin-msw/src/msw-plugin.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,11 @@ export class MSWPlugin implements Plugin {
240240
path = path.slice(baseUrl.length);
241241
}
242242

243+
if (this.options.logRequests) {
244+
// eslint-disable-next-line no-console
245+
console.log(`[MSW] Intercepted: ${request.method} ${url.pathname}`, { path });
246+
}
247+
243248
// Parse Body if present
244249
let body: any = undefined;
245250
if (request.method !== 'GET' && request.method !== 'HEAD') {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
2+
import { describe, it, expect, vi, beforeEach } from 'vitest';
3+
import { HttpDispatcher } from './http-dispatcher';
4+
import { ObjectKernel } from '@objectstack/core';
5+
6+
describe('HttpDispatcher Root Handling', () => {
7+
let kernel: ObjectKernel;
8+
let dispatcher: HttpDispatcher;
9+
10+
beforeEach(() => {
11+
// Mock minimal Kernel structure
12+
kernel = {
13+
services: {},
14+
broker: {
15+
call: vi.fn(),
16+
},
17+
context: {
18+
getService: vi.fn(),
19+
}
20+
} as any;
21+
22+
dispatcher = new HttpDispatcher(kernel);
23+
});
24+
25+
it('should handled GET request to root path ("") correctly', async () => {
26+
const context = { request: {} };
27+
const method = 'GET';
28+
// MSW passes empty string when stripping base URL
29+
const path = '';
30+
const body = undefined;
31+
const query = {};
32+
33+
const result = await dispatcher.dispatch(method, path, body, query, context);
34+
35+
expect(result.handled).toBe(true);
36+
expect(result.response).toBeDefined();
37+
expect(result.response?.status).toBe(200);
38+
39+
const data = result.response?.body?.data;
40+
expect(data).toBeDefined();
41+
// getDiscoveryInfo returns 'name' not 'apiName'
42+
expect(data.name).toBe('ObjectOS');
43+
expect(data.version).toBe('1.0.0');
44+
expect(data.routes).toBeDefined();
45+
// Since we passed empty prefix in dispatch code (hardcoded), routes should be relative
46+
expect(data.routes.metadata).toBe('/meta');
47+
});
48+
49+
it('should NOT handle POST request to root path ("")', async () => {
50+
const context = { request: {} };
51+
const method = 'POST';
52+
const path = '';
53+
54+
const result = await dispatcher.dispatch(method, path, {}, {}, context);
55+
56+
expect(result.handled).toBe(false);
57+
});
58+
});

packages/runtime/src/http-dispatcher.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,30 @@ export class HttpDispatcher {
173173

174174
try {
175175
// Try specific calls based on type
176-
if (type === 'objects') {
176+
if (type === 'objects' || type === 'object') {
177177
const data = await broker.call('metadata.getObject', { objectName: name }, { request: context.request });
178178
return { handled: true, response: this.success(data) };
179179
}
180-
// Generic call for other types if supported
181-
const data = await broker.call(`metadata.get${this.capitalize(type.slice(0, -1))}`, { name }, { request: context.request });
180+
181+
// If type is singular (e.g. 'app'), use it directly
182+
// If plural (e.g. 'apps'), slice it
183+
const singularType = type.endsWith('s') ? type.slice(0, -1) : type;
184+
185+
// Try Protocol Service First (Preferred)
186+
const protocol = this.kernel?.context?.getService ? this.kernel.context.getService('protocol') : null;
187+
if (protocol && typeof protocol.getMetaItem === 'function') {
188+
try {
189+
const data = await protocol.getMetaItem({ type: singularType, name });
190+
return { handled: true, response: this.success(data) };
191+
} catch (e: any) {
192+
// Protocol might throw if not found or not supported
193+
// Fallback to broker?
194+
}
195+
}
196+
197+
// Generic call for other types if supported via Broker (Legacy)
198+
const method = `metadata.get${this.capitalize(singularType)}`;
199+
const data = await broker.call(method, { name }, { request: context.request });
182200
return { handled: true, response: this.success(data) };
183201
} catch (e: any) {
184202
// Fallback: treat first part as object name if only 1 part (handled below)
@@ -537,6 +555,17 @@ export class HttpDispatcher {
537555
async dispatch(method: string, path: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
538556
const cleanPath = path.replace(/\/$/, ''); // Remove trailing slash if present, but strict on clean paths
539557

558+
// 0. Root Discovery Endpoint (GET /)
559+
// Handles request to base URL (e.g. /api/v1) which MSW strips to empty string
560+
if (cleanPath === '' && method === 'GET') {
561+
// We use '' as prefix since we are internal dispatcher
562+
const info = this.getDiscoveryInfo('');
563+
return {
564+
handled: true,
565+
response: this.success(info)
566+
};
567+
}
568+
540569
// 1. System Protocols (Prefix-based)
541570
if (cleanPath.startsWith('/auth')) {
542571
return this.handleAuth(cleanPath.substring(5), method, body, context);

0 commit comments

Comments
 (0)