Measured against a live seeded app (objectstack-ai/duly#13), driver SqlDriver(better-sqlite3), objectui at the current checkout.
Repro
Object duly_assignment has assignees: Field.user({ multiple: true }) — one assignment names N people. On a sys_user record page, the natural way to list "the assignments this person is named on" is:
{type: 'record:related_list',properties: {objectName: 'duly_assignment',relationshipField: 'assignees',columns: ['subject','assigner','due_date','status'],},}RecordRelatedListBody passes referenceField + parentId to RelatedList, which composes the parent filter as bare equality — {[relationshipField]: parentId}. The request and the answer:
GET /api/v1/data/duly_assignment?top=20&skip=0&sort=due_date
&filter=["assignees","=","ql8NO1TcKsuFrrH-"]
400 INVALID_FILTER
The bare equality spelling { "assignees": value } WAS NOT APPLIED: "assignees"
is a multi-value (or otherwise JSON-valued) field, stored by this driver as a
JSON TEXT column (e.g. ["a","b"]), and "=" compares that whole serialized text
against a single value — it can never equal one member. Use "$contains" for
membership ({ "assignees": { "$contains": "a" } }), or an $or of "$contains"
for any-of …
Why this is objectui's
Credit where due — the driver behaves exactly right. It refuses loudly, names the field, explains the storage, and prescribes the working spelling. This is not a silent-zero bug; the section renders an error and the console logs it.
The problem is that $containsis not reachable from the authoring surface.RecordRelatedListProps lets an author supply an additional filter, but the parent filter — the one that makes it a related list at all — is composed by the component from relationshipField alone. There is no operator, no relationshipOperator, nothing. So the list cannot be written correctly; it can only be not written.
Nor is there another route to the same data: RelatedList has no dotted-path column support, so the value cannot be reached from the child side either (duly_task.assignment → duly_assignment.assigner is one hop too far). On the page in question that meant one line of the requirement — "showing who assigned each" — could not be delivered at all, and the list was deleted rather than left showing a 400.
Suggested shape
Have the renderer pick the membership spelling when the parent field is multi-valued — the child object's field metadata is already in hand (useFieldPermissions(objectName) resolves the object), so multiple: true is knowable at compose time and {[relationshipField]: {$contains: parentId}} is what the driver is asking for. An explicit author-side escape (relationshipOperator: 'contains') would work too but puts the burden in the wrong place — the component knows the field's arity, the author is just naming the relationship.
Whichever way: the m2m-by-multi-value-field shape is common enough (it is the platform's own Field.user({ multiple: true })), and today it is the one relationship a related list cannot express.
🤖 Generated with Claude Code
https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Measured against a live seeded app (objectstack-ai/duly#13), driver
SqlDriver(better-sqlite3), objectui at the current checkout.Repro
Object
duly_assignmenthasassignees: Field.user({ multiple: true })— one assignment names N people. On asys_userrecord page, the natural way to list "the assignments this person is named on" is:RecordRelatedListBodypassesreferenceField+parentIdtoRelatedList, which composes the parent filter as bare equality —{[relationshipField]: parentId}. The request and the answer:Why this is objectui's
Credit where due — the driver behaves exactly right. It refuses loudly, names the field, explains the storage, and prescribes the working spelling. This is not a silent-zero bug; the section renders an error and the console logs it.
The problem is that
$containsis not reachable from the authoring surface.RecordRelatedListPropslets an author supply an additionalfilter, but the parent filter — the one that makes it a related list at all — is composed by the component fromrelationshipFieldalone. There is no operator, norelationshipOperator, nothing. So the list cannot be written correctly; it can only be not written.Nor is there another route to the same data:
RelatedListhas no dotted-path column support, so the value cannot be reached from the child side either (duly_task.assignment → duly_assignment.assigneris one hop too far). On the page in question that meant one line of the requirement — "showing who assigned each" — could not be delivered at all, and the list was deleted rather than left showing a 400.Suggested shape
Have the renderer pick the membership spelling when the parent field is multi-valued — the child object's field metadata is already in hand (
useFieldPermissions(objectName)resolves the object), somultiple: trueis knowable at compose time and{[relationshipField]: {$contains: parentId}}is what the driver is asking for. An explicit author-side escape (relationshipOperator: 'contains') would work too but puts the burden in the wrong place — the component knows the field's arity, the author is just naming the relationship.Whichever way: the m2m-by-multi-value-field shape is common enough (it is the platform's own
Field.user({ multiple: true })), and today it is the one relationship a related list cannot express.🤖 Generated with Claude Code
https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p