Q33: document edits and content deletions leave attributed audit records
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cb464c3ee4
commit
66de4ef015
9 files changed
+147
-16
No files matched your search
@@ -59,6 +59,14 @@ export class AuditService {
|
||||
);
|
||||
}
|
||||
|
||||
/** Content changes name a real member of the organization as the actor. */
|
||||
requireActor(orgId: string, actorType: "employee" | "agent", actorId: string): void {
|
||||
const table = { employee: "employees", agent: "agents" }[actorType];
|
||||
if (!table) throw new Error(`Unknown actor type "${actorType}".`);
|
||||
const actor = this.db.prepare(`select id from ${table} where id = ? and org_id = ?`).get(actorId, orgId);
|
||||
if (!actor) throw new Error(`No ${actorType} "${actorId}" in this organization.`);
|
||||
}
|
||||
|
||||
createThreadReplay(input: CreateAuditReplayInput): string {
|
||||
const thread = this.requireThread(input.orgId, input.threadId);
|
||||
this.requireEmployee(input.orgId, input.createdByEmployeeId);
|
||||
|
||||
@@ -48,8 +48,9 @@ export class DocumentService {
|
||||
update(input: UpdateDocumentInput): number {
|
||||
const reason = input.reason.trim();
|
||||
if (!reason) throw new Error("A document update needs a reason.");
|
||||
const current = this.db.prepare("select version from documents where id = ? and org_id = ?")
|
||||
.get(input.documentId, input.orgId) as { version: number } | undefined;
|
||||
this.audit.requireActor(input.orgId, input.changedByType, input.changedById);
|
||||
const current = this.db.prepare("select version, title, status from documents where id = ? and org_id = ?")
|
||||
.get(input.documentId, input.orgId) as { version: number; title: string; status: string } | undefined;
|
||||
if (!current) throw new Error("Document was not found.");
|
||||
const version = current.version + 1;
|
||||
const now = new Date().toISOString();
|
||||
@@ -60,6 +61,15 @@ export class DocumentService {
|
||||
orgId: input.orgId, documentId: input.documentId, version, title: input.title.trim(), content: input.content, status: input.status,
|
||||
changedByType: input.changedByType, changedById: input.changedById, reason, at: now,
|
||||
});
|
||||
this.audit.record({
|
||||
orgId: input.orgId,
|
||||
entityType: "document",
|
||||
entityId: input.documentId,
|
||||
actorType: input.changedByType,
|
||||
actorId: input.changedById,
|
||||
action: "revised",
|
||||
payload: { version, reason, title: input.title.trim(), status: input.status, previousTitle: current.title, previousStatus: current.status },
|
||||
});
|
||||
return version;
|
||||
}
|
||||
|
||||
@@ -71,8 +81,20 @@ export class DocumentService {
|
||||
}
|
||||
|
||||
delete(input: DeleteDocumentInput): void {
|
||||
const result = this.db.prepare("delete from documents where id = ? and org_id = ?").run(input.documentId, input.orgId);
|
||||
if (result.changes === 0) throw new Error("Document was not found.");
|
||||
this.audit.requireActor(input.orgId, "employee", input.deletedByEmployeeId);
|
||||
const doc = this.db.prepare("select title, doc_type, status, version, content from documents where id = ? and org_id = ?")
|
||||
.get(input.documentId, input.orgId) as { title: string; doc_type: string; status: string; version: number; content: string } | undefined;
|
||||
if (!doc) throw new Error("Document was not found.");
|
||||
this.db.prepare("delete from documents where id = ? and org_id = ?").run(input.documentId, input.orgId);
|
||||
this.audit.record({
|
||||
orgId: input.orgId,
|
||||
entityType: "document",
|
||||
entityId: input.documentId,
|
||||
actorType: "employee",
|
||||
actorId: input.deletedByEmployeeId,
|
||||
action: "deleted",
|
||||
payload: { title: doc.title, docType: doc.doc_type, status: doc.status, version: doc.version, content: doc.content },
|
||||
});
|
||||
}
|
||||
|
||||
private writeVersion(input: {
|
||||
|
||||
@@ -87,6 +87,7 @@ export class KnowledgeService {
|
||||
update(input: UpdateKnowledgeEntryInput): number {
|
||||
const reason = input.reason.trim();
|
||||
if (!reason) throw new Error("A knowledge correction needs a reason.");
|
||||
this.audit.requireActor(input.orgId, input.changedByType, input.changedById);
|
||||
const now = new Date().toISOString();
|
||||
const result = this.db.prepare(`
|
||||
update knowledge_entries set topic = ?, category = ?, content = ?, importance = ?, updated_at = ?
|
||||
@@ -104,7 +105,7 @@ export class KnowledgeService {
|
||||
orgId: input.orgId,
|
||||
entityType: "knowledge_entry",
|
||||
entityId: input.entryId,
|
||||
actorType: input.changedByType === "agent" ? "agent" : "employee",
|
||||
actorType: input.changedByType,
|
||||
actorId: input.changedById,
|
||||
action: "revised",
|
||||
payload: { revision, reason },
|
||||
@@ -120,8 +121,28 @@ export class KnowledgeService {
|
||||
}
|
||||
|
||||
delete(input: DeleteKnowledgeEntryInput): void {
|
||||
const result = this.db.prepare("delete from knowledge_entries where id = ? and org_id = ?").run(input.entryId, input.orgId);
|
||||
if (result.changes === 0) throw new Error("Knowledge entry was not found.");
|
||||
this.audit.requireActor(input.orgId, "employee", input.deletedByEmployeeId);
|
||||
const entry = this.db.prepare(`
|
||||
select topic, category, content, importance, scope_type, scope_id,
|
||||
(select max(revision) from knowledge_revisions where entry_id = knowledge_entries.id) as revision
|
||||
from knowledge_entries where id = ? and org_id = ?
|
||||
`).get(input.entryId, input.orgId) as {
|
||||
topic: string; category: string; content: string; importance: string; scope_type: string; scope_id: string | null; revision: number;
|
||||
} | undefined;
|
||||
if (!entry) throw new Error("Knowledge entry was not found.");
|
||||
this.db.prepare("delete from knowledge_entries where id = ? and org_id = ?").run(input.entryId, input.orgId);
|
||||
this.audit.record({
|
||||
orgId: input.orgId,
|
||||
entityType: "knowledge_entry",
|
||||
entityId: input.entryId,
|
||||
actorType: "employee",
|
||||
actorId: input.deletedByEmployeeId,
|
||||
action: "deleted",
|
||||
payload: {
|
||||
topic: entry.topic, category: entry.category, content: entry.content, importance: entry.importance,
|
||||
scopeType: entry.scope_type, scopeId: entry.scope_id, revision: entry.revision,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private writeRevision(input: {
|
||||
|
||||
Reference in new issue
Block a user