Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/KTS-o7/permission-mongo/llms.txt

Use this file to discover all available pages before exploring further.

Hooks allow you to execute custom logic at specific points in the document lifecycle, enabling validation, transformation, and integration with external systems.

Overview

Permission Mongo supports:
  • Pre-operation hooks - Execute before create, update, or delete
  • Post-operation hooks - Execute after operations complete
  • Multiple hook types - Validation, field setting, transformations, HTTP webhooks
  • Conditional execution - Run hooks based on conditions
  • Async webhooks - Non-blocking HTTP calls for integrations

Hook events

Hooks are triggered at these lifecycle points:
pre_create
event
Before document creation. Can modify the document, validate, or call webhooks.
post_create
event
After document is created. Useful for notifications and external system updates.
pre_update
event
Before document update. Can validate changes, transform fields, or prevent updates.
post_update
event
After document is updated. Includes change information.
pre_delete
event
Before document deletion. Can prevent deletion or log the operation.
post_delete
event
After document is deleted. Useful for cleanup and notifications.
const (
    HookPreCreate  HookEventType = "pre_create"
    HookPostCreate HookEventType = "post_create"
    HookPreUpdate  HookEventType = "pre_update"
    HookPostUpdate HookEventType = "post_update"
    HookPreDelete  HookEventType = "pre_delete"
    HookPostDelete HookEventType = "post_delete"
)

Hook actions

Each hook can perform one or more actions:

Set field

Automatically set field values:
hooks.yml
collections:
  documents:
    pre_create:
      - action: set_field
        field: created_at
        value: $now
      
      - action: set_field
        field: owner_id
        value: $user.id
      
      - action: set_field
        field: tenant_id
        value: $user.tenant_id
// executeSetField sets a field value on the document
func (e *Executor) executeSetField(doc map[string]interface{}, action *config.HookAction, hookCtx *HookContext) error {
    if action.Field == "" || action.Value == "" {
        return fmt.Errorf("set_field requires field and value")
    }

    // Substitute variables in the value
    value := SubstituteVariables(action.Value, hookCtx)

    // Set the field (supports nested fields with dot notation)
    setNestedField(doc, action.Field, value)

    return nil
}

Validate

Add custom validation rules:
hooks.yml
collections:
  documents:
    pre_create:
      - action: validate
        condition: "$doc.status == 'published'"
        when: "len($doc.content) == 0"
        error: "Published documents must have content"
      
      - action: validate
        condition: "$doc.priority > 0 && $doc.priority <= 5"
        error: "Priority must be between 1 and 5"
condition
string
Expression that must evaluate to true for validation to pass. If false, the hook fails with the error message.
when
string
Guard condition - only run this validation if the condition is true.
error
string
Error message returned if validation fails.

Validate reference

Ensure referenced documents exist:
hooks.yml
collections:
  documents:
    pre_create:
      - action: validate_ref
        field: customer_id
        collection: customers
        error: "Customer not found"
      
      - action: validate_ref
        field: project_id
        collection: projects
        optional: true  # Only validate if field is present
        error: "Project not found"
// executeValidateRef validates that a referenced document exists
func (e *Executor) executeValidateRef(ctx context.Context, doc map[string]interface{}, action *config.HookAction) error {
    if action.Field == "" || action.Collection == "" {
        return fmt.Errorf("validate_ref requires field and collection")
    }

    // Get the reference value from the document
    refValue := getNestedField(doc, action.Field)
    if refValue == nil {
        if action.Optional {
            return nil
        }
        return fmt.Errorf("%w: field %s is empty", ErrRefNotFound, action.Field)
    }

    refID, ok := refValue.(string)
    if !ok {
        return fmt.Errorf("reference field %s is not a string", action.Field)
    }

    // Check if the referenced document exists
    var filter bson.M
    if oid, err := primitive.ObjectIDFromHex(refID); err == nil {
        filter = bson.M{"_id": oid}
    } else {
        filter = bson.M{"_id": refID}
    }

    exists, err := e.store.Exists(ctx, action.Collection, filter)
    if err != nil {
        return fmt.Errorf("failed to check reference: %w", err)
    }

    if !exists {
        return fmt.Errorf("%w: %s", ErrRefNotFound, action.Error)
    }

    return nil
}

Transform

Apply transformations to field values:
hooks.yml
collections:
  users:
    pre_create:
      - action: transform
        field: email
        transform: lowercase
      
      - action: transform
        field: username
        transform: trim
    
    pre_update:
      - action: transform
        field: name
        transform: trim
Supported transformations:
  • lowercase - Convert to lowercase
  • uppercase - Convert to uppercase
  • trim - Remove leading/trailing whitespace
// executeTransform applies a transformation to a field
func (e *Executor) executeTransform(doc map[string]interface{}, action *config.HookAction) error {
    if action.Field == "" || action.Transform == "" {
        return fmt.Errorf("transform requires field and transform type")
    }

    value := getNestedField(doc, action.Field)
    if value == nil {
        return nil
    }

    strValue, ok := value.(string)
    if !ok {
        return nil
    }

    var transformed string
    switch config.TransformType(action.Transform) {
    case config.TransformLowercase:
        transformed = strings.ToLower(strValue)
    case config.TransformUppercase:
        transformed = strings.ToUpper(strValue)
    case config.TransformTrim:
        transformed = strings.TrimSpace(strValue)
    default:
        return fmt.Errorf("unknown transform type: %s", action.Transform)
    }

    setNestedField(doc, action.Field, transformed)
    return nil
}

HTTP webhook

Call external HTTP endpoints:
hooks.yml
collections:
  orders:
    post_create:
      - action: http
        url: https://api.example.com/webhooks/order-created
        method: POST
        headers:
          Authorization: "Bearer ${WEBHOOK_TOKEN}"
          X-Tenant-ID: $user.tenant_id
        body:
          order_id: $doc._id
          customer_id: $doc.customer_id
          total: $doc.total
          created_by: $user.id
        async: true
        timeout: 5000
        retry_count: 3
        on_error: log
url
string
required
Webhook endpoint URL
method
string
default:"POST"
HTTP method: GET, POST, PUT, PATCH, DELETE
headers
object
HTTP headers to send. Supports variable substitution.
body
object
JSON body to send. Supports variable substitution.
async
boolean
default:"false"
If true, execute webhook asynchronously without blocking the operation
timeout
integer
default:"30000"
Timeout in milliseconds
retry_count
integer
default:"0"
Number of retry attempts on failure
on_error
string
default:"fail"
Error handling: fail (abort operation), log (log and continue), ignore (silent failure)
// executeHTTP makes an HTTP webhook call
func (e *Executor) executeHTTP(ctx context.Context, action *config.HookAction, hookCtx *HookContext) error {
    if action.IsAsync() {
        // Execute asynchronously
        go func() {
            if err := e.doHTTPRequest(context.Background(), action, hookCtx); err != nil {
                switch action.GetOnError() {
                case config.OnErrorLog:
                    logging.Error("Async HTTP hook failed",
                        slog.String("url", action.URL),
                        slog.String("error", err.Error()),
                    )
                case config.OnErrorIgnore:
                    // Do nothing
                }
            }
        }()
        return nil
    }

    // Execute synchronously
    return e.doHTTPRequest(ctx, action, hookCtx)
}

Variable substitution

Hooks support variable substitution in value, url, headers, and body fields:

Document variables

$doc.field_name      # Current document field
$doc.nested.field    # Nested field with dot notation
$old.field_name      # Previous value (in pre_update/post_update)
$new.field_name      # New value (in post_update)

User variables

$user.id             # User ID
$user.tenant_id      # Tenant ID
$user.roles          # Array of user roles
$user.claims.custom  # Custom JWT claim

System variables

$now                 # Current timestamp
$changes             # Array of field changes (in post_update)
// resolveVariable resolves a variable expression
func resolveVariable(expr string, hookCtx *HookContext) interface{} {
    expr = strings.TrimSpace(expr)
    if !strings.HasPrefix(expr, "$") {
        return nil
    }

    expr = expr[1:]
    parts := strings.Split(expr, ".")

    var root interface{}
    rootName := parts[0]

    switch rootName {
    case "doc":
        root = hookCtx.Doc
    case "old":
        root = hookCtx.Old
    case "new":
        root = hookCtx.New
    case "user":
        root = userToMap(hookCtx.User)
    case "now":
        return hookCtx.Now
    case "changes":
        return changesToSlice(hookCtx.Changes)
    }

    // Navigate nested fields
    if len(parts) == 1 {
        return root
    }

    if rootMap, ok := root.(map[string]interface{}); ok {
        return getNestedField(rootMap, strings.Join(parts[1:], "."))
    }

    return nil
}

Conditional execution

Run hooks conditionally with when clauses:
hooks.yml
collections:
  documents:
    pre_update:
      # Only set published_at when status changes to published
      - action: set_field
        field: published_at
        value: $now
        when: "$new.status == 'published' && $old.status != 'published'"
      
      # Validate only for high-priority documents
      - action: validate
        condition: "len($doc.content) > 100"
        when: "$doc.priority >= 4"
        error: "High-priority documents need detailed content"
    
    post_update:
      # Send webhook only when status changes
      - action: http
        url: https://api.example.com/webhooks/status-changed
        when: "$old.status != $new.status"
        async: true
Condition expressions support:
  • Comparison: ==, !=, >, <, >=, <=
  • Logical: &&, ||, !
  • Membership: in, not in
  • Functions: len()

Hook execution order

Hooks execute in the order defined:
  1. Pre-operation hooks run first, in definition order
  2. Schema validation runs after pre-operation hooks
  3. RBAC authorization checks permissions
  4. Database operation executes (create/update/delete)
  5. Post-operation hooks run last, in definition order
If any synchronous hook fails, the operation is aborted.
// ExecuteHooks runs all hooks for an event
func (e *Executor) ExecuteHooks(ctx context.Context, opts *ExecuteOpts) (*HookResult, error) {
    collHooks := e.hooks.GetCollectionHooks(opts.Collection)
    if collHooks == nil {
        return &HookResult{Document: opts.Document}, nil
    }

    actions := collHooks.GetHooks(opts.Event)
    if len(actions) == 0 {
        return &HookResult{Document: opts.Document}, nil
    }

    hookCtx := &HookContext{
        Doc:     copyMap(opts.Document),
        Old:     opts.OldDoc,
        New:     opts.NewDoc,
        User:    opts.User,
        Now:     time.Now().UTC(),
        Changes: opts.Changes,
    }

    result := &HookResult{
        Document: hookCtx.Doc,
        Errors:   []HookError{},
    }

    // Execute each action in order
    for _, action := range actions {
        // Check guard condition
        if action.GetCondition() != "" {
            conditionMet, err := e.evaluateCondition(action.GetCondition(), hookCtx)
            if err != nil || !conditionMet {
                continue
            }
        }

        // Execute the action
        var actionErr error
        switch config.ActionType(action.Action) {
        case config.ActionSetField:
            actionErr = e.executeSetField(hookCtx.Doc, action, hookCtx)
        case config.ActionValidate:
            actionErr = e.executeValidate(hookCtx.Doc, action, hookCtx)
        case config.ActionValidateRef:
            actionErr = e.executeValidateRef(ctx, hookCtx.Doc, action)
        case config.ActionTransform:
            actionErr = e.executeTransform(hookCtx.Doc, action)
        case config.ActionHTTP:
            actionErr = e.executeHTTP(ctx, action, hookCtx)
        }

        if actionErr != nil {
            result.Errors = append(result.Errors, HookError{
                Action:  action.Action,
                Field:   action.Field,
                Message: actionErr.Error(),
            })

            // For validation errors, abort if not async
            if !action.IsAsync() {
                result.Aborted = true
                break
            }
        }
    }

    result.Document = hookCtx.Doc
    return result, nil
}

Use cases

Use set_field in pre_create to automatically set created_at, owner_id, tenant_id, and other metadata fields.
Add validate hooks for complex business rules that go beyond schema validation, like checking date ranges or cross-field dependencies.
Apply transform actions to normalize data (lowercase emails, trim whitespace) before storage.
Use http webhooks in post_create and post_update to notify external systems of changes.
Validate foreign keys with validate_ref to ensure referenced documents exist before creating relationships.
Add custom metadata in pre_create/pre_update hooks to enrich audit logs with business context.

Best practices

Set async: true for webhooks that shouldn’t block the main operation. Handle failures with on_error: log.
Hooks should perform focused, single-purpose actions. Complex logic belongs in external services called via HTTP hooks.
Set retry_count: 3 for webhooks to handle transient failures automatically.
Provide clear, actionable error messages in validation hooks to help users fix issues.
Hooks run on every operation - test them with various document states and edge cases.

Build docs developers (and LLMs) love