-
Notifications
You must be signed in to change notification settings - Fork 1
fix: studio CI test failures - fix simulateBrowser mock handlers #1155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,51 @@ import { http, HttpResponse } from 'msw'; | |
| import { ObjectStackClient } from '@objectstack/client'; | ||
| import studioConfig from '../../objectstack.config'; | ||
|
|
||
| /** | ||
| * Parse query parameters from a request URL into an ObjectQL query options object. | ||
| * Supports: top, skip, sort, select, filter (JSON), and individual key-value filters. | ||
| */ | ||
| function parseQueryOptions(url: URL): Record<string, any> { | ||
| const query: Record<string, any> = {}; | ||
| const where: Record<string, any> = {}; | ||
|
|
||
| url.searchParams.forEach((val, key) => { | ||
| switch (key) { | ||
| case 'top': | ||
| case '$top': | ||
| query.limit = parseInt(val, 10); | ||
| break; | ||
| case 'skip': | ||
| case '$skip': | ||
| query.offset = parseInt(val, 10); | ||
| break; | ||
| case 'sort': | ||
| case '$sort': { | ||
| // JSON array or comma-separated string | ||
| try { query.orderBy = JSON.parse(val); } catch { | ||
| query.orderBy = val.split(',').map((s: string) => s.trim()); | ||
| } | ||
| break; | ||
| } | ||
| case 'select': | ||
| case '$select': | ||
| query.fields = val.split(',').map((s: string) => s.trim()); | ||
| break; | ||
| case 'filter': | ||
| case '$filter': | ||
| try { Object.assign(where, JSON.parse(val)); } catch { /* ignore malformed */ } | ||
| break; | ||
|
Comment on lines
+39
to
+42
|
||
| default: | ||
| // Individual key-value filter params (e.g. id=abc) | ||
| where[key] = val; | ||
| break; | ||
|
Comment on lines
+43
to
+46
|
||
| } | ||
| }); | ||
|
|
||
| if (Object.keys(where).length > 0) query.where = where; | ||
| return query; | ||
| } | ||
|
|
||
| /** | ||
| * Creates a Realistic Browser Simulation | ||
| * | ||
|
|
@@ -28,6 +73,7 @@ export async function simulateBrowser() { | |
| // 2. Define Network Handlers (The "Virtual Router") | ||
| // These map HTTP requests -> Kernel ObjectQL service actions | ||
| const ql = (kernel as any).context?.getService('objectql'); | ||
| const protocol = (kernel as any).context?.getService('protocol'); | ||
| const handlers = [ | ||
| // Discovery | ||
| http.get('http://localhost:3000/.well-known/objectstack', () => { | ||
|
|
@@ -47,17 +93,14 @@ export async function simulateBrowser() { | |
| }); | ||
| }), | ||
|
|
||
| // Query / Find | ||
| // Query / Find — parse query params for filtering, pagination, sorting, field selection | ||
| http.get('http://localhost:3000/api/v1/data/:object', async ({ params, request }) => { | ||
| const url = new URL(request.url); | ||
| const filters = {}; | ||
| url.searchParams.forEach((val, key) => { | ||
| (filters as any)[key] = val; | ||
| }); | ||
| console.log(`[VirtualNetwork] GET /data/${params.object}`, filters); | ||
| const queryOpts = parseQueryOptions(url); | ||
| console.log(`[VirtualNetwork] GET /data/${params.object}`, queryOpts); | ||
|
|
||
| try { | ||
| let all = await ql.find(params.object); | ||
| let all = await ql.find(params.object, queryOpts); | ||
| if (!Array.isArray(all) && all && (all as any).value) all = (all as any).value; | ||
| if (!all) all = []; | ||
| const result = { object: params.object, records: all, total: all.length }; | ||
|
|
@@ -122,12 +165,12 @@ export async function simulateBrowser() { | |
| } | ||
| }), | ||
|
|
||
| // Metadata - Get all types (base route returns types) | ||
| // Metadata - Get all types (merges SchemaRegistry + MetadataService types) | ||
| http.get('http://localhost:3000/api/v1/meta', async () => { | ||
| console.log('[VirtualNetwork] GET /meta (types)'); | ||
| try { | ||
| const types = ql?.registry?.getRegisteredTypes?.() || []; | ||
| return HttpResponse.json({ success: true, data: { types } }); | ||
| const result = await protocol?.getMetaTypes?.(); | ||
| return HttpResponse.json({ success: true, data: result || { types: [] } }); | ||
| } catch (err: any) { | ||
| return HttpResponse.json({ error: err.message }, { status: 500 }); | ||
| } | ||
|
|
@@ -137,43 +180,44 @@ export async function simulateBrowser() { | |
| http.get('http://localhost:3000/api/v1/meta/object', async () => { | ||
| console.log('[VirtualNetwork] GET /meta/object'); | ||
| try { | ||
| const objects = ql?.getObjects?.() || {}; | ||
| return HttpResponse.json({ success: true, data: objects }); | ||
| const result = await protocol?.getMetaItems?.({ type: 'object' }); | ||
| return HttpResponse.json({ success: true, data: result || { type: 'object', items: [] } }); | ||
| } catch (err: any) { | ||
| return HttpResponse.json({ error: err.message }, { status: 500 }); | ||
| } | ||
| }), | ||
| http.get('http://localhost:3000/api/v1/meta/objects', async () => { | ||
| console.log('[VirtualNetwork] GET /meta/objects'); | ||
| try { | ||
| const objects = ql?.getObjects?.() || {}; | ||
| return HttpResponse.json({ success: true, data: objects }); | ||
| const result = await protocol?.getMetaItems?.({ type: 'object' }); | ||
| return HttpResponse.json({ success: true, data: result || { type: 'object', items: [] } }); | ||
| } catch (err: any) { | ||
| return HttpResponse.json({ error: err.message }, { status: 500 }); | ||
| } | ||
| }), | ||
|
|
||
| // Metadata - Object Detail (Singular & Plural support) | ||
| // Returns the raw ServiceObject directly (with name, fields, etc.) | ||
| http.get('http://localhost:3000/api/v1/meta/object/:name', async ({ params }) => { | ||
| console.log(`[VirtualNetwork] GET /meta/object/${params.name}`); | ||
| try { | ||
| const result = ql?.registry?.getObject?.(params.name); | ||
| if (!result) { | ||
| const result = await protocol?.getMetaItem?.({ type: 'object', name: params.name as string }); | ||
| if (!result?.item) { | ||
| return HttpResponse.json({ error: 'Not Found' }, { status: 404 }); | ||
| } | ||
| return HttpResponse.json({ success: true, data: result }); | ||
| return HttpResponse.json({ success: true, data: result.item }); | ||
| } catch (err: any) { | ||
| return HttpResponse.json({ error: err.message }, { status: 500 }); | ||
| } | ||
| }), | ||
| http.get('http://localhost:3000/api/v1/meta/objects/:name', async ({ params }) => { | ||
| console.log(`[VirtualNetwork] GET /meta/objects/${params.name}`); | ||
| try { | ||
| const result = ql?.registry?.getObject?.(params.name); | ||
| if (!result) { | ||
| const result = await protocol?.getMetaItem?.({ type: 'object', name: params.name as string }); | ||
| if (!result?.item) { | ||
| return HttpResponse.json({ error: 'Not Found' }, { status: 404 }); | ||
| } | ||
| return HttpResponse.json({ success: true, data: result }); | ||
| return HttpResponse.json({ success: true, data: result.item }); | ||
| } catch (err: any) { | ||
| return HttpResponse.json({ error: err.message }, { status: 500 }); | ||
| } | ||
|
|
@@ -186,8 +230,8 @@ export async function simulateBrowser() { | |
| } | ||
| console.log(`[VirtualNetwork] GET /meta/${params.type}`); | ||
| try { | ||
| const items = ql?.registry?.listItems?.(params.type) || []; | ||
| return HttpResponse.json({ success: true, data: items }); | ||
| const result = await protocol?.getMetaItems?.({ type: params.type as string }); | ||
| return HttpResponse.json({ success: true, data: result || { type: params.type, items: [] } }); | ||
| } catch (err: any) { | ||
| return HttpResponse.json({ error: err.message }, { status: 500 }); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parseQueryOptions()maps thesortquery param toquery.orderByas a string array when the value is comma-separated (e.g. "-created_at").EngineQueryOptions.orderByis an array of SortNode objects ({ field, order }), and the in-memory driver’sapplySort()ignores string items, so sorting won’t be applied for the commonsort=-fieldform sent byObjectStackClient.data.find(). Convert string tokens into SortNode objects (handle leading '-' for desc) instead of returning raw strings.