1use crate::AuditEntity;
2use sql_orm_core::{ColumnMetadata, ColumnValue, EntityMetadata, OrmError};
3use std::collections::BTreeSet;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum AuditOperation {
8 Insert,
10 Update,
12}
13
14#[derive(Debug, Clone, Default, PartialEq)]
15pub struct AuditRequestValues {
21 values: Vec<ColumnValue>,
22}
23
24impl AuditRequestValues {
25 pub fn new(values: Vec<ColumnValue>) -> Self {
27 Self { values }
28 }
29
30 pub fn values(&self) -> &[ColumnValue] {
32 &self.values
33 }
34}
35
36#[derive(Debug, Clone, Copy)]
37pub struct AuditContext<'a> {
39 pub entity: &'static EntityMetadata,
41 pub operation: AuditOperation,
43 pub request_values: Option<&'a AuditRequestValues>,
45}
46
47pub trait AuditProvider: Send + Sync {
53 fn values(&self, context: AuditContext<'_>) -> Result<Vec<ColumnValue>, OrmError>;
55}
56
57pub trait AuditValues {
59 fn audit_values(self) -> Vec<ColumnValue>;
61}
62
63pub(crate) fn apply_audit_values<E: AuditEntity>(
64 operation: AuditOperation,
65 values: Vec<ColumnValue>,
66 audit_provider: Option<&dyn AuditProvider>,
67 request_values: Option<&AuditRequestValues>,
68) -> Result<Vec<ColumnValue>, OrmError> {
69 validate_no_duplicate_columns("audit values", &values)?;
70
71 let Some(policy) = E::audit_policy() else {
72 return Ok(values);
73 };
74
75 let context = AuditContext {
76 entity: E::metadata(),
77 operation,
78 request_values,
79 };
80 let provider_values = match audit_provider {
81 Some(provider) => {
82 let values = provider.values(context)?;
83 validate_no_duplicate_columns("audit provider values", &values)?;
84 values
85 }
86 None => Vec::new(),
87 };
88
89 if let Some(request_values) = request_values {
90 validate_no_duplicate_columns("audit request values", request_values.values())?;
91 }
92
93 let mut resolved = values;
94 let mut seen = resolved
95 .iter()
96 .map(|value| value.column_name)
97 .collect::<BTreeSet<_>>();
98
99 for value in &resolved {
100 if let Some(column) = policy
101 .columns
102 .iter()
103 .find(|column| column.column_name == value.column_name)
104 {
105 validate_audit_column_value(operation, column, &value.value)?;
106 }
107 }
108
109 if let Some(request_values) = request_values {
110 append_missing_audit_values(
111 operation,
112 policy.columns,
113 &mut resolved,
114 &mut seen,
115 request_values.values(),
116 )?;
117 }
118
119 append_missing_audit_values(
120 operation,
121 policy.columns,
122 &mut resolved,
123 &mut seen,
124 &provider_values,
125 )?;
126
127 Ok(resolved)
128}
129
130#[doc(hidden)]
131pub fn resolve_audit_values(
132 values: Vec<ColumnValue>,
133 context: AuditContext<'_>,
134 audit_provider: Option<&dyn AuditProvider>,
135) -> Result<Vec<ColumnValue>, OrmError> {
136 validate_no_duplicate_columns("audit values", &values)?;
137
138 let mut resolved = values;
139 let mut seen = resolved
140 .iter()
141 .map(|value| value.column_name)
142 .collect::<BTreeSet<_>>();
143
144 if let Some(request_values) = context.request_values {
145 validate_no_duplicate_columns("audit request values", request_values.values())?;
146 append_missing_values(&mut resolved, &mut seen, request_values.values());
147 }
148
149 if let Some(provider) = audit_provider {
150 let provider_values = provider.values(context)?;
151 validate_no_duplicate_columns("audit provider values", &provider_values)?;
152 append_missing_values(&mut resolved, &mut seen, &provider_values);
153 }
154
155 Ok(resolved)
156}
157
158fn append_missing_audit_values(
159 operation: AuditOperation,
160 columns: &'static [ColumnMetadata],
161 resolved: &mut Vec<ColumnValue>,
162 seen: &mut BTreeSet<&'static str>,
163 values: &[ColumnValue],
164) -> Result<(), OrmError> {
165 for value in values {
166 let Some(column) = columns
167 .iter()
168 .find(|column| column.column_name == value.column_name)
169 else {
170 continue;
171 };
172
173 validate_audit_column_value(operation, column, &value.value)?;
174
175 if seen.insert(value.column_name) {
176 resolved.push(value.clone());
177 }
178 }
179
180 Ok(())
181}
182
183fn validate_audit_column_value(
184 operation: AuditOperation,
185 column: &ColumnMetadata,
186 value: &sql_orm_core::SqlValue,
187) -> Result<(), OrmError> {
188 match operation {
189 AuditOperation::Insert if !column.insertable => {
190 return Err(OrmError::new(format!(
191 "audit insert column `{}` is not insertable",
192 column.column_name
193 )));
194 }
195 AuditOperation::Update if !column.updatable => {
196 return Err(OrmError::new(format!(
197 "audit update column `{}` is not updatable",
198 column.column_name
199 )));
200 }
201 _ => {}
202 }
203
204 if value.is_null() && !column.nullable {
205 return Err(OrmError::new(format!(
206 "audit column `{}` is not nullable",
207 column.column_name
208 )));
209 }
210
211 Ok(())
212}
213
214fn validate_no_duplicate_columns(label: &str, values: &[ColumnValue]) -> Result<(), OrmError> {
215 let mut seen = BTreeSet::new();
216
217 for value in values {
218 if !seen.insert(value.column_name) {
219 return Err(OrmError::new(format!(
220 "duplicate column `{}` in {label}",
221 value.column_name
222 )));
223 }
224 }
225
226 Ok(())
227}
228
229fn append_missing_values(
230 resolved: &mut Vec<ColumnValue>,
231 seen: &mut BTreeSet<&'static str>,
232 values: &[ColumnValue],
233) {
234 for value in values {
235 if seen.insert(value.column_name) {
236 resolved.push(value.clone());
237 }
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::{
244 AuditContext, AuditOperation, AuditProvider, AuditRequestValues, apply_audit_values,
245 resolve_audit_values,
246 };
247 use crate::AuditEntity;
248 use sql_orm_core::{
249 ColumnMetadata, ColumnValue, Entity, EntityMetadata, EntityPolicyMetadata, OrmError,
250 PrimaryKeyMetadata, SqlServerType, SqlValue,
251 };
252
253 struct TestAuditedEntity;
254
255 static TEST_ENTITY_COLUMNS: [ColumnMetadata; 1] = [ColumnMetadata {
256 rust_field: "id",
257 column_name: "id",
258 renamed_from: None,
259 sql_type: SqlServerType::BigInt,
260 nullable: false,
261 primary_key: true,
262 identity: None,
263 default_sql: None,
264 computed_sql: None,
265 rowversion: false,
266 insertable: false,
267 updatable: false,
268 max_length: None,
269 precision: None,
270 scale: None,
271 }];
272
273 static TEST_ENTITY_METADATA: EntityMetadata = EntityMetadata {
274 rust_name: "AuditedEntity",
275 schema: "dbo",
276 table: "audited_entities",
277 renamed_from: None,
278 columns: &TEST_ENTITY_COLUMNS,
279 primary_key: PrimaryKeyMetadata::new(None, &["id"]),
280 indexes: &[],
281 foreign_keys: &[],
282 navigations: &[],
283 };
284
285 static TEST_AUDIT_COLUMNS: [ColumnMetadata; 3] = [
286 ColumnMetadata {
287 rust_field: "created_at",
288 column_name: "created_at",
289 renamed_from: None,
290 sql_type: SqlServerType::DateTime2,
291 nullable: false,
292 primary_key: false,
293 identity: None,
294 default_sql: Some("SYSUTCDATETIME()"),
295 computed_sql: None,
296 rowversion: false,
297 insertable: false,
298 updatable: false,
299 max_length: None,
300 precision: None,
301 scale: None,
302 },
303 ColumnMetadata {
304 rust_field: "created_by",
305 column_name: "created_by",
306 renamed_from: None,
307 sql_type: SqlServerType::BigInt,
308 nullable: false,
309 primary_key: false,
310 identity: None,
311 default_sql: None,
312 computed_sql: None,
313 rowversion: false,
314 insertable: true,
315 updatable: true,
316 max_length: None,
317 precision: None,
318 scale: None,
319 },
320 ColumnMetadata {
321 rust_field: "updated_by",
322 column_name: "updated_by",
323 renamed_from: None,
324 sql_type: SqlServerType::NVarChar,
325 nullable: true,
326 primary_key: false,
327 identity: None,
328 default_sql: None,
329 computed_sql: None,
330 rowversion: false,
331 insertable: true,
332 updatable: true,
333 max_length: Some(120),
334 precision: None,
335 scale: None,
336 },
337 ];
338
339 impl Entity for TestAuditedEntity {
340 fn metadata() -> &'static EntityMetadata {
341 &TEST_ENTITY_METADATA
342 }
343 }
344
345 impl AuditEntity for TestAuditedEntity {
346 fn audit_policy() -> Option<EntityPolicyMetadata> {
347 Some(EntityPolicyMetadata::new("audit", &TEST_AUDIT_COLUMNS))
348 }
349 }
350
351 struct FixedAuditProvider;
352
353 impl AuditProvider for FixedAuditProvider {
354 fn values(&self, context: AuditContext<'_>) -> Result<Vec<ColumnValue>, OrmError> {
355 assert_eq!(context.entity.rust_name, "AuditedEntity");
356 assert_eq!(context.operation, AuditOperation::Insert);
357 assert!(context.request_values.is_some());
358
359 Ok(vec![
360 ColumnValue::new(
361 "created_at",
362 SqlValue::String("provider-created-at".to_string()),
363 ),
364 ColumnValue::new(
365 "updated_by",
366 SqlValue::String("provider-updated-by".to_string()),
367 ),
368 ])
369 }
370 }
371
372 fn context<'a>(request_values: Option<&'a AuditRequestValues>) -> AuditContext<'a> {
373 AuditContext {
374 entity: &TEST_ENTITY_METADATA,
375 operation: AuditOperation::Insert,
376 request_values,
377 }
378 }
379
380 #[test]
381 fn apply_audit_values_completes_only_missing_insertable_audit_columns() {
382 let request_values = AuditRequestValues::new(vec![
383 ColumnValue::new("created_by", SqlValue::I64(7)),
384 ColumnValue::new(
385 "ignored",
386 SqlValue::String("not an audit column".to_string()),
387 ),
388 ]);
389
390 struct Provider;
391
392 impl AuditProvider for Provider {
393 fn values(&self, _context: AuditContext<'_>) -> Result<Vec<ColumnValue>, OrmError> {
394 Ok(vec![
395 ColumnValue::new("created_by", SqlValue::I64(9)),
396 ColumnValue::new("updated_by", SqlValue::String("provider".to_string())),
397 ColumnValue::new("other", SqlValue::String("not an audit column".to_string())),
398 ])
399 }
400 }
401
402 let values = apply_audit_values::<TestAuditedEntity>(
403 AuditOperation::Insert,
404 vec![ColumnValue::new(
405 "name",
406 SqlValue::String("existing".to_string()),
407 )],
408 Some(&Provider),
409 Some(&request_values),
410 )
411 .expect("audit insert values should resolve");
412
413 assert_eq!(
414 values,
415 vec![
416 ColumnValue::new("name", SqlValue::String("existing".to_string())),
417 ColumnValue::new("created_by", SqlValue::I64(7)),
418 ColumnValue::new("updated_by", SqlValue::String("provider".to_string())),
419 ]
420 );
421 }
422
423 #[test]
424 fn apply_audit_values_rejects_duplicate_insert_columns() {
425 let error = apply_audit_values::<TestAuditedEntity>(
426 AuditOperation::Insert,
427 vec![
428 ColumnValue::new("created_by", SqlValue::I64(7)),
429 ColumnValue::new("created_by", SqlValue::I64(8)),
430 ],
431 None,
432 None,
433 )
434 .unwrap_err();
435
436 assert_eq!(
437 error,
438 OrmError::new("duplicate column `created_by` in audit values")
439 );
440 }
441
442 #[test]
443 fn apply_audit_values_rejects_non_insertable_audit_column() {
444 let request_values = AuditRequestValues::new(vec![ColumnValue::new(
445 "created_at",
446 SqlValue::String("runtime".to_string()),
447 )]);
448
449 let error = apply_audit_values::<TestAuditedEntity>(
450 AuditOperation::Insert,
451 Vec::new(),
452 None,
453 Some(&request_values),
454 )
455 .unwrap_err();
456
457 assert_eq!(
458 error,
459 OrmError::new("audit insert column `created_at` is not insertable")
460 );
461 }
462
463 #[test]
464 fn apply_audit_values_rejects_null_for_non_nullable_audit_column() {
465 let request_values =
466 AuditRequestValues::new(vec![ColumnValue::new("created_by", SqlValue::Null)]);
467
468 let error = apply_audit_values::<TestAuditedEntity>(
469 AuditOperation::Insert,
470 Vec::new(),
471 None,
472 Some(&request_values),
473 )
474 .unwrap_err();
475
476 assert_eq!(
477 error,
478 OrmError::new("audit column `created_by` is not nullable")
479 );
480 }
481
482 #[test]
483 fn apply_audit_values_completes_only_missing_updatable_audit_columns() {
484 struct Provider;
485
486 impl AuditProvider for Provider {
487 fn values(&self, _context: AuditContext<'_>) -> Result<Vec<ColumnValue>, OrmError> {
488 Ok(vec![
489 ColumnValue::new("created_by", SqlValue::I64(9)),
490 ColumnValue::new("updated_by", SqlValue::String("provider".to_string())),
491 ])
492 }
493 }
494
495 let values = apply_audit_values::<TestAuditedEntity>(
496 AuditOperation::Update,
497 vec![
498 ColumnValue::new("name", SqlValue::String("updated".to_string())),
499 ColumnValue::new("updated_by", SqlValue::String("explicit".to_string())),
500 ],
501 Some(&Provider),
502 None,
503 )
504 .expect("audit update values should resolve");
505
506 assert_eq!(
507 values,
508 vec![
509 ColumnValue::new("name", SqlValue::String("updated".to_string())),
510 ColumnValue::new("updated_by", SqlValue::String("explicit".to_string())),
511 ColumnValue::new("created_by", SqlValue::I64(9)),
512 ]
513 );
514 }
515
516 #[test]
517 fn apply_audit_values_rejects_non_updatable_audit_column() {
518 let error = apply_audit_values::<TestAuditedEntity>(
519 AuditOperation::Update,
520 vec![ColumnValue::new(
521 "created_at",
522 SqlValue::String("runtime".to_string()),
523 )],
524 None,
525 None,
526 )
527 .unwrap_err();
528
529 assert_eq!(
530 error,
531 OrmError::new("audit update column `created_at` is not updatable")
532 );
533 }
534
535 #[test]
536 fn resolve_audit_values_preserves_user_values_before_request_and_provider_values() {
537 let request_values = AuditRequestValues::new(vec![
538 ColumnValue::new(
539 "created_at",
540 SqlValue::String("request-created-at".to_string()),
541 ),
542 ColumnValue::new(
543 "created_by",
544 SqlValue::String("request-created-by".to_string()),
545 ),
546 ]);
547
548 let resolved = resolve_audit_values(
549 vec![ColumnValue::new(
550 "created_at",
551 SqlValue::String("user-created-at".to_string()),
552 )],
553 context(Some(&request_values)),
554 Some(&FixedAuditProvider),
555 )
556 .expect("audit values should resolve");
557
558 assert_eq!(
559 resolved,
560 vec![
561 ColumnValue::new(
562 "created_at",
563 SqlValue::String("user-created-at".to_string())
564 ),
565 ColumnValue::new(
566 "created_by",
567 SqlValue::String("request-created-by".to_string())
568 ),
569 ColumnValue::new(
570 "updated_by",
571 SqlValue::String("provider-updated-by".to_string())
572 ),
573 ]
574 );
575 }
576
577 #[test]
578 fn resolve_audit_values_uses_request_values_without_provider() {
579 let request_values = AuditRequestValues::new(vec![ColumnValue::new(
580 "updated_by",
581 SqlValue::String("request-updated-by".to_string()),
582 )]);
583
584 let resolved = resolve_audit_values(vec![], context(Some(&request_values)), None)
585 .expect("request audit values should resolve");
586
587 assert_eq!(
588 resolved,
589 vec![ColumnValue::new(
590 "updated_by",
591 SqlValue::String("request-updated-by".to_string())
592 )]
593 );
594 }
595
596 #[test]
597 fn resolve_audit_values_rejects_duplicate_user_columns() {
598 let error = resolve_audit_values(
599 vec![
600 ColumnValue::new("created_at", SqlValue::String("first".to_string())),
601 ColumnValue::new("created_at", SqlValue::String("second".to_string())),
602 ],
603 context(None),
604 None,
605 )
606 .unwrap_err();
607
608 assert_eq!(
609 error,
610 OrmError::new("duplicate column `created_at` in audit values")
611 );
612 }
613
614 #[test]
615 fn resolve_audit_values_rejects_duplicate_request_columns() {
616 let request_values = AuditRequestValues::new(vec![
617 ColumnValue::new("created_by", SqlValue::String("first".to_string())),
618 ColumnValue::new("created_by", SqlValue::String("second".to_string())),
619 ]);
620
621 let error = resolve_audit_values(vec![], context(Some(&request_values)), None).unwrap_err();
622
623 assert_eq!(
624 error,
625 OrmError::new("duplicate column `created_by` in audit request values")
626 );
627 }
628
629 #[test]
630 fn resolve_audit_values_rejects_duplicate_provider_columns() {
631 struct DuplicateProvider;
632
633 impl AuditProvider for DuplicateProvider {
634 fn values(&self, _context: AuditContext<'_>) -> Result<Vec<ColumnValue>, OrmError> {
635 Ok(vec![
636 ColumnValue::new("updated_at", SqlValue::String("first".to_string())),
637 ColumnValue::new("updated_at", SqlValue::String("second".to_string())),
638 ])
639 }
640 }
641
642 let error =
643 resolve_audit_values(vec![], context(None), Some(&DuplicateProvider)).unwrap_err();
644
645 assert_eq!(
646 error,
647 OrmError::new("duplicate column `updated_at` in audit provider values")
648 );
649 }
650}