velocious 1.0.484 → 1.0.486
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/README.md +2 -1
- package/build/current.js +3 -2
- package/build/database/query/create-index-base.js +1 -1
- package/build/database/query/create-table-base.js +2 -2
- package/build/database/record/auditing.js +407 -0
- package/build/database/record/index.js +101 -0
- package/build/src/current.d.ts +4 -3
- package/build/src/current.d.ts.map +1 -1
- package/build/src/current.js +4 -3
- package/build/src/database/query/create-index-base.js +2 -2
- package/build/src/database/query/create-table-base.js +4 -4
- package/build/src/database/record/auditing.d.ts +156 -0
- package/build/src/database/record/auditing.d.ts.map +1 -0
- package/build/src/database/record/auditing.js +349 -0
- package/build/src/database/record/index.d.ts +69 -0
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +89 -1
- package/build/src/tenants/tenant.d.ts +4 -3
- package/build/src/tenants/tenant.d.ts.map +1 -1
- package/build/src/tenants/tenant.js +4 -3
- package/build/tenants/tenant.js +3 -2
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/current.js +3 -2
- package/src/database/query/create-index-base.js +1 -1
- package/src/database/query/create-table-base.js +2 -2
- package/src/database/record/auditing.js +407 -0
- package/src/database/record/index.js +101 -0
- package/src/tenants/tenant.js +3 -2
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import UUID from "pure-uuid"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AuditChanges type.
|
|
7
|
+
* @typedef {Record<string, ?>} AuditChanges
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* AuditEventPayload type.
|
|
12
|
+
* @typedef {object} AuditEventPayload
|
|
13
|
+
* @property {string} action - Audit action name.
|
|
14
|
+
* @property {number | string} auditId - Created audit row id.
|
|
15
|
+
* @property {AuditChanges | null} auditedChanges - Changes captured for the audit.
|
|
16
|
+
* @property {Record<string, ?> | null} params - Optional caller-supplied audit params.
|
|
17
|
+
* @property {import("./index.js").default} record - Audited record.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* AuditCallback type.
|
|
22
|
+
* @typedef {(payload: AuditEventPayload) => void | Promise<void>} AuditCallback
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* CreateAuditArgs type.
|
|
27
|
+
* @typedef {object} CreateAuditArgs
|
|
28
|
+
* @property {string} action - Audit action name.
|
|
29
|
+
* @property {AuditChanges | null} [auditedChanges] - Explicit changes to persist.
|
|
30
|
+
* @property {Record<string, ?> | null} [params] - Optional metadata to store with the audit.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* AuditedModelClass type.
|
|
35
|
+
* @typedef {typeof import("./index.js").default & {
|
|
36
|
+
* _auditCallbacks?: Record<string, AuditCallback[]>,
|
|
37
|
+
* _auditLifecycleCallbacksRegistered?: boolean
|
|
38
|
+
* }} AuditedModelClass
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Adds automatic create/update/destroy auditing callbacks to a model class.
|
|
43
|
+
* @param {AuditedModelClass} modelClass - Model class to audit.
|
|
44
|
+
* @returns {void}
|
|
45
|
+
*/
|
|
46
|
+
function registerAuditing(modelClass) {
|
|
47
|
+
if (Object.prototype.hasOwnProperty.call(modelClass, "_auditLifecycleCallbacksRegistered") && modelClass._auditLifecycleCallbacksRegistered) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
modelClass._auditLifecycleCallbacksRegistered = true
|
|
52
|
+
modelClass.beforeCreate("captureCreateAuditChanges")
|
|
53
|
+
modelClass.afterCreate("createCreateAudit")
|
|
54
|
+
modelClass.beforeUpdate("captureUpdateAuditChanges")
|
|
55
|
+
modelClass.afterUpdate("createUpdateAudit")
|
|
56
|
+
modelClass.afterDestroy("createDestroyAudit")
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Registers an audit event callback for a model/action pair.
|
|
61
|
+
* @param {AuditedModelClass} modelClass - Model class to observe.
|
|
62
|
+
* @param {string} action - Audit action name.
|
|
63
|
+
* @param {AuditCallback} callback - Callback invoked after matching audit rows are created.
|
|
64
|
+
* @returns {() => void} Unsubscribe function.
|
|
65
|
+
*/
|
|
66
|
+
function registerAuditCallback(modelClass, action, callback) {
|
|
67
|
+
const normalizedAction = normalizeAction(action)
|
|
68
|
+
const callbacks = auditCallbacksForModelClass(modelClass)
|
|
69
|
+
|
|
70
|
+
if (!callbacks[normalizedAction]) {
|
|
71
|
+
callbacks[normalizedAction] = []
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
callbacks[normalizedAction].push(callback)
|
|
75
|
+
|
|
76
|
+
return () => {
|
|
77
|
+
const actionCallbacks = callbacks[normalizedAction]
|
|
78
|
+
const index = actionCallbacks.indexOf(callback)
|
|
79
|
+
|
|
80
|
+
if (index >= 0) {
|
|
81
|
+
actionCallbacks.splice(index, 1)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Captures the new values for fields changed on a record.
|
|
88
|
+
* @param {import("./index.js").default} record - Record whose pending changes should be captured.
|
|
89
|
+
* @returns {AuditChanges} New values keyed by attribute name.
|
|
90
|
+
*/
|
|
91
|
+
function auditChangesForCurrentChanges(record) {
|
|
92
|
+
const changes = record.changes()
|
|
93
|
+
/** @type {AuditChanges} */
|
|
94
|
+
const auditedChanges = {}
|
|
95
|
+
const columnNameToAttributeName = record.getModelClass().getColumnNameToAttributeNameMap()
|
|
96
|
+
|
|
97
|
+
for (const [attributeName, change] of Object.entries(changes)) {
|
|
98
|
+
auditedChanges[columnNameToAttributeName[attributeName] || attributeName] = change[1]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return auditedChanges
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Captures the current attributes for a destroy audit.
|
|
106
|
+
* @param {import("./index.js").default} record - Record being destroyed.
|
|
107
|
+
* @returns {AuditChanges} Current attributes keyed by attribute name.
|
|
108
|
+
*/
|
|
109
|
+
function auditChangesForDestroy(record) {
|
|
110
|
+
return {...record.attributes()}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Creates an audit row for a record.
|
|
115
|
+
* @param {import("./index.js").default} record - Record to audit.
|
|
116
|
+
* @param {CreateAuditArgs} args - Audit row options.
|
|
117
|
+
* @returns {Promise<number | string>} Created audit row id.
|
|
118
|
+
*/
|
|
119
|
+
async function createAudit(record, args) {
|
|
120
|
+
const modelClass = /** @type {AuditedModelClass} */ (record.getModelClass())
|
|
121
|
+
|
|
122
|
+
return await modelClass._getConfiguration().ensureConnections({name: `${modelClass.getModelName()} audit`}, async () => {
|
|
123
|
+
return await createAuditWithCurrentConnection(record, args, modelClass)
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Creates an audit row using the current model database connection.
|
|
129
|
+
* @param {import("./index.js").default} record - Record to audit.
|
|
130
|
+
* @param {CreateAuditArgs} args - Audit row options.
|
|
131
|
+
* @param {AuditedModelClass} modelClass - Audited model class.
|
|
132
|
+
* @returns {Promise<number | string>} Created audit row id.
|
|
133
|
+
*/
|
|
134
|
+
async function createAuditWithCurrentConnection(record, args, modelClass) {
|
|
135
|
+
if (!record.isPersisted()) throw new Error(`Cannot audit unpersisted ${modelClass.getModelName()} record`)
|
|
136
|
+
|
|
137
|
+
const action = normalizeAction(args.action)
|
|
138
|
+
const auditedChanges = args.auditedChanges === undefined ? null : args.auditedChanges
|
|
139
|
+
const params = args.params === undefined ? null : args.params
|
|
140
|
+
const db = modelClass.connection()
|
|
141
|
+
const currentDate = new Date()
|
|
142
|
+
const auditActionId = await findOrCreateLookupId({
|
|
143
|
+
columnName: "action",
|
|
144
|
+
db,
|
|
145
|
+
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
|
|
155
|
+
})
|
|
156
|
+
const auditId = new UUID(4).format()
|
|
157
|
+
|
|
158
|
+
const insertResult = await db.query(db.insertSql({
|
|
159
|
+
returnLastInsertedColumnNames: ["id"],
|
|
160
|
+
tableName: "audits",
|
|
161
|
+
data: {
|
|
162
|
+
id: auditId,
|
|
163
|
+
audit_action_id: auditActionId,
|
|
164
|
+
audit_auditable_type_id: auditAuditableTypeId,
|
|
165
|
+
auditable_id: record.id(),
|
|
166
|
+
auditable_type: modelClass.getModelName(),
|
|
167
|
+
audited_changes: auditedChanges,
|
|
168
|
+
params,
|
|
169
|
+
created_at: currentDate,
|
|
170
|
+
updated_at: currentDate
|
|
171
|
+
}
|
|
172
|
+
}))
|
|
173
|
+
const insertedAuditId = auditIdFromInsertResult(insertResult) ?? auditId
|
|
174
|
+
|
|
175
|
+
await emitAuditEvent(modelClass, action, {
|
|
176
|
+
action,
|
|
177
|
+
auditId: insertedAuditId,
|
|
178
|
+
auditedChanges,
|
|
179
|
+
params,
|
|
180
|
+
record
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
return insertedAuditId
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Reads an audit id returned by INSERT ... RETURNING / OUTPUT.
|
|
188
|
+
* @param {Array<Record<string, ?>> | null | undefined} insertResult - Insert query result.
|
|
189
|
+
* @returns {number | string | null} Created audit id when the driver returned it.
|
|
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
|
|
201
|
+
|
|
202
|
+
return null
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Captures create changes on a model instance.
|
|
207
|
+
* @param {import("./index.js").default & {_pendingCreateAuditChanges?: AuditChanges}} record - Record to capture.
|
|
208
|
+
* @returns {void}
|
|
209
|
+
*/
|
|
210
|
+
function captureCreateAuditChanges(record) {
|
|
211
|
+
record._pendingCreateAuditChanges = auditChangesForCurrentChanges(record)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Creates the create audit row for a model instance.
|
|
216
|
+
* @param {import("./index.js").default & {_pendingCreateAuditChanges?: AuditChanges}} record - Record to audit.
|
|
217
|
+
* @returns {Promise<void>}
|
|
218
|
+
*/
|
|
219
|
+
async function createCreateAudit(record) {
|
|
220
|
+
await createAudit(record, {
|
|
221
|
+
action: "create",
|
|
222
|
+
auditedChanges: record._pendingCreateAuditChanges || null
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
record._pendingCreateAuditChanges = undefined
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Captures update changes on a model instance.
|
|
230
|
+
* @param {import("./index.js").default & {_pendingUpdateAuditChanges?: AuditChanges}} record - Record to capture.
|
|
231
|
+
* @returns {void}
|
|
232
|
+
*/
|
|
233
|
+
function captureUpdateAuditChanges(record) {
|
|
234
|
+
record._pendingUpdateAuditChanges = auditChangesForCurrentChanges(record)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Creates the update audit row for a model instance.
|
|
239
|
+
* @param {import("./index.js").default & {_pendingUpdateAuditChanges?: AuditChanges}} record - Record to audit.
|
|
240
|
+
* @returns {Promise<void>}
|
|
241
|
+
*/
|
|
242
|
+
async function createUpdateAudit(record) {
|
|
243
|
+
const auditedChanges = record._pendingUpdateAuditChanges || null
|
|
244
|
+
|
|
245
|
+
record._pendingUpdateAuditChanges = undefined
|
|
246
|
+
|
|
247
|
+
if (!auditedChanges || Object.keys(auditedChanges).length <= 0) return
|
|
248
|
+
|
|
249
|
+
await createAudit(record, {
|
|
250
|
+
action: "update",
|
|
251
|
+
auditedChanges
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Creates the destroy audit row for a model instance.
|
|
257
|
+
* @param {import("./index.js").default} record - Record to audit.
|
|
258
|
+
* @returns {Promise<void>}
|
|
259
|
+
*/
|
|
260
|
+
async function createDestroyAudit(record) {
|
|
261
|
+
await createAudit(record, {
|
|
262
|
+
action: "destroy",
|
|
263
|
+
auditedChanges: auditChangesForDestroy(record)
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Returns records without an audit row for the given action.
|
|
269
|
+
* @template {AuditedModelClass} MC
|
|
270
|
+
* @param {MC} modelClass - Model class to scope.
|
|
271
|
+
* @param {string} action - Audit action to exclude.
|
|
272
|
+
* @returns {import("../query/model-class-query.js").default<MC>} Query scoped to records without that audit action.
|
|
273
|
+
*/
|
|
274
|
+
function withoutAudit(modelClass, action) {
|
|
275
|
+
const db = modelClass.connection()
|
|
276
|
+
const modelTableSql = db.quoteTable(modelClass.tableName())
|
|
277
|
+
const auditsTableSql = db.quoteTable("audits")
|
|
278
|
+
const auditActionsTableSql = db.quoteTable("audit_actions")
|
|
279
|
+
const modelPrimaryKeySql = `${modelTableSql}.${db.quoteColumn(modelClass.primaryKey())}`
|
|
280
|
+
const auditAuditableIdSql = `${auditsTableSql}.${db.quoteColumn("auditable_id")}`
|
|
281
|
+
const auditAuditableTypeSql = `${auditsTableSql}.${db.quoteColumn("auditable_type")}`
|
|
282
|
+
const auditActionIdSql = `${auditsTableSql}.${db.quoteColumn("audit_action_id")}`
|
|
283
|
+
const auditActionsIdSql = `${auditActionsTableSql}.${db.quoteColumn("id")}`
|
|
284
|
+
const auditActionsActionSql = `${auditActionsTableSql}.${db.quoteColumn("action")}`
|
|
285
|
+
|
|
286
|
+
return modelClass
|
|
287
|
+
.all()
|
|
288
|
+
.where(`
|
|
289
|
+
NOT EXISTS (
|
|
290
|
+
SELECT 1
|
|
291
|
+
FROM ${auditsTableSql}
|
|
292
|
+
INNER JOIN ${auditActionsTableSql}
|
|
293
|
+
ON ${auditActionsIdSql} = ${auditActionIdSql}
|
|
294
|
+
WHERE ${auditAuditableTypeSql} = ${db.quote(modelClass.getModelName())}
|
|
295
|
+
AND ${auditAuditableIdSql} = ${modelPrimaryKeySql}
|
|
296
|
+
AND ${auditActionsActionSql} = ${db.quote(normalizeAction(action))}
|
|
297
|
+
)
|
|
298
|
+
`)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Finds or creates a lookup row and returns its id.
|
|
303
|
+
* @param {object} args - Options object.
|
|
304
|
+
* @param {string} args.columnName - Lookup value column.
|
|
305
|
+
* @param {Date} args.currentDate - Timestamp to write when inserting.
|
|
306
|
+
* @param {import("../drivers/base.js").default} args.db - Database driver.
|
|
307
|
+
* @param {string} args.tableName - Lookup table.
|
|
308
|
+
* @param {string} args.value - Lookup value.
|
|
309
|
+
* @returns {Promise<number | string>} Lookup row id.
|
|
310
|
+
*/
|
|
311
|
+
async function findOrCreateLookupId({columnName, currentDate, db, tableName, value}) {
|
|
312
|
+
await db.upsert({
|
|
313
|
+
tableName,
|
|
314
|
+
conflictColumns: [columnName],
|
|
315
|
+
updateColumns: [columnName],
|
|
316
|
+
data: {
|
|
317
|
+
id: new UUID(4).format(),
|
|
318
|
+
[columnName]: value,
|
|
319
|
+
created_at: currentDate,
|
|
320
|
+
updated_at: currentDate
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
const id = await findLookupId({columnName, db, tableName, value})
|
|
325
|
+
|
|
326
|
+
if (id === null) throw new Error(`Couldn't find ${tableName}.${columnName} after upsert`)
|
|
327
|
+
|
|
328
|
+
return id
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Finds a lookup id by value.
|
|
333
|
+
* @param {object} args - Options object.
|
|
334
|
+
* @param {string} args.columnName - Lookup value column.
|
|
335
|
+
* @param {import("../drivers/base.js").default} args.db - Database driver.
|
|
336
|
+
* @param {string} args.tableName - Lookup table.
|
|
337
|
+
* @param {string} args.value - Lookup value.
|
|
338
|
+
* @returns {Promise<number | string | null>} Lookup row id or null.
|
|
339
|
+
*/
|
|
340
|
+
async function findLookupId({columnName, db, tableName, value}) {
|
|
341
|
+
const rows = /** @type {Array<{id: number | string}>} */ (await db.query(`
|
|
342
|
+
SELECT ${db.quoteColumn("id")} AS id
|
|
343
|
+
FROM ${db.quoteTable(tableName)}
|
|
344
|
+
WHERE ${db.quoteColumn(columnName)} = ${db.quote(value)}
|
|
345
|
+
`))
|
|
346
|
+
|
|
347
|
+
if (rows[0]) return rows[0].id
|
|
348
|
+
|
|
349
|
+
return null
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Normalizes an audit action.
|
|
354
|
+
* @param {string} action - Action name.
|
|
355
|
+
* @returns {string} Normalized action.
|
|
356
|
+
*/
|
|
357
|
+
function normalizeAction(action) {
|
|
358
|
+
const normalizedAction = action.trim()
|
|
359
|
+
|
|
360
|
+
if (!normalizedAction) throw new Error("Audit action must be present")
|
|
361
|
+
|
|
362
|
+
return normalizedAction
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Returns the callback map owned by a model class.
|
|
367
|
+
* @param {AuditedModelClass} modelClass - Model class.
|
|
368
|
+
* @returns {Record<string, AuditCallback[]>} Callback map.
|
|
369
|
+
*/
|
|
370
|
+
function auditCallbacksForModelClass(modelClass) {
|
|
371
|
+
if (!Object.prototype.hasOwnProperty.call(modelClass, "_auditCallbacks")) {
|
|
372
|
+
modelClass._auditCallbacks = {}
|
|
373
|
+
}
|
|
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
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Emits audit callbacks for a model/action pair.
|
|
384
|
+
* @param {AuditedModelClass} modelClass - Model class.
|
|
385
|
+
* @param {string} action - Audit action.
|
|
386
|
+
* @param {AuditEventPayload} payload - Event payload.
|
|
387
|
+
* @returns {Promise<void>}
|
|
388
|
+
*/
|
|
389
|
+
async function emitAuditEvent(modelClass, action, payload) {
|
|
390
|
+
const callbacks = auditCallbacksForModelClass(modelClass)[action] || []
|
|
391
|
+
|
|
392
|
+
for (const callback of [...callbacks]) {
|
|
393
|
+
await callback(payload)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export {
|
|
398
|
+
captureCreateAuditChanges,
|
|
399
|
+
captureUpdateAuditChanges,
|
|
400
|
+
createAudit,
|
|
401
|
+
createCreateAudit,
|
|
402
|
+
createDestroyAudit,
|
|
403
|
+
createUpdateAudit,
|
|
404
|
+
registerAuditCallback,
|
|
405
|
+
registerAuditing,
|
|
406
|
+
withoutAudit
|
|
407
|
+
}
|
|
@@ -46,6 +46,7 @@ import singularizeModelName from "../../utils/singularize-model-name.js"
|
|
|
46
46
|
import {defineModelScope} from "../../utils/model-scope.js"
|
|
47
47
|
import { normalizeDateStringForWrite, normalizeDateValueForRead, normalizeDateValueForWrite } from "../datetime-storage.js"
|
|
48
48
|
import {formatValue} from "../../utils/format-value.js"
|
|
49
|
+
import {captureCreateAuditChanges, captureUpdateAuditChanges, createAudit, createCreateAudit, createDestroyAudit, createUpdateAudit, registerAuditCallback, registerAuditing, withoutAudit} from "./auditing.js"
|
|
49
50
|
import ValidatorsFormat from "./validators/format.js"
|
|
50
51
|
import ValidatorsPresence from "./validators/presence.js"
|
|
51
52
|
import ValidatorsUniqueness from "./validators/uniqueness.js"
|
|
@@ -247,6 +248,16 @@ class VelociousDatabaseRecord {
|
|
|
247
248
|
* @type {boolean | undefined} */
|
|
248
249
|
static _eagerLoadRecordMetadata
|
|
249
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Narrows the runtime value to the documented type.
|
|
253
|
+
* @type {Record<string, import("./auditing.js").AuditCallback[]> | undefined} */
|
|
254
|
+
static _auditCallbacks
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Narrows the runtime value to the documented type.
|
|
258
|
+
* @type {boolean | undefined} */
|
|
259
|
+
static _auditLifecycleCallbacksRegistered
|
|
260
|
+
|
|
250
261
|
/**
|
|
251
262
|
* Returns the model name, preferring an explicit `static modelName` declaration
|
|
252
263
|
* over the JavaScript class `.name` property. This allows minified builds to
|
|
@@ -444,6 +455,16 @@ class VelociousDatabaseRecord {
|
|
|
444
455
|
* @type {Record<string, ?>} */
|
|
445
456
|
_changes = {}
|
|
446
457
|
|
|
458
|
+
/**
|
|
459
|
+
* Changes captured before a create audit is written.
|
|
460
|
+
* @type {import("./auditing.js").AuditChanges | undefined} */
|
|
461
|
+
_pendingCreateAuditChanges = undefined
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Changes captured before an update audit is written.
|
|
465
|
+
* @type {import("./auditing.js").AuditChanges | undefined} */
|
|
466
|
+
_pendingUpdateAuditChanges = undefined
|
|
467
|
+
|
|
447
468
|
/**
|
|
448
469
|
* Attribute names explicitly assigned in the current update call.
|
|
449
470
|
* @type {Set<string> | undefined}
|
|
@@ -613,6 +634,37 @@ class VelociousDatabaseRecord {
|
|
|
613
634
|
VelociousDatabaseRecord.registerLifecycleCallback.call(this, "afterDestroy", /** @type {LifecycleCallbackType} */ (callback))
|
|
614
635
|
}
|
|
615
636
|
|
|
637
|
+
/**
|
|
638
|
+
* Enables automatic create/update/destroy auditing for this model.
|
|
639
|
+
* @returns {void}
|
|
640
|
+
*/
|
|
641
|
+
static audited() {
|
|
642
|
+
registerAuditing(this)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Registers a callback invoked after this model writes an audit row for the action.
|
|
647
|
+
* @template {typeof VelociousDatabaseRecord} MC
|
|
648
|
+
* @this {MC}
|
|
649
|
+
* @param {string} action - Audit action name.
|
|
650
|
+
* @param {import("./auditing.js").AuditCallback} callback - Callback to run after audit creation.
|
|
651
|
+
* @returns {() => void} Unsubscribe function.
|
|
652
|
+
*/
|
|
653
|
+
static onAudit(action, callback) {
|
|
654
|
+
return registerAuditCallback(this, action, callback)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Returns records that do not have an audit row for the given action.
|
|
659
|
+
* @template {typeof VelociousDatabaseRecord} MC
|
|
660
|
+
* @this {MC}
|
|
661
|
+
* @param {string} action - Audit action name.
|
|
662
|
+
* @returns {ModelClassQuery<MC>} Query scoped to records without that audit action.
|
|
663
|
+
*/
|
|
664
|
+
static withoutAudit(action) {
|
|
665
|
+
return withoutAudit(this, action)
|
|
666
|
+
}
|
|
667
|
+
|
|
616
668
|
/**
|
|
617
669
|
* Runs get validator type.
|
|
618
670
|
* @param {string} validatorName - Validator name.
|
|
@@ -3597,6 +3649,55 @@ class VelociousDatabaseRecord {
|
|
|
3597
3649
|
await this._runLifecycleCallbacks("afterDestroy")
|
|
3598
3650
|
}
|
|
3599
3651
|
|
|
3652
|
+
/**
|
|
3653
|
+
* Stores an audit row for this record.
|
|
3654
|
+
* @param {import("./auditing.js").CreateAuditArgs} args - Audit row options.
|
|
3655
|
+
* @returns {Promise<number | string>} Created audit row id.
|
|
3656
|
+
*/
|
|
3657
|
+
async createAudit(args) {
|
|
3658
|
+
return await createAudit(this, args)
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
/**
|
|
3662
|
+
* Captures create changes before persistence clears the change set.
|
|
3663
|
+
* @returns {void}
|
|
3664
|
+
*/
|
|
3665
|
+
captureCreateAuditChanges() {
|
|
3666
|
+
captureCreateAuditChanges(this)
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
/**
|
|
3670
|
+
* Writes the create audit row.
|
|
3671
|
+
* @returns {Promise<void>}
|
|
3672
|
+
*/
|
|
3673
|
+
async createCreateAudit() {
|
|
3674
|
+
await createCreateAudit(this)
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
/**
|
|
3678
|
+
* Captures update changes before persistence clears the change set.
|
|
3679
|
+
* @returns {void}
|
|
3680
|
+
*/
|
|
3681
|
+
captureUpdateAuditChanges() {
|
|
3682
|
+
captureUpdateAuditChanges(this)
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
/**
|
|
3686
|
+
* Writes the update audit row.
|
|
3687
|
+
* @returns {Promise<void>}
|
|
3688
|
+
*/
|
|
3689
|
+
async createUpdateAudit() {
|
|
3690
|
+
await createUpdateAudit(this)
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
/**
|
|
3694
|
+
* Writes the destroy audit row.
|
|
3695
|
+
* @returns {Promise<void>}
|
|
3696
|
+
*/
|
|
3697
|
+
async createDestroyAudit() {
|
|
3698
|
+
await createDestroyAudit(this)
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3600
3701
|
/**
|
|
3601
3702
|
* Runs run lifecycle callbacks.
|
|
3602
3703
|
* @param {"afterCreate" | "afterDestroy" | "afterSave" | "afterUpdate" | "beforeCreate" | "beforeDestroy" | "beforeSave" | "beforeUpdate" | "beforeValidation"} callbackName - Callback type.
|
package/src/tenants/tenant.js
CHANGED
|
@@ -23,9 +23,10 @@ export default class Tenant {
|
|
|
23
23
|
* the caller wiring up connections. Already-checked-out connections are reused, so nesting
|
|
24
24
|
* `Tenant.with` calls does not open redundant connections. The callback receives the active
|
|
25
25
|
* connections keyed by identifier, the same as `ensureConnections`.
|
|
26
|
+
* @template T
|
|
26
27
|
* @param {object} tenant Descriptor understood by the app's tenantDatabaseResolver.
|
|
27
|
-
* @param {(connections: Record<string, import("../database/drivers/base.js").default>) => Promise
|
|
28
|
-
* @returns {Promise
|
|
28
|
+
* @param {(connections: Record<string, import("../database/drivers/base.js").default>) => Promise<T>} callback
|
|
29
|
+
* @returns {Promise<T>}
|
|
29
30
|
*/
|
|
30
31
|
static async with(tenant, callback) {
|
|
31
32
|
const configuration = Current.configuration()
|