Created
April 21, 2024 14:52
-
-
Save noam-honig/462066e1bbabacf370a8e487dd940135 to your computer and use it in GitHub Desktop.
Chagelog / audit trail
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
Entity, | |
FieldRef, | |
Fields, | |
FieldsRef, | |
getEntityRef, | |
IdEntity, | |
isBackend, | |
LifecycleEvent, | |
remult, | |
} from "remult" | |
import { Roles } from "../../../model/roles" | |
@Entity<ChangeLog>("changeLog", { | |
allowApiRead: Roles.admin, | |
defaultOrderBy: { | |
changeDate: "desc", | |
}, | |
}) | |
export class ChangeLog { | |
@Fields.cuid() | |
id = "" | |
@Fields.string() | |
relatedId: string = "" | |
@Fields.string() | |
relatedName: string = "" | |
@Fields.string() | |
entity: string = "" | |
@Fields.string() | |
appUrl: string = "" | |
@Fields.string() | |
apiUrl: string = "" | |
@Fields.date() | |
changeDate: Date = new Date() | |
@Fields.string() | |
userId = "" | |
@Fields.string() | |
userName = "" | |
@Fields.json({ dbName: "changesJson" }) | |
changes: change[] = [] | |
@Fields.json({ dbName: "changedFieldsJson" }) | |
changedFields: string[] = [] | |
@Fields.boolean() | |
deleted = false | |
} | |
export interface changeEvent { | |
date: Date | |
userId: string | |
userName: string | |
changes: change[] | |
} | |
export interface change { | |
key: string | |
oldValue: string | |
oldDisplayValue: string | |
newValue: string | |
newDisplayValue: string | |
} | |
export async function recordChanges<entityType>( | |
self: entityType, | |
e: LifecycleEvent<entityType>, | |
options?: ColumnDeciderArgs<entityType> | |
) { | |
if (isBackend()) { | |
let changes = [] as change[] | |
const decider = new FieldDecider(self, options) | |
const isNew = options?.forceNew || e.isNew | |
const changeDate = options?.forceDate || new Date() | |
for (const c of decider.fields.filter( | |
(c) => c.valueChanged() || (isNew && c.value) | |
)) { | |
try { | |
let transValue = (val: any) => val | |
if (c.metadata.options.displayValue) | |
transValue = (val) => c.metadata.options.displayValue!(self, val) | |
else if (c.metadata.valueType === Boolean) | |
transValue = (val) => (val ? "V" : "X") | |
const noVal = decider.excludedValues.includes(c) | |
changes.push({ | |
key: c.metadata.key, | |
newDisplayValue: noVal ? "***" : transValue(c.value), | |
oldDisplayValue: e.isNew | |
? "" | |
: noVal | |
? "***" | |
: transValue(c.originalValue), | |
newValue: noVal | |
? "***" | |
: c.value instanceof IdEntity | |
? c.value.id | |
: c.metadata.options.valueConverter!.toJson!(c.value), | |
oldValue: e.isNew | |
? "" | |
: noVal | |
? "***" | |
: c.originalValue instanceof IdEntity | |
? c.originalValue.id | |
: c.metadata.options.valueConverter!.toJson!(c.originalValue), | |
}) | |
} catch (err) { | |
console.log(c) | |
throw err | |
} | |
} | |
if (changes.length > 0) { | |
await remult.repo(ChangeLog).insert({ | |
changeDate, | |
changedFields: changes.map((x) => x.key), | |
changes, | |
entity: e.metadata.key, | |
relatedId: e.id.toString(), | |
relatedName: e.fields.find("name")?.value, | |
userId: remult.user?.id || "", | |
userName: remult.user?.name || "", | |
}) | |
} | |
} | |
} | |
export async function deleted<entityType>(e: LifecycleEvent<entityType>) { | |
await remult.repo(ChangeLog).insert({ | |
entity: e.metadata.key, | |
relatedId: e.id.toString(), | |
relatedName: e.fields.find("name")?.value, | |
userId: remult.user?.id || "", | |
userName: remult.user?.name || "", | |
deleted: true, | |
}) | |
} | |
interface ColumnDeciderArgs<entityType> { | |
excludeColumns?: (e: FieldsRef<entityType>) => FieldRef<any>[] | |
excludeValues?: (e: FieldsRef<entityType>) => FieldRef<any>[] | |
forceDate?: Date | |
forceNew?: boolean | |
} | |
export class FieldDecider<entityType> { | |
fields: FieldRef<entityType>[] | |
excludedFields: FieldRef<entityType>[] | |
excludedValues: FieldRef<entityType>[] | |
constructor(entity: entityType, options?: ColumnDeciderArgs<entityType>) { | |
const meta = getEntityRef(entity) | |
if (!options?.excludeColumns) this.excludedFields = [] | |
else this.excludedFields = options.excludeColumns(meta.fields) | |
if (!options?.excludeValues) this.excludedValues = [] | |
else this.excludedValues = options.excludeValues(meta.fields) | |
this.excludedFields.push( | |
...meta.fields | |
.toArray() | |
.filter((c) => c.metadata.options.serverExpression) | |
) | |
this.excludedFields.push( | |
...meta.fields.toArray().filter((c) => c.metadata.options.sqlExpression) | |
) | |
this.fields = meta.fields | |
.toArray() | |
.filter((f) => !this.excludedFields.includes(f)) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment