Skip to content

Commit a056839

Browse files
committed
feat: refactor startMockServer to manually create MSW handlers and remove MSWPlugin usage
1 parent 3f01fbb commit a056839

1 file changed

Lines changed: 213 additions & 62 deletions

File tree

apps/console/src/mocks/browser.ts

Lines changed: 213 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
/**
22
* MSW Browser Worker Setup via ObjectStack Runtime
33
*
4-
* Uses the standard @objectstack/plugin-msw to create a complete ObjectStack
5-
* environment in the browser with In-Memory Driver and MSW-intercepted API.
4+
* Bootstraps a full ObjectStack kernel (ObjectQL + InMemoryDriver + AppPlugin)
5+
* in the browser, then creates MSW handlers manually so responses match the
6+
* format the ObjectStackClient expects (same as HonoServerPlugin / RestServer).
7+
*
8+
* This intentionally does NOT use @objectstack/plugin-msw because its
9+
* HttpDispatcher wraps every response in a { success, data } envelope that
10+
* the client does not expect.
611
*/
712

813
import { ObjectKernel, DriverPlugin, AppPlugin } from '@objectstack/runtime';
914
import { ObjectQLPlugin } from '@objectstack/objectql';
1015
import { InMemoryDriver } from '@objectstack/driver-memory';
11-
import { MSWPlugin } from '@objectstack/plugin-msw';
16+
import { setupWorker } from 'msw/browser';
17+
import { http, HttpResponse } from 'msw';
1218
import appConfig from '../../objectstack.shared';
1319

1420
let kernel: ObjectKernel | null = null;
1521
let driver: InMemoryDriver | null = null;
22+
let worker: ReturnType<typeof setupWorker> | null = null;
1623

1724
export async function startMockServer() {
1825
// Polyfill process.on for ObjectKernel in browser environment
@@ -41,77 +48,221 @@ export async function startMockServer() {
4148
await kernel.use(new DriverPlugin(driver, 'memory'));
4249
await kernel.use(new AppPlugin(appConfig));
4350

44-
// MSW Plugin handles worker setup automatically with enableBrowser: true
45-
const mswPlugin = new MSWPlugin({
46-
enableBrowser: true,
47-
baseUrl: '/api/v1',
48-
logRequests: true
49-
});
50-
await kernel.use(mswPlugin);
51-
51+
// Bootstrap kernel WITHOUT MSW plugin — we create handlers manually below
5252
await kernel.bootstrap();
5353

54-
// Broker shim: routes HttpDispatcher action calls to kernel services.
55-
// Required because no current plugin initializes kernel.broker.
56-
const ql = kernel.getService<any>('objectql');
57-
const protocol = kernel.getService<any>('protocol');
58-
(kernel as any).broker = {
59-
call: async (action: string, params: any) => {
60-
const [service, method] = action.split('.');
61-
62-
if (service === 'data') {
63-
if (method === 'query' || method === 'find') {
64-
const filter = params.filters?.filters ?? params.filters ?? params.filter;
65-
const data = await ql.find(params.object, {
66-
filter,
67-
top: params.filters?.top ?? params.top,
68-
skip: params.filters?.skip ?? params.skip,
69-
sort: params.filters?.sort ?? params.sort,
70-
});
71-
return { data, count: data.length };
72-
}
73-
if (method === 'get') {
74-
return ql.findOne(params.object, { filter: { id: params.id } });
75-
}
76-
if (method === 'create') {
77-
const res = await ql.insert(params.object, params.data);
78-
return { ...params.data, ...res };
79-
}
80-
if (method === 'update') {
81-
return ql.update(params.object, params.data, { filter: { id: params.id } });
82-
}
83-
if (method === 'delete') {
84-
return ql.delete(params.object, { filter: { id: params.id } });
85-
}
86-
}
87-
88-
if (service === 'metadata') {
89-
if (method === 'getObject') return protocol.getMetaItem('object', params.objectName);
90-
if (method === 'objects') return protocol.getMetaItems('object');
91-
if (method === 'types') return { types: ['object', 'app'] };
92-
}
93-
94-
if (service === 'ui') {
95-
if (method === 'getView') return protocol.getUiView(params.object, params.type);
96-
}
97-
98-
if (service === 'auth') {
99-
return { token: 'mock-token', user: { name: 'Admin' } };
100-
}
54+
// Create MSW handlers that match the response format of HonoServerPlugin
55+
const baseUrl = '/api/v1';
56+
const handlers = createHandlers(baseUrl, kernel, driver);
10157

102-
if (import.meta.env.DEV) console.warn(`[Broker] Unhandled: ${action}`, params);
103-
return null;
104-
}
105-
};
58+
// Start MSW service worker
59+
worker = setupWorker(...handlers);
60+
await worker.start({ onUnhandledRequest: 'bypass' });
10661

10762
if (import.meta.env.DEV) console.log('[MSW] ObjectStack Runtime ready');
10863
return kernel;
10964
}
11065

66+
export function stopMockServer() {
67+
if (worker) {
68+
worker.stop();
69+
worker = null;
70+
}
71+
kernel = null;
72+
driver = null;
73+
}
74+
11175
export function getKernel(): ObjectKernel | null {
11276
return kernel;
11377
}
11478

11579
export function getDriver(): InMemoryDriver | null {
11680
return driver;
11781
}
82+
83+
/**
84+
* Create MSW request handlers for ObjectStack API.
85+
*
86+
* Response shapes intentionally match HonoServerPlugin / RestServer so that
87+
* ObjectStackClient works identically in both MSW and server mode.
88+
*/
89+
function createHandlers(baseUrl: string, kernel: ObjectKernel, driver: InMemoryDriver) {
90+
const protocol = kernel.getService('protocol') as any;
91+
92+
return [
93+
// ── Discovery ────────────────────────────────────────────────────────
94+
http.get('*/.well-known/objectstack', async () => {
95+
const response = await protocol.getDiscovery();
96+
return HttpResponse.json(response, { status: 200 });
97+
}),
98+
99+
http.get(`*${baseUrl}`, async () => {
100+
const response = await protocol.getDiscovery();
101+
return HttpResponse.json(response, { status: 200 });
102+
}),
103+
http.get(`*${baseUrl}/`, async () => {
104+
const response = await protocol.getDiscovery();
105+
return HttpResponse.json(response, { status: 200 });
106+
}),
107+
108+
// ── Metadata: list objects ───────────────────────────────────────────
109+
http.get(`*${baseUrl}/meta/objects`, async () => {
110+
const response = await protocol.getMetaItems({ type: 'object' });
111+
return HttpResponse.json(response, { status: 200 });
112+
}),
113+
http.get(`*${baseUrl}/metadata/objects`, async () => {
114+
const response = await protocol.getMetaItems({ type: 'object' });
115+
return HttpResponse.json(response, { status: 200 });
116+
}),
117+
118+
// ── Metadata: single object (legacy /meta/objects/:name) ─────────────
119+
http.get(`*${baseUrl}/meta/objects/:objectName`, async ({ params }) => {
120+
if (import.meta.env.DEV) console.log('[MSW] meta/objects/', params.objectName);
121+
try {
122+
const response = await protocol.getMetaItem({
123+
type: 'object',
124+
name: params.objectName as string
125+
});
126+
return HttpResponse.json(response || { error: 'Not found' }, { status: response ? 200 : 404 });
127+
} catch (e) {
128+
return HttpResponse.json({ error: String(e) }, { status: 500 });
129+
}
130+
}),
131+
132+
// ── Metadata: single object (/meta/object/:name & /metadata/object/:name)
133+
http.get(`*${baseUrl}/meta/object/:objectName`, async ({ params }) => {
134+
if (import.meta.env.DEV) console.log('[MSW] meta/object/', params.objectName);
135+
try {
136+
const response = await protocol.getMetaItem({
137+
type: 'object',
138+
name: params.objectName as string
139+
});
140+
const payload = (response && response.item) ? response.item : response;
141+
return HttpResponse.json(payload || { error: 'Not found' }, { status: payload ? 200 : 404 });
142+
} catch (e) {
143+
console.error('[MSW] error getting meta item', e);
144+
return HttpResponse.json({ error: String(e) }, { status: 500 });
145+
}
146+
}),
147+
148+
http.get(`*${baseUrl}/metadata/object/:objectName`, async ({ params }) => {
149+
if (import.meta.env.DEV) console.log('[MSW] metadata/object/', params.objectName);
150+
try {
151+
const response = await protocol.getMetaItem({
152+
type: 'object',
153+
name: params.objectName as string
154+
});
155+
const payload = (response && response.item) ? response.item : response;
156+
return HttpResponse.json(payload || { error: 'Not found' }, { status: payload ? 200 : 404 });
157+
} catch (e) {
158+
console.error('[MSW] error getting meta item', e);
159+
return HttpResponse.json({ error: String(e) }, { status: 500 });
160+
}
161+
}),
162+
163+
// ── Metadata: apps ──────────────────────────────────────────────────
164+
http.get(`*${baseUrl}/meta/apps`, async () => {
165+
const response = await protocol.getMetaItems({ type: 'app' });
166+
return HttpResponse.json(response, { status: 200 });
167+
}),
168+
http.get(`*${baseUrl}/metadata/apps`, async () => {
169+
const response = await protocol.getMetaItems({ type: 'app' });
170+
return HttpResponse.json(response, { status: 200 });
171+
}),
172+
173+
// ── Metadata: dashboards ────────────────────────────────────────────
174+
http.get(`*${baseUrl}/meta/dashboards`, async () => {
175+
const response = await protocol.getMetaItems({ type: 'dashboard' });
176+
return HttpResponse.json(response, { status: 200 });
177+
}),
178+
http.get(`*${baseUrl}/metadata/dashboards`, async () => {
179+
const response = await protocol.getMetaItems({ type: 'dashboard' });
180+
return HttpResponse.json(response, { status: 200 });
181+
}),
182+
183+
// ── Metadata: reports ───────────────────────────────────────────────
184+
http.get(`*${baseUrl}/meta/reports`, async () => {
185+
const response = await protocol.getMetaItems({ type: 'report' });
186+
return HttpResponse.json(response, { status: 200 });
187+
}),
188+
http.get(`*${baseUrl}/metadata/reports`, async () => {
189+
const response = await protocol.getMetaItems({ type: 'report' });
190+
return HttpResponse.json(response, { status: 200 });
191+
}),
192+
193+
// ── Metadata: pages ─────────────────────────────────────────────────
194+
http.get(`*${baseUrl}/meta/pages`, async () => {
195+
const response = await protocol.getMetaItems({ type: 'page' });
196+
return HttpResponse.json(response, { status: 200 });
197+
}),
198+
http.get(`*${baseUrl}/metadata/pages`, async () => {
199+
const response = await protocol.getMetaItems({ type: 'page' });
200+
return HttpResponse.json(response, { status: 200 });
201+
}),
202+
203+
// ── Data: find all ──────────────────────────────────────────────────
204+
http.get(`*${baseUrl}/data/:objectName`, async ({ params, request }) => {
205+
const url = new URL(request.url);
206+
const query: any = {};
207+
208+
url.searchParams.forEach((value, key) => {
209+
try {
210+
query[key] = JSON.parse(value);
211+
} catch {
212+
query[key] = value;
213+
}
214+
});
215+
216+
if (import.meta.env.DEV) console.log('[MSW] find', params.objectName, query);
217+
const response = await driver.find(params.objectName as string, query);
218+
return HttpResponse.json({ value: response }, { status: 200 });
219+
}),
220+
221+
// ── Data: find by ID ────────────────────────────────────────────────
222+
http.get(`*${baseUrl}/data/:objectName/:id`, async ({ params }) => {
223+
try {
224+
if (import.meta.env.DEV) console.log('[MSW] getData', params.objectName, params.id);
225+
226+
const allRecords = await driver.find(params.objectName as string, {
227+
object: params.objectName as string
228+
});
229+
const record = allRecords
230+
? allRecords.find((r: any) =>
231+
String(r.id) === String(params.id) ||
232+
String(r._id) === String(params.id)
233+
)
234+
: null;
235+
236+
if (import.meta.env.DEV) console.log('[MSW] getData result', JSON.stringify(record));
237+
return HttpResponse.json({ record }, { status: record ? 200 : 404 });
238+
} catch (e) {
239+
console.error('[MSW] getData error', e);
240+
return HttpResponse.json({ error: String(e) }, { status: 500 });
241+
}
242+
}),
243+
244+
// ── Data: create ────────────────────────────────────────────────────
245+
http.post(`*${baseUrl}/data/:objectName`, async ({ params, request }) => {
246+
const body = await request.json();
247+
if (import.meta.env.DEV) console.log('[MSW] create', params.objectName, JSON.stringify(body));
248+
const response = await driver.create(params.objectName as string, body as any);
249+
if (import.meta.env.DEV) console.log('[MSW] create result', JSON.stringify(response));
250+
return HttpResponse.json({ record: response }, { status: 201 });
251+
}),
252+
253+
// ── Data: update ────────────────────────────────────────────────────
254+
http.patch(`*${baseUrl}/data/:objectName/:id`, async ({ params, request }) => {
255+
const body = await request.json();
256+
if (import.meta.env.DEV) console.log('[MSW] update', params.objectName, params.id, JSON.stringify(body));
257+
const response = await driver.update(params.objectName as string, params.id as string, body as any);
258+
if (import.meta.env.DEV) console.log('[MSW] update result', JSON.stringify(response));
259+
return HttpResponse.json({ record: response }, { status: 200 });
260+
}),
261+
262+
// ── Data: delete ────────────────────────────────────────────────────
263+
http.delete(`*${baseUrl}/data/:objectName/:id`, async ({ params }) => {
264+
const response = await driver.delete(params.objectName as string, params.id as string);
265+
return HttpResponse.json(response, { status: 200 });
266+
}),
267+
];
268+
}

0 commit comments

Comments
 (0)