Skip to content

Commit f58a57c

Browse files
Copilothuangyiirene
andcommitted
Fix code review issues: operator precedence, MongoDB filter conversion, bulk operations, and hooks dependency array
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
1 parent 8642810 commit f58a57c

3 files changed

Lines changed: 115 additions & 34 deletions

File tree

packages/data-objectql/src/ObjectQLDataSource.ts

Lines changed: 81 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,47 @@ export class ObjectQLDataSource<T = any> implements DataSource<T> {
156156
if (Array.isArray(params.$filter)) {
157157
objectqlParams.filter = params.$filter as FilterExpression;
158158
} else {
159-
// Convert object format to FilterExpression format
160-
objectqlParams.filter = Object.entries(params.$filter).map(
161-
([key, value]) => [key, '=', value] as [string, string, any]
162-
) as FilterExpression;
159+
// Convert object format (including Mongo-like operator objects) to FilterExpression format
160+
const filterEntries = Object.entries(params.$filter);
161+
const filters: any[] = [];
162+
163+
const operatorMap: Record<string, string> = {
164+
$eq: '=',
165+
$ne: '!=',
166+
$gt: '>',
167+
$gte: '>=',
168+
$lt: '<',
169+
$lte: '<=',
170+
$in: 'in',
171+
$nin: 'not-in',
172+
};
173+
174+
for (const [key, value] of filterEntries) {
175+
const isPlainObject =
176+
value !== null &&
177+
typeof value === 'object' &&
178+
!Array.isArray(value);
179+
180+
if (isPlainObject) {
181+
const opEntries = Object.entries(value as Record<string, unknown>);
182+
const hasDollarOperator = opEntries.some(([op]) => op.startsWith('$'));
183+
184+
if (hasDollarOperator) {
185+
for (const [rawOp, opValue] of opEntries) {
186+
const mappedOp =
187+
operatorMap[rawOp as keyof typeof operatorMap] ??
188+
rawOp.replace(/^\$/, '');
189+
filters.push([key, mappedOp, opValue]);
190+
}
191+
continue;
192+
}
193+
}
194+
195+
// Fallback: treat as simple equality
196+
filters.push([key, '=', value]);
197+
}
198+
199+
objectqlParams.filter = filters as FilterExpression;
163200
}
164201
}
165202

@@ -243,7 +280,7 @@ export class ObjectQLDataSource<T = any> implements DataSource<T> {
243280
return filtered as T;
244281
}
245282

246-
return response as T || null;
283+
return response ? (response as T) : null;
247284
} catch (err: any) {
248285
// Return null for not found errors
249286
if (err.code === 'NOT_FOUND' || err.status === 404) {
@@ -333,7 +370,7 @@ export class ObjectQLDataSource<T = any> implements DataSource<T> {
333370
*
334371
* @param resource - Object name
335372
* @param operation - Operation type
336-
* @param data - Bulk data or filters for update/delete
373+
* @param data - Bulk data (array of records for create/update/delete)
337374
* @returns Promise resolving to operation results
338375
*/
339376
async bulk(
@@ -346,38 +383,49 @@ export class ObjectQLDataSource<T = any> implements DataSource<T> {
346383
const response = await this.client.createMany<T>(resource, data);
347384
return response.items || [];
348385
} else if (operation === 'update') {
349-
// For bulk updates with SDK, we need filters and update data
350-
// This is a limitation - the old API accepted array of records
351-
// The new SDK requires filters + data
352-
if (Array.isArray(data)) {
353-
// If array of records is provided, we need to update them individually
354-
// This is less efficient but maintains compatibility
355-
const results: T[] = [];
356-
for (const item of data) {
357-
if ('_id' in item && item._id) {
358-
const updated = await this.client.update<T>(resource, item._id, item);
359-
results.push(updated as T);
360-
}
386+
// Fallback implementation: iterate and call single-record update
387+
if (!Array.isArray(data)) {
388+
throw new Error('Bulk update requires array of records');
389+
}
390+
391+
const results: T[] = [];
392+
for (const item of data) {
393+
const record: any = item as any;
394+
const id = record?.id ?? record?._id;
395+
if (id === undefined || id === null) {
396+
throw new Error(
397+
'Bulk update requires each item to include an `id` or `_id` field.'
398+
);
399+
}
400+
// Do not send id as part of the update payload
401+
const { id: _omitId, _id: _omitUnderscore, ...updateData } = record;
402+
const updated = await this.client.update<T>(resource, id, updateData);
403+
if (updated !== undefined && updated !== null) {
404+
results.push(updated as T);
361405
}
362-
return results;
363-
} else {
364-
throw new Error('Bulk update requires array of records with _id field');
365406
}
407+
return results;
366408
} else if (operation === 'delete') {
367-
// For bulk deletes with SDK, similar approach
368-
if (Array.isArray(data)) {
369-
// Delete each record individually
370-
const results: T[] = [];
371-
for (const item of data) {
372-
if ('_id' in item && item._id) {
373-
await this.client.delete(resource, item._id);
374-
results.push(item as T);
375-
}
409+
// Fallback implementation: iterate and call single-record delete
410+
if (!Array.isArray(data)) {
411+
throw new Error('Bulk delete requires array of records or IDs');
412+
}
413+
414+
for (const item of data) {
415+
const record: any = item as any;
416+
// Support both direct ID values and objects with id/_id field
417+
const id = typeof record === 'object'
418+
? (record?.id ?? record?._id)
419+
: record;
420+
if (id === undefined || id === null) {
421+
throw new Error(
422+
'Bulk delete requires each item to include an `id` or `_id` field or be an id value.'
423+
);
376424
}
377-
return results;
378-
} else {
379-
throw new Error('Bulk delete requires array of records with _id field');
425+
await this.client.delete(resource, id);
380426
}
427+
// For delete operations, we return an empty array by convention
428+
return [];
381429
}
382430

383431
throw new Error(`Unknown bulk operation: ${operation}`);

packages/data-objectql/src/__tests__/ObjectQLDataSource.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,40 @@ describe('ObjectQLDataSource', () => {
6666
expect(url).toContain('limit=20');
6767
});
6868

69+
it('should convert MongoDB-like operators in filters', async () => {
70+
const mockData = { items: [], meta: { total: 0 } };
71+
72+
(global.fetch as any).mockResolvedValueOnce({
73+
ok: true,
74+
json: async () => mockData,
75+
});
76+
77+
await dataSource.find('contacts', {
78+
$filter: {
79+
age: { $gte: 18, $lte: 65 },
80+
status: { $in: ['active', 'pending'] }
81+
}
82+
});
83+
84+
const fetchCall = (global.fetch as any).mock.calls[0];
85+
const url = fetchCall[0];
86+
87+
// Verify the filter parameter is present
88+
expect(url).toContain('filter=');
89+
90+
// The filter should be encoded as a JSON array with operators
91+
const urlObj = new URL(url, 'http://localhost');
92+
const filterParam = urlObj.searchParams.get('filter');
93+
if (filterParam) {
94+
const filter = JSON.parse(filterParam);
95+
// Should have converted to FilterExpression format
96+
expect(Array.isArray(filter)).toBe(true);
97+
// Should have converted $gte to '>=' and $lte to '<='
98+
expect(filter.some((f: any) => f[1] === '>=')).toBe(true);
99+
expect(filter.some((f: any) => f[1] === '<=')).toBe(true);
100+
}
101+
});
102+
69103
it('should include authentication token in headers', async () => {
70104
const mockData = { items: [] };
71105

packages/data-objectql/src/hooks.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ export function useObjectQL(options: UseObjectQLOptions): ObjectQLDataSource {
7979
return useMemo(
8080
() => new ObjectQLDataSource(options.config),
8181
[options.config]
82-
[JSON.stringify(options.config)]
8382
);
8483
}
8584

0 commit comments

Comments
 (0)