Skip to content

Instantly share code, notes, and snippets.

@noam-honig
Created April 21, 2024 14:52
Show Gist options
  • Save noam-honig/462066e1bbabacf370a8e487dd940135 to your computer and use it in GitHub Desktop.
Save noam-honig/462066e1bbabacf370a8e487dd940135 to your computer and use it in GitHub Desktop.
Chagelog / audit trail
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