velocious 1.0.507 → 1.0.508
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 +659 -120
- 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 +561 -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 +659 -120
- 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,117 @@ 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 && auditedModelClass._initialized && canResolveAuditTableData(auditedModelClass)) {
|
|
389
|
+
await resolveAuditTableData(auditedModelClass)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
79
392
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
for (const modelClass of Object.values(configuration.getModelClasses())) {
|
|
400
|
+
await initializeAuditing(modelClass, {resolveTableData: true})
|
|
83
401
|
}
|
|
84
402
|
}
|
|
85
403
|
|
|
86
404
|
/**
|
|
87
|
-
*
|
|
88
|
-
* @param {
|
|
89
|
-
* @
|
|
405
|
+
* Registers the audits relationship without forcing audit table detection.
|
|
406
|
+
* @param {AuditedModelClass} modelClass - Model class to audit.
|
|
407
|
+
* @param {AuditTableData} [tableData] - Resolved audit table data when available.
|
|
408
|
+
* @returns {void}
|
|
90
409
|
*/
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
410
|
+
function registerAuditRelationship(modelClass, tableData = defaultAuditRelationshipTableData(modelClass)) {
|
|
411
|
+
if (modelClass._relationshipExists("audits")) {
|
|
412
|
+
const relationship = modelClass.getRelationshipByName("audits")
|
|
413
|
+
|
|
414
|
+
if (generatedAuditRelationships.has(relationship)) {
|
|
415
|
+
relationship.className = undefined
|
|
416
|
+
relationship.klass = tableData.auditClass
|
|
417
|
+
relationship.foreignKey = tableData.foreignKey
|
|
418
|
+
relationship._explicitForeignKey = tableData.foreignKey
|
|
419
|
+
relationship._polymorphic = !tableData.dedicated
|
|
420
|
+
relationship._polymorphicTypeColumn = undefined
|
|
421
|
+
}
|
|
96
422
|
|
|
97
|
-
|
|
98
|
-
auditedChanges[columnNameToAttributeName[attributeName] || attributeName] = change[1]
|
|
423
|
+
return
|
|
99
424
|
}
|
|
100
425
|
|
|
101
|
-
|
|
426
|
+
modelClass.hasMany("audits", auditRelationshipScope, {
|
|
427
|
+
foreignKey: tableData.foreignKey,
|
|
428
|
+
klass: tableData.auditClass,
|
|
429
|
+
polymorphic: !tableData.dedicated
|
|
430
|
+
})
|
|
431
|
+
generatedAuditRelationships.add(modelClass.getRelationshipByName("audits"))
|
|
102
432
|
}
|
|
103
433
|
|
|
104
434
|
/**
|
|
105
|
-
*
|
|
106
|
-
* @param {
|
|
107
|
-
* @returns {
|
|
435
|
+
* Returns unresolved shared-audit defaults for early relationship registration.
|
|
436
|
+
* @param {AuditedModelClass} modelClass - Model class to audit.
|
|
437
|
+
* @returns {AuditTableData} Shared audit relationship defaults.
|
|
108
438
|
*/
|
|
109
|
-
function
|
|
110
|
-
return {
|
|
439
|
+
function defaultAuditRelationshipTableData(modelClass) {
|
|
440
|
+
return {
|
|
441
|
+
auditClass: cachedSharedAuditClass(modelClass),
|
|
442
|
+
dedicated: false,
|
|
443
|
+
foreignKey: "auditable_id",
|
|
444
|
+
tableName: "audits"
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Applies default audit ordering to generated audit relationships.
|
|
450
|
+
* @param {import("../query/model-class-query.js").default<typeof import("./index.js").default>} query - Audit query.
|
|
451
|
+
* @returns {import("../query/model-class-query.js").default<typeof import("./index.js").default>} Ordered audit query.
|
|
452
|
+
*/
|
|
453
|
+
function auditRelationshipScope(query) {
|
|
454
|
+
return query.order({column: "created_at", direction: "DESC"})
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Checks whether the current tenant context can resolve audit table data.
|
|
459
|
+
* @param {AuditedModelClass} modelClass - Model class to inspect.
|
|
460
|
+
* @returns {boolean} Whether audit table data can be resolved in the current scope.
|
|
461
|
+
*/
|
|
462
|
+
function canResolveAuditTableData(modelClass) {
|
|
463
|
+
if (!modelClass.hasTenantDatabaseIdentifierResolver()) {
|
|
464
|
+
return true
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const tenantDatabaseIdentifier = modelClass.getTenantDatabaseIdentifier()
|
|
468
|
+
|
|
469
|
+
if (!tenantDatabaseIdentifier) {
|
|
470
|
+
return false
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return modelClass._getConfiguration().isDatabaseIdentifierActive(tenantDatabaseIdentifier)
|
|
111
474
|
}
|
|
112
475
|
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
// Creating audits
|
|
478
|
+
// ---------------------------------------------------------------------------
|
|
479
|
+
|
|
113
480
|
/**
|
|
114
481
|
* Creates an audit row for a record.
|
|
115
482
|
* @param {import("./index.js").default} record - Record to audit.
|
|
116
|
-
* @param {CreateAuditArgs} args - Audit row options.
|
|
483
|
+
* @param {CreateAuditArgs} args - Audit row options (action, auditedChanges, params).
|
|
117
484
|
* @returns {Promise<number | string>} Created audit row id.
|
|
118
485
|
*/
|
|
119
486
|
async function createAudit(record, args) {
|
|
@@ -126,85 +493,100 @@ async function createAudit(record, args) {
|
|
|
126
493
|
|
|
127
494
|
/**
|
|
128
495
|
* Creates an audit row using the current model database connection.
|
|
496
|
+
* Routes to shared table or dedicated table based on resolved audit table data.
|
|
129
497
|
* @param {import("./index.js").default} record - Record to audit.
|
|
130
|
-
* @param {CreateAuditArgs} args - Audit row options.
|
|
498
|
+
* @param {CreateAuditArgs} args - Audit row options (action, auditedChanges, params).
|
|
131
499
|
* @param {AuditedModelClass} modelClass - Audited model class.
|
|
132
500
|
* @returns {Promise<number | string>} Created audit row id.
|
|
133
501
|
*/
|
|
134
502
|
async function createAuditWithCurrentConnection(record, args, modelClass) {
|
|
135
503
|
if (!record.isPersisted()) throw new Error(`Cannot audit unpersisted ${modelClass.getModelName()} record`)
|
|
136
504
|
|
|
505
|
+
const tableData = await resolveAuditTableData(modelClass)
|
|
137
506
|
const action = normalizeAction(args.action)
|
|
138
507
|
const auditedChanges = args.auditedChanges === undefined ? null : args.auditedChanges
|
|
139
508
|
const params = args.params === undefined ? null : args.params
|
|
140
509
|
const db = modelClass.connection()
|
|
141
510
|
const currentDate = new Date()
|
|
511
|
+
|
|
142
512
|
const auditActionId = await findOrCreateLookupId({
|
|
143
513
|
columnName: "action",
|
|
514
|
+
currentDate,
|
|
144
515
|
db,
|
|
145
516
|
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
|
|
517
|
+
value: action
|
|
155
518
|
})
|
|
519
|
+
|
|
156
520
|
const auditId = new UUID(4).format()
|
|
157
521
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
522
|
+
if (tableData.dedicated) {
|
|
523
|
+
const modelKey = modelParamKey(modelClass)
|
|
524
|
+
|
|
525
|
+
await db.query(db.insertSql({
|
|
526
|
+
returnLastInsertedColumnNames: ["id"],
|
|
527
|
+
tableName: tableData.tableName,
|
|
528
|
+
data: {
|
|
529
|
+
id: auditId,
|
|
530
|
+
[`${modelKey}_id`]: record.id(),
|
|
531
|
+
audit_action_id: auditActionId,
|
|
532
|
+
audited_changes: auditedChanges,
|
|
533
|
+
params,
|
|
534
|
+
created_at: currentDate,
|
|
535
|
+
updated_at: currentDate
|
|
536
|
+
}
|
|
537
|
+
}))
|
|
538
|
+
} else {
|
|
539
|
+
const auditAuditableTypeId = await findOrCreateLookupId({
|
|
540
|
+
columnName: "name",
|
|
541
|
+
currentDate,
|
|
542
|
+
db,
|
|
543
|
+
tableName: "audit_auditable_types",
|
|
544
|
+
value: modelClass.getModelName()
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
await db.query(db.insertSql({
|
|
548
|
+
returnLastInsertedColumnNames: ["id"],
|
|
549
|
+
tableName: "audits",
|
|
550
|
+
data: {
|
|
551
|
+
id: auditId,
|
|
552
|
+
audit_action_id: auditActionId,
|
|
553
|
+
audit_auditable_type_id: auditAuditableTypeId,
|
|
554
|
+
auditable_id: record.id(),
|
|
555
|
+
auditable_type: modelClass.getModelName(),
|
|
556
|
+
audited_changes: auditedChanges,
|
|
557
|
+
params,
|
|
558
|
+
created_at: currentDate,
|
|
559
|
+
updated_at: currentDate
|
|
560
|
+
}
|
|
561
|
+
}))
|
|
562
|
+
}
|
|
174
563
|
|
|
175
564
|
await emitAuditEvent(modelClass, action, {
|
|
176
565
|
action,
|
|
177
|
-
auditId
|
|
566
|
+
auditId,
|
|
178
567
|
auditedChanges,
|
|
179
568
|
params,
|
|
180
569
|
record
|
|
181
570
|
})
|
|
182
571
|
|
|
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
|
|
572
|
+
AuditEvents.call(modelClass.getModelName(), action, {
|
|
573
|
+
action,
|
|
574
|
+
auditId,
|
|
575
|
+
auditedChanges,
|
|
576
|
+
params,
|
|
577
|
+
record
|
|
578
|
+
})
|
|
201
579
|
|
|
202
|
-
return
|
|
580
|
+
return auditId
|
|
203
581
|
}
|
|
204
582
|
|
|
583
|
+
// ---------------------------------------------------------------------------
|
|
584
|
+
// Lifecycle callbacks
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
|
|
205
587
|
/**
|
|
206
|
-
* Captures create changes
|
|
207
|
-
* @param {import("./index.js").default & {_pendingCreateAuditChanges?: AuditChanges}} record - Record
|
|
588
|
+
* Captures create changes before persistence clears the change set.
|
|
589
|
+
* @param {import("./index.js").default & {_pendingCreateAuditChanges?: AuditChanges}} record - Record whose pending changes should be captured.
|
|
208
590
|
* @returns {void}
|
|
209
591
|
*/
|
|
210
592
|
function captureCreateAuditChanges(record) {
|
|
@@ -212,7 +594,7 @@ function captureCreateAuditChanges(record) {
|
|
|
212
594
|
}
|
|
213
595
|
|
|
214
596
|
/**
|
|
215
|
-
*
|
|
597
|
+
* Writes the create audit row for a model instance.
|
|
216
598
|
* @param {import("./index.js").default & {_pendingCreateAuditChanges?: AuditChanges}} record - Record to audit.
|
|
217
599
|
* @returns {Promise<void>}
|
|
218
600
|
*/
|
|
@@ -226,8 +608,8 @@ async function createCreateAudit(record) {
|
|
|
226
608
|
}
|
|
227
609
|
|
|
228
610
|
/**
|
|
229
|
-
* Captures update changes
|
|
230
|
-
* @param {import("./index.js").default & {_pendingUpdateAuditChanges?: AuditChanges}} record - Record
|
|
611
|
+
* Captures update changes before persistence clears the change set.
|
|
612
|
+
* @param {import("./index.js").default & {_pendingUpdateAuditChanges?: AuditChanges}} record - Record whose pending changes should be captured.
|
|
231
613
|
* @returns {void}
|
|
232
614
|
*/
|
|
233
615
|
function captureUpdateAuditChanges(record) {
|
|
@@ -235,7 +617,7 @@ function captureUpdateAuditChanges(record) {
|
|
|
235
617
|
}
|
|
236
618
|
|
|
237
619
|
/**
|
|
238
|
-
*
|
|
620
|
+
* Writes the update audit row for a model instance.
|
|
239
621
|
* @param {import("./index.js").default & {_pendingUpdateAuditChanges?: AuditChanges}} record - Record to audit.
|
|
240
622
|
* @returns {Promise<void>}
|
|
241
623
|
*/
|
|
@@ -253,7 +635,7 @@ async function createUpdateAudit(record) {
|
|
|
253
635
|
}
|
|
254
636
|
|
|
255
637
|
/**
|
|
256
|
-
*
|
|
638
|
+
* Writes the destroy audit row for a model instance.
|
|
257
639
|
* @param {import("./index.js").default} record - Record to audit.
|
|
258
640
|
* @returns {Promise<void>}
|
|
259
641
|
*/
|
|
@@ -264,8 +646,45 @@ async function createDestroyAudit(record) {
|
|
|
264
646
|
})
|
|
265
647
|
}
|
|
266
648
|
|
|
649
|
+
// ---------------------------------------------------------------------------
|
|
650
|
+
// Changes helpers
|
|
651
|
+
// ---------------------------------------------------------------------------
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Captures the new values for fields changed on a record.
|
|
655
|
+
* @param {import("./index.js").default} record - Record whose pending changes should be captured.
|
|
656
|
+
* @returns {AuditChanges} New values keyed by attribute name.
|
|
657
|
+
*/
|
|
658
|
+
function auditChangesForCurrentChanges(record) {
|
|
659
|
+
const changes = record.changes()
|
|
660
|
+
/** @type {AuditChanges} */
|
|
661
|
+
const auditedChanges = {}
|
|
662
|
+
const columnNameToAttributeName = record.getModelClass().getColumnNameToAttributeNameMap()
|
|
663
|
+
|
|
664
|
+
for (const [attributeName, change] of Object.entries(changes)) {
|
|
665
|
+
auditedChanges[columnNameToAttributeName[attributeName] || attributeName] = change[1]
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return auditedChanges
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Captures the current attributes for a destroy audit.
|
|
673
|
+
* @param {import("./index.js").default} record - Record being destroyed.
|
|
674
|
+
* @returns {AuditChanges} Current attributes keyed by attribute name.
|
|
675
|
+
*/
|
|
676
|
+
function auditChangesForDestroy(record) {
|
|
677
|
+
return {...record.attributes()}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// ---------------------------------------------------------------------------
|
|
681
|
+
// withoutAudit
|
|
682
|
+
// ---------------------------------------------------------------------------
|
|
683
|
+
|
|
267
684
|
/**
|
|
268
685
|
* Returns records without an audit row for the given action.
|
|
686
|
+
* Uses shared-table defaults when table data is not yet resolved;
|
|
687
|
+
* switches to the dedicated table path once resolved.
|
|
269
688
|
* @template {AuditedModelClass} MC
|
|
270
689
|
* @param {MC} modelClass - Model class to scope.
|
|
271
690
|
* @param {string} action - Audit action to exclude.
|
|
@@ -274,14 +693,36 @@ async function createDestroyAudit(record) {
|
|
|
274
693
|
function withoutAudit(modelClass, action) {
|
|
275
694
|
const db = modelClass.connection()
|
|
276
695
|
const modelTableSql = db.quoteTable(modelClass.tableName())
|
|
277
|
-
const auditsTableSql = db.quoteTable("audits")
|
|
278
696
|
const auditActionsTableSql = db.quoteTable("audit_actions")
|
|
279
697
|
const modelPrimaryKeySql = `${modelTableSql}.${db.quoteColumn(modelClass.primaryKey())}`
|
|
698
|
+
const auditActionsIdSql = `${auditActionsTableSql}.${db.quoteColumn("id")}`
|
|
699
|
+
const auditActionsActionSql = `${auditActionsTableSql}.${db.quoteColumn("action")}`
|
|
700
|
+
|
|
701
|
+
if (modelClass._auditTableResolved && modelClass._auditTableData?.dedicated) {
|
|
702
|
+
const tableData = modelClass._auditTableData
|
|
703
|
+
const modelKey = modelParamKey(modelClass)
|
|
704
|
+
const auditsTableSql = db.quoteTable(tableData.tableName)
|
|
705
|
+
const auditActionIdSql = `${auditsTableSql}.${db.quoteColumn("audit_action_id")}`
|
|
706
|
+
const modelIdSql = `${auditsTableSql}.${db.quoteColumn(`${modelKey}_id`)}`
|
|
707
|
+
|
|
708
|
+
return modelClass
|
|
709
|
+
.all()
|
|
710
|
+
.where(`
|
|
711
|
+
NOT EXISTS (
|
|
712
|
+
SELECT 1
|
|
713
|
+
FROM ${auditsTableSql}
|
|
714
|
+
INNER JOIN ${auditActionsTableSql}
|
|
715
|
+
ON ${auditActionsIdSql} = ${auditActionIdSql}
|
|
716
|
+
WHERE ${modelIdSql} = ${modelPrimaryKeySql}
|
|
717
|
+
AND ${auditActionsActionSql} = ${db.quote(normalizeAction(action))}
|
|
718
|
+
)
|
|
719
|
+
`)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const auditsTableSql = db.quoteTable("audits")
|
|
280
723
|
const auditAuditableIdSql = `${auditsTableSql}.${db.quoteColumn("auditable_id")}`
|
|
281
724
|
const auditAuditableTypeSql = `${auditsTableSql}.${db.quoteColumn("auditable_type")}`
|
|
282
725
|
const auditActionIdSql = `${auditsTableSql}.${db.quoteColumn("audit_action_id")}`
|
|
283
|
-
const auditActionsIdSql = `${auditActionsTableSql}.${db.quoteColumn("id")}`
|
|
284
|
-
const auditActionsActionSql = `${auditActionsTableSql}.${db.quoteColumn("action")}`
|
|
285
726
|
|
|
286
727
|
return modelClass
|
|
287
728
|
.all()
|
|
@@ -298,13 +739,80 @@ function withoutAudit(modelClass, action) {
|
|
|
298
739
|
`)
|
|
299
740
|
}
|
|
300
741
|
|
|
742
|
+
// ---------------------------------------------------------------------------
|
|
743
|
+
// Event callbacks
|
|
744
|
+
// ---------------------------------------------------------------------------
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Registers a per-model callback fired after an audit row is created.
|
|
748
|
+
* @param {AuditedModelClass} modelClass - Model class to observe.
|
|
749
|
+
* @param {string} action - Audit action name (e.g. "create").
|
|
750
|
+
* @param {AuditCallback} callback - Callback invoked after matching audit rows are created.
|
|
751
|
+
* @returns {() => void} Unsubscribe function.
|
|
752
|
+
*/
|
|
753
|
+
function registerAuditCallback(modelClass, action, callback) {
|
|
754
|
+
const normalizedAction = normalizeAction(action)
|
|
755
|
+
const callbacks = auditCallbacksForModelClass(modelClass)
|
|
756
|
+
|
|
757
|
+
if (!callbacks[normalizedAction]) {
|
|
758
|
+
callbacks[normalizedAction] = []
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
callbacks[normalizedAction].push(callback)
|
|
762
|
+
|
|
763
|
+
return () => {
|
|
764
|
+
const actionCallbacks = callbacks[normalizedAction]
|
|
765
|
+
const index = actionCallbacks.indexOf(callback)
|
|
766
|
+
|
|
767
|
+
if (index >= 0) {
|
|
768
|
+
actionCallbacks.splice(index, 1)
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Emits per-model audit callbacks for a model/action pair.
|
|
775
|
+
* @param {AuditedModelClass} modelClass - Model class.
|
|
776
|
+
* @param {string} action - Audit action.
|
|
777
|
+
* @param {AuditEventPayload} payload - Event payload.
|
|
778
|
+
* @returns {Promise<void>}
|
|
779
|
+
*/
|
|
780
|
+
async function emitAuditEvent(modelClass, action, payload) {
|
|
781
|
+
const callbacks = auditCallbacksForModelClass(modelClass)[action] || []
|
|
782
|
+
|
|
783
|
+
for (const callback of [...callbacks]) {
|
|
784
|
+
await callback(payload)
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Returns the per-model callback map.
|
|
790
|
+
* @param {AuditedModelClass} modelClass - Model class.
|
|
791
|
+
* @returns {Record<string, AuditCallback[]>} Callback map keyed by action.
|
|
792
|
+
*/
|
|
793
|
+
function auditCallbacksForModelClass(modelClass) {
|
|
794
|
+
if (!Object.prototype.hasOwnProperty.call(modelClass, "_auditCallbacks")) {
|
|
795
|
+
modelClass._auditCallbacks = {}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const callbacks = modelClass._auditCallbacks
|
|
799
|
+
|
|
800
|
+
if (!callbacks) throw new Error(`Audit callbacks weren't initialized for ${modelClass.getModelName()}`)
|
|
801
|
+
|
|
802
|
+
return callbacks
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// ---------------------------------------------------------------------------
|
|
806
|
+
// Lookup helpers
|
|
807
|
+
// ---------------------------------------------------------------------------
|
|
808
|
+
|
|
301
809
|
/**
|
|
302
810
|
* Finds or creates a lookup row and returns its id.
|
|
303
811
|
* @param {object} args - Options object.
|
|
304
|
-
* @param {string} args.columnName - Lookup value column.
|
|
812
|
+
* @param {string} args.columnName - Lookup value column name.
|
|
305
813
|
* @param {Date} args.currentDate - Timestamp to write when inserting.
|
|
306
814
|
* @param {import("../drivers/base.js").default} args.db - Database driver.
|
|
307
|
-
* @param {string} args.tableName - Lookup table.
|
|
815
|
+
* @param {string} args.tableName - Lookup table name.
|
|
308
816
|
* @param {string} args.value - Lookup value.
|
|
309
817
|
* @returns {Promise<number | string>} Lookup row id.
|
|
310
818
|
*/
|
|
@@ -331,9 +839,9 @@ async function findOrCreateLookupId({columnName, currentDate, db, tableName, val
|
|
|
331
839
|
/**
|
|
332
840
|
* Finds a lookup id by value.
|
|
333
841
|
* @param {object} args - Options object.
|
|
334
|
-
* @param {string} args.columnName - Lookup value column.
|
|
842
|
+
* @param {string} args.columnName - Lookup value column name.
|
|
335
843
|
* @param {import("../drivers/base.js").default} args.db - Database driver.
|
|
336
|
-
* @param {string} args.tableName - Lookup table.
|
|
844
|
+
* @param {string} args.tableName - Lookup table name.
|
|
337
845
|
* @param {string} args.value - Lookup value.
|
|
338
846
|
* @returns {Promise<number | string | null>} Lookup row id or null.
|
|
339
847
|
*/
|
|
@@ -350,9 +858,9 @@ async function findLookupId({columnName, db, tableName, value}) {
|
|
|
350
858
|
}
|
|
351
859
|
|
|
352
860
|
/**
|
|
353
|
-
* Normalizes an audit action.
|
|
861
|
+
* Normalizes an audit action string.
|
|
354
862
|
* @param {string} action - Action name.
|
|
355
|
-
* @returns {string}
|
|
863
|
+
* @returns {string} Trimmed, non-empty action name.
|
|
356
864
|
*/
|
|
357
865
|
function normalizeAction(action) {
|
|
358
866
|
const normalizedAction = action.trim()
|
|
@@ -362,45 +870,76 @@ function normalizeAction(action) {
|
|
|
362
870
|
return normalizedAction
|
|
363
871
|
}
|
|
364
872
|
|
|
873
|
+
// ---------------------------------------------------------------------------
|
|
874
|
+
// Migration helpers
|
|
875
|
+
// ---------------------------------------------------------------------------
|
|
876
|
+
|
|
365
877
|
/**
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
* @
|
|
878
|
+
* Creates the shared audit tables migration up/down callbacks for use inside
|
|
879
|
+
* a Migration class. The `table` parameter is a Migration instance.
|
|
880
|
+
* @param {{id?: {type: string}}} [options] - ID column options.
|
|
881
|
+
* @returns {{down: (table: import("../migration/index.js").default) => Promise<void>, up: (table: import("../migration/index.js").default) => Promise<void>}}
|
|
369
882
|
*/
|
|
370
|
-
function
|
|
371
|
-
|
|
372
|
-
|
|
883
|
+
function createSharedAuditTablesMigration(options = {}) {
|
|
884
|
+
const opts = /** @type {{id?: {type: string}}} */ (options)
|
|
885
|
+
const idOptions = /** @type {{type?: string}} */ (opts.id || {})
|
|
886
|
+
const type = idOptions.type
|
|
887
|
+
|
|
888
|
+
return {
|
|
889
|
+
async up(table) {
|
|
890
|
+
await table.createTable("audit_actions", {id: /** @type {{type?: string}} */ (opts.id || {})}, (/** @type {{string(name: string, options: Record<string, unknown>): void, timestamps(): void}} */ t) => {
|
|
891
|
+
t.string("action", {index: {unique: true}, null: false})
|
|
892
|
+
t.timestamps()
|
|
893
|
+
})
|
|
894
|
+
|
|
895
|
+
await table.createTable("audit_auditable_types", {id: /** @type {{type?: string}} */ (opts.id || {})}, (/** @type {{string(name: string, options: Record<string, unknown>): void, timestamps(): void}} */ t) => {
|
|
896
|
+
t.string("name", {index: {unique: true}, null: false})
|
|
897
|
+
t.timestamps()
|
|
898
|
+
})
|
|
899
|
+
|
|
900
|
+
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) => {
|
|
901
|
+
t.references("audit_action", {foreignKey: true, null: false, type})
|
|
902
|
+
t.references("audit_auditable_type", {foreignKey: true, null: false, type})
|
|
903
|
+
t.references("auditable", {null: false, polymorphic: true, type})
|
|
904
|
+
t.json("audited_changes")
|
|
905
|
+
t.json("params")
|
|
906
|
+
t.timestamps()
|
|
907
|
+
})
|
|
908
|
+
},
|
|
909
|
+
|
|
910
|
+
async down(table) {
|
|
911
|
+
await table.dropTable("audits")
|
|
912
|
+
await table.dropTable("audit_auditable_types")
|
|
913
|
+
await table.dropTable("audit_actions")
|
|
914
|
+
}
|
|
373
915
|
}
|
|
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
916
|
}
|
|
381
917
|
|
|
382
918
|
/**
|
|
383
|
-
*
|
|
384
|
-
* @param {
|
|
385
|
-
* @
|
|
386
|
-
* @param {AuditEventPayload} payload - Event payload.
|
|
387
|
-
* @returns {Promise<void>}
|
|
919
|
+
* Returns the dedicated audit table name for a given model table name.
|
|
920
|
+
* @param {string} modelTableName - Model table name (e.g. "projects").
|
|
921
|
+
* @returns {string} Dedicated audit table name (e.g. "project_audits").
|
|
388
922
|
*/
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
for (const callback of [...callbacks]) {
|
|
393
|
-
await callback(payload)
|
|
923
|
+
function dedicatedAuditTableNameForTable(modelTableName) {
|
|
924
|
+
if (modelTableName.endsWith("s")) {
|
|
925
|
+
return `${modelTableName.slice(0, -1)}_audits`
|
|
394
926
|
}
|
|
927
|
+
|
|
928
|
+
return `${modelTableName}_audits`
|
|
395
929
|
}
|
|
396
930
|
|
|
397
931
|
export {
|
|
932
|
+
AuditEvents,
|
|
398
933
|
captureCreateAuditChanges,
|
|
399
934
|
captureUpdateAuditChanges,
|
|
400
935
|
createAudit,
|
|
401
936
|
createCreateAudit,
|
|
402
937
|
createDestroyAudit,
|
|
403
938
|
createUpdateAudit,
|
|
939
|
+
createSharedAuditTablesMigration,
|
|
940
|
+
dedicatedAuditTableNameForTable,
|
|
941
|
+
initializeAuditedModelRelationships,
|
|
942
|
+
initializeAuditing,
|
|
404
943
|
registerAuditCallback,
|
|
405
944
|
registerAuditing,
|
|
406
945
|
withoutAudit
|