Mastering MindbricksWrite Your Own Code
Mastering Mindbricks

Writing Your Own Code in Mindbricks

Most of your backend is defined through patterns (DataObject, BusinessApi, Service, ProjectAuthentication, etc.) and generated automatically. However, real-world systems always have edge cases—business rules, transformations, integrations, or performance hacks that can’t (and shouldn’t) be fully captured by declarative patterns alone.

Mindbricks is declarative-first, but it is not a closed box.

This document explains how to extend Mindbricks with custom code in three main ways:

  1. MScript – Inline JavaScript expressions embedded in pattern fields.

  2. Service Library – Reusable JS modules, templates, and assets inside ServiceLibrary.

  3. Edge Controllers – Fully custom HTTP endpoints backed by edge functions.

The goal is to help both human architects and AI agents confidently answer the question:

The answer is: Yes. Always. You just choose the right level of extension.


1. Three Layers of Custom Code

Before diving into details, it’s helpful to see the three main extension layers side by side.

1. MScript         – Lightweight, inline expressions & queries inside patterns.
2. Service Library – Reusable functions, templates, assets, public files per service.
3. Edge Controllers– Full-control routes mapped to custom JS handlers.

When to Use Which?

  • MScript Use when you can express the logic as an expression, a query, or a small function call:

    • where clauses

    • access checks

    • formulas & derived fields

    • building queries or payloads

    • mapping and filtering lists

  • Service Library Use when logic is a bit larger or shared:

    • utility functions for price, tax, scoring, etc.

    • building complex MScript Query objects

    • calling external APIs with shared logic

    • complex string building (logs, messages)

    • implementing a small rules engine

  • Edge Controllers Use when you need full control:

    • very custom HTTP endpoints

    • multi-step integrations that don’t map nicely to a BusinessApi

    • admin/maintenance tools

    • specialized export/import endpoints

    • very custom workflows where you want to write “plain” Node.js logic

All three are compatible with the declarative patterns. You don’t leave Mindbricks—you extend it from inside.


2. MScript – Inline, Context-Aware Expressions

2.1 What Is MScript?

In the Mindbricks ontology, MScript is a special string-based type that stores JavaScript expressions.

{
  "MScript": {
    "__isStringType": true
  }
}

MScript is used across many patterns:

  • DataMapItem.value

  • PropertyFormulaSettingsConfig.formula & updateFormula

  • WhereClauseSettings.fullWhereClause

  • ExtendedClause.doWhen, excludeWhen, whereClause

  • VerificationConfig.verificationTemplate (rendering context)

  • StripeOrderConfig.amount, description

  • BusinessApiAction.condition

  • Actions like FunctionCallAction.callScript, FetchObjectAction.matchValue, ListMapAction.mapScript, IntegrationParameter.parameterValue, ValidationAction.validationScript, and many more.

MScript lets you “drop” logic into the generated code, while Mindbricks manages structure, lifecycle, and context.

2.2 First Rule

MScript values are injected directly into generated code after syntax validation. A good rule of thumb:

Example in design:

{
  "condition": "this.user.age > 18"
}

Mindbricks might generate code like:

const condition = this.user.age > 18;
if (!condition) {
  throw new Error("User should be older than 18");
}

If you’re unsure, you can always wrap your expression with module.exports:

module.exports = (this.user.age > 18)

Mindbricks will extract the exported expression.

2.3 The Context: this

Most MScript runs inside a Business API manager class. In that scope, this is the runtime context.

Common things you can access:

  • Business API parameters

    this.productId
    this.searchKeyword
    
  • Session data

    this.session.userId
    this.session.roleId
    this.session.tenantId
    
  • Main data object instance(s) Depending on crudType:

    • create / get / update / delete: this.customer, this.order, etc.

    • list: this.customers, this.orders (plural list).

  • Action outputs (from BusinessApiAction with contextPropertyName)

    // Example: FetchObjectAction with contextPropertyName = "userCompany"
    this.userCompany.name
    
  • Library functions via LIB

    LIB.common.md5(this.email ?? "nullValue")
    LIB.getSearchQueryForProduct(this.keyword)
    
  • Other context fields added by AddToContextAction or CreateObjectAction.

Keeping this mental model makes MScript much more intuitive.

2.4 MScript Examples by Use Case

2.4.1 Conditional Checks

You want to ensure the current user owns the resource:

{
  "condition": "this.customer.userId === this.session.userId"
}

Used in:

  • ValidationAction.validationScript

  • ExtendedClause.doWhen

  • MembershipCheckAction.checkFor

2.4.2 Dynamic Formula

A totalPrice property as unitPrice * quantity:

{
  "formulaSettings": {
    "isCalculated": true,
    "configuration": {
      "formula": "this.unitPrice * this.quantity"
    }
  }
}

This is placed under a DataProperty’s formulaSettings for that field.

2.4.3 Inline Arrow Function for Complex Logic

Sometimes you need a few steps:

{
  "formula": "(() => { const net = this.grossAmount - this.discountAmount; return Math.max(net, 0); })()"
}

2.4.4 MScript Query (Where Clause)

Filtering orders for the current user:

{
  "fullWhereClause": "({ userId: { "$eq": this.session.userId } })"
}

Using the MScript Query syntax, you can also do more complex filters:

{
  "fullWhereClause": "({ "$and": [ { status: { "$eq": 'published' } }, { tenantId: { "$eq": this.session.tenantId } } ] })"
}

Mindbricks converts this unified syntax into SQL/Sequelize, MongoDB, or Elasticsearch queries depending on deployment.

2.4.5 Building Queries via Library Functions

// In service library functions
const getSearchQueryForProduct = (pName) => {
  return { name: { "$ilike": `%${pName}%` } };
};

module.exports = getSearchQueryForProduct;

Used in a BusinessApi where clause:

{
  "fullWhereClause": "LIB.getSearchQueryForProduct(this.productName)"
}

This is a perfect example of MScript + Library working together.


3. Service Library – Your Private Utility Toolbox

Patterns: ServiceLibrary, LibModule

Every service has a library field:

{
  "library": {
    "functions": [ /* LibModule */ ],
    "edgeFunctions": [ /* LibModule */ ],
    "templates": [ /* LibModule */ ],
    "assets": [ /* LibModule */ ],
    "public": [ /* LibModule */ ]
  }
}

Each LibModule has:

  • moduleName – unique name within the library

  • moduleExtension – e.g. js, ejs, txt, svg, pdf

  • moduleBody – the source code/content (string)

This is where you write real code:

  • shared validation functions

  • calculators (price, tax, commissions)

  • query builders

  • AI prompt builders

  • integration wrappers

  • document templates and static assets

3.1 Functions – Reusable Business Logic

Use case examples:

  • Normalize product names.

  • Calculate totals and discounts.

  • Generate a standard search query.

  • Validate country-specific ID numbers.

  • Map external statuses to internal ones.

Example: version sorter

{
  "library": {
    "functions": [
      {
        "moduleName": "sortVersions",
        "moduleExtension": "js",
        "moduleBody": "module.exports = function sortVersions(versions) {
  return versions.sort((a, b) => {
    const partsA = a.split('.').map(Number);
    const partsB = b.split('.').map(Number);
    const maxLength = Math.max(partsA.length, partsB.length);
    for (let i = 0; i < maxLength; i++) {
      const numA = partsA[i] || 0;
      const numB = partsB[i] || 0;
      if (numA > numB) return 1;
      if (numA < numB) return -1;
    }
    return 0;
  });
}"
      }
    ]
  }
}

Use it in MScript:

{
  "callScript": "LIB.sortVersions(this.versionList)"
}

Or use it to compute derived data in a FunctionCallAction or AddToContextAction.

Real-Life Case: Shipping Fee Calculation

{
  "library": {
    "functions": [
      {
        "moduleName": "calculateShippingFee",
        "moduleExtension": "js",
        "moduleBody": "module.exports = function calculateShippingFee(order) {
  const base = 5;
  const weight = order.totalWeight || 0;
  const distanceFactor = order.distanceKm || 0;
  return base + weight * 0.5 + distanceFactor * 0.1;
}"
      }
    ]
  }
}

Then in a DataProperty formula for shippingFee:

{
  "formulaSettings": {
    "isCalculated": true,
    "configuration": {
      "formula": "LIB.calculateShippingFee(this.order)"
    }
  }
}

Best of both worlds: pattern-driven property + custom calculation logic.

Passing Business API Context into Library Functions

Library functions can use any signature you design. If you need access to the Business API runtime context (this), explicitly receive it as an argument (recommended name: context) and pass this when you call the function.

Library function that expects context:

{
  "moduleName": "calculateOffer",
  "moduleExtension": "js",
  "moduleBody": "module.exports = function calculateOffer(context, order) {
  const locale = context.session?.locale ?? 'en';
  const tenant = context.session?.tenantId;
  const base = order.subtotal ?? 0;
  const discount = context.user?.loyaltyLevel === 'gold' ? 0.1 : 0;
  return { total: Math.max(base * (1 - discount), 0), locale, tenant };
}"
}

Call it inside an action (MScript area of a FunctionCallAction):

{
  "actions": {
    "functionCallActions": [
      {
        "extendClassName": "FunctionCallAction",
        "name": "computeOffer",
        "callScript": "LIB.calculateOffer(this, this.order)",
        "contextPropertyName": "offer"
      }
    ]
  }
}

Use it inline in other MScript fields (e.g., a condition):

{
  "condition": "LIB.calculateOffer(this, this.order).total > 0"
}

The key pattern: design the signature, accept context, and pass this wherever you need runtime data from the API in your library code.

3.2 Edge Functions – Custom Route Handlers

edgeFunctions are used by EdgeController and must export an async function:

{
  "library": {
    "edgeFunctions": [
      {
        "moduleName": "helloWorld",
        "moduleExtension": "js",
        "moduleBody": "module.exports = async (request) => {
  return { status: 200, message: 'Hello from the edge function', now: new Date().toISOString() };
};"
      }
    ]
  }
}

We’ll dive into edge controllers in the next section, but keep in mind: this is where their logic lives.

3.3 Templates – Dynamic Content

Use templates with RenderDataAction or other rendering logic.

Use case examples:

  • Welcome email

  • Payment receipt

  • Multi-language document

  • HTML snippet for PDF export

{
  "library": {
    "templates": [
      {
        "moduleName": "orderInvoiceHtml",
        "moduleExtension": "ejs",
        "moduleBody": "<html><body><h1>Invoice #<%= order.id %></h1><p>Total: <%= order.totalAmount %> <%= order.currency %></p></body></html>"
      }
    ]
  }
}

Then:

{
  "actions": {
    "renderDataActions": [
      {
        "extendClassName": "RenderDataAction",
        "name": "renderInvoice",
        "template": "orderInvoiceHtml",
        "inputData": "{ order: this.order }",
        "contextPropertyName": "invoiceHtml",
        "writeToResponse": false
      }
    ],
    "dataToFileActions": [
      {
        "extendClassName": "DataToFileAction",
        "name": "exportInvoicePdf",
        "inputData": "this.invoiceHtml",
        "outputFormat": "pdf",
        "sendToClient": true
      }
    ]
  }
}

3.4 Assets and Public Files

  • assets: internal text/binary data (stored as text in moduleBody)

  • public: static assets exposed via HTTP (logos, favicons, PDFs, static docs)

Examples:

{
  "library": {
    "assets": [
      {
        "moduleName": "awsCredentials",
        "moduleExtension": "txt",
        "moduleBody": "AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=..."
      }
    ],
    "public": [
      {
        "moduleName": "favicon",
        "moduleExtension": "png",
        "moduleBody": "<base64-image-data>"
      }
    ]
  }
}

Then, internal code can read awsCredentials via special asset loader, while favicon is served via a static route.


4. Edge Controllers – “There Is Always a Route”

Patterns: EdgeController, EdgeControllerOptions, EdgeRestSettings, ServiceLibrary.edgeFunctions

Sometimes you need to go beyond BusinessApi workflows and auto-generated routes:

  • A maintenance endpoint to trigger a re-indexing job.

  • A special integration that aggregates multiple external APIs and returns a custom response.

  • A migration tool exposed temporarily for controlled admin use.

  • A custom webhook handler not yet covered by native integrations.

  • A debug endpoint for introspecting specific state in controlled environments.

This is where Edge Controllers shine.

4.1 How Edge Controllers Work

An EdgeController ties a URL + HTTP method to an edge function:

{
  "edgeControllers": [
    {
      "edgeControllerOptions": {
        "functionName": "sendMail",
        "loginRequired": true
      },
      "edgeRestSettings": {
        "path": "/sendmail",
        "method": "POST"
      }
    }
  ]
}
  • edgeControllerOptions.functionNamelibrary.edgeFunctions[].moduleName

  • edgeControllerOptions.loginRequired → whether to enforce session/auth

  • edgeRestSettings.path → exposed REST path

  • edgeRestSettings.method → HTTP method (from HTTPRequestMethods)

Edge function example:

{
  "library": {
    "edgeFunctions": [
      {
        "moduleName": "sendMail",
        "moduleExtension": "js",
        "moduleBody": "const { sendSmtpEmail } = require('common');

module.exports = async (request) => {
  const { to, subject, text } = request.body;
  const emailFrom = request.session?.email ?? 'noreply@myapp.com';

  await sendSmtpEmail({ emailFrom, to, subject, text });

  return {
    status: 201,
    message: 'Email sent',
    date: new Date().toISOString()
  };
};"
      }
    ]
  }
}

From the client’s perspective, this looks like any other endpoint:

POST /sendmail
Content-Type: application/json

{
  "to": "user@example.com",
  "subject": "Hello",
  "text": "Welcome to our service!"
}

4.2 Real-Life Edge Controller Scenarios

4.2.1 Custom Reporting Endpoint

You want a /admin/export-users endpoint that:

  1. Fetches users with specific filters

  2. Generates a CSV

  3. Returns it as a downloadable file

You might choose an Edge Controller because:

  • You want full flexibility over the CSV format.

  • You may need to call multiple services.

  • You might use Node streams or large-memory operations.

Edge function:

// moduleName: exportUsersCsv
module.exports = async (request) => {
  const { role, since } = request.query;
  const users = await request.services.auth.getUsers({ role, since });

  const header = "id,email,role
";
  const rows = users.map(u => `${u.id},${u.email},${u.roleId}`).join("
");
  const csv = header + rows + "
";

  return {
    status: 200,
    headers: {
      "Content-Type": "text/csv",
      "Content-Disposition": "attachment; filename="users.csv""
    },
    body: csv
  };
};

Edge controller:

{
  "edgeControllers": [
    {
      "edgeControllerOptions": {
        "functionName": "exportUsersCsv",
        "loginRequired": true
      },
      "edgeRestSettings": {
        "path": "/admin/export-users",
        "method": "GET"
      }
    }
  ]
}

4.2.2 External System Sync

You need to call a legacy SOAP API, combine it with local DB data, and return a derived result:

  • Patterns handle 95% of your CRUD.

  • For this one special case, you create an Edge Controller.

The edge function can:

  • Read query/body

  • Use request.services to call internal Business APIs

  • Use any Node.js library you added via nodejsPackages

  • Perform a custom algorithm

  • Return the response you want

This gives you escape hatches without breaking the Mindbricks structure.


5. Importing Internal Mindbricks Libraries (common, serviceCommon, dbLayer, models)

When writing custom logic inside your service library, edge controllers, or MScript-powered functions, you are not limited to raw JavaScript. Every Mindbricks service includes a set of internal libraries—automatically generated or shared—that encapsulate common infrastructure, data access, and service behaviors.

These libraries are globally available in every service at runtime and can be imported directly:

const COMMON = require("common");
const SERVICE_COMMON = require("serviceCommon");
const DB = require("dbLayer");
const MODELS = require("models");

No relative paths are required—Mindbricks injects these libraries into the service environment in a standardized manner.


1.common — Shared Infrastructure Utilities (Project-Wide)

The common library is identical across all services in the project. It contains production-ready, low-level utilities that would otherwise require thousands of lines of handwritten infrastructure code.

Common categories include:

🔐 Crypto & Token Utilities

  • createJWT, validateJWT

  • RSA variants: createJWT_RSA, validateJWT_RSA

  • Token encoding/decoding: encodeToken, decodeToken

  • Hashing utilities: hashString, hashCompare, md5, intHash

  • OTP generator: randomCode

🗄 Redis Cache

  • redisClient

  • getRedisData, setRedisData

🦄 Elasticsearch / CQRS

  • ElasticIndexer

  • elasticClient

📨 Messaging & Events (Kafka)

  • sendMessageToKafka

🌐 External HTTP / File Retrieval

  • getRestData, sendRestRequest, downloadFile

🛠 Utility Helpers

  • UUID/ObjectID helpers: newUUID, shortUUID, newObjectId, isValidUUID

  • Array utilities: concatListResults, mapArrayItems

  • Query builders: buildSequelizeClause, buildMongooseClause, buildElasticClause

  • Logging: hexaLogger, HexaLogTypes


2.serviceCommon — Helpers Specific to the Current Service

Unlike common, which is universal, serviceCommon is generated specifically for the current service. It contains service-level helpers that adapt to the service’s domain.

📡 ServicePublisher

Used to publish events from this service to Kafka. The difference of this service publisher from the common sendMessageToKafka is that, this one can attach current users session to the message, so any login required route that has a kafka controller listening to the topic will be able to use the publisher user's session.

const { ServicePublisher } = require("serviceCommon");

await ServicePublisher.publish("order.created", {
  orderId: order.id,
  userId: order.userId
});

📊 ElasticIndexer

Manages Elasticsearch indexes to index data, search data, update data etc.

const { ElasticIndexer } = require("serviceCommon");
const indexer = new ElasticIndexer("order");
await indexer.indexData({...});

3.dbLayer — Generated DB Layer Functions Based on Your DataObjects

Every service has a generated data-access layer (dbLayer), which provides formally defined CRUD, query, and aggregation helpers derived from your DataObject patterns.

Important: Always use dbLayer functions instead of directly accessing models. These utility functions ensure data consistency between the database, Elasticsearch, and Kafka events triggered on database changes.

Example imports:

// Using destructuring (recommended)
const { getUserById, createUser } = require("dbLayer");

// Or using the full module
const DB = require("dbLayer");

For each data object in your service, Mindbricks automatically generates the following utility functions (where ModelName is the PascalCase name of your data object):

Create Functions

  • create${ModelName}(data, context) — Creates a single record

    • Parameters:
      • data (Object) — The data object to create. If id is provided and exists, it will update the existing record instead.
      • context (Object, optional) — Context object with session and requestId. When called from Business API actions, pass this.
    • Returns: Object — The created/updated record data
    • Auto-handles: ID generation (if not provided), codename generation (if applicable), Elasticsearch indexing, Kafka events
  • createBulk${ModelName}(dataList, context) — Creates multiple records in bulk

    • Parameters:
      • dataList (Array) — Array of data objects to create
      • context (Object, optional) — Context object with session and requestId. When called from Business API actions, pass this.
    • Returns: Array — Array of created/updated record data
    • Auto-handles: Same as single create, uses optimized bulk operations

Read Functions

  • get${ModelName}ById(id) — Retrieves a record by ID

    • Parameters: id (String|Array) — Single ID or array of IDs
    • Returns: Object|Array|null — Single record, array of records, or null if not found
    • Respects: Soft delete settings (only returns active records if soft delete is enabled)
  • get${ModelName}AggById(id) — Retrieves a record with aggregated/joined data

    • Parameters: id (String|Array) — Single ID or array of IDs
    • Returns: Object|Array|null — Record(s) with joined relations populated
    • Use case: When you need related data from both local database relations and remote services
    • How it works:
      • For local relations (same service): Fetches related data directly from the database
      • For remote relations (other services): Fetches related data from Elasticsearch indices
      • Automatically populates all defined relations on the data object, whether local or remote
  • get${ModelName}ListByQuery(query) — Retrieves multiple records matching a query

    • Parameters: query (Object) — Sequelize or MongoDB query object (depending on your DB type)
    • Returns: Array — Array of matching records (empty array if none found)
    • Respects: Soft delete settings
  • get${ModelName}ByQuery(query) — Retrieves a single record matching a query

    • Parameters: query (Object) — Sequelize or MongoDB query object
    • Returns: Object|null — First matching record (ordered by createdAt DESC) or null
    • Respects: Soft delete settings
  • get${ModelName}StatsByQuery(query, stats) — Calculates statistics on matching records

    • Parameters:
      • query (Object) — Sequelize or MongoDB query object
      • stats (Array|String) — Array of stat operations (e.g., ["count", "sum(amount)", "avg(price)", "min(price)", "max(price)"]) or single stat string
    • Returns: Number|Object — Single stat value or object with stat labels as keys
    • Example: await DB.getOrderStatsByQuery({ status: "paid" }, ["count", "sum(totalAmount)"]) returns { count: 10, "sum-totalAmount": 5000 }
  • getIdListOf${ModelName}ByField(fieldName, fieldValue, isArray) — Gets list of IDs matching a field value

    • Parameters:
      • fieldName (String) — Field name to match
      • fieldValue (Any) — Value to match
      • isArray (Boolean) — If true, uses array containment check (for array fields)
    • Returns: Array — Array of IDs matching the criteria
    • Use case: Quick ID lookups for building relationships or filters

Update Functions

  • update${ModelName}ById(id, dataClause, context) — Updates a record by ID

    • Parameters:
      • id (String|Object) — Record ID or object with id property
      • dataClause (Object) — Fields to update (if id is object, can be omitted)
      • context (Object, optional) — Context object with session and requestId. When called from Business API actions, pass this.
    • Returns: Object — Updated record data
    • Auto-handles: Elasticsearch indexing, Kafka events, soft delete checks
  • update${ModelName}ByIdList(idList, dataClause, context) — Updates multiple records by ID list

    • Parameters:
      • idList (Array) — Array of IDs to update
      • dataClause (Object) — Fields to update for all records
      • context (Object, optional) — Context object with session and requestId. When called from Business API actions, pass this.
    • Returns: Array — Array of updated record IDs
    • Auto-handles: Elasticsearch indexing for all updated records
  • update${ModelName}ByQuery(query, dataClause, context) — Updates all records matching a query

    • Parameters:
      • query (Object) — Sequelize or MongoDB query object
      • dataClause (Object) — Fields to update
      • context (Object, optional) — Context object with session and requestId. When called from Business API actions, pass this.
    • Returns: Array — Array of updated record data
    • Auto-handles: Elasticsearch indexing for all updated records

Delete Functions

  • delete${ModelName}ById(id, context) — Deletes a record by ID

    • Parameters:
      • id (String|Object) — Record ID or object with id property
      • context (Object, optional) — Context object with session and requestId. When called from Business API actions, pass this.
    • Returns: Object — Deleted record data
    • Auto-handles: Soft delete (if enabled), Elasticsearch deletion, Kafka events
  • delete${ModelName}ByQuery(query, context) — Deletes all records matching a query

    • Parameters:
      • query (Object) — Sequelize or MongoDB query object
      • context (Object, optional) — Context object with session and requestId. When called from Business API actions, pass this.
    • Returns: Array — Array of deleted record data
    • Auto-handles: Soft delete (if enabled), Elasticsearch deletion for all records

Query Format

All functions that accept a query parameter use the same format as your selected database type:

  • Sequelize (PostgreSQL, MySQL, etc.): Standard Sequelize query syntax

    { status: "active", age: { [Op.gte]: 18 } }
    
  • MongoDB: Standard MongoDB query syntax

    { status: "active", age: { $gte: 18 } }
    

Why Use dbLayer Instead of Direct Model Access?

These functions provide a safe, typed, generated repository layer that:

  • Ensures data consistency — Automatically syncs with Elasticsearch
  • Triggers events — Publishes Kafka events on data changes
  • Respects constraints — Applies soft deletes, multitenancy, and access control
  • Normalizes results — Returns consistent data structures via getData()
  • Handles edge cases — Manages codename generation, ID creation, etc.
  • Minimizes errors — Reduces the need for raw ORM access and potential inconsistencies

Example usage in a library function:

const { getOrderAggById, updateOrderById, getItemListByQuery, getOrderStatsByQuery } = require("dbLayer");

// If called from Business API action, receive context and pass it through
module.exports = async function processOrder(orderId, context = null) {
  // Get order with aggregated data
  const order = await getOrderAggById(orderId);
  if (!order) throw new Error("Order not found");
  
  // Update order status (pass context for event tracking)
  await updateOrderById(orderId, { status: "processed" }, context);
  
  // Get related items
  const items = await getItemListByQuery({ orderId: orderId });
  
  // Calculate total
  const stats = await getOrderStatsByQuery(
    { orderId: orderId },
    ["sum(amount)"]
  );
  
  return { order, items, total: stats["sum-amount"] };
};

// When called from a Business API action:
// LIB.processOrder(this.orderId, this)

4.models — Raw ORM Models (Advanced Only)

Finally, the models library gives you direct access to Sequelize or Mongoose models for the service:

const MODELS = require("models");

// Example:
const { User, UserGroup } = MODELS;

This gives you full control, but also full responsibility:

const admins = await MODELS.User.findAll({
  where: { roleId: "admin" },
  limit: 50
});

This is powerful for:

  • Custom aggregation queries

  • Service-level patches or migrations

  • Temporary or emergency tooling

But note:

  • It bypasses BusinessApi validations

  • It bypasses access control layers

  • It may break invariants if misused

Please check common libraries reference document to get complete information for all importable function and class.

5. Fetching Data from Other Services

In a microservices architecture, services often need to access data from other services. Mindbricks provides utility functions in serviceCommon to fetch remote objects from Elasticsearch indices, which are automatically indexed when data is created or updated in other services.

Why Fetch from Elasticsearch Instead of Direct Database Access?

  • Service Independence: Services don't need direct database access to other services
  • Performance: Elasticsearch provides fast, indexed queries across services
  • Consistency: Data is automatically indexed when changes occur in the source service
  • Multi-tenant Support: Elasticsearch indices are properly scoped per tenant

Available Functions

All remote fetch functions are available from serviceCommon:

const { 
  fetchRemoteObjectByMQuery,
  fetchRemoteObjectByElasticQuery,
  fetchRemoteListByMQuery,
  fetchRemoteListByElasticQuery
} = require("serviceCommon");

1. Fetching a Single Remote Object

MQuery is Mindbricks' query format that works across database types. Use this when you want to write queries in a database-agnostic way:

const { fetchRemoteObjectByMQuery } = require("serviceCommon");

// Fetch a user by email from the auth service
const user = await fetchRemoteObjectByMQuery("User", {
  email: "user@example.com"
});

if (user) {
  console.log(`Found user: ${user.name}`);
}
Using Elasticsearch Query

For advanced queries or when you need Elasticsearch-specific features:

const { fetchRemoteObjectByElasticQuery } = require("serviceCommon");

// Fetch a user using Elasticsearch query syntax
const user = await fetchRemoteObjectByElasticQuery("User", {
  match: {
    email: "user@example.com"
  }
});

2. Fetching a List of Remote Objects

Using MQuery with Pagination
const { fetchRemoteListByMQuery } = require("serviceCommon");

// Fetch first 20 active orders from the order service
const orders = await fetchRemoteListByMQuery(
  "Order",
  { status: "active" },
  0,    // from (starting index)
  20,   // size (number of results)
  [{ createdAt: { order: "desc" } }]  // sort (optional)
);

orders.forEach(order => {
  console.log(`Order ${order.id}: ${order.totalAmount}`);
});
Using Elasticsearch Query with Pagination
const { fetchRemoteListByElasticQuery } = require("serviceCommon");

// Fetch orders using Elasticsearch query with pagination
const orders = await fetchRemoteListByElasticQuery(
  "Order",
  {
    bool: {
      must: [
        { match: { status: "active" } },
        { range: { totalAmount: { gte: 100 } } }
      ]
    }
  },
  0,    // from
  50,   // size
  [{ createdAt: { order: "desc" } }]  // sort
);

3. Using in MScript

You can call these functions directly in MScript expressions within patterns:

{
  "action": {
    "patternName": "FunctionCallAction",
    "callScript": "await LIB.fetchUserFromAuthService(this.userEmail)",
    "contextPropertyName": "remoteUser"
  }
}

Then in your service library function:

// In service library function
const { fetchRemoteObjectByMQuery } = require("serviceCommon");

module.exports = async function fetchUserFromAuthService(email) {
  return await fetchRemoteObjectByMQuery("User", { email });
};

4. Using in Service Library Functions

Service library functions are ideal for encapsulating remote data fetching logic:

const { fetchRemoteListByMQuery } = require("serviceCommon");

module.exports = async function getActiveOrdersForUser(userId, context = null) {
  // Fetch orders from order service
  const orders = await fetchRemoteListByMQuery(
    "Order",
    { 
      userId: userId,
      status: "active"
    },
    0,
    100,
    [{ createdAt: { order: "desc" } }]
  );
  
  return orders;
};

5. Error Handling

All fetch functions include built-in error handling:

  • Returns null for single object queries if not found or on error
  • Returns [] (empty array) for list queries if none found or on error
  • Logs errors to console with console.error
  • Never throws - always returns a safe default value
const { fetchRemoteObjectByMQuery } = require("serviceCommon");

const user = await fetchRemoteObjectByMQuery("User", { email: "test@example.com" });

// Safe to use - will be null if not found or on error
if (user) {
  // Process user data
} else {
  // Handle not found case
}

6. Real-World Example: Order Service Fetching User Data

// In order service library function
const { fetchRemoteObjectByMQuery } = require("serviceCommon");

module.exports = async function enrichOrderWithUserData(order, context = null) {
  if (!order.userId) return order;
  
  // Fetch user from auth service
  const user = await fetchRemoteObjectByMQuery("User", { id: order.userId });
  
  if (user) {
    order.userName = user.name;
    order.userEmail = user.email;
    order.userCompany = user.companyName;
  }
  
  return order;
};

7. Performance Considerations

  • Caching: Elasticsearch queries are fast, but consider caching frequently accessed remote data
  • Pagination: Always use pagination for list queries to avoid fetching too much data
  • Indexing: Ensure the remote service has properly indexed the fields you're querying
  • Batch Operations: For multiple fetches, consider batching or using aggregation queries

8. Multi-Tenant Support

Remote fetch functions automatically respect multi-tenant boundaries. The Elasticsearch indices are scoped per tenant, so queries will only return data for the current tenant context.

// In a multi-tenant environment, this automatically filters by tenant
const orders = await fetchRemoteListByMQuery("Order", { status: "active" });
// Only returns orders for the current tenant

6. Pattern + Code: Always a Solution

It’s easy to fall into two extremes:

  • “Everything must be declarative; no code.”

  • “Everything must be code; patterns get in the way.”

Mindbricks is designed to sit between these: patterns for structure, code where it matters.

6.1 A Typical Design Journey

  1. Start with patterns Model your domain via DataObject, BusinessApi, AccessControl, etc.

  2. Use MScript liberally Add MScript conditions, formulas, where clauses, map functions, and function calls.

  3. Factor out complexity into the Service Library When an MScript expression becomes too complex or is reused in multiple places:

    • Move the heavy logic into a functions module.

    • Call it via LIB.someFunction(...) in MScript.

  4. Use Edge Controllers for truly custom flows When a whole route’s behavior does not fit a BusinessApi pattern:

    • Create a small edgeFunctions module.

    • Expose it via an EdgeController path and method.

    • Keep using patterns elsewhere.

6.2 Decision Hints

  • Can it be expressed as a pure expression or query? → MScript.

  • Is the logic reused in several places or growing in size? → Service Library function.

  • Does it require full control over request/response or multi-service orchestration? → Edge Controller.

6.3 Philosophical Note

The combination of:

  • strong ontology (patterns),

  • flexible expressions (MScript),

  • and precise code anchors (Service Library & Edge Controllers),

makes Mindbricks less like a rigid code generator and more like a language for backend architecture—where you can always “drop down” one level to express what you need, without losing the benefits of structure, documentation, and automation.

There is always a route:

  • If it’s small → MScript.

  • If it’s shared → Library.

  • If it’s a one-off but important endpoint → Edge Controller.

You stay inside Mindbricks, but you are never trapped by it.

Was this page helpful?
Built with Documentation.AI

Last updated 4 weeks ago