Skip to content

Commit b045190

Browse files
Copilothotlong
andcommitted
refactor: replace custom MSW handlers with MSWPlugin + protocol broker shim
- Remove apps/console/src/mocks/handlers.ts (custom createHandlers) - Update createKernel.ts with MSWPlugin support and protocol broker shim - Simplify browser.ts to use MSWPlugin (enableBrowser: true) - Simplify server.ts to use MSWPlugin (enableBrowser: false) + getHandlers() - Add filter/sort/top HTTP tests to MSWServer.test.tsx Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 17dace4 commit b045190

File tree

5 files changed

+185
-266
lines changed

5 files changed

+185
-266
lines changed

apps/console/src/__tests__/MSWServer.test.tsx

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
/**
2-
* Simple MSW Integration Test
2+
* MSW Integration Tests
33
*
4-
* Minimal test to verify MSW server is working
4+
* Verifies the MSW server (powered by MSWPlugin) correctly handles:
5+
* - Basic CRUD operations via the ObjectStack protocol
6+
* - Query parameters: filter, sort, top, skip
7+
*
8+
* MSWPlugin routes all requests through the ObjectStack protocol stack,
9+
* ensuring behaviour is identical to server mode (HonoServerPlugin).
510
*/
611

712
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
@@ -40,4 +45,54 @@ describe('MSW Server Integration', () => {
4045
expect(newContact.name).toBe('Test User');
4146
expect(newContact.email).toBe('test@example.com');
4247
});
48+
49+
// ── Protocol-level query tests (filter / sort / top) ───────────────────
50+
// These tests hit the MSW HTTP layer via fetch, which MSWPlugin routes
51+
// through HttpDispatcher → ObjectStack protocol. If the protocol handles
52+
// filter/sort/top correctly, these will pass.
53+
54+
it('should support top (limit) via HTTP', async () => {
55+
const res = await fetch('http://localhost/api/v1/data/contact?top=3');
56+
expect(res.ok).toBe(true);
57+
const body = await res.json();
58+
59+
// HttpDispatcher wraps in { success, data: { value: [...] } }
60+
const data = body.data ?? body;
61+
const records = data.value ?? data.records ?? data;
62+
expect(Array.isArray(records)).toBe(true);
63+
expect(records.length).toBeLessThanOrEqual(3);
64+
});
65+
66+
it('should support sort via HTTP', async () => {
67+
const res = await fetch('http://localhost/api/v1/data/contact?sort=name');
68+
expect(res.ok).toBe(true);
69+
const body = await res.json();
70+
71+
const data = body.data ?? body;
72+
const records = data.value ?? data.records ?? data;
73+
expect(Array.isArray(records)).toBe(true);
74+
expect(records.length).toBeGreaterThan(0);
75+
76+
// Verify records are sorted by name ascending
77+
const names = records.map((r: any) => r.name);
78+
const sorted = [...names].sort((a: string, b: string) => a.localeCompare(b));
79+
expect(names).toEqual(sorted);
80+
});
81+
82+
it('should support filter via HTTP', async () => {
83+
// Filter contacts where priority = "high" using tuple format
84+
const filter = JSON.stringify(['priority', '=', 'high']);
85+
const res = await fetch(`http://localhost/api/v1/data/contact?filter=${encodeURIComponent(filter)}`);
86+
expect(res.ok).toBe(true);
87+
const body = await res.json();
88+
89+
const data = body.data ?? body;
90+
const records = data.value ?? data.records ?? data;
91+
expect(Array.isArray(records)).toBe(true);
92+
expect(records.length).toBeGreaterThan(0);
93+
// All returned records should have priority 'high'
94+
for (const record of records) {
95+
expect(record.priority).toBe('high');
96+
}
97+
});
4398
});

apps/console/src/mocks/browser.ts

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
/**
22
* MSW Browser Worker Setup via ObjectStack Runtime
33
*
4-
* Uses the shared createKernel() factory to bootstrap the ObjectStack kernel,
5-
* then creates MSW handlers via the shared handler factory.
4+
* Uses the shared createKernel() factory to bootstrap the ObjectStack kernel
5+
* with MSWPlugin, which automatically exposes all ObjectStack API endpoints
6+
* via MSW. This ensures filter/sort/top/pagination work identically to
7+
* server mode.
68
*
79
* This pattern follows @objectstack/studio — see https://github.com/objectstack-ai/spec
810
*/
911

1012
import { ObjectKernel } from '@objectstack/runtime';
1113
import { InMemoryDriver } from '@objectstack/driver-memory';
12-
import { setupWorker } from 'msw/browser';
13-
import appConfig, { sharedConfig } from '../../objectstack.shared';
14+
import type { MSWPlugin } from '@objectstack/plugin-msw';
15+
import appConfig from '../../objectstack.shared';
1416
import { createKernel } from './createKernel';
15-
import { createHandlers } from './handlers';
1617

1718
let kernel: ObjectKernel | null = null;
1819
let driver: InMemoryDriver | null = null;
19-
let worker: ReturnType<typeof setupWorker> | null = null;
20+
let mswPlugin: MSWPlugin | null = null;
2021

2122
export async function startMockServer() {
2223
// Polyfill process.on for ObjectKernel in browser environment
@@ -35,36 +36,27 @@ export async function startMockServer() {
3536

3637
if (import.meta.env.DEV) console.log('[MSW] Starting ObjectStack Runtime (Browser Mode)...');
3738

38-
const result = await createKernel({ appConfig });
39-
kernel = result.kernel;
40-
driver = result.driver;
41-
42-
// Create MSW handlers that match the response format of HonoServerPlugin
43-
// Include both /api/v1 and legacy /api paths so the ObjectStackClient can
44-
// reach the mock server regardless of which base URL it probes.
45-
// Pass sharedConfig (pre-defineStack) so handlers can enrich object metadata
46-
// with listViews that defineStack's Zod parse strips.
47-
const v1Handlers = createHandlers('/api/v1', kernel, driver, sharedConfig);
48-
const legacyHandlers = createHandlers('/api', kernel, driver, sharedConfig);
49-
const handlers = [...v1Handlers, ...legacyHandlers];
50-
51-
// Start MSW service worker
52-
worker = setupWorker(...handlers);
53-
await worker.start({
54-
onUnhandledRequest: 'bypass',
55-
serviceWorker: {
56-
url: `${import.meta.env.BASE_URL}mockServiceWorker.js`,
39+
const result = await createKernel({
40+
appConfig,
41+
mswOptions: {
42+
enableBrowser: true,
43+
baseUrl: '/api/v1',
44+
logRequests: import.meta.env.DEV,
5745
},
5846
});
47+
kernel = result.kernel;
48+
driver = result.driver;
49+
mswPlugin = result.mswPlugin ?? null;
5950

6051
if (import.meta.env.DEV) console.log('[MSW] ObjectStack Runtime ready');
6152
return kernel;
6253
}
6354

6455
export function stopMockServer() {
65-
if (worker) {
66-
worker.stop();
67-
worker = null;
56+
if (mswPlugin) {
57+
const worker = mswPlugin.getWorker();
58+
if (worker) worker.stop();
59+
mswPlugin = null;
6860
}
6961
kernel = null;
7062
driver = null;

apps/console/src/mocks/createKernel.ts

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,94 @@
44
* Creates a fully bootstrapped ObjectStack kernel for use in both
55
* browser (MSW setupWorker) and test (MSW setupServer) environments.
66
*
7+
* Uses MSWPlugin from @objectstack/plugin-msw to expose the full
8+
* ObjectStack protocol via MSW. This ensures filter/sort/top/pagination
9+
* work identically to server mode.
10+
*
711
* Follows the same pattern as @objectstack/studio's createKernel —
812
* see https://github.com/objectstack-ai/spec
913
*/
1014

1115
import { ObjectKernel, DriverPlugin, AppPlugin } from '@objectstack/runtime';
1216
import { ObjectQLPlugin } from '@objectstack/objectql';
1317
import { InMemoryDriver } from '@objectstack/driver-memory';
18+
import { MSWPlugin } from '@objectstack/plugin-msw';
19+
import type { MSWPluginOptions } from '@objectstack/plugin-msw';
1420

1521
export interface KernelOptions {
1622
/** Application configuration (defineStack output) */
1723
appConfig: any;
1824
/** Whether to skip system validation (useful in browser) */
1925
skipSystemValidation?: boolean;
26+
/** MSWPlugin options; when provided, MSWPlugin is added to the kernel. */
27+
mswOptions?: MSWPluginOptions;
2028
}
2129

2230
export interface KernelResult {
2331
kernel: ObjectKernel;
2432
driver: InMemoryDriver;
33+
/** The MSWPlugin instance (if mswOptions was provided). */
34+
mswPlugin?: MSWPlugin;
35+
}
36+
37+
/**
38+
* Install a lightweight broker shim on the kernel so that
39+
* HttpDispatcher (used by MSWPlugin) can route data/metadata
40+
* calls through the ObjectStack protocol service.
41+
*
42+
* A full Moleculer-based broker is only available in server mode
43+
* (HonoServerPlugin). In MSW/browser mode we bridge the gap with
44+
* this thin adapter that delegates to the protocol service.
45+
*/
46+
async function installBrokerShim(kernel: ObjectKernel): Promise<void> {
47+
let protocol: any;
48+
try {
49+
protocol = await kernel.getService('protocol');
50+
} catch {
51+
return;
52+
}
53+
if (!protocol) return;
54+
55+
(kernel as any).broker = {
56+
async call(action: string, params: any = {}) {
57+
const [service, method] = action.split('.');
58+
59+
if (service === 'data') {
60+
switch (method) {
61+
case 'query':
62+
return protocol.findData({ object: params.object, query: params.query ?? params });
63+
case 'get':
64+
return protocol.getData({ object: params.object, id: params.id });
65+
case 'create':
66+
return protocol.createData({ object: params.object, data: params.data });
67+
case 'update':
68+
return protocol.updateData({ object: params.object, id: params.id, data: params.data });
69+
case 'delete':
70+
return protocol.deleteData({ object: params.object, id: params.id });
71+
case 'batch':
72+
return protocol.batchData?.({ object: params.object, ...params }) ?? { results: [] };
73+
}
74+
}
75+
76+
if (service === 'metadata') {
77+
if (method === 'types') return protocol.getMetaTypes({});
78+
if (method === 'getObject') {
79+
return protocol.getMetaItem({ type: 'object', name: params.objectName });
80+
}
81+
if (method === 'saveItem') {
82+
return protocol.saveMetaItem?.({ type: params.type, name: params.name, item: params.item });
83+
}
84+
if (method.startsWith('get')) {
85+
const type = method.replace('get', '').toLowerCase();
86+
return protocol.getMetaItem({ type, name: params.name });
87+
}
88+
// list-style calls: metadata.objects, metadata.apps, etc.
89+
return protocol.getMetaItems({ type: method, packageId: params.packageId });
90+
}
91+
92+
throw new Error(`[BrokerShim] Unhandled action: ${action}`);
93+
},
94+
};
2595
}
2696

2797
/**
@@ -31,7 +101,7 @@ export interface KernelResult {
31101
* so that kernel setup logic is not duplicated.
32102
*/
33103
export async function createKernel(options: KernelOptions): Promise<KernelResult> {
34-
const { appConfig, skipSystemValidation = true } = options;
104+
const { appConfig, skipSystemValidation = true, mswOptions } = options;
35105

36106
const driver = new InMemoryDriver();
37107

@@ -43,7 +113,24 @@ export async function createKernel(options: KernelOptions): Promise<KernelResult
43113
await kernel.use(new DriverPlugin(driver, 'memory'));
44114
await kernel.use(new AppPlugin(appConfig));
45115

116+
let mswPlugin: MSWPlugin | undefined;
117+
if (mswOptions) {
118+
// Install a protocol-based broker shim BEFORE MSWPlugin's start phase
119+
// so that HttpDispatcher (inside MSWPlugin) can resolve data/metadata
120+
// calls without requiring a full Moleculer broker.
121+
await installBrokerShim(kernel);
122+
123+
mswPlugin = new MSWPlugin(mswOptions);
124+
await kernel.use(mswPlugin);
125+
}
126+
46127
await kernel.bootstrap();
47128

48-
return { kernel, driver };
129+
// Re-install broker shim after bootstrap to ensure the protocol service
130+
// is fully initialised (some plugins register services during start phase).
131+
if (mswOptions) {
132+
await installBrokerShim(kernel);
133+
}
134+
135+
return { kernel, driver, mswPlugin };
49136
}

0 commit comments

Comments
 (0)