Skip to Content

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_relations directly for any nested field that isn’t found in daas_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 processM2OResults instead 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 patternBefore this fixAfter 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.

Last updated on