Skip to content

Commit fe1cfa6

Browse files
authored
Merge pull request #442 from objectstack-ai/copilot/create-msw-plugin-for-console
2 parents 451c4ea + 19b630b commit fe1cfa6

5 files changed

Lines changed: 325 additions & 400 deletions

File tree

apps/console/src/mocks/browser.ts

Lines changed: 9 additions & 208 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
/**
22
* MSW Browser Worker Setup via ObjectStack Runtime
33
*
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).
4+
* Uses the shared createKernel() factory to bootstrap the ObjectStack kernel,
5+
* then creates MSW handlers via the shared handler factory.
76
*
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.
7+
* This pattern follows @objectstack/studio — see https://github.com/objectstack-ai/spec
118
*/
129

13-
import { ObjectKernel, DriverPlugin, AppPlugin } from '@objectstack/runtime';
14-
import { ObjectQLPlugin } from '@objectstack/objectql';
10+
import { ObjectKernel } from '@objectstack/runtime';
1511
import { InMemoryDriver } from '@objectstack/driver-memory';
1612
import { setupWorker } from 'msw/browser';
17-
import { http, HttpResponse } from 'msw';
1813
import appConfig from '../../objectstack.shared';
14+
import { createKernel } from './createKernel';
15+
import { createHandlers } from './handlers';
1916

2017
let kernel: ObjectKernel | null = null;
2118
let driver: InMemoryDriver | null = null;
@@ -38,18 +35,9 @@ export async function startMockServer() {
3835

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

41-
driver = new InMemoryDriver();
42-
43-
kernel = new ObjectKernel({
44-
skipSystemValidation: true
45-
});
46-
47-
await kernel.use(new ObjectQLPlugin());
48-
await kernel.use(new DriverPlugin(driver, 'memory'));
49-
await kernel.use(new AppPlugin(appConfig));
50-
51-
// Bootstrap kernel WITHOUT MSW plugin — we create handlers manually below
52-
await kernel.bootstrap();
38+
const result = await createKernel({ appConfig });
39+
kernel = result.kernel;
40+
driver = result.driver;
5341

5442
// Create MSW handlers that match the response format of HonoServerPlugin
5543
// Include both /api/v1 and legacy /api paths so the ObjectStackClient can
@@ -82,190 +70,3 @@ export function getKernel(): ObjectKernel | null {
8270
export function getDriver(): InMemoryDriver | null {
8371
return driver;
8472
}
85-
86-
/**
87-
* Create MSW request handlers for ObjectStack API.
88-
*
89-
* Response shapes intentionally match HonoServerPlugin / RestServer so that
90-
* ObjectStackClient works identically in both MSW and server mode.
91-
*/
92-
function createHandlers(baseUrl: string, kernel: ObjectKernel, driver: InMemoryDriver) {
93-
const protocol = kernel.getService('protocol') as any;
94-
95-
return [
96-
// ── Discovery ────────────────────────────────────────────────────────
97-
http.get('*/.well-known/objectstack', async () => {
98-
const response = await protocol.getDiscovery();
99-
return HttpResponse.json(response, { status: 200 });
100-
}),
101-
102-
http.get(`*${baseUrl}`, async () => {
103-
const response = await protocol.getDiscovery();
104-
return HttpResponse.json(response, { status: 200 });
105-
}),
106-
http.get(`*${baseUrl}/`, async () => {
107-
const response = await protocol.getDiscovery();
108-
return HttpResponse.json(response, { status: 200 });
109-
}),
110-
111-
// ── Metadata: list objects ───────────────────────────────────────────
112-
http.get(`*${baseUrl}/meta/objects`, async () => {
113-
const response = await protocol.getMetaItems({ type: 'object' });
114-
return HttpResponse.json(response, { status: 200 });
115-
}),
116-
http.get(`*${baseUrl}/metadata/objects`, async () => {
117-
const response = await protocol.getMetaItems({ type: 'object' });
118-
return HttpResponse.json(response, { status: 200 });
119-
}),
120-
121-
// ── Metadata: single object (legacy /meta/objects/:name) ─────────────
122-
http.get(`*${baseUrl}/meta/objects/:objectName`, async ({ params }) => {
123-
if (import.meta.env.DEV) console.log('[MSW] meta/objects/', params.objectName);
124-
try {
125-
const response = await protocol.getMetaItem({
126-
type: 'object',
127-
name: params.objectName as string
128-
});
129-
return HttpResponse.json(response || { error: 'Not found' }, { status: response ? 200 : 404 });
130-
} catch (e) {
131-
return HttpResponse.json({ error: String(e) }, { status: 500 });
132-
}
133-
}),
134-
135-
// ── Metadata: single object (/meta/object/:name & /metadata/object/:name)
136-
http.get(`*${baseUrl}/meta/object/:objectName`, async ({ params }) => {
137-
if (import.meta.env.DEV) console.log('[MSW] meta/object/', params.objectName);
138-
try {
139-
const response = await protocol.getMetaItem({
140-
type: 'object',
141-
name: params.objectName as string
142-
});
143-
const payload = (response && response.item) ? response.item : response;
144-
return HttpResponse.json(payload || { error: 'Not found' }, { status: payload ? 200 : 404 });
145-
} catch (e) {
146-
console.error('[MSW] error getting meta item', e);
147-
return HttpResponse.json({ error: String(e) }, { status: 500 });
148-
}
149-
}),
150-
151-
http.get(`*${baseUrl}/metadata/object/:objectName`, async ({ params }) => {
152-
if (import.meta.env.DEV) console.log('[MSW] metadata/object/', params.objectName);
153-
try {
154-
const response = await protocol.getMetaItem({
155-
type: 'object',
156-
name: params.objectName as string
157-
});
158-
const payload = (response && response.item) ? response.item : response;
159-
return HttpResponse.json(payload || { error: 'Not found' }, { status: payload ? 200 : 404 });
160-
} catch (e) {
161-
console.error('[MSW] error getting meta item', e);
162-
return HttpResponse.json({ error: String(e) }, { status: 500 });
163-
}
164-
}),
165-
166-
// ── Metadata: apps ──────────────────────────────────────────────────
167-
http.get(`*${baseUrl}/meta/apps`, async () => {
168-
const response = await protocol.getMetaItems({ type: 'app' });
169-
return HttpResponse.json(response, { status: 200 });
170-
}),
171-
http.get(`*${baseUrl}/metadata/apps`, async () => {
172-
const response = await protocol.getMetaItems({ type: 'app' });
173-
return HttpResponse.json(response, { status: 200 });
174-
}),
175-
176-
// ── Metadata: dashboards ────────────────────────────────────────────
177-
http.get(`*${baseUrl}/meta/dashboards`, async () => {
178-
const response = await protocol.getMetaItems({ type: 'dashboard' });
179-
return HttpResponse.json(response, { status: 200 });
180-
}),
181-
http.get(`*${baseUrl}/metadata/dashboards`, async () => {
182-
const response = await protocol.getMetaItems({ type: 'dashboard' });
183-
return HttpResponse.json(response, { status: 200 });
184-
}),
185-
186-
// ── Metadata: reports ───────────────────────────────────────────────
187-
http.get(`*${baseUrl}/meta/reports`, async () => {
188-
const response = await protocol.getMetaItems({ type: 'report' });
189-
return HttpResponse.json(response, { status: 200 });
190-
}),
191-
http.get(`*${baseUrl}/metadata/reports`, async () => {
192-
const response = await protocol.getMetaItems({ type: 'report' });
193-
return HttpResponse.json(response, { status: 200 });
194-
}),
195-
196-
// ── Metadata: pages ─────────────────────────────────────────────────
197-
http.get(`*${baseUrl}/meta/pages`, async () => {
198-
const response = await protocol.getMetaItems({ type: 'page' });
199-
return HttpResponse.json(response, { status: 200 });
200-
}),
201-
http.get(`*${baseUrl}/metadata/pages`, async () => {
202-
const response = await protocol.getMetaItems({ type: 'page' });
203-
return HttpResponse.json(response, { status: 200 });
204-
}),
205-
206-
// ── Data: find all ──────────────────────────────────────────────────
207-
http.get(`*${baseUrl}/data/:objectName`, async ({ params, request }) => {
208-
const url = new URL(request.url);
209-
const query: any = {};
210-
211-
url.searchParams.forEach((value, key) => {
212-
try {
213-
query[key] = JSON.parse(value);
214-
} catch {
215-
query[key] = value;
216-
}
217-
});
218-
219-
if (import.meta.env.DEV) console.log('[MSW] find', params.objectName, query);
220-
const response = await driver.find(params.objectName as string, query);
221-
return HttpResponse.json({ value: response }, { status: 200 });
222-
}),
223-
224-
// ── Data: find by ID ────────────────────────────────────────────────
225-
http.get(`*${baseUrl}/data/:objectName/:id`, async ({ params }) => {
226-
try {
227-
if (import.meta.env.DEV) console.log('[MSW] getData', params.objectName, params.id);
228-
229-
const allRecords = await driver.find(params.objectName as string, {
230-
object: params.objectName as string
231-
});
232-
const record = allRecords
233-
? allRecords.find((r: any) =>
234-
String(r.id) === String(params.id) ||
235-
String(r._id) === String(params.id)
236-
)
237-
: null;
238-
239-
if (import.meta.env.DEV) console.log('[MSW] getData result', JSON.stringify(record));
240-
return HttpResponse.json({ record }, { status: record ? 200 : 404 });
241-
} catch (e) {
242-
console.error('[MSW] getData error', e);
243-
return HttpResponse.json({ error: String(e) }, { status: 500 });
244-
}
245-
}),
246-
247-
// ── Data: create ────────────────────────────────────────────────────
248-
http.post(`*${baseUrl}/data/:objectName`, async ({ params, request }) => {
249-
const body = await request.json();
250-
if (import.meta.env.DEV) console.log('[MSW] create', params.objectName, JSON.stringify(body));
251-
const response = await driver.create(params.objectName as string, body as any);
252-
if (import.meta.env.DEV) console.log('[MSW] create result', JSON.stringify(response));
253-
return HttpResponse.json({ record: response }, { status: 201 });
254-
}),
255-
256-
// ── Data: update ────────────────────────────────────────────────────
257-
http.patch(`*${baseUrl}/data/:objectName/:id`, async ({ params, request }) => {
258-
const body = await request.json();
259-
if (import.meta.env.DEV) console.log('[MSW] update', params.objectName, params.id, JSON.stringify(body));
260-
const response = await driver.update(params.objectName as string, params.id as string, body as any);
261-
if (import.meta.env.DEV) console.log('[MSW] update result', JSON.stringify(response));
262-
return HttpResponse.json({ record: response }, { status: 200 });
263-
}),
264-
265-
// ── Data: delete ────────────────────────────────────────────────────
266-
http.delete(`*${baseUrl}/data/:objectName/:id`, async ({ params }) => {
267-
const response = await driver.delete(params.objectName as string, params.id as string);
268-
return HttpResponse.json(response, { status: 200 });
269-
}),
270-
];
271-
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Shared Kernel Factory for MSW Mock Environment
3+
*
4+
* Creates a fully bootstrapped ObjectStack kernel for use in both
5+
* browser (MSW setupWorker) and test (MSW setupServer) environments.
6+
*
7+
* Follows the same pattern as @objectstack/studio's createKernel —
8+
* see https://github.com/objectstack-ai/spec
9+
*/
10+
11+
import { ObjectKernel, DriverPlugin, AppPlugin } from '@objectstack/runtime';
12+
import { ObjectQLPlugin } from '@objectstack/objectql';
13+
import { InMemoryDriver } from '@objectstack/driver-memory';
14+
15+
export interface KernelOptions {
16+
/** Application configuration (defineStack output) */
17+
appConfig: any;
18+
/** Whether to skip system validation (useful in browser) */
19+
skipSystemValidation?: boolean;
20+
}
21+
22+
export interface KernelResult {
23+
kernel: ObjectKernel;
24+
driver: InMemoryDriver;
25+
}
26+
27+
/**
28+
* Create and bootstrap an ObjectStack kernel with in-memory driver.
29+
*
30+
* This is the single factory used by both browser.ts and server.ts
31+
* so that kernel setup logic is not duplicated.
32+
*/
33+
export async function createKernel(options: KernelOptions): Promise<KernelResult> {
34+
const { appConfig, skipSystemValidation = true } = options;
35+
36+
const driver = new InMemoryDriver();
37+
38+
const kernel = new ObjectKernel({
39+
skipSystemValidation
40+
});
41+
42+
await kernel.use(new ObjectQLPlugin());
43+
await kernel.use(new DriverPlugin(driver, 'memory'));
44+
await kernel.use(new AppPlugin(appConfig));
45+
46+
await kernel.bootstrap();
47+
48+
return { kernel, driver };
49+
}

0 commit comments

Comments
 (0)