11import type { ActionType , Models } from "@prisma/client" ;
22import type { PrismaClient } from "@prisma/client/extension" ;
3+ import { requestContext } from "@/context/request.context" ;
34
5+ type AuditOperation = "create" | "update" | "delete" ;
46
5- import type { Prisma } from "@prisma/client" ;
6- import { requestContext } from "@/context/request.context" ;
7+ type QueryContext = {
8+ model : string ;
9+ operation : string ;
10+ args : any ;
11+ query : ( args : any ) => Promise < any > ;
12+ } ;
13+
14+ // List of operations
15+ const actionsToAudit : AuditOperation [ ] = [ "create" , "update" , "delete" ] ;
16+
17+ // Map model names to their values
18+ const modelToEnumMap : Record < string , Models > = {
19+ User : "USER" ,
20+ user : "USER" ,
21+ member : "MEMBER" ,
22+ Member : "MEMBER" ,
23+ Events : "EVENT" ,
24+ events : "EVENT" ,
25+ EventSchedule : "EVENTSCHEDULE" ,
26+ eventSchedule : "EVENTSCHEDULE" ,
27+ Project : "PROJECT" ,
28+ project : "PROJECT" ,
29+ ProjectContributors : "PROJECTCONTRIBUTORS" ,
30+ projectContributors : "PROJECTCONTRIBUTORS" ,
31+ Contributor : "CONTRIBUTOR" ,
32+ contributor : "CONTRIBUTOR" ,
33+ Tag : "TAG" ,
34+ tag : "TAG" ,
35+ } ;
36+
37+ const resolveDelegate = ( client : PrismaClient , model : string ) => {
38+ // Checking if the provided model exist on the prisma client
39+ const exact = ( client as any ) [ model ] ;
40+ if ( exact ) {
41+ // if the model exist, then return it
42+ return exact ;
43+ }
44+ // if the model name doesn't exit, try using the camelCase version.
45+ const camel = model . charAt ( 0 ) . toLowerCase ( ) + model . slice ( 1 ) ;
46+ return ( client as any ) [ camel ] ;
47+ } ;
48+
49+ // finds the enum value for the given model name by capitalizing the first letter of the model name
50+ // or converting the entire model name to upper case
51+ const resolveEntity = ( model : string ) : Models | undefined => {
52+ return modelToEnumMap [ model ] ?? modelToEnumMap [ model . charAt ( 0 ) . toUpperCase ( ) + model . slice ( 1 ) ] ?? ( model . toUpperCase ( ) as Models ) ;
53+ } ;
754
55+
56+ const shouldDebug = ( ) => process . env . AUDIT_DEBUG === "true" || process . env . NODE_ENV === "development" ;
57+
58+ // Middleware to handle auditing
859export const auditMiddleware = ( prisma : PrismaClient ) => {
960 return prisma . $extends ( {
10- model : {
61+ query : {
1162 $allModels : {
12- async execute ( args : any , next : ( args : any ) => Promise < any > ) {
13- const actionsToAudit = [ "create" , "update" , "delete" ] ;
63+ async $allOperations ( { model, operation, args, query } : QueryContext ) {
64+ const op = operation as AuditOperation ;
65+
66+ // Skip if db transaction is about AuditLogs or models which doesnt need to be audited
67+ if ( model === "AuditLogs" || ! actionsToAudit . includes ( op ) ) {
68+ return query ( args ) ;
69+ }
1470
15- // Only track the defined actions.
16- if ( ! actionsToAudit . includes ( args . action ) ) return next ( args ) ;
71+ const debug = shouldDebug ( ) ;
72+ if ( debug ) {
73+ console . debug ( `[AUDIT] ${ model } .${ operation } intercepted` ) ;
74+ }
1775
18- let before : any = null ;
76+ let before : Record < string , unknown > | null = null ;
77+ const delegate = resolveDelegate ( prisma , model ) ;
1978
20- if ( args . action === "update" || args . action === "delete" ) {
21- before = await next ( { ...args , action : "findUnique" } )
79+ // Fetch the current state of the entity which will be needed when returing before and after
80+ if ( ( op === "update" || op === "delete" ) && delegate && args ?. where ) {
81+ try {
82+ before = await delegate . findUnique ( { where : args . where } ) ;
83+ if ( debug ) {
84+ console . debug ( `[AUDIT] Loaded before state for ${ model } .${ operation } ` ) ;
85+ }
86+ } catch ( error ) {
87+ if ( debug ) {
88+ console . debug ( `[AUDIT] Failed to load before state for ${ model } .${ operation } ` , error ) ;
89+ }
90+ }
2291 }
2392
24- let result : any = null ;
2593 const userId = requestContext . getStore ( ) ?. userId ?? "UNKNOWN" ;
2694
95+ let result : any ;
2796 try {
28- const result = await next ( args )
29- } catch ( error : any ) {
30- throw new Error ( "Error while performing database query: " , error )
97+ // Executing the original query
98+ result = await query ( args ) ;
99+ if ( debug ) {
100+ console . debug ( `[AUDIT] ${ model } .${ operation } executed successfully (id: ${ result ?. id ?? "n/a" } )` ) ;
101+ }
102+ } catch ( error ) {
103+ if ( debug ) {
104+ console . error ( `[AUDIT] ${ model } .${ operation } failed` , error ) ;
105+ }
106+ throw error ;
31107 }
32108
33- let changes : Record < string , { before : any , after : any } > = { } ;
34- if ( args . action === "update" && before ) {
35- for ( const key in args . data ) {
36- if ( before [ key ] !== result [ key ] ) {
37- changes [ key ] = { before : before [ key ] , after : result [ key ] }
109+ // On update operaiton, tracking changes of which field changed and what changed.
110+ const changes : Record < string , { before : unknown ; after : unknown } > = { } ;
111+ if ( op === "update" && before && args ?. data ) {
112+ for ( const key of Object . keys ( args . data ) ) {
113+ const beforeValue = ( before as any ) [ key ] ;
114+ const afterValue = result ?. [ key ] ;
115+ if ( ! Object . is ( beforeValue , afterValue ) ) {
116+ changes [ key ] = {
117+ before : beforeValue ,
118+ after : afterValue ,
119+ } ;
38120 }
39121 }
122+ if ( debug ) {
123+ console . debug ( `[AUDIT] Change set for ${ model } .${ operation } :` , changes ) ;
124+ }
40125 }
41126
127+
128+ const entity = resolveEntity ( model ) ;
42129
130+ if ( ! entity ) {
131+ if ( debug ) {
132+ console . debug ( `[AUDIT] Skipping audit log for model ${ model } ; no enum mapping found.` ) ;
133+ }
134+ return result ;
135+ }
136+
137+ // Create an audit log
43138 await prisma . auditLogs . create ( {
44139 data : {
45- action : args . action . toUpperCase ( ) as ActionType ,
46- userId : userId ,
47- entity : args . model . toUpperCase ( ) as Models ,
48- entitiyId : result . id ,
49- changes : Object . keys ( changes ) . length ? changes : null
140+ action : op . toUpperCase ( ) as ActionType ,
141+ userId,
142+ entity,
143+ entityId : result ?. id ?? args ?. where ?. id ?? "UNKNOWN" ,
144+ changes : Object . keys ( changes ) . length ? changes : null ,
145+ } ,
146+ } ) ;
50147
51- }
52- } )
148+ if ( debug ) {
149+ console . debug ( `[AUDIT] Audit log written for ${ model } .${ operation } ` ) ;
150+ }
53151
54152 return result ;
55-
56- }
57- }
58- }
59- } )
60- }
153+ } ,
154+ } ,
155+ } ,
156+ } ) ;
157+ } ;
0 commit comments