v0.1.77 — 28 May 2026
Bug Fix — Nested M2M relations now resolve correctly at arbitrary depth
Who is affected: Anyone whose API queries traverse two or more levels of M2M relations — for example, fetching a user’s roles and then the policies assigned to each role in a single request.
What was wrong
Querying through two consecutive M2M relations returned an empty array for the second level. For example:
GET /api/users/{id}?fields=id,roles.role_id.policies.*Returned the correct junction rows for roles, but every nested role_id
object had policies: [] — the second-level M2M was silently dropped.
The same failure affected M2O → M2M chains. Querying:
GET /api/items/daas_user_roles?fields=id,role_id.policies.*Returned a 500 with a PostgREST parse error because the query builder generated
role_id(id,policies(*)), treating policies as if it were a real FK column
in the database.
Root cause
Two independent bugs combined to produce the failure:
Bug 1 — Virtual alias fields on system collections were invisible.
The query builder discovered alias fields (like policies on daas_roles) by
looking them up in daas_fields. For system collections — daas_roles,
daas_users, etc. — that table has no rows. The policies alias exists only
in daas_relations (one_field = 'policies'). When daas_fields came up
empty, the builder fell back to a raw PostgREST embedded select (policies(*))
which PostgREST rejected because there is no FK column by that name.
Bug 2 — Admin M2O was never post-processed.
For admin users, M2O fields were fetched via PostgREST embedded joins in the
SELECT string. PostgREST has no knowledge of virtual alias relations, so nested
alias fields on the related collection were silently dropped. The
processM2OResults post-processing path — which correctly handles virtual
alias fields — was only used for non-admin users.
What changed
- The query builder now consults
daas_relationsdirectly for any nested field that isn’t found indaas_fields, so virtual alias fields on system collections are correctly identified and routed through the relation pipeline. - Admin M2O fields with nested alias relations are now processed through
processM2OResultsinstead of PostgREST embedding, consistent with how non-admin users have always been handled. - M2M and M2O post-processors now recursively resolve nested relations on related-collection items, enabling arbitrarily deep chains (M2M → M2M, M2O → M2M → M2O, etc.).
Before / After
| Query pattern | Before this fix | After this fix |
|---|---|---|
fields=roles.role_id.policies.* (M2M → M2M) | ❌ policies silently empty | ✅ Junction rows returned |
fields=roles.role_id.policies.policy.* (expand FK at level 2) | ❌ Empty | ✅ Policy objects returned |
fields=role_id.policies.* on junction table (M2O → M2M) | ❌ 500 parse error | ✅ Junction rows returned |
fields=*.*.* (wildcard three levels deep) | ❌ Level-2 M2M empty | ✅ Full expansion |
| Non-admin with permissions on all collections in chain | ❌ Same failures | ✅ Correct, with permission enforcement |
Permission enforcement is unchanged
The fix does not relax any access controls. Non-admin users must still hold
read permission on every collection in the chain — the junction table, the
related collection, and any further-nested collection. A missing permission at
any level returns [] for that field rather than an error, matching the
behaviour of all other relation types.