Skip to main content
Permission Mongo supports computed fields that derive their values from other fields using MongoDB aggregation expressions. Computed fields can be virtual (calculated on read) or stored (persisted to database).

Overview

Computed field features:
  • MongoDB aggregation syntax - Use familiar operators (sum,sum, multiply, $concat, etc.)
  • Virtual fields - Calculated on-the-fly when reading
  • Stored fields - Computed on write and persisted
  • Selective recomputation - Only recompute when dependencies change
  • Complex expressions - Support for conditionals, arrays, and nested objects
Source: /home/daytona/workspace/source/pkg/schema/computed.go:1-12

Configuration

Virtual Computed Fields

Calculated when documents are read, not stored in database:
collections:
  products:
    fields:
      price:
        type: number
      quantity:
        type: integer
      tax_rate:
        type: number
      
      # Virtual computed fields
      subtotal:
        type: number
        computed:
          expr:
            $multiply: ["$price", "$quantity"]
          store: false  # Virtual - not stored
      
      tax:
        type: number
        computed:
          expr:
            $multiply:
              - { $multiply: ["$price", "$quantity"] }
              - "$tax_rate"
          store: false
      
      total:
        type: number
        computed:
          expr:
            $add:
              - { $multiply: ["$price", "$quantity"] }
              - { $multiply: [{ $multiply: ["$price", "$quantity"] }, "$tax_rate"] }
          store: false
Read request:
GET /products/507f1f77bcf86cd799439011
Response (computed fields added):
{
  "_id": "507f1f77bcf86cd799439011",
  "price": 29.99,
  "quantity": 3,
  "tax_rate": 0.08,
  "subtotal": 89.97,      // Computed: 29.99 * 3
  "tax": 7.20,            // Computed: 89.97 * 0.08
  "total": 97.17          // Computed: 89.97 + 7.20
}
Source: /home/daytona/workspace/source/pkg/schema/computed.go:65-95

Stored Computed Fields

Calculated on write and persisted to database:
collections:
  orders:
    fields:
      items:
        type: array
        items:
          type: object
          properties:
            product:
              type: string
            price:
              type: number
            quantity:
              type: integer
      
      # Stored computed fields
      total_items:
        type: integer
        computed:
          expr:
            $size: "$items"
          store: true
          recompute_on: ["items"]
      
      order_total:
        type: number
        computed:
          expr:
            $sum:
              $map:
                input: "$items"
                as: "item"
                in:
                  $multiply: ["$$item.price", "$$item.quantity"]
          store: true
          recompute_on: ["items"]
Create request:
POST /orders
Content-Type: application/json

{
  "customer": "cust-123",
  "items": [
    { "product": "Widget", "price": 10.00, "quantity": 2 },
    { "product": "Gadget", "price": 15.00, "quantity": 1 }
  ]
}
Stored document:
{
  "_id": "507f1f77bcf86cd799439011",
  "customer": "cust-123",
  "items": [
    { "product": "Widget", "price": 10.00, "quantity": 2 },
    { "product": "Gadget", "price": 15.00, "quantity": 1 }
  ],
  "total_items": 2,      // Computed and stored
  "order_total": 35.00,  // Computed and stored
  "created_at": "2024-01-15T10:30:00Z"
}
Source: /home/daytona/workspace/source/pkg/schema/computed.go:26-63

Supported Operators

Permission Mongo supports MongoDB aggregation operators:

Arithmetic Operators

# Addition
total:
  computed:
    expr:
      $add: ["$price", "$tax", "$shipping"]

# Subtraction
profit:
  computed:
    expr:
      $subtract: ["$revenue", "$costs"]

# Multiplication
subtotal:
  computed:
    expr:
      $multiply: ["$price", "$quantity"]

# Division
average:
  computed:
    expr:
      $divide: ["$total", "$count"]

# Rounding
rounded_price:
  computed:
    expr:
      $round: ["$price", 2]  # Round to 2 decimal places
Source: /home/daytona/workspace/source/pkg/schema/computed.go:298-723

String Operators

# Concatenation
full_name:
  computed:
    expr:
      $concat: ["$first_name", " ", "$last_name"]

# Uppercase
product_code:
  computed:
    expr:
      $toUpper: "$sku"

# Lowercase
email_lower:
  computed:
    expr:
      $toLower: "$email"

# String length
name_length:
  computed:
    expr:
      $strLenCP: "$name"
Source: /home/daytona/workspace/source/pkg/schema/computed.go:729-890

Array Operators

# Array size
item_count:
  computed:
    expr:
      $size: "$items"

# Sum array values
total_quantity:
  computed:
    expr:
      $sum: "$quantities"

# Map array
product_names:
  computed:
    expr:
      $map:
        input: "$items"
        as: "item"
        in: "$$item.name"

# Filter array
active_items:
  computed:
    expr:
      $filter:
        input: "$items"
        as: "item"
        cond:
          $eq: ["$$item.status", "active"]

# Array element
first_item:
  computed:
    expr:
      $arrayElemAt: ["$items", 0]
Source: /home/daytona/workspace/source/pkg/schema/computed.go:895-1301

Conditional Operators

# If-then-else
status_label:
  computed:
    expr:
      $cond:
        if: { $gte: ["$stock", 10] }
        then: "In Stock"
        else: "Low Stock"

# Switch statement
priority:
  computed:
    expr:
      $switch:
        branches:
          - case: { $gte: ["$amount", 10000] }
            then: "high"
          - case: { $gte: ["$amount", 1000] }
            then: "medium"
        default: "low"

# Null coalescing
display_name:
  computed:
    expr:
      $ifNull: ["$nickname", "$full_name"]
Source: /home/daytona/workspace/source/pkg/schema/computed.go:1475-1558

Comparison Operators

# Equality
is_premium:
  computed:
    expr:
      $eq: ["$tier", "premium"]

# Greater than
is_expensive:
  computed:
    expr:
      $gt: ["$price", 100]

# In array
is_manager:
  computed:
    expr:
      $in: ["manager", "$roles"]
Source: /home/daytona/workspace/source/pkg/schema/computed.go:1306-1421

Selective Recomputation

Stored computed fields only recompute when dependencies change:
collections:
  orders:
    fields:
      subtotal:
        type: number
        computed:
          expr:
            $sum:
              $map:
                input: "$items"
                as: "item"
                in:
                  $multiply: ["$$item.price", "$$item.quantity"]
          store: true
          recompute_on: ["items"]  # Only recompute when items change
      
      discount:
        type: number
      
      total:
        type: number
        computed:
          expr:
            $subtract: ["$subtotal", "$discount"]
          store: true
          recompute_on: ["subtotal", "discount"]  # Recompute when either changes
Update that changes items:
PUT /orders/507f1f77bcf86cd799439011

{
  "items": [...],  // Changed
  "status": "processing"
}
Result: subtotal and total are recomputed. Update that doesn’t change items:
PUT /orders/507f1f77bcf86cd799439011

{
  "status": "shipped"  // items unchanged
}
Result: subtotal and total are NOT recomputed (performance optimization). Source: /home/daytona/workspace/source/pkg/schema/computed.go:98-133

Complex Examples

Order Totals with Tax

collections:
  orders:
    fields:
      items:
        type: array
      tax_rate:
        type: number
      shipping:
        type: number
      
      subtotal:
        type: number
        computed:
          expr:
            $reduce:
              input: "$items"
              initialValue: 0
              in:
                $add:
                  - "$$value"
                  - $multiply: ["$$this.price", "$$this.quantity"]
          store: true
          recompute_on: ["items"]
      
      tax:
        type: number
        computed:
          expr:
            $multiply: ["$subtotal", "$tax_rate"]
          store: true
          recompute_on: ["subtotal", "tax_rate"]
      
      total:
        type: number
        computed:
          expr:
            $add: ["$subtotal", "$tax", "$shipping"]
          store: true
          recompute_on: ["subtotal", "tax", "shipping"]
Source: /home/daytona/workspace/source/pkg/schema/computed.go:1108-1159

User Full Name

collections:
  users:
    fields:
      first_name:
        type: string
      middle_name:
        type: string
      last_name:
        type: string
      
      full_name:
        type: string
        computed:
          expr:
            $trim:
              input:
                $concat:
                  - "$first_name"
                  - " "
                  - $ifNull: ["$middle_name", ""]
                  - " "
                  - "$last_name"
          store: false  # Virtual - always fresh

Inventory Status

collections:
  products:
    fields:
      stock:
        type: integer
      reserved:
        type: integer
      reorder_point:
        type: integer
      
      available:
        type: integer
        computed:
          expr:
            $subtract: ["$stock", "$reserved"]
          store: true
          recompute_on: ["stock", "reserved"]
      
      status:
        type: string
        computed:
          expr:
            $switch:
              branches:
                - case: { $eq: ["$available", 0] }
                  then: "out_of_stock"
                - case: { $lte: ["$available", "$reorder_point"] }
                  then: "low_stock"
                - case: { $gte: ["$available", 100] }
                  then: "in_stock"
              default: "available"
          store: true
          recompute_on: ["available", "reorder_point"]

Performance Considerations

When to Use Virtual Fields

Use virtual fields when:
  • Values change frequently
  • Computation is inexpensive
  • Data must always be current
  • Storage space is a concern
Example: User age from birthdate
age:
  type: integer
  computed:
    expr:
      $dateDiff:
        startDate: "$birth_date"
        endDate: "$$NOW"
        unit: "year"
    store: false  # Virtual - always current

When to Use Stored Fields

Use stored fields when:
  • Values rarely change
  • Computation is expensive
  • Need to query/sort by computed value
  • Performance is critical
Example: Order total for sorting
total:
  type: number
  computed:
    expr: { $sum: "$items.price" }
    store: true  # Stored - queryable, sortable

Indexing Computed Fields

Stored computed fields can be indexed:
collections:
  orders:
    fields:
      total:
        type: number
        computed:
          expr: { $sum: "$items.price" }
          store: true
    indexes:
      - fields: ["total"]  # Index for fast queries
        order: [1]
Query by computed field:
GET /orders?total[gte]=100&sort=total:desc
Source: /home/daytona/workspace/source/pkg/schema/computed.go:135-224

Testing Computed Fields

func TestComputedFields(t *testing.T) {
	// Create product with price and quantity
	resp := createProduct(map[string]interface{}{
		"name":     "Widget",
		"price":    10.00,
		"quantity": 5,
		"tax_rate": 0.08,
	})

	assert.Equal(t, 201, resp.StatusCode)
	productID := resp.JSON["_id"].(string)

	// Read product - computed fields should be present
	resp = getProduct(productID)
	assert.Equal(t, 200, resp.StatusCode)

	data := resp.JSON
	assert.Equal(t, 50.00, data["subtotal"])  // 10 * 5
	assert.Equal(t, 4.00, data["tax"])        // 50 * 0.08
	assert.Equal(t, 54.00, data["total"])     // 50 + 4

	// Update quantity - stored fields should recompute
	resp = updateProduct(productID, map[string]interface{}{
		"quantity": 10,  // Changed
	})

	resp = getProduct(productID)
	assert.Equal(t, 100.00, resp.JSON["subtotal"])  // Recomputed: 10 * 10
}

Best Practices

Use virtual fields for frequently changing values
Use stored fields for expensive computations
Define recompute_on for stored fields
Index stored fields when querying by them
Test computed fields with edge cases (nulls, zeros)
Document computed field logic for maintainability
Use $ifNull for optional dependencies
Validate computed values in tests

Common Patterns

Pattern: Derived Status

status:
  computed:
    expr:
      $switch:
        branches:
          - case: { $and: [{ $ne: ["$paid_at", null] }, { $ne: ["$shipped_at", null] }] }
            then: "completed"
          - case: { $ne: ["$paid_at", null] }
            then: "paid"
          - case: { $ne: ["$created_at", null] }
            then: "pending"
        default: "unknown"
    store: true

Pattern: Percentage Calculation

completion_percentage:
  computed:
    expr:
      $multiply:
        - $divide: ["$completed_tasks", "$total_tasks"]
        - 100
    store: false

Pattern: Age Calculation

account_age_days:
  computed:
    expr:
      $dateDiff:
        startDate: "$created_at"
        endDate: "$$NOW"
        unit: "day"
    store: false

Next Steps

Batch Operations

Bulk create, update, and delete operations

Schema Definition

Define collection schemas

Build docs developers (and LLMs) love