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::compile(format!(
191 "audit insert column `{}` is not insertable",
192 column.column_name
193 )));
194 }
195 AuditOperation::Update if !column.updatable => {
196 return Err(OrmError::compile(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::compile(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::compile(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 OrmErrorKind, 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.message(),
438 "duplicate column `created_by` in audit values"
439 );
440 assert_eq!(error.kind(), OrmErrorKind::Compile);
441 }
442
443 #[test]
444 fn apply_audit_values_rejects_non_insertable_audit_column() {
445 let request_values = AuditRequestValues::new(vec![ColumnValue::new(
446 "created_at",
447 SqlValue::String("runtime".to_string()),
448 )]);
449
450 let error = apply_audit_values::<TestAuditedEntity>(
451 AuditOperation::Insert,
452 Vec::new(),
453 None,
454 Some(&request_values),
455 )
456 .unwrap_err();
457
458 assert_eq!(
459 error.message(),
460 "audit insert column `created_at` is not insertable"
461 );
462 assert_eq!(error.kind(), OrmErrorKind::Compile);
463 }
464
465 #[test]
466 fn apply_audit_values_rejects_null_for_non_nullable_audit_column() {
467 let request_values =
468 AuditRequestValues::new(vec![ColumnValue::new("created_by", SqlValue::Null)]);
469
470 let error = apply_audit_values::<TestAuditedEntity>(
471 AuditOperation::Insert,
472 Vec::new(),
473 None,
474 Some(&request_values),
475 )
476 .unwrap_err();
477
478 assert_eq!(error.message(), "audit column `created_by` is not nullable");
479 assert_eq!(error.kind(), OrmErrorKind::Compile);
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.message(),
531 "audit update column `created_at` is not updatable"
532 );
533 assert_eq!(error.kind(), OrmErrorKind::Compile);
534 }
535
536 #[test]
537 fn resolve_audit_values_preserves_user_values_before_request_and_provider_values() {
538 let request_values = AuditRequestValues::new(vec![
539 ColumnValue::new(
540 "created_at",
541 SqlValue::String("request-created-at".to_string()),
542 ),
543 ColumnValue::new(
544 "created_by",
545 SqlValue::String("request-created-by".to_string()),
546 ),
547 ]);
548
549 let resolved = resolve_audit_values(
550 vec![ColumnValue::new(
551 "created_at",
552 SqlValue::String("user-created-at".to_string()),
553 )],
554 context(Some(&request_values)),
555 Some(&FixedAuditProvider),
556 )
557 .expect("audit values should resolve");
558
559 assert_eq!(
560 resolved,
561 vec![
562 ColumnValue::new(
563 "created_at",
564 SqlValue::String("user-created-at".to_string())
565 ),
566 ColumnValue::new(
567 "created_by",
568 SqlValue::String("request-created-by".to_string())
569 ),
570 ColumnValue::new(
571 "updated_by",
572 SqlValue::String("provider-updated-by".to_string())
573 ),
574 ]
575 );
576 }
577
578 #[test]
579 fn resolve_audit_values_uses_request_values_without_provider() {
580 let request_values = AuditRequestValues::new(vec![ColumnValue::new(
581 "updated_by",
582 SqlValue::String("request-updated-by".to_string()),
583 )]);
584
585 let resolved = resolve_audit_values(vec![], context(Some(&request_values)), None)
586 .expect("request audit values should resolve");
587
588 assert_eq!(
589 resolved,
590 vec![ColumnValue::new(
591 "updated_by",
592 SqlValue::String("request-updated-by".to_string())
593 )]
594 );
595 }
596
597 #[test]
598 fn resolve_audit_values_rejects_duplicate_user_columns() {
599 let error = resolve_audit_values(
600 vec![
601 ColumnValue::new("created_at", SqlValue::String("first".to_string())),
602 ColumnValue::new("created_at", SqlValue::String("second".to_string())),
603 ],
604 context(None),
605 None,
606 )
607 .unwrap_err();
608
609 assert_eq!(
610 error.message(),
611 "duplicate column `created_at` in audit values"
612 );
613 assert_eq!(error.kind(), OrmErrorKind::Compile);
614 }
615
616 #[test]
617 fn resolve_audit_values_rejects_duplicate_request_columns() {
618 let request_values = AuditRequestValues::new(vec![
619 ColumnValue::new("created_by", SqlValue::String("first".to_string())),
620 ColumnValue::new("created_by", SqlValue::String("second".to_string())),
621 ]);
622
623 let error = resolve_audit_values(vec![], context(Some(&request_values)), None).unwrap_err();
624
625 assert_eq!(
626 error.message(),
627 "duplicate column `created_by` in audit request values"
628 );
629 assert_eq!(error.kind(), OrmErrorKind::Compile);
630 }
631
632 #[test]
633 fn resolve_audit_values_rejects_duplicate_provider_columns() {
634 struct DuplicateProvider;
635
636 impl AuditProvider for DuplicateProvider {
637 fn values(&self, _context: AuditContext<'_>) -> Result<Vec<ColumnValue>, OrmError> {
638 Ok(vec![
639 ColumnValue::new("updated_at", SqlValue::String("first".to_string())),
640 ColumnValue::new("updated_at", SqlValue::String("second".to_string())),
641 ])
642 }
643 }
644
645 let error =
646 resolve_audit_values(vec![], context(None), Some(&DuplicateProvider)).unwrap_err();
647
648 assert_eq!(
649 error.message(),
650 "duplicate column `updated_at` in audit provider values"
651 );
652 assert_eq!(error.kind(), OrmErrorKind::Compile);
653 }
654}