Power PatternsSelect Join Pattern
Power Patterns

SelectJoin & PropertyRelation in Mindbricks

Lightweight, relation-aware joins inside Get & List Business APIs. Mindbricks lets you connect DataObjects using Property Relations, then reuse those relations in many places (Business APIs, BFF DataViews, etc.).

1. Conceptual Overview

Mindbricks lets you connect DataObjects using Property Relations, then reuse those relations in many places (Business APIs, BFF DataViews, etc.).

SelectJoin is the “lightweight join” feature built on top of those relations:

A SelectJoin works like a left join in SQL, but it runs after the main data is fetched: Mindbricks first queries the main object, then runs extra queries to fetch related records based on the defined relations and collates those results into the response.

Key properties of SelectJoin:

  • It is configured inside the selectClause of Get and List Business APIs.

  • It does not change the schema; it only enriches the API response.

  • It uses existing PropertyRelation metadata between objects.

  • It can join:

    • from main → joined object (one-to-one)

    • or from joined → main object (children join / one-to-many).

  • It can disambiguate multiple relations between the same objects using a selector string.


2. PropertyRelation: How Objects are Linked

Before we talk about SelectJoin, we need the foundation: Property Relations.

A relation is defined on a property of a DataObject (the “foreign key” field), via hasRelation = true and PropertyRelationConfig.

From patterns.json:

"PropertyRelation": {
  "__hasRelation.doc": "...",
  "hasRelation": "Boolean",
  "configuration": "PropertyRelationConfig",
  "__activation": "hasRelation",
  "__nullables": ["configuration"]
}
"PropertyRelationConfig": {
  "relationName": "String",
  "relationTargetObject": "DataObjectName",
  "relationTargetKey": "PropRefer",
  "onDeleteAction": "RelationOnDeleteAction",
  "relationIsRequired": "Boolean"
}

2.1 What This Means

When you set hasRelation = true on a property, you’re saying:

“This property is a foreign key that points to a row in another DataObject.”

  • relationTargetObject – which object is being referenced (e.g., auth:user, project, order).

  • relationTargetKey – which field in the target object is referenced (usually id).

  • relationName – human-friendly name for the relationship, used in generated code/docs.

  • onDeleteAction – what to do when the target (parent) is deleted (doDelete, setNull).

  • relationIsRequired – whether this FK must always point to a valid target.

2.2 One-to-One vs One-to-Many

With the new semantics:

  • Every relation is defined as “many-to-one” from the foreign key holder to the target:

    • e.g., order.userIduser.id

    • In words: “Many orders belong to one user.”

  • From the source object point of view (where the property lives), it behaves like a one-to-one reference to the target:

    • One order has one user.
  • From the target object point of view, the reverse is naturally one-to-many:

    • One user has many orders.

Mindbricks uses this rule everywhere:

Any relation made to an object defines that object as the “parent” of those records that reference it.

So:

  • If you configure order.userId as a relation to user:

    • order is the child.

    • user is the parent.

    • From order to user → one-to-one reference.

    • From user to order → zero-or-many orders.


3. Simple PropertyRelation Examples

3.1 Order → User

Goal: Link each order to the user who owns it.

In order DataObject, property userId:

{
  "basicSettings": { "name": "userId", "type": "ID" },
  "relationSettings": {
    "hasRelation": true,
    "configuration": {
      "relationName": "orderUser",
      "relationTargetObject": "auth:user",
      "relationTargetKey": "id",
      "onDeleteAction": "setNull",
      "relationIsRequired": true
    }
  }
}

Meaning:

  • order.userId references auth:user.id.

  • If user is deleted: onDeleteAction = setNull will null-out userId.

  • The relation is required on create (relationIsRequired = true).

3.2 Project → Owner User

In project DataObject, property ownerId:

{
  "basicSettings": { "name": "ownerId", "type": "ID" },
  "relationSettings": {
    "hasRelation": true,
    "configuration": {
      "relationName": "projectOwner",
      "relationTargetObject": "auth:user",
      "relationTargetKey": "id",
      "onDeleteAction": "setNull",
      "relationIsRequired": true
    }
  }
}

Now project has a one-to-one reference to user (its owner), and user implicitly has many projects.

3.3 Task → Project

In task DataObject, property projectId:

{
  "basicSettings": { "name": "projectId", "type": "ID" },
  "relationSettings": {
    "hasRelation": true,
    "configuration": {
      "relationName": "taskProject",
      "relationTargetObject": "project",
      "relationTargetKey": "id",
      "onDeleteAction": "doDelete",
      "relationIsRequired": true
    }
  }
}

If project is deleted, all tasks are deleted (doDelete).


4. SelectJoin Pattern: Structure & Semantics

Now, with relations defined, we can use SelectJoin in selectClause of get and list Business APIs.

From the updated pattern:

"SelectJoin": {
  "joinName": "String",
  "condition": "MScript",
  "joinedDataObject": "DataObjectName",
  "selector": "String",
  "properties": ["PropRefer"],
  "sort": ["SortByItem"],
  "topPick": "Integer"
}

With docs:

  • It is used in SelectClauseSettings.selectJoins[].

  • It uses relations between main object and joinedDataObject, as configured via PropertyRelation.

4.1 What SelectJoin Does

For a given get or list Business API:

  1. Mindbricks executes the main query (fetch main object or list of objects).

  2. For each SelectJoin:

    • It identifies the relevant relation(s) between main object and joined object, using joinedDataObject and optional selector.

    • It runs extra queries to fetch related records from joinedDataObject:

      • one record (one-to-one) or

      • multiple records (children list) depending on direction.

    • It picks specific properties (properties) from those records.

    • It attaches the joined data under the property name joinName of each main object in the API response.

  3. The behavior is left join-like:

    • If no related record exists, the joined property is null (for one) or [] (for many).

4.2 Fields in Detail

  • joinName

    • A code-safe string used as:

      • Identifier in the SelectJoin config.

      • Property name in the returned JSON.

    • Example: "owner", "orders", "lastUpdatedBy".

  • joinedDataObject (DataObjectName)

    • Which DataObject to join with, e.g., "auth:user", "order", "project".

    • This object must be related to the main object via one or more PropertyRelations.

  • selector (String)

    • New field that disambiguates which relation to use if more than one relation exists between main and joined objects.

    • Format: a special “map syntax” string: foreignKey->localKey

    • Not strictly a query, but a relation hint.

  • condition (MScript, optional)

    • Boolean expression to decide whether to perform this join at code level:

      • Example: "this.includeOwner === true".
    • Docs note:

      “This condition can not reference the main object or main object list; for complex joins use a BFF View.” So use request parameters or session (this.session, this.params), NOT row data.

  • properties (PropRefer[])

    • Which fields of the joined object should be selected.

    • If empty, all fields are returned.

  • sort (SortByItem[], optional)

    • Only relevant when join result is a list (children join).

    • Sorts the joined records before topPick is applied.

    • SortByItem pattern: name, property, order ("asc"/"desc").

  • topPick (Integer, optional)

    • How many joined records to keep after sorting in a children join.

    • If topPick = 1 → result becomes a single object (instead of an array).

    • Great for things like “latest order”, “oldest member” etc.


5. SelectJoin Relation Direction: One or Many

SelectJoin can traverse relations either way:

5.1 Main → Joined (one-to-one)

If the relation is defined on the main object pointing to joinedDataObject (like project.ownerIduser.id):

  • SelectJoin will return a single joined object (or null).

  • Example: “project owner user”.

5.2 Joined → Main (children / one-to-many)

If the relation is defined on the joined object pointing back to the main object (like order.userIduser.id):

  • SelectJoin will collect all joined records for that main object (children list).

  • Example: “user orders” inside a getUser or listUser API.

Mindbricks detects which direction to take based purely on relation definitions:

  • If it finds a PropertyRelation on main object that references joined → main→joined path.

  • If it finds a PropertyRelation on joined object that references main → joined→main path.

If both exist, or multiple different relations exist between these objects, selector tells which one to use.


6. The selector String Explained

The selector is a disambiguator when there are multiple relations between main and joined objects.

Docs:

“A relation selector is a special string with a map syntax foreignKey->localKey. Leave null to select the first in case you are sure there is only one relation with the target data object. Eg. Main object is user, joined object is project, id->ownerId (user projects). Main object is project and joined object is user, ownerId->id (project owner). Optional syntaxes: -> or empty (first relation), ->lastUpdateUserId, ownerId->, etc.”

To reconcile this, the safe interpretation (and easiest to communicate) is:

selector = mainSideProperty -> joinedSideProperty

You can think of it as: “Match main[mainSideProperty] with joined[joinedSideProperty]”.

Let’s go through the examples:

6.1 Example: Main = user, Joined = project (user projects)

  • Relationship: project.ownerId references user.id.

  • To get user’s projects in a getUser API (main user, joined project), you want to join on:

    • main.id → joined.ownerId.
  • selector: "id->ownerId"

    • mainSide property: id (on user)

    • joinedSide property: ownerId (on project)

6.2 Example: Main = project, Joined = user (project owner)

  • Relationship: project.ownerId references user.id.

  • To get project’s owner in a getProject API:

    • main.ownerId → joined.id.
  • selector: "ownerId->id"

    • mainSide property: ownerId (project)

    • joinedSide property: id (user)

6.3 Optional Shorthand Forms

Docs also mention partial shorthand:

  • "" or "->" – means “use the first relation between main and joined objects.”

    • Only safe if there is exactly one relation.
  • "->lastUpdateUserId" – means “first relation whose joined side property is lastUpdateUserId.”

  • "ownerId->" – means “first relation whose main side property is ownerId.”

These are syntactic sugar; for readability, it’s best to use a complete**mainField->joinedField** syntax.


7. SelectJoin Examples

7.1 Example 1 – Get Project with Owner User

Use case: getProject API should return:

  • project fields (id, name, ownerId)

  • plus a nested owner object (user)

Relations:

// In project DataObject
{
  "name": "ownerId",
  "relationSettings": {
    "hasRelation": true,
    "configuration": {
      "relationName": "projectOwner",
      "relationTargetObject": "auth:user",
      "relationTargetKey": "id",
      "onDeleteAction": "setNull",
      "relationIsRequired": true
    }
  }
}

SelectClause:

"selectClause": {
  "selectProperties": ["id", "name", "ownerId"],
  "selectJoins": [
    {
      "joinName": "owner",
      "joinedDataObject": "auth:user",
      "selector": "ownerId->id",
      "condition": "true",
      "properties": ["id", "email", "fullname"],
      "sort": [],
      "topPick": null
    }
  ]
}

Runtime behavior:

  • Main project loaded by ID.

  • SelectJoin finds relation from project.ownerIdauth:user.id.

  • Queries auth:user where id = project.ownerId.

  • Attaches the selected user fields under project.owner in the response.

Example response:

{
  "id": "proj-1",
  "name": "My Project",
  "ownerId": "user-123",
  "owner": {
    "id": "user-123",
    "email": "owner@example.com",
    "fullname": "Jane Doe"
  }
}

7.2 Example 2 – Get User with Orders (Children Join)

Use case: getUser API should return all orders of the user as orders.

Relations:

// In order DataObject
{
  "name": "userId",
  "relationSettings": {
    "hasRelation": true,
    "configuration": {
      "relationName": "orderUser",
      "relationTargetObject": "auth:user",
      "relationTargetKey": "id",
      "onDeleteAction": "setNull",
      "relationIsRequired": true
    }
  }
}

Here, relation is defined on order pointing to user:

  • From order perspective: one-to-one reference.

  • From user perspective: children/one-to-many.

SelectClause (inside**getUser****):**

"selectClause": {
  "selectProperties": ["id", "email", "fullname"],
  "selectJoins": [
    {
      "joinName": "orders",
      "joinedDataObject": "order",
      "selector": "id->userId", 
      "condition": "true",
      "properties": ["id", "totalAmount", "status", "createdAt"],
      "sort": [
        {
          "name": "byCreatedAtDesc",
          "property": "createdAt",
          "order": "desc"
        }
      ],
      "topPick": null
    }
  ]
}

How the selector works:

  • main object: user

  • joined object: order

  • id->userId means: match user.id with order.userId.

Result:

  • For each user, Mindbricks fetches all orders where userId = user.id.

  • Sorted by createdAt desc.

  • Returned as an array in user.orders.

Example response:

{
  "id": "user-123",
  "email": "alice@example.com",
  "fullname": "Alice",
  "orders": [
    { "id": "ord-10", "totalAmount": 120, "status": "paid", "createdAt": "..." },
    { "id": "ord-09", "totalAmount": 75, "status": "paid", "createdAt": "..." }
  ]
}

7.3 Example 3 – Get User with Last Order Only (topPick)

Same as above, but we only want the latest order as lastOrder.

"selectJoins": [
  {
    "joinName": "lastOrder",
    "joinedDataObject": "order",
    "selector": "id->userId",
    "condition": "true",
    "properties": ["id", "totalAmount", "status", "createdAt"],
    "sort": [
      {
        "name": "byCreatedAtDesc",
        "property": "createdAt",
        "order": "desc"
      }
    ],
    "topPick": 1
  }
]
  • Now, join result is an array sorted by createdAt desc, but topPick = 1 means we keep only the first record and return it as a single object.

Example response:

{
  "id": "user-123",
  "email": "alice@example.com",
  "lastOrder": {
    "id": "ord-10",
    "totalAmount": 120,
    "status": "paid",
    "createdAt": "..."
  }
}

7.4 Example 4 – Multiple Relations to Same Object (Using selector)

Suppose project has two properties referencing user:

  • ownerId – project owner

  • lastUpdateUserId – last user who updated the project

Both have relationSettings pointing to auth:user.

Now we want:

  • getProject to include owner user

  • getProject to include lastUpdater user

In project DataObject:

{
  "name": "ownerId",
  "relationSettings": {
    "hasRelation": true,
    "configuration": {
      "relationName": "projectOwner",
      "relationTargetObject": "auth:user",
      "relationTargetKey": "id",
      "onDeleteAction": "setNull",
      "relationIsRequired": true
    }
  }
},
{
  "name": "lastUpdateUserId",
  "relationSettings": {
    "hasRelation": true,
    "configuration": {
      "relationName": "projectLastUpdater",
      "relationTargetObject": "auth:user",
      "relationTargetKey": "id",
      "onDeleteAction": "setNull",
      "relationIsRequired": false
    }
  }
}

SelectClause:

"selectClause": {
  "selectProperties": ["id", "name", "ownerId", "lastUpdateUserId"],
  "selectJoins": [
    {
      "joinName": "owner",
      "joinedDataObject": "auth:user",
      "selector": "ownerId->id",
      "condition": "true",
      "properties": ["id", "email", "fullname"],
      "sort": [],
      "topPick": null
    },
    {
      "joinName": "lastUpdater",
      "joinedDataObject": "auth:user",
      "selector": "lastUpdateUserId->id",
      "condition": "true",
      "properties": ["id", "email", "fullname"],
      "sort": [],
      "topPick": null
    }
  ]
}

Two SelectJoins to the same target object, distinguished by selector.


8. SelectJoin in List APIs

SelectJoin works the same way in List Business APIs; it just runs once per row in the result list.

8.1 Example – List Projects with Owner and Task Count

Assume:

  • Relations:

    • project.ownerIduser.id

    • task.projectIdproject.id

We want listProjects to return:

  • Basic project fields

  • owner object (user)

  • tasks array (list of tasks)

SelectClause:

"selectClause": {
  "selectProperties": ["id", "name", "ownerId"],
  "selectJoins": [
    {
      "joinName": "owner",
      "joinedDataObject": "auth:user",
      "selector": "ownerId->id",
      "condition": "true",
      "properties": ["id", "fullname"],
      "sort": [],
      "topPick": null
    },
    {
      "joinName": "tasks",
      "joinedDataObject": "task",
      "selector": "id->projectId",
      "condition": "true",
      "properties": ["id", "title", "status"],
      "sort": [
        {
          "name": "byTitle",
          "property": "title",
          "order": "asc"
        }
      ],
      "topPick": null
    }
  ]
}

Response (simplified):

[
  {
    "id": "proj-1",
    "name": "Main Project",
    "ownerId": "user-1",
    "owner": { "id": "user-1", "fullname": "Alice" },
    "tasks": [ /* tasks for proj-1 */ ]
  },
  {
    "id": "proj-2",
    "name": "Side Project",
    "ownerId": "user-2",
    "owner": { "id": "user-2", "fullname": "Bob" },
    "tasks": [ /* tasks for proj-2 */ ]
  }
]

Behind the scenes, Mindbricks behaves like performing multiple “left joins” but implemented as:

  • Main query → fetch projects (with filters/sort).

  • Per SelectJoin:

    • Identify relations and queries.

    • Fetch and collate joined data efficiently.


9. When to Use SelectJoin vs Other Tools

SelectJoin is ideal when:

  • You have simple, well-defined relations between objects.

  • You want to enrich a Get or List response with related data.

  • You don’t want to build a full BFF DataView (global read model).

  • You don’t want to write extra actions like FetchObjectAction for each join.

Use BFF DataViews when:

  • You’re building complex, cross-service read models used by many screens.

  • You need stored views in Elasticsearch for performance or analytics.

Use**FetchObjectAction**** /FetchFromElasticAction when:**

  • You want fully custom queries or logic not expressible as “follow relation X”.

10. ListJointFilterSettings & ListJointFilter

Join-like list filtering with pre-fetched ID lists

So far we covered PropertyRelation and SelectJoin, which enrich Get/List responses with related object data.

There is a sibling feature in List-type APIs:

ListJointFilterSettings + ListJointFilter let you filter the main list using conditions on a related DataObject, without writing an explicit join in your whereClause.

Conceptually:

  • SelectJoin adds data from a related object to each row.

  • ListJointFilter filters rows based on a related object.

Both rely on PropertyRelation metadata and both allow disambiguation via a selector string if there are multiple relations.


10.1 Pattern Overview

From patterns.json (slightly extended with selector):

"ListJointFilterSettings": {
  "__operator.doc": "The combination operator of multi joint filters...",
  "operator": "FilterOperator",       // "AND" or "OR"
  "filters": ["ListJointFilter"],
  "__filters.single": "filter",
  "__combination.default": "AND"
}
"FilterOperator": ["AND", "OR"]
"ListJointFilter": {
  "__ListJointFilter.doc": "ListJointFilter definition object for a joint filter from the main object to another related data object to filter the main list result...",
  "name": "String",
  "joinedDataObject": "DataObjectName",
  "condition": "MScript",
  "whereClause": "MScript",     // MScript Query on joined object
  "selector": "String"          // NEW: relation selector (assumed)
}

Note: selector is assumed as a new field, analogous to SelectJoin.selector, to resolve ambiguity when multiple relations exist between main and joined objects.

These appear inside a ListOptions:

"listOptions": {
  "jointFilters": {
    "operator": "AND",
    "filters": [
      { /* ListJointFilter #1 */ },
      { /* ListJointFilter #2 */ }
    ]
  }
}

10.2 What ListJointFilter Does

In a list Business API, the execution flow is conceptually:

  1. Build the base whereClause from WhereClauseSettings (if any).

  2. Apply permissionFilters, membershipFilters, and searchFilter.

  3. For each ListJointFilter:

    • Use PropertyRelation metadata and selector (if provided) to understand the relationship between main object and joinedDataObject.

    • Run a separate query on the joinedDataObject using whereClause (MScript Query) in the context of that joined object.

    • From these results, collect the IDs of main objects related to those joined objects.

  4. Combine the ID sets from all joint filters using operator (“AND” / “OR”).

  5. Restrict the main query to only those main IDs that survived the combination.

  6. Finally, fetch the main list and return results.

This behaves like a join filter, but implemented as:

  • “Prefetch relevant joined objects → gather matching main IDs → restrict main query” instead of writing explicit SQL/ORM joins yourself.

10.3 Relation Direction

Just like SelectJoin, ListJointFilter can leverage relations in either direction:

  • Main → Joined (FK on main referencing joined)

  • Joined → Main (FK on joined referencing main)

Mindbricks determines direction based on PropertyRelation settings between the two DataObjects and uses that to map which IDs to collect.


10.4 The selector String for Joint Filters

Because there can be multiple relations between the same two objects, selector disambiguates which one to use.

We reuse the same mental model as for SelectJoin.selector:

selector = mainSideProperty -> joinedSideProperty meaning: “match main[mainSideProperty] with joined[joinedSideProperty]

Examples:

  • Main: user, Joined: order

    • Relation: order.userIduser.id.

    • For “users who have orders”:

      • selector: "id->userId".
  • Main: project, Joined: user (project owner)

    • Relation: project.ownerIduser.id.

    • For “projects where owner matches some criteria”:

      • selector: "ownerId->id".

Shorthands (for completeness; same idea as SelectJoin):

  • "" or "->" – first available relation between main & joined.

  • "->fieldName" – first relation whose joined side field is fieldName.

  • "fieldName->" – first relation whose main side field is fieldName.

But recommended: always use full mainField->joinedField for clarity.


10.5 Example: List Users Who Have Orders Over 200 USD

Goal: List only users that have at least one order with:

  • totalAmount > 200

  • currency = 'USD'

Data model:

  • order.userId → relation to auth:user.id (user is parent).

  • So: one user → many orders.

PropertyRelation (on**order.userId****):**

{
  "basicSettings": { "name": "userId", "type": "ID" },
  "relationSettings": {
    "hasRelation": true,
    "configuration": {
      "relationName": "orderUser",
      "relationTargetObject": "auth:user",
      "relationTargetKey": "id",
      "onDeleteAction": "setNull",
      "relationIsRequired": true
    }
  }
}

List Business API: listUsers (main object = auth:user)

ListOptions with Joint Filter:

"listOptions": {
  "jointFilters": {
    "operator": "AND",
    "filters": [
      {
        "name": "hasHighValueOrder",
        "joinedDataObject": "order",
        "selector": "id->userId",     // user.id = order.userId
        "condition": "true",
        "whereClause": "({ totalAmount: { "$gt": 200 }, currency: { "$eq": 'USD' } })"
      }
    ]
  }
}

What Mindbricks does under the hood:

  1. Executes order search with query:

    • totalAmount > 200 and currency = "USD".
  2. Collects the userId of each matching order as potential main IDs.

  3. Restricts listUsers to id IN collectedUserIds.

  4. Applies any other filters (whereClause, permissionFilters, etc.) on user.

Result: Only users with at least one qualifying order show up in the list.


10.6 Combining Multiple Joint Filters with AND/OR

ListJointFilterSettings.operator defines how multiple joint filters are combined:

  • "AND" – main object must satisfy all joint filters.

  • "OR" – main object must satisfy at least one joint filter.

10.6.1 Example: Users with High-Value Orders AND Open Tickets

Goal: List users who:

  • Have orders with totalAmount > 200 (USD), and

  • Have at least one open support ticket.

Assume:

  • order.userIduser.id

  • supportTicket.userIduser.id

ListOptions:

"listOptions": {
  "jointFilters": {
    "operator": "AND",
    "filters": [
      {
        "name": "hasHighValueOrder",
        "joinedDataObject": "order",
        "selector": "id->userId",
        "condition": "true",
        "whereClause": "({ totalAmount: { "$gt": 200 }, currency: { "$eq": 'USD' } })"
      },
      {
        "name": "hasOpenTicket",
        "joinedDataObject": "supportTicket",
        "selector": "id->userId",
        "condition": "true",
        "whereClause": "({ status: { "$eq": 'open' } })"
      }
    ]
  }
}

Process:

  • Prefetch order IDs → userId set A.

  • Prefetch supportTicket IDs → userId set B.

  • AND → intersection A ∩ B.

  • Only users in that intersection survive.

10.6.2 Example: Users with High-Value Orders OR Open Tickets

To get a “union” instead:

"jointFilters": {
  "operator": "OR",
  "filters": [
    { ...hasHighValueOrder... },
    { ...hasOpenTicket... }
  ]
}

Main IDs = A ∪ B.


10.7 Interaction with Other List Filters

ListJointFilter works alongside:

  • Base whereClause (WhereClauseSettings)

  • permissionFilters (ListPermissionFilter)

  • membershipFilters (ListMembershipFilter)

  • searchFilter (ListSearchFilter)

The combined filtering logic is roughly:

MainWhereClause
  AND permissionFilters
  AND membershipFilters
  AND searchFilterIds
  AND jointFilterIds

Where jointFilterIds is either:

  • IDs from one filter, or

  • AND/OR combination of multiple filter ID sets.

So you can express powerful queries like:

“List users who are in my tenant, have permission X, are members of project Y, whose name matches keyword K, and who have an order > $200.”

All this, purely declaratively.


10.8 When to Use Joint Filters vs BFF/DataViews

Use ListJointFilter when:

  • You want to constrain a List API based on related object conditions.

  • Your conditions are essentially “exists some joined record matching X.”

Use BFF DataViews when:

  • You’re building complex multi-object reporting or analytics views.

  • You want to pre-store read models in Elasticsearch.

Use FetchFromElasticAction or multiple BusinessApi actions when:

  • You need very custom joining, cross-aggregation, or logic that doesn’t cleanly fit the “related object” model.

10.9 Summary: JointFilter at a Glance

  • What it is: A join-like filter mechanism for list APIs that uses prefetching and relation metadata.

  • What it does: Looks at a related object, collects main IDs that satisfy conditions, and filters the main list.

  • How it’s configured:

    • Inside listOptions.jointFilters.

    • operator chooses AND/OR semantics across multiple filters.

    • Each ListJointFilter has joinedDataObject, selector, whereClause, condition.

  • Why it’s useful:

    • Express complex list constraints on related data declaratively, without writing joins or low-level query code.

    • Works in synergy with permission/membership/search filters and base whereClause.

Combined with PropertyRelation and SelectJoin, ListJointFilter completes a powerful trio:

  • PropertyRelation → models relationships.

  • SelectJoin → enriches responses with related data.

  • ListJointFilter → shapes which main objects are returned based on related data.

All of it driven by your patterns—no hand-written repository layer required.

11. Summary

Mindbricks’ relational and join-aware features form a clean, layered system:

1. PropertyRelation

Defines how DataObjects relate (one-to-one on the referencing side, one-to-many on the reverse). It is the foundation for all relation-driven logic.

2. SelectJoin

Enriches Get and List API responses by pulling in related records. Useful for adding nested data like:

  • project.owner

  • user.orders (children join)

  • project.lastUpdater

  • latest task, top N children, sorted lists

Supports relation disambiguation with a selector.

3. ListJointFilter

Filters List APIs by conditions on related objects. Equivalent to:

SELECT main FROM main WHERE EXISTS (related WHERE condition)

Also supports selector for choosing the correct relation path.

4. Both SelectJoin and ListJointFilter:

  • Use PropertyRelation metadata

  • Support multi-service and cross-object relationships

  • Require no custom SQL or ORM

  • Are fully declarative in patterns

  • Work for both main→joined and joined→main semantics

5. When to choose what:

GoalFeature
Include related data in responseSelectJoin
Filter main list by related dataListJointFilter
Build complex read modelsBFF DataViews
Implement advanced custom queriesFetchObjectAction / FetchFromElasticAction

Together, these features give Mindbricks:

  • ORM-like relational decoding

  • SQL-like join capabilities

  • ES-backed query power

  • A strongly declarative experience

—all while generating clean, scalable service code.


Was this page helpful?
Built with Documentation.AI

Last updated Dec 29, 2025