Recipes · 4 of 6
Audit log — actor + action + diff
A reusable audit pattern: every state-changing procedure also writes an audit_log entry in the same transaction.
Schema
audit_log: {actor, action, collection, doc_id, before, after, ts, ip}
Wrapper procedure
proc audited_update(collection, query, modification, actor, action) {
let before = find_one(collection, query)
if before == null { abort "target not found" }
update(collection, query, modification)
let after = find_one(collection, query)
insert("audit_log", {
actor: actor, action: action,
collection: collection, doc_id: before._id,
before: before, after: after
})
return {ok: true}
}
Domain procs use it
proc change_email(user_id, new_email, actor) {
audited_update({
collection: "users", query: {_id: user_id},
modification: {$set: {email: new_email}},
actor: actor, action: "email_change"
})
return {ok: true}
}
proc change_role(user_id, new_role, actor) {
audited_update({
collection: "users", query: {_id: user_id},
modification: {$set: {role: new_role}},
actor: actor, action: "role_change"
})
return {ok: true}
}
Audited delete
proc audited_delete(collection, id, actor) {
let original = find_one(collection, {_id: id})
if original == null { abort "not found" }
delete_one(collection, {_id: id})
insert("audit_log", {
actor: actor, action: "delete",
collection: collection, doc_id: id, before: original
})
return {ok: true}
}
Query the log
proc actor_history(actor) {
return find("audit_log", {actor: actor})
}
proc doc_history(collection, doc_id) {
return find("audit_log", {collection: collection, doc_id: doc_id})
}
proc recent_actions(action, n) {
return aggregate("audit_log", [
{$match: {action: action}},
{$sort: {ts: -1}},
{$limit: n}
])
}
Compliance summary
proc compliance_summary(year, month) {
return aggregate("audit_log", [
{$match: {year: year, month: month}},
{$group: {_id: "$action", n: {$sum: 1}, actors: {$addToSet: "$actor"}}},
{$sort: {n: -1}}
])
}
Why this works: the audit insert is in the same transaction as the change. There is no path where the data changes but the audit entry is missing.