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
The name of the collection containing the document
The unique identifier (_id) of the document to update
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.
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.
The document’s unique identifier
ISO 8601 timestamp of the update (automatically set)
User ID who performed the update (automatically set)
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
- Extract collection and ID from URL path (handlers_crud.go:276-277)
- Validate parameters - collection and ID required (handlers_crud.go:279-286)
- Authenticate request and get auth context (handlers_crud.go:289)
- Validate collection exists in schema (handlers_crud.go:296)
- Fetch existing document from MongoDB (handlers_crud.go:308)
- Check RBAC update permission on the document (handlers_crud.go:330)
- Parse update fields from JSON body (handlers_crud.go:339)
- Check deny_write fields - verify user can modify these fields (handlers_crud.go:348)
- Merge documents - combine old and new data (handlers_crud.go:361)
- Set auto fields -
updated_at, updated_by (handlers_crud.go:364-365)
- Validate merged document against schema (handlers_crud.go:369)
- Check immutable fields - ensure immutable fields unchanged (handlers_crud.go:377)
- Execute pre-update hooks (handlers_crud.go:392)
- Save version snapshot if versioning enabled (handlers_crud.go:416)
- Update in MongoDB (handlers_crud.go:421)
- Execute post-update hooks (best-effort) (handlers_crud.go:430)
- Log audit event with field changes (handlers_crud.go:446)
- Fetch updated document (handlers_crud.go:449)
- Apply field policy to filter response (handlers_crud.go:456)
- 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
}'