Skip to main content
PUT
/
{collection}
/
{id}
Update Document
curl --request PUT \
  --url https://api.example.com/{collection}/{id} \
  --header 'Authorization: <authorization>' \
  --header 'Content-Type: application/json' \
  --data '
{
  "field_name": "<any>"
}
'
{
  "_id": "<string>",
  "updated_at": "<string>",
  "updated_by": "<string>",
  "...": "<any>"
}

Overview

Update a document in a collection by its ID. The endpoint provides:
  • Schema validation - Ensures updates comply with schema rules
  • Immutable field protection - Prevents modification of immutable fields
  • Field-level write permissions - Enforces deny_write policies
  • Automatic field updates - Sets updated_at and updated_by
  • Version snapshots - Saves previous version if versioning is enabled
  • Pre/post-update hooks - Custom validation and side effects
  • Audit logging - Tracks field-level changes

Request

Path Parameters

collection
string
required
The name of the collection containing the document
id
string
required
The unique identifier (_id) of the document to update

Headers

Authorization
string
required
Bearer token for authentication

Body

The request body should contain a JSON object with the fields to update. Only the fields you want to change need to be included.
field_name
any
Fields to update. The system automatically:
  • Validates against schema constraints
  • Checks immutable field restrictions
  • Verifies deny_write permissions
  • Updates updated_at timestamp
  • Sets updated_by to current user ID

Response

Returns the complete updated document with all fields.
_id
string
The document’s unique identifier
updated_at
string
ISO 8601 timestamp of the update (automatically set)
updated_by
string
User ID who performed the update (automatically set)
...
any
All other fields from the document, including both updated and unchanged fields

Examples

Basic Update

curl -X PUT https://api.example.com/users/507f1f77bcf86cd799439011 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "John Updated",
    "status": "inactive"
  }'

Partial Update

curl -X PUT https://api.example.com/orders/ORD-12345 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "shipped",
    "tracking_number": "1Z999AA10123456784"
  }'

Response

{
  "_id": "507f1f77bcf86cd799439011",
  "name": "John Updated",
  "email": "[email protected]",
  "role": "member",
  "status": "inactive",
  "created_at": "2024-03-15T10:30:00Z",
  "updated_at": "2024-03-16T15:45:00Z",
  "created_by": "user_123",
  "updated_by": "user_456",
  "company_id": "company_456"
}

Implementation Details

MongoDB Query Pattern

The handler uses MongoDB’s replaceOne operation with merged document:
// Fetch existing document
oldDoc, err := collection.FindOne(ctx, bson.M{"_id": objectID}).Decode(&doc)

// Merge update into existing document
for k, v := range updateDoc {
    mergedDoc[k] = v
}
mergedDoc["updated_at"] = time.Now().UTC()
mergedDoc["updated_by"] = authCtx.UserID

// Replace document
result, err := collection.ReplaceOne(ctx, bson.M{"_id": objectID}, mergedDoc)

Workflow

  1. Extract collection and ID from URL path (handlers_crud.go:276-277)
  2. Validate parameters - collection and ID required (handlers_crud.go:279-286)
  3. Authenticate request and get auth context (handlers_crud.go:289)
  4. Validate collection exists in schema (handlers_crud.go:296)
  5. Fetch existing document from MongoDB (handlers_crud.go:308)
  6. Check RBAC update permission on the document (handlers_crud.go:330)
  7. Parse update fields from JSON body (handlers_crud.go:339)
  8. Check deny_write fields - verify user can modify these fields (handlers_crud.go:348)
  9. Merge documents - combine old and new data (handlers_crud.go:361)
  10. Set auto fields - updated_at, updated_by (handlers_crud.go:364-365)
  11. Validate merged document against schema (handlers_crud.go:369)
  12. Check immutable fields - ensure immutable fields unchanged (handlers_crud.go:377)
  13. Execute pre-update hooks (handlers_crud.go:392)
  14. Save version snapshot if versioning enabled (handlers_crud.go:416)
  15. Update in MongoDB (handlers_crud.go:421)
  16. Execute post-update hooks (best-effort) (handlers_crud.go:430)
  17. Log audit event with field changes (handlers_crud.go:446)
  18. Fetch updated document (handlers_crud.go:449)
  19. Apply field policy to filter response (handlers_crud.go:456)
  20. Return updated document with 200 OK (handlers_crud.go:459)

Field-Level Write Permissions

The system enforces deny_write policies:
// Check deny_write fields
deniedFields := h.getDenyWriteFields(authCtx, collection)
for field := range updateDoc {
    for _, denied := range deniedFields {
        if field == denied {
            return ErrForbidden // Cannot modify this field
        }
    }
}
Example policy:
policies:
  users:
    member:
      actions: [read, update]
      fields:
        deny_write: [role, company_id, created_by, created_at]

Immutable Field Protection

Fields marked as immutable cannot be changed:
immutableFields := collConfig.GetImmutableFields()
for _, field := range immutableFields {
    if _, changed := updateDoc[field]; changed {
        oldVal := oldDoc[field]
        newVal := updateDoc[field]
        if oldVal != newVal {
            return ErrImmutableField
        }
    }
}
Example schema:
fields:
  email:
    type: string
    immutable: true  # Cannot be changed after creation
  user_id:
    type: string
    immutable: true

Version Snapshots

If versioning is enabled, the system saves a snapshot before updating:
if collConfig.Versioning.Enabled && h.version != nil {
    h.saveVersionSnapshot(id, collection, oldDoc, authCtx.UserID)
}
Versioning modes:
  • full - Store complete document snapshot
  • diff - Store only changed fields
  • metadata - Store minimal metadata only
See Version History for querying versions.

Audit Logging

The system logs field-level changes:
changes := computeChanges(oldDoc, mergedDoc)
// Changes: [{field: "status", from: "active", to: "inactive"}, ...]

auditEvent := &audit.AuditEvent{
    TenantID:   authCtx.TenantID,
    UserID:     authCtx.UserID,
    Action:     "update",
    Collection: collection,
    DocID:      id,
    Changes:    changes,
    Success:    true,
}

Error Responses

400 Bad Request

Returned when:
  • Invalid JSON body
  • Schema validation fails
  • Immutable field modified
  • Pre-update hook fails
{
  "error": "Cannot modify immutable field",
  "code": "schema_validation",
  "details": {
    "field": "email"
  }
}

401 Unauthorized

{
  "error": "Authentication required"
}

403 Forbidden

Returned when:
  • User doesn’t have update permission
  • Attempting to modify deny_write field
{
  "error": "You don't have permission to modify this field",
  "code": "forbidden",
  "details": {
    "field": "role"
  }
}

404 Not Found

Returned when document or collection doesn’t exist:
{
  "error": "Document not found",
  "code": "document_not_found",
  "details": {
    "collection": "users",
    "id": "507f1f77bcf86cd799439011"
  }
}

500 Internal Server Error

{
  "error": "Failed to update document",
  "code": "internal_error",
  "details": {
    "error": "database write failed"
  }
}

Hooks

Pre-Update Hook

Executed before the update is applied. Can modify the document or reject the update:
function preUpdate(event) {
  // event.before = original document
  // event.after = merged document to be saved
  
  // Validate business logic
  if (event.after.status === 'published' && !event.after.reviewed_by) {
    throw new Error('Cannot publish without review');
  }
  
  // Auto-compute fields
  if (event.after.price !== event.before.price) {
    event.after.price_updated_at = new Date();
  }
  
  return event;
}

Post-Update Hook

Executed after successful update (best-effort, won’t fail the request):
function postUpdate(event) {
  // event.before = original document
  // event.after = updated document
  
  // Trigger side effects
  if (event.after.status !== event.before.status) {
    sendStatusChangeNotification(event.after);
  }
  
  // Update related documents
  if (event.after.name !== event.before.name) {
    updateRelatedRecords(event.after._id, event.after.name);
  }
}

Use Cases

Status Update

curl -X PUT https://api.example.com/tasks/task_789 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status": "completed"}'

Profile Update

curl -X PUT https://api.example.com/users/me \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "John Doe",
    "bio": "Software engineer",
    "avatar_url": "https://example.com/avatar.jpg"
  }'

Bulk Field Update

curl -X PUT https://api.example.com/products/SKU-12345 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "price": 29.99,
    "stock": 150,
    "on_sale": true,
    "discount_percent": 10
  }'

Build docs developers (and LLMs) love