use crate::AuditEntity;
use sql_orm_core::{ColumnMetadata, ColumnValue, EntityMetadata, OrmError};
use std::collections::BTreeSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuditOperation {
Insert,
Update,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct AuditRequestValues {
values: Vec<ColumnValue>,
}
impl AuditRequestValues {
pub fn new(values: Vec<ColumnValue>) -> Self {
Self { values }
}
pub fn values(&self) -> &[ColumnValue] {
&self.values
}
}
#[derive(Debug, Clone, Copy)]
pub struct AuditContext<'a> {
pub entity: &'static EntityMetadata,
pub operation: AuditOperation,
pub request_values: Option<&'a AuditRequestValues>,
}
pub trait AuditProvider: Send + Sync {
fn values(&self, context: AuditContext<'_>) -> Result<Vec<ColumnValue>, OrmError>;
}
pub trait AuditValues {
fn audit_values(self) -> Vec<ColumnValue>;
}
pub(crate) fn apply_audit_values<E: AuditEntity>(
operation: AuditOperation,
values: Vec<ColumnValue>,
audit_provider: Option<&dyn AuditProvider>,
request_values: Option<&AuditRequestValues>,
) -> Result<Vec<ColumnValue>, OrmError> {
validate_no_duplicate_columns("audit values", &values)?;
let Some(policy) = E::audit_policy() else {
return Ok(values);
};
let context = AuditContext {
entity: E::metadata(),
operation,
request_values,
};
let provider_values = match audit_provider {
Some(provider) => {
let values = provider.values(context)?;
validate_no_duplicate_columns("audit provider values", &values)?;
values
}
None => Vec::new(),
};
if let Some(request_values) = request_values {
validate_no_duplicate_columns("audit request values", request_values.values())?;
}
let mut resolved = values;
let mut seen = resolved
.iter()
.map(|value| value.column_name)
.collect::<BTreeSet<_>>();
for value in &resolved {
if let Some(column) = policy
.columns
.iter()
.find(|column| column.column_name == value.column_name)
{
validate_audit_column_value(operation, column, &value.value)?;
}
}
if let Some(request_values) = request_values {
append_missing_audit_values(
operation,
policy.columns,
&mut resolved,
&mut seen,
request_values.values(),
)?;
}
append_missing_audit_values(
operation,
policy.columns,
&mut resolved,
&mut seen,
&provider_values,
)?;
Ok(resolved)
}
#[doc(hidden)]
pub fn resolve_audit_values(
values: Vec<ColumnValue>,
context: AuditContext<'_>,
audit_provider: Option<&dyn AuditProvider>,
) -> Result<Vec<ColumnValue>, OrmError> {
validate_no_duplicate_columns("audit values", &values)?;
let mut resolved = values;
let mut seen = resolved
.iter()
.map(|value| value.column_name)
.collect::<BTreeSet<_>>();
if let Some(request_values) = context.request_values {
validate_no_duplicate_columns("audit request values", request_values.values())?;
append_missing_values(&mut resolved, &mut seen, request_values.values());
}
if let Some(provider) = audit_provider {
let provider_values = provider.values(context)?;
validate_no_duplicate_columns("audit provider values", &provider_values)?;
append_missing_values(&mut resolved, &mut seen, &provider_values);
}
Ok(resolved)
}
fn append_missing_audit_values(
operation: AuditOperation,
columns: &'static [ColumnMetadata],
resolved: &mut Vec<ColumnValue>,
seen: &mut BTreeSet<&'static str>,
values: &[ColumnValue],
) -> Result<(), OrmError> {
for value in values {
let Some(column) = columns
.iter()
.find(|column| column.column_name == value.column_name)
else {
continue;
};
validate_audit_column_value(operation, column, &value.value)?;
if seen.insert(value.column_name) {
resolved.push(value.clone());
}
}
Ok(())
}
fn validate_audit_column_value(
operation: AuditOperation,
column: &ColumnMetadata,
value: &sql_orm_core::SqlValue,
) -> Result<(), OrmError> {
match operation {
AuditOperation::Insert if !column.insertable => {
return Err(OrmError::new(format!(
"audit insert column `{}` is not insertable",
column.column_name
)));
}
AuditOperation::Update if !column.updatable => {
return Err(OrmError::new(format!(
"audit update column `{}` is not updatable",
column.column_name
)));
}
_ => {}
}
if value.is_null() && !column.nullable {
return Err(OrmError::new(format!(
"audit column `{}` is not nullable",
column.column_name
)));
}
Ok(())
}
fn validate_no_duplicate_columns(label: &str, values: &[ColumnValue]) -> Result<(), OrmError> {
let mut seen = BTreeSet::new();
for value in values {
if !seen.insert(value.column_name) {
return Err(OrmError::new(format!(
"duplicate column `{}` in {label}",
value.column_name
)));
}
}
Ok(())
}
fn append_missing_values(
resolved: &mut Vec<ColumnValue>,
seen: &mut BTreeSet<&'static str>,
values: &[ColumnValue],
) {
for value in values {
if seen.insert(value.column_name) {
resolved.push(value.clone());
}
}
}
#[cfg(test)]
mod tests {
use super::{
AuditContext, AuditOperation, AuditProvider, AuditRequestValues, apply_audit_values,
resolve_audit_values,
};
use crate::AuditEntity;
use sql_orm_core::{
ColumnMetadata, ColumnValue, Entity, EntityMetadata, EntityPolicyMetadata, OrmError,
PrimaryKeyMetadata, SqlServerType, SqlValue,
};
struct TestAuditedEntity;
static TEST_ENTITY_COLUMNS: [ColumnMetadata; 1] = [ColumnMetadata {
rust_field: "id",
column_name: "id",
renamed_from: None,
sql_type: SqlServerType::BigInt,
nullable: false,
primary_key: true,
identity: None,
default_sql: None,
computed_sql: None,
rowversion: false,
insertable: false,
updatable: false,
max_length: None,
precision: None,
scale: None,
}];
static TEST_ENTITY_METADATA: EntityMetadata = EntityMetadata {
rust_name: "AuditedEntity",
schema: "dbo",
table: "audited_entities",
renamed_from: None,
columns: &TEST_ENTITY_COLUMNS,
primary_key: PrimaryKeyMetadata::new(None, &["id"]),
indexes: &[],
foreign_keys: &[],
navigations: &[],
};
static TEST_AUDIT_COLUMNS: [ColumnMetadata; 3] = [
ColumnMetadata {
rust_field: "created_at",
column_name: "created_at",
renamed_from: None,
sql_type: SqlServerType::DateTime2,
nullable: false,
primary_key: false,
identity: None,
default_sql: Some("SYSUTCDATETIME()"),
computed_sql: None,
rowversion: false,
insertable: false,
updatable: false,
max_length: None,
precision: None,
scale: None,
},
ColumnMetadata {
rust_field: "created_by",
column_name: "created_by",
renamed_from: None,
sql_type: SqlServerType::BigInt,
nullable: false,
primary_key: false,
identity: None,
default_sql: None,
computed_sql: None,
rowversion: false,
insertable: true,
updatable: true,
max_length: None,
precision: None,
scale: None,
},
ColumnMetadata {
rust_field: "updated_by",
column_name: "updated_by",
renamed_from: None,
sql_type: SqlServerType::NVarChar,
nullable: true,
primary_key: false,
identity: None,
default_sql: None,
computed_sql: None,
rowversion: false,
insertable: true,
updatable: true,
max_length: Some(120),
precision: None,
scale: None,
},
];
impl Entity for TestAuditedEntity {
fn metadata() -> &'static EntityMetadata {
&TEST_ENTITY_METADATA
}
}
impl AuditEntity for TestAuditedEntity {
fn audit_policy() -> Option<EntityPolicyMetadata> {
Some(EntityPolicyMetadata::new("audit", &TEST_AUDIT_COLUMNS))
}
}
struct FixedAuditProvider;
impl AuditProvider for FixedAuditProvider {
fn values(&self, context: AuditContext<'_>) -> Result<Vec<ColumnValue>, OrmError> {
assert_eq!(context.entity.rust_name, "AuditedEntity");
assert_eq!(context.operation, AuditOperation::Insert);
assert!(context.request_values.is_some());
Ok(vec![
ColumnValue::new(
"created_at",
SqlValue::String("provider-created-at".to_string()),
),
ColumnValue::new(
"updated_by",
SqlValue::String("provider-updated-by".to_string()),
),
])
}
}
fn context<'a>(request_values: Option<&'a AuditRequestValues>) -> AuditContext<'a> {
AuditContext {
entity: &TEST_ENTITY_METADATA,
operation: AuditOperation::Insert,
request_values,
}
}
#[test]
fn apply_audit_values_completes_only_missing_insertable_audit_columns() {
let request_values = AuditRequestValues::new(vec![
ColumnValue::new("created_by", SqlValue::I64(7)),
ColumnValue::new(
"ignored",
SqlValue::String("not an audit column".to_string()),
),
]);
struct Provider;
impl AuditProvider for Provider {
fn values(&self, _context: AuditContext<'_>) -> Result<Vec<ColumnValue>, OrmError> {
Ok(vec![
ColumnValue::new("created_by", SqlValue::I64(9)),
ColumnValue::new("updated_by", SqlValue::String("provider".to_string())),
ColumnValue::new("other", SqlValue::String("not an audit column".to_string())),
])
}
}
let values = apply_audit_values::<TestAuditedEntity>(
AuditOperation::Insert,
vec![ColumnValue::new(
"name",
SqlValue::String("existing".to_string()),
)],
Some(&Provider),
Some(&request_values),
)
.expect("audit insert values should resolve");
assert_eq!(
values,
vec![
ColumnValue::new("name", SqlValue::String("existing".to_string())),
ColumnValue::new("created_by", SqlValue::I64(7)),
ColumnValue::new("updated_by", SqlValue::String("provider".to_string())),
]
);
}
#[test]
fn apply_audit_values_rejects_duplicate_insert_columns() {
let error = apply_audit_values::<TestAuditedEntity>(
AuditOperation::Insert,
vec![
ColumnValue::new("created_by", SqlValue::I64(7)),
ColumnValue::new("created_by", SqlValue::I64(8)),
],
None,
None,
)
.unwrap_err();
assert_eq!(
error,
OrmError::new("duplicate column `created_by` in audit values")
);
}
#[test]
fn apply_audit_values_rejects_non_insertable_audit_column() {
let request_values = AuditRequestValues::new(vec![ColumnValue::new(
"created_at",
SqlValue::String("runtime".to_string()),
)]);
let error = apply_audit_values::<TestAuditedEntity>(
AuditOperation::Insert,
Vec::new(),
None,
Some(&request_values),
)
.unwrap_err();
assert_eq!(
error,
OrmError::new("audit insert column `created_at` is not insertable")
);
}
#[test]
fn apply_audit_values_rejects_null_for_non_nullable_audit_column() {
let request_values =
AuditRequestValues::new(vec![ColumnValue::new("created_by", SqlValue::Null)]);
let error = apply_audit_values::<TestAuditedEntity>(
AuditOperation::Insert,
Vec::new(),
None,
Some(&request_values),
)
.unwrap_err();
assert_eq!(
error,
OrmError::new("audit column `created_by` is not nullable")
);
}
#[test]
fn apply_audit_values_completes_only_missing_updatable_audit_columns() {
struct Provider;
impl AuditProvider for Provider {
fn values(&self, _context: AuditContext<'_>) -> Result<Vec<ColumnValue>, OrmError> {
Ok(vec![
ColumnValue::new("created_by", SqlValue::I64(9)),
ColumnValue::new("updated_by", SqlValue::String("provider".to_string())),
])
}
}
let values = apply_audit_values::<TestAuditedEntity>(
AuditOperation::Update,
vec![
ColumnValue::new("name", SqlValue::String("updated".to_string())),
ColumnValue::new("updated_by", SqlValue::String("explicit".to_string())),
],
Some(&Provider),
None,
)
.expect("audit update values should resolve");
assert_eq!(
values,
vec![
ColumnValue::new("name", SqlValue::String("updated".to_string())),
ColumnValue::new("updated_by", SqlValue::String("explicit".to_string())),
ColumnValue::new("created_by", SqlValue::I64(9)),
]
);
}
#[test]
fn apply_audit_values_rejects_non_updatable_audit_column() {
let error = apply_audit_values::<TestAuditedEntity>(
AuditOperation::Update,
vec![ColumnValue::new(
"created_at",
SqlValue::String("runtime".to_string()),
)],
None,
None,
)
.unwrap_err();
assert_eq!(
error,
OrmError::new("audit update column `created_at` is not updatable")
);
}
#[test]
fn resolve_audit_values_preserves_user_values_before_request_and_provider_values() {
let request_values = AuditRequestValues::new(vec![
ColumnValue::new(
"created_at",
SqlValue::String("request-created-at".to_string()),
),
ColumnValue::new(
"created_by",
SqlValue::String("request-created-by".to_string()),
),
]);
let resolved = resolve_audit_values(
vec![ColumnValue::new(
"created_at",
SqlValue::String("user-created-at".to_string()),
)],
context(Some(&request_values)),
Some(&FixedAuditProvider),
)
.expect("audit values should resolve");
assert_eq!(
resolved,
vec![
ColumnValue::new(
"created_at",
SqlValue::String("user-created-at".to_string())
),
ColumnValue::new(
"created_by",
SqlValue::String("request-created-by".to_string())
),
ColumnValue::new(
"updated_by",
SqlValue::String("provider-updated-by".to_string())
),
]
);
}
#[test]
fn resolve_audit_values_uses_request_values_without_provider() {
let request_values = AuditRequestValues::new(vec![ColumnValue::new(
"updated_by",
SqlValue::String("request-updated-by".to_string()),
)]);
let resolved = resolve_audit_values(vec![], context(Some(&request_values)), None)
.expect("request audit values should resolve");
assert_eq!(
resolved,
vec![ColumnValue::new(
"updated_by",
SqlValue::String("request-updated-by".to_string())
)]
);
}
#[test]
fn resolve_audit_values_rejects_duplicate_user_columns() {
let error = resolve_audit_values(
vec![
ColumnValue::new("created_at", SqlValue::String("first".to_string())),
ColumnValue::new("created_at", SqlValue::String("second".to_string())),
],
context(None),
None,
)
.unwrap_err();
assert_eq!(
error,
OrmError::new("duplicate column `created_at` in audit values")
);
}
#[test]
fn resolve_audit_values_rejects_duplicate_request_columns() {
let request_values = AuditRequestValues::new(vec![
ColumnValue::new("created_by", SqlValue::String("first".to_string())),
ColumnValue::new("created_by", SqlValue::String("second".to_string())),
]);
let error = resolve_audit_values(vec![], context(Some(&request_values)), None).unwrap_err();
assert_eq!(
error,
OrmError::new("duplicate column `created_by` in audit request values")
);
}
#[test]
fn resolve_audit_values_rejects_duplicate_provider_columns() {
struct DuplicateProvider;
impl AuditProvider for DuplicateProvider {
fn values(&self, _context: AuditContext<'_>) -> Result<Vec<ColumnValue>, OrmError> {
Ok(vec![
ColumnValue::new("updated_at", SqlValue::String("first".to_string())),
ColumnValue::new("updated_at", SqlValue::String("second".to_string())),
])
}
}
let error =
resolve_audit_values(vec![], context(None), Some(&DuplicateProvider)).unwrap_err();
assert_eq!(
error,
OrmError::new("duplicate column `updated_at` in audit provider values")
);
}
}