Skip to content

Commit 1756c0d

Browse files
authored
fix(db-mongodb): hasMany relationship filtering with equals operator returns no results (#15204)
### What Fixes filtering `hasMany` `relationship` fields (both polymorphic and non-polymorphic) with the `equals` and `not_equals` operators in the MongoDB adapter. When using the WhereBuilder UI to filter by these fields, queries would return zero results instead of matching documents. ### Why The MongoDB adapter's `sanitizeQueryValue` function wasn't handling array values properly for `equals`/`not_equals` operators on hasMany relationship fields. The values coming from the UI are string IDs that need to be converted to MongoDB ObjectIds before comparison: ``` // Before fix - strings don't match ObjectIds { relationshipHasMany: { $eq: ['507f1f77bcf86cd799439011', '507f191e810c19729de860ea'] } } ``` ``` // After fix - converted to ObjectIds { relationshipHasMany: { $eq: [ObjectId('507f1f77bcf86cd799439011'), ObjectId('507f191e810c19729de860ea')] } } ``` ### How Added handling in `sanitizeQueryValue.ts` to detect when: - The operator is `equals` or `not_equals` - The value is an array - The field has `hasMany: true` For these cases, the array values are converted to the proper ID types (ObjectId for MongoDB IDs, or custom ID types like numbers) before the query is built. **Note:** This fix is MongoDB-specific. Exact array equality filtering for hasMany relationships is not currently supported in SQL adapters (Drizzle/Postgres).
1 parent f98d915 commit 1756c0d

3 files changed

Lines changed: 383 additions & 0 deletions

File tree

packages/db-mongodb/src/queries/sanitizeQueryValue.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,51 @@ export const sanitizeQueryValue = ({
307307
}, [])
308308
}
309309

310+
// Handle hasMany relationships with equals operator and array values
311+
// For array equality checking
310312
if (
313+
['equals', 'not_equals'].includes(operator) &&
314+
Array.isArray(formattedValue) &&
315+
'hasMany' in field &&
316+
field.hasMany
317+
) {
318+
if (typeof relationTo === 'string') {
319+
const customIDType = payload.collections[relationTo]?.customIDType
320+
321+
// Convert array values to proper types (ObjectId or custom ID type)
322+
formattedValue = formattedValue.map((v) => {
323+
if (customIDType === 'number') {
324+
const parsed = parseFloat(v)
325+
return Number.isNaN(parsed) ? v : parsed
326+
}
327+
if (!Types.ObjectId.isValid(v)) {
328+
return v
329+
}
330+
return new Types.ObjectId(v)
331+
})
332+
} else {
333+
// Polymorphic hasMany - convert array of {relationTo, value} objects
334+
formattedValue = formattedValue.map((item) => {
335+
if (typeof item === 'object' && 'value' in item) {
336+
const relTo = item.relationTo
337+
const customIDType = payload.collections[relTo]?.customIDType
338+
if (customIDType === 'number') {
339+
const parsed = parseFloat(item.value)
340+
return { relationTo: relTo, value: Number.isNaN(parsed) ? item.value : parsed }
341+
}
342+
if (Types.ObjectId.isValid(item.value)) {
343+
return { relationTo: relTo, value: new Types.ObjectId(item.value) }
344+
}
345+
return item
346+
}
347+
// Non-polymorphic format - just IDs
348+
if (Types.ObjectId.isValid(item)) {
349+
return new Types.ObjectId(item)
350+
}
351+
return item
352+
})
353+
}
354+
} else if (
311355
['contains', 'equals', 'like', 'not_equals'].includes(operator) &&
312356
(!Array.isArray(relationTo) || !path.endsWith('.relationTo'))
313357
) {

test/fields/collections/Relationship/e2e.spec.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,74 @@ describe('relationship', () => {
726726
await expect(page.locator(tableRowLocator)).toHaveCount(1)
727727
})
728728

729+
test('should allow filtering by non-polymorphic hasMany relationship field / equals', async () => {
730+
const textDoc1 = await createTextFieldDoc({ text: 'Text 1' })
731+
const textDoc2 = await createTextFieldDoc({ text: 'Text 2' })
732+
const textDoc3 = await createTextFieldDoc({ text: 'Text 3' })
733+
734+
await createRelationshipFieldDoc(
735+
{ value: textDoc1.id, relationTo: 'text-fields' },
736+
{
737+
relationshipHasMany: [textDoc1.id],
738+
},
739+
)
740+
741+
await createRelationshipFieldDoc(
742+
{ value: textDoc2.id, relationTo: 'text-fields' },
743+
{
744+
relationshipHasMany: [textDoc2.id, textDoc3.id],
745+
},
746+
)
747+
748+
await page.goto(url.list)
749+
await wait(1000)
750+
751+
await addListFilter({
752+
page,
753+
fieldLabel: 'Relationship Has Many',
754+
operatorLabel: 'equals',
755+
value: 'Text 1',
756+
multiSelect: true,
757+
})
758+
759+
await expect(page.locator(tableRowLocator)).toHaveCount(1)
760+
})
761+
762+
test('should allow filtering by polymorphic hasMany relationship field / equals', async () => {
763+
const textDoc1 = await createTextFieldDoc({ text: 'Poly Text 1' })
764+
const textDoc2 = await createTextFieldDoc({ text: 'Poly Text 2' })
765+
766+
await createRelationshipFieldDoc(
767+
{ value: textDoc1.id, relationTo: 'text-fields' },
768+
{
769+
relationHasManyPolymorphic: [{ relationTo: 'text-fields', value: textDoc1.id }],
770+
},
771+
)
772+
773+
await createRelationshipFieldDoc(
774+
{ value: textDoc2.id, relationTo: 'text-fields' },
775+
{
776+
relationHasManyPolymorphic: [
777+
{ relationTo: 'text-fields', value: textDoc1.id },
778+
{ relationTo: 'text-fields', value: textDoc2.id },
779+
],
780+
},
781+
)
782+
783+
await page.goto(url.list)
784+
await wait(1000)
785+
786+
await addListFilter({
787+
page,
788+
fieldLabel: 'Relation Has Many Polymorphic',
789+
operatorLabel: 'equals',
790+
value: 'Poly Text 1',
791+
multiSelect: true,
792+
})
793+
794+
await expect(page.locator(tableRowLocator)).toHaveCount(1)
795+
})
796+
729797
test('should be able to select relationship with drawer appearance', async () => {
730798
await loadCreatePage()
731799

0 commit comments

Comments
 (0)