Skip to content

Commit 864c106

Browse files
committed
feat: enhance CRUD operations with robust error handling and support for pagination, sorting, and field selection
1 parent 8285a98 commit 864c106

3 files changed

Lines changed: 152 additions & 10 deletions

File tree

examples/app-react-crud/src/mocks/createKernel.ts

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,41 @@ export async function createKernel(options: KernelOptions) {
5656
return match || null;
5757
}
5858
if (method === 'update') {
59-
return ql.update(params.object, { ...params.data, id: params.id });
59+
if (params.id) {
60+
// Robust check: Manually find the record in memory since ql.find(obj, id) might not be supported by this specific mock driver setup
61+
let all = await ql.find(params.object);
62+
63+
if (all && (all as any).value) all = (all as any).value;
64+
if (!all) all = [];
65+
66+
const existing = all.find((i: any) => i.id === params.id || i._id === params.id);
67+
68+
if (!existing) {
69+
console.warn(`[BrokerShim] Update failed: Record ${params.id} not found.`);
70+
throw new Error('[ObjectStack] Not Found');
71+
}
72+
73+
// Perform update using the ObjectQL Engine signature: update(object, data, options)
74+
// where options.filter can be the ID string
75+
try {
76+
await ql.update(params.object, params.data, { filter: params.id });
77+
} catch (err: any) {
78+
console.warn(`[BrokerShim] update failed: ${err.message}`);
79+
throw err;
80+
}
81+
82+
return { ...existing, ...params.data };
83+
}
84+
return null;
6085
}
6186
if (method === 'delete') {
62-
return ql.delete(params.object, { filter: params.id });
87+
try {
88+
// ql.delete(object, options) where options.filter is ID
89+
return await ql.delete(params.object, { filter: params.id });
90+
} catch (err: any) {
91+
console.warn(`[BrokerShim] delete failed: ${err.message}`);
92+
throw err;
93+
}
6394
}
6495
if (method === 'find' || method === 'query') {
6596
let all = await ql.find(params.object);
@@ -77,6 +108,17 @@ export async function createKernel(options: KernelOptions) {
77108
if (!all) all = [];
78109

79110
const filters = params.filters;
111+
// Extract standard query options possibly passed via filters (due to MSW plugin mapping)
112+
let queryOptions: any = {};
113+
if (filters && typeof filters === 'object') {
114+
const reserved = ['top', 'skip', 'sort', 'select', 'expand', 'count', 'search'];
115+
reserved.forEach(opt => {
116+
if (filters[opt] !== undefined) {
117+
queryOptions[opt] = filters[opt];
118+
}
119+
});
120+
}
121+
80122
if (filters && typeof filters === 'object' && !Array.isArray(filters)) {
81123
// Filter out reserved query parameters that are NOT field names
82124
const reserved = ['top', 'skip', 'sort', 'select', 'expand', 'count', 'search'];
@@ -92,6 +134,47 @@ export async function createKernel(options: KernelOptions) {
92134
});
93135
}
94136
}
137+
138+
// --- Sort ---
139+
if (queryOptions.sort) {
140+
const sortFields = String(queryOptions.sort).split(',').map(s => s.trim());
141+
all.sort((a: any, b: any) => {
142+
for (const field of sortFields) {
143+
const desc = field.startsWith('-');
144+
const key = desc ? field.substring(1) : field;
145+
if (a[key] < b[key]) return desc ? 1 : -1;
146+
if (a[key] > b[key]) return desc ? -1 : 1;
147+
}
148+
return 0;
149+
});
150+
}
151+
152+
// --- Select ---
153+
if (queryOptions.select) {
154+
const selectFields = Array.isArray(queryOptions.select)
155+
? queryOptions.select
156+
: String(queryOptions.select).split(',').map((s: string) => s.trim());
157+
158+
all = all.map((item: any) => {
159+
const projected: any = { id: item.id, _id: item._id }; // Always include ID
160+
selectFields.forEach((f: string) => {
161+
if (item[f] !== undefined) projected[f] = item[f];
162+
});
163+
return projected;
164+
});
165+
}
166+
167+
// --- Skip/Top ---
168+
const skip = parseInt(queryOptions.skip) || 0;
169+
const top = parseInt(queryOptions.top); // undefined is fine
170+
171+
if (skip > 0) {
172+
all = all.slice(skip);
173+
}
174+
if (!isNaN(top)) {
175+
all = all.slice(0, top);
176+
}
177+
95178
console.log(`[BrokerShim] find/query(${params.object}) -> count: ${all.length}`);
96179
return { data: all, count: all.length };
97180
}

examples/app-react-crud/src/mocks/simulateBrowser.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,15 @@ export async function simulateBrowser() {
2929
// Query / Find
3030
http.get('http://localhost:3000/api/v1/data/:object', async ({ params, request }) => {
3131
const url = new URL(request.url);
32-
const filters = {}; // Todo: parse query params if needed
32+
const filters = {};
3333

34-
// Extract query params for simple filtering
34+
// Extract query params
35+
// Simulate MSW Plugin behavior: Put ALL Query Params into `filters`
3536
url.searchParams.forEach((val, key) => {
36-
if (key !== 'select' && key !== 'sort' && key !== 'top') {
37-
(filters as any)[key] = val;
38-
}
37+
(filters as any)[key] = val;
3938
});
4039

41-
console.log(`[VirtualNetwork] GET /data/${params.object}`);
40+
console.log(`[VirtualNetwork] GET /data/${params.object}`, filters);
4241

4342
try {
4443
// Call Kernel
@@ -70,18 +69,37 @@ export async function simulateBrowser() {
7069
}
7170
}),
7271

73-
// Update
72+
// Update (PUT)
7473
http.put('http://localhost:3000/api/v1/data/:object/:id', async ({ params, request }) => {
7574
const body = await request.json();
7675
console.log(`[VirtualNetwork] PUT /data/${params.object}/${params.id}`);
7776

7877
try {
78+
// Ensure broker receives { object, id, data } explicitly
7979
const result = await (kernel as any).broker.call('data.update', {
8080
object: params.object,
8181
id: params.id,
8282
data: body
8383
});
84-
return HttpResponse.json(result);
84+
return HttpResponse.json(result || { success: true });
85+
} catch (err: any) {
86+
return HttpResponse.json({ error: err.message }, { status: 500 });
87+
}
88+
}),
89+
90+
// Update (PATCH) - Support both verbs
91+
http.patch('http://localhost:3000/api/v1/data/:object/:id', async ({ params, request }) => {
92+
const body = await request.json();
93+
console.log(`[VirtualNetwork] PATCH /data/${params.object}/${params.id}`);
94+
95+
try {
96+
// Ensure broker receives { object, id, data } explicitly
97+
const result = await (kernel as any).broker.call('data.update', {
98+
object: params.object,
99+
id: params.id,
100+
data: body
101+
});
102+
return HttpResponse.json(result || { success: true });
85103
} catch (err: any) {
86104
return HttpResponse.json({ error: err.message }, { status: 500 });
87105
}

examples/app-react-crud/test/api.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,46 @@ describe('App React CRUD Integration Tests (Virtual Browser)', () => {
6363
const list = Array.isArray(fetched) ? fetched : (fetched as any).value;
6464
expect(list).toHaveLength(1);
6565
expect(list[0].id).toBe(newTask.id);
66+
67+
// UPDATE
68+
const updated = await client.data.update('todo_task', newTask.id, {
69+
subject: 'Updated Task Title'
70+
});
71+
expect(updated.subject).toBe('Updated Task Title');
72+
73+
// DELETE
74+
await client.data.delete('todo_task', newTask.id);
75+
const afterDelete = await client.data.find('todo_task', { filters: { id: newTask.id } });
76+
const missingList = Array.isArray(afterDelete) ? afterDelete : (afterDelete as any).value;
77+
expect(missingList).toHaveLength(0);
78+
});
79+
80+
it('should support pagination, sorting and field selection', async () => {
81+
const { client } = env;
82+
83+
// 1. Test Sorting
84+
// default data has priorities 1, 2, 3
85+
const sorted = await client.data.find('todo_task', {
86+
sort: ['priority'] // Ascending
87+
});
88+
const sortedItems = Array.isArray(sorted) ? sorted : (sorted as any).value;
89+
expect(sortedItems[0].priority).toBeLessThanOrEqual(sortedItems[1].priority);
90+
91+
// 2. Test Pagination (Top)
92+
const top2 = await client.data.find('todo_task', {
93+
top: 2
94+
});
95+
const top2Items = Array.isArray(top2) ? top2 : (top2 as any).value;
96+
expect(top2Items).toHaveLength(2);
97+
98+
// 3. Test Select
99+
const selected = await client.data.find('todo_task', {
100+
top: 1,
101+
select: ['subject']
102+
});
103+
const selectedItems = Array.isArray(selected) ? selected : (selected as any).value;
104+
expect(selectedItems[0]).toHaveProperty('subject');
105+
expect(selectedItems[0]).not.toHaveProperty('priority'); // Should be excluded
106+
expect(selectedItems[0]).toHaveProperty('id'); // ID is always returned
66107
});
67108
});

0 commit comments

Comments
 (0)