velocious 1.0.507 → 1.0.509
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/configuration.js +2 -0
- package/build/database/migration/index.js +67 -0
- package/build/database/record/auditing.js +686 -119
- package/build/database/record/index.js +2 -1
- package/build/database/record/instance-relationships/has-many.js +6 -0
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +3 -1
- package/build/src/database/migration/index.d.ts +30 -0
- package/build/src/database/migration/index.d.ts.map +1 -1
- package/build/src/database/migration/index.js +58 -1
- package/build/src/database/record/auditing.d.ts +97 -40
- package/build/src/database/record/auditing.d.ts.map +1 -1
- package/build/src/database/record/auditing.js +583 -118
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +3 -2
- package/build/src/database/record/instance-relationships/has-many.d.ts.map +1 -1
- package/build/src/database/record/instance-relationships/has-many.js +5 -1
- package/package.json +1 -1
- package/src/configuration.js +2 -0
- package/src/database/migration/index.js +67 -0
- package/src/database/record/auditing.js +686 -119
- package/src/database/record/index.js +2 -1
- package/src/database/record/instance-relationships/has-many.js +6 -0
|
@@ -34,12 +34,324 @@ import UUID from "pure-uuid"
|
|
|
34
34
|
* AuditedModelClass type.
|
|
35
35
|
* @typedef {typeof import("./index.js").default & {
|
|
36
36
|
* _auditCallbacks?: Record<string, AuditCallback[]>,
|
|
37
|
-
* _auditLifecycleCallbacksRegistered?: boolean
|
|
37
|
+
* _auditLifecycleCallbacksRegistered?: boolean,
|
|
38
|
+
* _auditTableResolved?: boolean,
|
|
39
|
+
* _auditTableData?: AuditTableData
|
|
38
40
|
* }} AuditedModelClass
|
|
39
41
|
*/
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
|
-
*
|
|
44
|
+
* AuditTableData type.
|
|
45
|
+
* @typedef {object} AuditTableData
|
|
46
|
+
* @property {boolean} dedicated - Whether a dedicated audit table exists.
|
|
47
|
+
* @property {string} tableName - Name of the audit table.
|
|
48
|
+
* @property {string} foreignKey - Column name that references the audited model.
|
|
49
|
+
* @property {typeof import("./index.js").default} auditClass - The audit model class to use.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/** @type {Map<string, boolean>} */
|
|
53
|
+
const dedicatedTableCache = new Map()
|
|
54
|
+
|
|
55
|
+
/** @type {Map<string, typeof import("./index.js").default>} */
|
|
56
|
+
const auditClassCache = new Map()
|
|
57
|
+
|
|
58
|
+
const generatedAuditRelationships = new WeakSet()
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Global event bus (like ActiveRecordAuditable::Events)
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/** @type {Record<string, Record<string, Array<(args: AuditEventPayload) => void>>>} */
|
|
65
|
+
let globalEventConnections = {}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Global audit event bus matching ActiveRecordAuditable::Events.
|
|
69
|
+
* @typedef {object} AuditEventsType
|
|
70
|
+
* @property {(type: string, action: string, args: AuditEventPayload) => void} call - Fire all callbacks for a model type + action.
|
|
71
|
+
* @property {(type: string, action: string, callback: (args: AuditEventPayload) => void) => () => void} connect - Register a callback for a model type + action. Returns an unsubscribe function.
|
|
72
|
+
* @property {() => void} reset - Clear all registered callbacks.
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/** @type {AuditEventsType} */
|
|
76
|
+
const AuditEvents = {
|
|
77
|
+
/** Fire all registered callbacks for a model type + action. @param {string} type @param {string} action @param {AuditEventPayload} args */
|
|
78
|
+
call(type, action, args) {
|
|
79
|
+
const actions = globalEventConnections[type] || {}
|
|
80
|
+
const callbacks = actions[action] || []
|
|
81
|
+
|
|
82
|
+
for (const callback of [...callbacks]) {
|
|
83
|
+
callback(args)
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/** Register a callback for a model type + action. Returns an unsubscribe function. @param {string} type @param {string} action @param {(args: AuditEventPayload) => void} callback @returns {() => void} */
|
|
88
|
+
connect(type, action, callback) {
|
|
89
|
+
if (!globalEventConnections[type]) {
|
|
90
|
+
globalEventConnections[type] = {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!globalEventConnections[type][action]) {
|
|
94
|
+
globalEventConnections[type][action] = []
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
globalEventConnections[type][action].push(callback)
|
|
98
|
+
|
|
99
|
+
return () => {
|
|
100
|
+
const list = globalEventConnections[type]?.[action]
|
|
101
|
+
|
|
102
|
+
if (list) {
|
|
103
|
+
const index = list.indexOf(callback)
|
|
104
|
+
|
|
105
|
+
if (index >= 0) {
|
|
106
|
+
list.splice(index, 1)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
/** Clear all registered callbacks. @returns {void} */
|
|
113
|
+
reset() {
|
|
114
|
+
globalEventConnections = {}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Table detection
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns the dedicated audit table name for a model class.
|
|
124
|
+
* @param {AuditedModelClass} modelClass - Audited model class.
|
|
125
|
+
* @returns {string} e.g. "project_audits" for a "projects" table.
|
|
126
|
+
*/
|
|
127
|
+
function dedicatedAuditTableName(modelClass) {
|
|
128
|
+
const table = modelClass.tableName()
|
|
129
|
+
|
|
130
|
+
if (table.endsWith("s")) {
|
|
131
|
+
return `${table.slice(0, -1)}_audits`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return `${table}_audits`
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resolves audit table data for a model class. Cached per model.
|
|
139
|
+
* Called lazily on first createAudit / withoutAudit / relationship usage.
|
|
140
|
+
* @param {AuditedModelClass} modelClass - Audited model class.
|
|
141
|
+
* @returns {Promise<AuditTableData>} Resolved audit table metadata.
|
|
142
|
+
*/
|
|
143
|
+
async function resolveAuditTableData(modelClass) {
|
|
144
|
+
if (modelClass._auditTableResolved && modelClass._auditTableData) {
|
|
145
|
+
return modelClass._auditTableData
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const tableData = await buildAuditTableData(modelClass)
|
|
149
|
+
const configuration = modelClass._getConfiguration()
|
|
150
|
+
|
|
151
|
+
tableData.auditClass.registerRecordClass({configuration})
|
|
152
|
+
await tableData.auditClass.initializeRecord({configuration})
|
|
153
|
+
|
|
154
|
+
modelClass._auditTableData = tableData
|
|
155
|
+
modelClass._auditTableResolved = true
|
|
156
|
+
|
|
157
|
+
registerAuditRelationship(modelClass, tableData)
|
|
158
|
+
|
|
159
|
+
return tableData
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Builds audit table metadata for a model class. Prefers the consumer's
|
|
164
|
+
* registered Audit model for shared tables; falls back to a framework-owned
|
|
165
|
+
* dynamic class.
|
|
166
|
+
* @param {AuditedModelClass} modelClass - Audited model class.
|
|
167
|
+
* @returns {Promise<AuditTableData>} Audit table metadata.
|
|
168
|
+
*/
|
|
169
|
+
async function buildAuditTableData(modelClass) {
|
|
170
|
+
const dedicatedTable = dedicatedAuditTableName(modelClass)
|
|
171
|
+
const dedicatedExists = await dedicatedTableExistsForConnection(modelClass, dedicatedTable)
|
|
172
|
+
|
|
173
|
+
if (dedicatedExists) {
|
|
174
|
+
const auditClass = dedicatedAuditClass(modelClass, dedicatedTable)
|
|
175
|
+
const modelKey = modelParamKey(modelClass)
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
auditClass,
|
|
179
|
+
dedicated: true,
|
|
180
|
+
foreignKey: `${modelKey}_id`,
|
|
181
|
+
tableName: dedicatedTable
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const configuration = modelClass._getConfiguration()
|
|
186
|
+
|
|
187
|
+
const consumerAuditClass = configuration.getModelClasses().Audit
|
|
188
|
+
|
|
189
|
+
if (consumerAuditClass) {
|
|
190
|
+
return {
|
|
191
|
+
auditClass: consumerAuditClass,
|
|
192
|
+
dedicated: false,
|
|
193
|
+
foreignKey: "auditable_id",
|
|
194
|
+
tableName: "audits"
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
auditClass: cachedSharedAuditClass(modelClass),
|
|
200
|
+
dedicated: false,
|
|
201
|
+
foreignKey: "auditable_id",
|
|
202
|
+
tableName: "audits"
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Checks whether a dedicated audit table exists for a model's connection.
|
|
208
|
+
* @param {AuditedModelClass} modelClass - Audited model class.
|
|
209
|
+
* @param {string} tableName - Dedicated audit table name to check.
|
|
210
|
+
* @returns {Promise<boolean>} Whether the table exists.
|
|
211
|
+
*/
|
|
212
|
+
async function dedicatedTableExistsForConnection(modelClass, tableName) {
|
|
213
|
+
const databaseIdentifier = modelClass.getDatabaseIdentifier()
|
|
214
|
+
const cacheKey = `${databaseIdentifier}:${tableName}`
|
|
215
|
+
const cached = dedicatedTableCache.get(cacheKey)
|
|
216
|
+
|
|
217
|
+
if (typeof cached === "boolean") {
|
|
218
|
+
return cached
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const connection = modelClass.connection()
|
|
222
|
+
const table = await connection.getTableByName(tableName, {throwError: false})
|
|
223
|
+
const exists = Boolean(table)
|
|
224
|
+
|
|
225
|
+
dedicatedTableCache.set(cacheKey, exists)
|
|
226
|
+
|
|
227
|
+
return exists
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Dynamic audit classes
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Returns a framework-owned shared Audit class for the `audits` table.
|
|
236
|
+
* This is only used when no consumer-registered Audit model exists.
|
|
237
|
+
* @param {AuditedModelClass} modelClass - Any audited model class (used to locate DatabaseRecord).
|
|
238
|
+
* @returns {typeof import("./index.js").default} Shared Audit model class.
|
|
239
|
+
*/
|
|
240
|
+
function sharedAuditClass(modelClass) {
|
|
241
|
+
const dbRecordClass = findDatabaseRecordClass(modelClass)
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Framework-owned Audit model for the shared `audits` table.
|
|
245
|
+
*/
|
|
246
|
+
class Audit extends dbRecordClass {
|
|
247
|
+
/** Returns the backing table name. @returns {string} */
|
|
248
|
+
static tableName() {
|
|
249
|
+
return "audits"
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
Object.defineProperty(Audit, "modelName", {value: "Audit", writable: false})
|
|
254
|
+
applyAuditClassDatabaseRouting(modelClass, Audit)
|
|
255
|
+
|
|
256
|
+
return /** @type {typeof import("./index.js").default} */ (Audit)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Returns the cached framework-owned shared Audit class for a database.
|
|
261
|
+
* @param {AuditedModelClass} modelClass - Any audited model class.
|
|
262
|
+
* @returns {typeof import("./index.js").default} Shared Audit model class.
|
|
263
|
+
*/
|
|
264
|
+
function cachedSharedAuditClass(modelClass) {
|
|
265
|
+
const routingKey = modelClass.hasTenantDatabaseIdentifierResolver()
|
|
266
|
+
? `tenant:${modelClass.getModelName()}`
|
|
267
|
+
: `database:${modelClass.getConfiguredDatabaseIdentifier()}`
|
|
268
|
+
const cacheKey = `shared:${routingKey}`
|
|
269
|
+
let auditClass = auditClassCache.get(cacheKey)
|
|
270
|
+
|
|
271
|
+
if (!auditClass) {
|
|
272
|
+
auditClass = sharedAuditClass(modelClass)
|
|
273
|
+
auditClassCache.set(cacheKey, auditClass)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return auditClass
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Returns a dynamic per-model audit class for a dedicated table.
|
|
281
|
+
* @param {AuditedModelClass} modelClass - Audited model class.
|
|
282
|
+
* @param {string} tableName - Dedicated audit table name (e.g. "project_audits").
|
|
283
|
+
* @returns {typeof import("./index.js").default} Dedicated audit model class.
|
|
284
|
+
*/
|
|
285
|
+
function dedicatedAuditClass(modelClass, tableName) {
|
|
286
|
+
const dbRecordClass = findDatabaseRecordClass(modelClass)
|
|
287
|
+
const modelName = modelClass.getModelName()
|
|
288
|
+
const modelKey = modelParamKey(modelClass)
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Framework-owned per-model Audit class.
|
|
292
|
+
*/
|
|
293
|
+
class ModelAudit extends dbRecordClass {
|
|
294
|
+
/** Returns the backing table name. @returns {string} */
|
|
295
|
+
static tableName() {
|
|
296
|
+
return tableName
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
Object.defineProperty(ModelAudit, "modelName", {value: `${modelName}Audit`, writable: false})
|
|
301
|
+
applyAuditClassDatabaseRouting(modelClass, ModelAudit)
|
|
302
|
+
ModelAudit.belongsTo(modelKey, {className: modelName})
|
|
303
|
+
|
|
304
|
+
return /** @type {typeof import("./index.js").default} */ (ModelAudit)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Makes framework-owned audit classes read the same database as the audited model.
|
|
309
|
+
* @param {AuditedModelClass} modelClass - Audited source model class.
|
|
310
|
+
* @param {typeof import("./index.js").default} auditClass - Generated audit class.
|
|
311
|
+
* @returns {void}
|
|
312
|
+
*/
|
|
313
|
+
function applyAuditClassDatabaseRouting(modelClass, auditClass) {
|
|
314
|
+
auditClass.setDatabaseIdentifier(modelClass.getConfiguredDatabaseIdentifier())
|
|
315
|
+
|
|
316
|
+
if (modelClass.hasTenantDatabaseIdentifierResolver()) {
|
|
317
|
+
auditClass.switchesTenantDatabase(({tenant}) => modelClass.getTenantDatabaseIdentifier(tenant))
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Walks the prototype chain to find the root DatabaseRecord class.
|
|
323
|
+
* @param {AuditedModelClass} modelClass - Audited model class.
|
|
324
|
+
* @returns {typeof import("./index.js").default} Root DatabaseRecord class.
|
|
325
|
+
*/
|
|
326
|
+
function findDatabaseRecordClass(modelClass) {
|
|
327
|
+
let recordClass = Object.getPrototypeOf(modelClass)
|
|
328
|
+
|
|
329
|
+
while (recordClass && Object.getPrototypeOf(recordClass) !== Function.prototype) {
|
|
330
|
+
recordClass = Object.getPrototypeOf(recordClass)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return /** @type {typeof import("./index.js").default} */ (recordClass)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Returns the parameter-key name for a model class.
|
|
338
|
+
* @param {AuditedModelClass} modelClass - Audited model class.
|
|
339
|
+
* @returns {string} e.g. "project" for "Project".
|
|
340
|
+
*/
|
|
341
|
+
function modelParamKey(modelClass) {
|
|
342
|
+
const name = modelClass.getModelName()
|
|
343
|
+
|
|
344
|
+
return name.charAt(0).toLowerCase() + name.slice(1)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
// Registration (sync — callbacks only; table detection deferred)
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Registers lifecycle callbacks for automatic create/update/destroy auditing.
|
|
353
|
+
* Table detection and relationship registration happen lazily on first usage.
|
|
354
|
+
* Called synchronously from Model.audited() at module-load time.
|
|
43
355
|
* @param {AuditedModelClass} modelClass - Model class to audit.
|
|
44
356
|
* @returns {void}
|
|
45
357
|
*/
|
|
@@ -49,6 +361,7 @@ function registerAuditing(modelClass) {
|
|
|
49
361
|
}
|
|
50
362
|
|
|
51
363
|
modelClass._auditLifecycleCallbacksRegistered = true
|
|
364
|
+
registerAuditRelationship(modelClass)
|
|
52
365
|
modelClass.beforeCreate("captureCreateAuditChanges")
|
|
53
366
|
modelClass.afterCreate("createCreateAudit")
|
|
54
367
|
modelClass.beforeUpdate("captureUpdateAuditChanges")
|
|
@@ -57,63 +370,145 @@ function registerAuditing(modelClass) {
|
|
|
57
370
|
}
|
|
58
371
|
|
|
59
372
|
/**
|
|
60
|
-
*
|
|
61
|
-
* @param {
|
|
62
|
-
* @param {
|
|
63
|
-
* @
|
|
64
|
-
* @returns {() => void} Unsubscribe function.
|
|
373
|
+
* Initializes audit metadata for audited model classes.
|
|
374
|
+
* @param {typeof import("./index.js").default} modelClass - Model class to initialize.
|
|
375
|
+
* @param {{resolveTableData?: boolean}} [args] - Initialization options.
|
|
376
|
+
* @returns {Promise<void>}
|
|
65
377
|
*/
|
|
66
|
-
function
|
|
67
|
-
const
|
|
68
|
-
const
|
|
378
|
+
async function initializeAuditing(modelClass, args = {}) {
|
|
379
|
+
const {resolveTableData = false} = args
|
|
380
|
+
const auditedModelClass = /** @type {AuditedModelClass} */ (modelClass)
|
|
69
381
|
|
|
70
|
-
if (!
|
|
71
|
-
|
|
382
|
+
if (!auditedModelClass._auditLifecycleCallbacksRegistered) {
|
|
383
|
+
return
|
|
72
384
|
}
|
|
73
385
|
|
|
74
|
-
|
|
386
|
+
registerAuditRelationship(auditedModelClass)
|
|
75
387
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
388
|
+
if (resolveTableData && shouldResolveAuditTableData(auditedModelClass)) {
|
|
389
|
+
await resolveAuditTableData(auditedModelClass)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
79
392
|
|
|
80
|
-
|
|
81
|
-
|
|
393
|
+
/**
|
|
394
|
+
* Resolves audit metadata after application and package model classes are registered.
|
|
395
|
+
* @param {import("../../configuration.js").default} configuration - Configuration whose models should be finalized.
|
|
396
|
+
* @returns {Promise<void>}
|
|
397
|
+
*/
|
|
398
|
+
async function initializeAuditedModelRelationships(configuration) {
|
|
399
|
+
const modelClasses = Object.values(configuration.getModelClasses())
|
|
400
|
+
const shouldResolveTableData = modelClasses.some((modelClass) => shouldResolveAuditTableData(modelClass))
|
|
401
|
+
|
|
402
|
+
if (!shouldResolveTableData) {
|
|
403
|
+
for (const modelClass of modelClasses) {
|
|
404
|
+
await initializeAuditing(modelClass)
|
|
82
405
|
}
|
|
406
|
+
|
|
407
|
+
return
|
|
83
408
|
}
|
|
409
|
+
|
|
410
|
+
await configuration.ensureConnections({name: "Initialize audited model relationships"}, async () => {
|
|
411
|
+
for (const modelClass of modelClasses) {
|
|
412
|
+
await initializeAuditing(modelClass, {resolveTableData: true})
|
|
413
|
+
}
|
|
414
|
+
})
|
|
84
415
|
}
|
|
85
416
|
|
|
86
417
|
/**
|
|
87
|
-
*
|
|
88
|
-
* @param {import("./index.js").default}
|
|
89
|
-
* @returns {
|
|
418
|
+
* Checks whether audit table metadata should be resolved for a model class.
|
|
419
|
+
* @param {typeof import("./index.js").default} modelClass - Model class to inspect.
|
|
420
|
+
* @returns {boolean} Whether table metadata resolution should run now.
|
|
90
421
|
*/
|
|
91
|
-
function
|
|
92
|
-
const
|
|
93
|
-
/** @type {AuditChanges} */
|
|
94
|
-
const auditedChanges = {}
|
|
95
|
-
const columnNameToAttributeName = record.getModelClass().getColumnNameToAttributeNameMap()
|
|
422
|
+
function shouldResolveAuditTableData(modelClass) {
|
|
423
|
+
const auditedModelClass = /** @type {AuditedModelClass} */ (modelClass)
|
|
96
424
|
|
|
97
|
-
|
|
98
|
-
|
|
425
|
+
if (!auditedModelClass._auditLifecycleCallbacksRegistered) {
|
|
426
|
+
return false
|
|
99
427
|
}
|
|
100
428
|
|
|
101
|
-
return
|
|
429
|
+
return Boolean(auditedModelClass._initialized) && canResolveAuditTableData(auditedModelClass)
|
|
102
430
|
}
|
|
103
431
|
|
|
104
432
|
/**
|
|
105
|
-
*
|
|
106
|
-
* @param {
|
|
107
|
-
* @
|
|
433
|
+
* Registers the audits relationship without forcing audit table detection.
|
|
434
|
+
* @param {AuditedModelClass} modelClass - Model class to audit.
|
|
435
|
+
* @param {AuditTableData} [tableData] - Resolved audit table data when available.
|
|
436
|
+
* @returns {void}
|
|
108
437
|
*/
|
|
109
|
-
function
|
|
110
|
-
|
|
438
|
+
function registerAuditRelationship(modelClass, tableData = defaultAuditRelationshipTableData(modelClass)) {
|
|
439
|
+
if (modelClass._relationshipExists("audits")) {
|
|
440
|
+
const relationship = modelClass.getRelationshipByName("audits")
|
|
441
|
+
|
|
442
|
+
if (generatedAuditRelationships.has(relationship)) {
|
|
443
|
+
relationship.className = undefined
|
|
444
|
+
relationship.klass = tableData.auditClass
|
|
445
|
+
relationship.foreignKey = tableData.foreignKey
|
|
446
|
+
relationship._explicitForeignKey = tableData.foreignKey
|
|
447
|
+
relationship._polymorphic = !tableData.dedicated
|
|
448
|
+
relationship._polymorphicTypeColumn = undefined
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
modelClass.hasMany("audits", auditRelationshipScope, {
|
|
455
|
+
foreignKey: tableData.foreignKey,
|
|
456
|
+
klass: tableData.auditClass,
|
|
457
|
+
polymorphic: !tableData.dedicated
|
|
458
|
+
})
|
|
459
|
+
generatedAuditRelationships.add(modelClass.getRelationshipByName("audits"))
|
|
111
460
|
}
|
|
112
461
|
|
|
462
|
+
/**
|
|
463
|
+
* Returns unresolved shared-audit defaults for early relationship registration.
|
|
464
|
+
* @param {AuditedModelClass} modelClass - Model class to audit.
|
|
465
|
+
* @returns {AuditTableData} Shared audit relationship defaults.
|
|
466
|
+
*/
|
|
467
|
+
function defaultAuditRelationshipTableData(modelClass) {
|
|
468
|
+
return {
|
|
469
|
+
auditClass: cachedSharedAuditClass(modelClass),
|
|
470
|
+
dedicated: false,
|
|
471
|
+
foreignKey: "auditable_id",
|
|
472
|
+
tableName: "audits"
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Applies default audit ordering to generated audit relationships.
|
|
478
|
+
* @param {import("../query/model-class-query.js").default<typeof import("./index.js").default>} query - Audit query.
|
|
479
|
+
* @returns {import("../query/model-class-query.js").default<typeof import("./index.js").default>} Ordered audit query.
|
|
480
|
+
*/
|
|
481
|
+
function auditRelationshipScope(query) {
|
|
482
|
+
return query.order({column: "created_at", direction: "DESC"})
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Checks whether the current tenant context can resolve audit table data.
|
|
487
|
+
* @param {AuditedModelClass} modelClass - Model class to inspect.
|
|
488
|
+
* @returns {boolean} Whether audit table data can be resolved in the current scope.
|
|
489
|
+
*/
|
|
490
|
+
function canResolveAuditTableData(modelClass) {
|
|
491
|
+
if (!modelClass.hasTenantDatabaseIdentifierResolver()) {
|
|
492
|
+
return true
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const tenantDatabaseIdentifier = modelClass.getTenantDatabaseIdentifier()
|
|
496
|
+
|
|
497
|
+
if (!tenantDatabaseIdentifier) {
|
|
498
|
+
return false
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return modelClass._getConfiguration().isDatabaseIdentifierActive(tenantDatabaseIdentifier)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ---------------------------------------------------------------------------
|
|
505
|
+
// Creating audits
|
|
506
|
+
// ---------------------------------------------------------------------------
|
|
507
|
+
|
|
113
508
|
/**
|
|
114
509
|
* Creates an audit row for a record.
|
|
115
510
|
* @param {import("./index.js").default} record - Record to audit.
|
|
116
|
-
* @param {CreateAuditArgs} args - Audit row options.
|
|
511
|
+
* @param {CreateAuditArgs} args - Audit row options (action, auditedChanges, params).
|
|
117
512
|
* @returns {Promise<number | string>} Created audit row id.
|
|
118
513
|
*/
|
|
119
514
|
async function createAudit(record, args) {
|
|
@@ -126,85 +521,100 @@ async function createAudit(record, args) {
|
|
|
126
521
|
|
|
127
522
|
/**
|
|
128
523
|
* Creates an audit row using the current model database connection.
|
|
524
|
+
* Routes to shared table or dedicated table based on resolved audit table data.
|
|
129
525
|
* @param {import("./index.js").default} record - Record to audit.
|
|
130
|
-
* @param {CreateAuditArgs} args - Audit row options.
|
|
526
|
+
* @param {CreateAuditArgs} args - Audit row options (action, auditedChanges, params).
|
|
131
527
|
* @param {AuditedModelClass} modelClass - Audited model class.
|
|
132
528
|
* @returns {Promise<number | string>} Created audit row id.
|
|
133
529
|
*/
|
|
134
530
|
async function createAuditWithCurrentConnection(record, args, modelClass) {
|
|
135
531
|
if (!record.isPersisted()) throw new Error(`Cannot audit unpersisted ${modelClass.getModelName()} record`)
|
|
136
532
|
|
|
533
|
+
const tableData = await resolveAuditTableData(modelClass)
|
|
137
534
|
const action = normalizeAction(args.action)
|
|
138
535
|
const auditedChanges = args.auditedChanges === undefined ? null : args.auditedChanges
|
|
139
536
|
const params = args.params === undefined ? null : args.params
|
|
140
537
|
const db = modelClass.connection()
|
|
141
538
|
const currentDate = new Date()
|
|
539
|
+
|
|
142
540
|
const auditActionId = await findOrCreateLookupId({
|
|
143
541
|
columnName: "action",
|
|
542
|
+
currentDate,
|
|
144
543
|
db,
|
|
145
544
|
tableName: "audit_actions",
|
|
146
|
-
value: action
|
|
147
|
-
currentDate
|
|
148
|
-
})
|
|
149
|
-
const auditAuditableTypeId = await findOrCreateLookupId({
|
|
150
|
-
columnName: "name",
|
|
151
|
-
db,
|
|
152
|
-
tableName: "audit_auditable_types",
|
|
153
|
-
value: modelClass.getModelName(),
|
|
154
|
-
currentDate
|
|
545
|
+
value: action
|
|
155
546
|
})
|
|
547
|
+
|
|
156
548
|
const auditId = new UUID(4).format()
|
|
157
549
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
550
|
+
if (tableData.dedicated) {
|
|
551
|
+
const modelKey = modelParamKey(modelClass)
|
|
552
|
+
|
|
553
|
+
await db.query(db.insertSql({
|
|
554
|
+
returnLastInsertedColumnNames: ["id"],
|
|
555
|
+
tableName: tableData.tableName,
|
|
556
|
+
data: {
|
|
557
|
+
id: auditId,
|
|
558
|
+
[`${modelKey}_id`]: record.id(),
|
|
559
|
+
audit_action_id: auditActionId,
|
|
560
|
+
audited_changes: auditedChanges,
|
|
561
|
+
params,
|
|
562
|
+
created_at: currentDate,
|
|
563
|
+
updated_at: currentDate
|
|
564
|
+
}
|
|
565
|
+
}))
|
|
566
|
+
} else {
|
|
567
|
+
const auditAuditableTypeId = await findOrCreateLookupId({
|
|
568
|
+
columnName: "name",
|
|
569
|
+
currentDate,
|
|
570
|
+
db,
|
|
571
|
+
tableName: "audit_auditable_types",
|
|
572
|
+
value: modelClass.getModelName()
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
await db.query(db.insertSql({
|
|
576
|
+
returnLastInsertedColumnNames: ["id"],
|
|
577
|
+
tableName: "audits",
|
|
578
|
+
data: {
|
|
579
|
+
id: auditId,
|
|
580
|
+
audit_action_id: auditActionId,
|
|
581
|
+
audit_auditable_type_id: auditAuditableTypeId,
|
|
582
|
+
auditable_id: record.id(),
|
|
583
|
+
auditable_type: modelClass.getModelName(),
|
|
584
|
+
audited_changes: auditedChanges,
|
|
585
|
+
params,
|
|
586
|
+
created_at: currentDate,
|
|
587
|
+
updated_at: currentDate
|
|
588
|
+
}
|
|
589
|
+
}))
|
|
590
|
+
}
|
|
174
591
|
|
|
175
592
|
await emitAuditEvent(modelClass, action, {
|
|
176
593
|
action,
|
|
177
|
-
auditId
|
|
594
|
+
auditId,
|
|
178
595
|
auditedChanges,
|
|
179
596
|
params,
|
|
180
597
|
record
|
|
181
598
|
})
|
|
182
599
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
*/
|
|
191
|
-
function auditIdFromInsertResult(insertResult) {
|
|
192
|
-
if (!Array.isArray(insertResult)) return null
|
|
193
|
-
|
|
194
|
-
const row = insertResult[0]
|
|
195
|
-
|
|
196
|
-
if (!row) return null
|
|
197
|
-
|
|
198
|
-
const id = row.id
|
|
199
|
-
|
|
200
|
-
if (typeof id == "number" || typeof id == "string") return id
|
|
600
|
+
AuditEvents.call(modelClass.getModelName(), action, {
|
|
601
|
+
action,
|
|
602
|
+
auditId,
|
|
603
|
+
auditedChanges,
|
|
604
|
+
params,
|
|
605
|
+
record
|
|
606
|
+
})
|
|
201
607
|
|
|
202
|
-
return
|
|
608
|
+
return auditId
|
|
203
609
|
}
|
|
204
610
|
|
|
611
|
+
// ---------------------------------------------------------------------------
|
|
612
|
+
// Lifecycle callbacks
|
|
613
|
+
// ---------------------------------------------------------------------------
|
|
614
|
+
|
|
205
615
|
/**
|
|
206
|
-
* Captures create changes
|
|
207
|
-
* @param {import("./index.js").default & {_pendingCreateAuditChanges?: AuditChanges}} record - Record
|
|
616
|
+
* Captures create changes before persistence clears the change set.
|
|
617
|
+
* @param {import("./index.js").default & {_pendingCreateAuditChanges?: AuditChanges}} record - Record whose pending changes should be captured.
|
|
208
618
|
* @returns {void}
|
|
209
619
|
*/
|
|
210
620
|
function captureCreateAuditChanges(record) {
|
|
@@ -212,7 +622,7 @@ function captureCreateAuditChanges(record) {
|
|
|
212
622
|
}
|
|
213
623
|
|
|
214
624
|
/**
|
|
215
|
-
*
|
|
625
|
+
* Writes the create audit row for a model instance.
|
|
216
626
|
* @param {import("./index.js").default & {_pendingCreateAuditChanges?: AuditChanges}} record - Record to audit.
|
|
217
627
|
* @returns {Promise<void>}
|
|
218
628
|
*/
|
|
@@ -226,8 +636,8 @@ async function createCreateAudit(record) {
|
|
|
226
636
|
}
|
|
227
637
|
|
|
228
638
|
/**
|
|
229
|
-
* Captures update changes
|
|
230
|
-
* @param {import("./index.js").default & {_pendingUpdateAuditChanges?: AuditChanges}} record - Record
|
|
639
|
+
* Captures update changes before persistence clears the change set.
|
|
640
|
+
* @param {import("./index.js").default & {_pendingUpdateAuditChanges?: AuditChanges}} record - Record whose pending changes should be captured.
|
|
231
641
|
* @returns {void}
|
|
232
642
|
*/
|
|
233
643
|
function captureUpdateAuditChanges(record) {
|
|
@@ -235,7 +645,7 @@ function captureUpdateAuditChanges(record) {
|
|
|
235
645
|
}
|
|
236
646
|
|
|
237
647
|
/**
|
|
238
|
-
*
|
|
648
|
+
* Writes the update audit row for a model instance.
|
|
239
649
|
* @param {import("./index.js").default & {_pendingUpdateAuditChanges?: AuditChanges}} record - Record to audit.
|
|
240
650
|
* @returns {Promise<void>}
|
|
241
651
|
*/
|
|
@@ -253,7 +663,7 @@ async function createUpdateAudit(record) {
|
|
|
253
663
|
}
|
|
254
664
|
|
|
255
665
|
/**
|
|
256
|
-
*
|
|
666
|
+
* Writes the destroy audit row for a model instance.
|
|
257
667
|
* @param {import("./index.js").default} record - Record to audit.
|
|
258
668
|
* @returns {Promise<void>}
|
|
259
669
|
*/
|
|
@@ -264,8 +674,45 @@ async function createDestroyAudit(record) {
|
|
|
264
674
|
})
|
|
265
675
|
}
|
|
266
676
|
|
|
677
|
+
// ---------------------------------------------------------------------------
|
|
678
|
+
// Changes helpers
|
|
679
|
+
// ---------------------------------------------------------------------------
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Captures the new values for fields changed on a record.
|
|
683
|
+
* @param {import("./index.js").default} record - Record whose pending changes should be captured.
|
|
684
|
+
* @returns {AuditChanges} New values keyed by attribute name.
|
|
685
|
+
*/
|
|
686
|
+
function auditChangesForCurrentChanges(record) {
|
|
687
|
+
const changes = record.changes()
|
|
688
|
+
/** @type {AuditChanges} */
|
|
689
|
+
const auditedChanges = {}
|
|
690
|
+
const columnNameToAttributeName = record.getModelClass().getColumnNameToAttributeNameMap()
|
|
691
|
+
|
|
692
|
+
for (const [attributeName, change] of Object.entries(changes)) {
|
|
693
|
+
auditedChanges[columnNameToAttributeName[attributeName] || attributeName] = change[1]
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return auditedChanges
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Captures the current attributes for a destroy audit.
|
|
701
|
+
* @param {import("./index.js").default} record - Record being destroyed.
|
|
702
|
+
* @returns {AuditChanges} Current attributes keyed by attribute name.
|
|
703
|
+
*/
|
|
704
|
+
function auditChangesForDestroy(record) {
|
|
705
|
+
return {...record.attributes()}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// ---------------------------------------------------------------------------
|
|
709
|
+
// withoutAudit
|
|
710
|
+
// ---------------------------------------------------------------------------
|
|
711
|
+
|
|
267
712
|
/**
|
|
268
713
|
* Returns records without an audit row for the given action.
|
|
714
|
+
* Uses shared-table defaults when table data is not yet resolved;
|
|
715
|
+
* switches to the dedicated table path once resolved.
|
|
269
716
|
* @template {AuditedModelClass} MC
|
|
270
717
|
* @param {MC} modelClass - Model class to scope.
|
|
271
718
|
* @param {string} action - Audit action to exclude.
|
|
@@ -274,14 +721,36 @@ async function createDestroyAudit(record) {
|
|
|
274
721
|
function withoutAudit(modelClass, action) {
|
|
275
722
|
const db = modelClass.connection()
|
|
276
723
|
const modelTableSql = db.quoteTable(modelClass.tableName())
|
|
277
|
-
const auditsTableSql = db.quoteTable("audits")
|
|
278
724
|
const auditActionsTableSql = db.quoteTable("audit_actions")
|
|
279
725
|
const modelPrimaryKeySql = `${modelTableSql}.${db.quoteColumn(modelClass.primaryKey())}`
|
|
726
|
+
const auditActionsIdSql = `${auditActionsTableSql}.${db.quoteColumn("id")}`
|
|
727
|
+
const auditActionsActionSql = `${auditActionsTableSql}.${db.quoteColumn("action")}`
|
|
728
|
+
|
|
729
|
+
if (modelClass._auditTableResolved && modelClass._auditTableData?.dedicated) {
|
|
730
|
+
const tableData = modelClass._auditTableData
|
|
731
|
+
const modelKey = modelParamKey(modelClass)
|
|
732
|
+
const auditsTableSql = db.quoteTable(tableData.tableName)
|
|
733
|
+
const auditActionIdSql = `${auditsTableSql}.${db.quoteColumn("audit_action_id")}`
|
|
734
|
+
const modelIdSql = `${auditsTableSql}.${db.quoteColumn(`${modelKey}_id`)}`
|
|
735
|
+
|
|
736
|
+
return modelClass
|
|
737
|
+
.all()
|
|
738
|
+
.where(`
|
|
739
|
+
NOT EXISTS (
|
|
740
|
+
SELECT 1
|
|
741
|
+
FROM ${auditsTableSql}
|
|
742
|
+
INNER JOIN ${auditActionsTableSql}
|
|
743
|
+
ON ${auditActionsIdSql} = ${auditActionIdSql}
|
|
744
|
+
WHERE ${modelIdSql} = ${modelPrimaryKeySql}
|
|
745
|
+
AND ${auditActionsActionSql} = ${db.quote(normalizeAction(action))}
|
|
746
|
+
)
|
|
747
|
+
`)
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const auditsTableSql = db.quoteTable("audits")
|
|
280
751
|
const auditAuditableIdSql = `${auditsTableSql}.${db.quoteColumn("auditable_id")}`
|
|
281
752
|
const auditAuditableTypeSql = `${auditsTableSql}.${db.quoteColumn("auditable_type")}`
|
|
282
753
|
const auditActionIdSql = `${auditsTableSql}.${db.quoteColumn("audit_action_id")}`
|
|
283
|
-
const auditActionsIdSql = `${auditActionsTableSql}.${db.quoteColumn("id")}`
|
|
284
|
-
const auditActionsActionSql = `${auditActionsTableSql}.${db.quoteColumn("action")}`
|
|
285
754
|
|
|
286
755
|
return modelClass
|
|
287
756
|
.all()
|
|
@@ -298,13 +767,80 @@ function withoutAudit(modelClass, action) {
|
|
|
298
767
|
`)
|
|
299
768
|
}
|
|
300
769
|
|
|
770
|
+
// ---------------------------------------------------------------------------
|
|
771
|
+
// Event callbacks
|
|
772
|
+
// ---------------------------------------------------------------------------
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Registers a per-model callback fired after an audit row is created.
|
|
776
|
+
* @param {AuditedModelClass} modelClass - Model class to observe.
|
|
777
|
+
* @param {string} action - Audit action name (e.g. "create").
|
|
778
|
+
* @param {AuditCallback} callback - Callback invoked after matching audit rows are created.
|
|
779
|
+
* @returns {() => void} Unsubscribe function.
|
|
780
|
+
*/
|
|
781
|
+
function registerAuditCallback(modelClass, action, callback) {
|
|
782
|
+
const normalizedAction = normalizeAction(action)
|
|
783
|
+
const callbacks = auditCallbacksForModelClass(modelClass)
|
|
784
|
+
|
|
785
|
+
if (!callbacks[normalizedAction]) {
|
|
786
|
+
callbacks[normalizedAction] = []
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
callbacks[normalizedAction].push(callback)
|
|
790
|
+
|
|
791
|
+
return () => {
|
|
792
|
+
const actionCallbacks = callbacks[normalizedAction]
|
|
793
|
+
const index = actionCallbacks.indexOf(callback)
|
|
794
|
+
|
|
795
|
+
if (index >= 0) {
|
|
796
|
+
actionCallbacks.splice(index, 1)
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Emits per-model audit callbacks for a model/action pair.
|
|
803
|
+
* @param {AuditedModelClass} modelClass - Model class.
|
|
804
|
+
* @param {string} action - Audit action.
|
|
805
|
+
* @param {AuditEventPayload} payload - Event payload.
|
|
806
|
+
* @returns {Promise<void>}
|
|
807
|
+
*/
|
|
808
|
+
async function emitAuditEvent(modelClass, action, payload) {
|
|
809
|
+
const callbacks = auditCallbacksForModelClass(modelClass)[action] || []
|
|
810
|
+
|
|
811
|
+
for (const callback of [...callbacks]) {
|
|
812
|
+
await callback(payload)
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Returns the per-model callback map.
|
|
818
|
+
* @param {AuditedModelClass} modelClass - Model class.
|
|
819
|
+
* @returns {Record<string, AuditCallback[]>} Callback map keyed by action.
|
|
820
|
+
*/
|
|
821
|
+
function auditCallbacksForModelClass(modelClass) {
|
|
822
|
+
if (!Object.prototype.hasOwnProperty.call(modelClass, "_auditCallbacks")) {
|
|
823
|
+
modelClass._auditCallbacks = {}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const callbacks = modelClass._auditCallbacks
|
|
827
|
+
|
|
828
|
+
if (!callbacks) throw new Error(`Audit callbacks weren't initialized for ${modelClass.getModelName()}`)
|
|
829
|
+
|
|
830
|
+
return callbacks
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// ---------------------------------------------------------------------------
|
|
834
|
+
// Lookup helpers
|
|
835
|
+
// ---------------------------------------------------------------------------
|
|
836
|
+
|
|
301
837
|
/**
|
|
302
838
|
* Finds or creates a lookup row and returns its id.
|
|
303
839
|
* @param {object} args - Options object.
|
|
304
|
-
* @param {string} args.columnName - Lookup value column.
|
|
840
|
+
* @param {string} args.columnName - Lookup value column name.
|
|
305
841
|
* @param {Date} args.currentDate - Timestamp to write when inserting.
|
|
306
842
|
* @param {import("../drivers/base.js").default} args.db - Database driver.
|
|
307
|
-
* @param {string} args.tableName - Lookup table.
|
|
843
|
+
* @param {string} args.tableName - Lookup table name.
|
|
308
844
|
* @param {string} args.value - Lookup value.
|
|
309
845
|
* @returns {Promise<number | string>} Lookup row id.
|
|
310
846
|
*/
|
|
@@ -331,9 +867,9 @@ async function findOrCreateLookupId({columnName, currentDate, db, tableName, val
|
|
|
331
867
|
/**
|
|
332
868
|
* Finds a lookup id by value.
|
|
333
869
|
* @param {object} args - Options object.
|
|
334
|
-
* @param {string} args.columnName - Lookup value column.
|
|
870
|
+
* @param {string} args.columnName - Lookup value column name.
|
|
335
871
|
* @param {import("../drivers/base.js").default} args.db - Database driver.
|
|
336
|
-
* @param {string} args.tableName - Lookup table.
|
|
872
|
+
* @param {string} args.tableName - Lookup table name.
|
|
337
873
|
* @param {string} args.value - Lookup value.
|
|
338
874
|
* @returns {Promise<number | string | null>} Lookup row id or null.
|
|
339
875
|
*/
|
|
@@ -350,9 +886,9 @@ async function findLookupId({columnName, db, tableName, value}) {
|
|
|
350
886
|
}
|
|
351
887
|
|
|
352
888
|
/**
|
|
353
|
-
* Normalizes an audit action.
|
|
889
|
+
* Normalizes an audit action string.
|
|
354
890
|
* @param {string} action - Action name.
|
|
355
|
-
* @returns {string}
|
|
891
|
+
* @returns {string} Trimmed, non-empty action name.
|
|
356
892
|
*/
|
|
357
893
|
function normalizeAction(action) {
|
|
358
894
|
const normalizedAction = action.trim()
|
|
@@ -362,45 +898,76 @@ function normalizeAction(action) {
|
|
|
362
898
|
return normalizedAction
|
|
363
899
|
}
|
|
364
900
|
|
|
901
|
+
// ---------------------------------------------------------------------------
|
|
902
|
+
// Migration helpers
|
|
903
|
+
// ---------------------------------------------------------------------------
|
|
904
|
+
|
|
365
905
|
/**
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
* @
|
|
906
|
+
* Creates the shared audit tables migration up/down callbacks for use inside
|
|
907
|
+
* a Migration class. The `table` parameter is a Migration instance.
|
|
908
|
+
* @param {{id?: {type: string}}} [options] - ID column options.
|
|
909
|
+
* @returns {{down: (table: import("../migration/index.js").default) => Promise<void>, up: (table: import("../migration/index.js").default) => Promise<void>}}
|
|
369
910
|
*/
|
|
370
|
-
function
|
|
371
|
-
|
|
372
|
-
|
|
911
|
+
function createSharedAuditTablesMigration(options = {}) {
|
|
912
|
+
const opts = /** @type {{id?: {type: string}}} */ (options)
|
|
913
|
+
const idOptions = /** @type {{type?: string}} */ (opts.id || {})
|
|
914
|
+
const type = idOptions.type
|
|
915
|
+
|
|
916
|
+
return {
|
|
917
|
+
async up(table) {
|
|
918
|
+
await table.createTable("audit_actions", {id: /** @type {{type?: string}} */ (opts.id || {})}, (/** @type {{string(name: string, options: Record<string, unknown>): void, timestamps(): void}} */ t) => {
|
|
919
|
+
t.string("action", {index: {unique: true}, null: false})
|
|
920
|
+
t.timestamps()
|
|
921
|
+
})
|
|
922
|
+
|
|
923
|
+
await table.createTable("audit_auditable_types", {id: /** @type {{type?: string}} */ (opts.id || {})}, (/** @type {{string(name: string, options: Record<string, unknown>): void, timestamps(): void}} */ t) => {
|
|
924
|
+
t.string("name", {index: {unique: true}, null: false})
|
|
925
|
+
t.timestamps()
|
|
926
|
+
})
|
|
927
|
+
|
|
928
|
+
await table.createTable("audits", {id: /** @type {{type?: string}} */ (opts.id || {})}, (/** @type {{json(name: string): void, references(name: string, options: Record<string, unknown>): void, timestamps(): void}} */ t) => {
|
|
929
|
+
t.references("audit_action", {foreignKey: true, null: false, type})
|
|
930
|
+
t.references("audit_auditable_type", {foreignKey: true, null: false, type})
|
|
931
|
+
t.references("auditable", {null: false, polymorphic: true, type})
|
|
932
|
+
t.json("audited_changes")
|
|
933
|
+
t.json("params")
|
|
934
|
+
t.timestamps()
|
|
935
|
+
})
|
|
936
|
+
},
|
|
937
|
+
|
|
938
|
+
async down(table) {
|
|
939
|
+
await table.dropTable("audits")
|
|
940
|
+
await table.dropTable("audit_auditable_types")
|
|
941
|
+
await table.dropTable("audit_actions")
|
|
942
|
+
}
|
|
373
943
|
}
|
|
374
|
-
|
|
375
|
-
const callbacks = modelClass._auditCallbacks
|
|
376
|
-
|
|
377
|
-
if (!callbacks) throw new Error(`Audit callbacks weren't initialized for ${modelClass.getModelName()}`)
|
|
378
|
-
|
|
379
|
-
return callbacks
|
|
380
944
|
}
|
|
381
945
|
|
|
382
946
|
/**
|
|
383
|
-
*
|
|
384
|
-
* @param {
|
|
385
|
-
* @
|
|
386
|
-
* @param {AuditEventPayload} payload - Event payload.
|
|
387
|
-
* @returns {Promise<void>}
|
|
947
|
+
* Returns the dedicated audit table name for a given model table name.
|
|
948
|
+
* @param {string} modelTableName - Model table name (e.g. "projects").
|
|
949
|
+
* @returns {string} Dedicated audit table name (e.g. "project_audits").
|
|
388
950
|
*/
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
for (const callback of [...callbacks]) {
|
|
393
|
-
await callback(payload)
|
|
951
|
+
function dedicatedAuditTableNameForTable(modelTableName) {
|
|
952
|
+
if (modelTableName.endsWith("s")) {
|
|
953
|
+
return `${modelTableName.slice(0, -1)}_audits`
|
|
394
954
|
}
|
|
955
|
+
|
|
956
|
+
return `${modelTableName}_audits`
|
|
395
957
|
}
|
|
396
958
|
|
|
397
959
|
export {
|
|
960
|
+
AuditEvents,
|
|
398
961
|
captureCreateAuditChanges,
|
|
399
962
|
captureUpdateAuditChanges,
|
|
400
963
|
createAudit,
|
|
401
964
|
createCreateAudit,
|
|
402
965
|
createDestroyAudit,
|
|
403
966
|
createUpdateAudit,
|
|
967
|
+
createSharedAuditTablesMigration,
|
|
968
|
+
dedicatedAuditTableNameForTable,
|
|
969
|
+
initializeAuditedModelRelationships,
|
|
970
|
+
initializeAuditing,
|
|
404
971
|
registerAuditCallback,
|
|
405
972
|
registerAuditing,
|
|
406
973
|
withoutAudit
|