Skip to content

Commit

Permalink
Added support for basic validation rules on Ceen.Database
Browse files Browse the repository at this point in the history
  • Loading branch information
kenkendk committed Aug 4, 2019
1 parent 2ae6370 commit 2cec7e1
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 4 deletions.
13 changes: 13 additions & 0 deletions Ceen.Database/DatabaseDialectSQLite.cs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,19 @@ private string RenderWhereClause(Type type, object element, List<object> args)
string.Equals(compare.Operator, "NOT IN", StringComparison.OrdinalIgnoreCase)
)
{
// Support for "IN" with sub-query
if (compare.RightHandSide is Query rhq)
{
if (rhq.Parsed.Type != QueryType.Select)
throw new ArgumentException("The query must be a select statement for exactly one column", nameof(compare.RightHandSide));
if (rhq.Parsed.SelectColumns.Count() != 1)
throw new ArgumentException("The query must be a select statement for exactly one column", nameof(compare.RightHandSide));

var rvp = RenderStatement(rhq);
args.AddRange(rvp.Value);
return $"{RenderWhereClause(type, compare.LeftHandSide, args)} {compare.Operator} ({rvp.Key})";
}

var rhsel = compare.RightHandSide;
IEnumerable items = null;

Expand Down
11 changes: 9 additions & 2 deletions Ceen.Database/DatabaseHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,11 @@ public static int Update(this IDbConnection connection, Query query)
{
if (query.Parsed.Type != QueryType.Update)
throw new InvalidOperationException($"Cannot use a query of type {query.Parsed.Type} for UPDATE");

var dialect = GetDialect(connection);
var mapping = dialect.GetTypeMap(query.Parsed.DataType);
mapping.Validate(query.Parsed.UpdateValues);

var q = connection.GetDialect().RenderStatement(query);
using (var cmd = connection.CreateCommandWithParameters(q.Key))
return cmd.ExecuteNonQuery(q.Value);
Expand Down Expand Up @@ -936,9 +941,11 @@ public static T Insert<T>(this IDbConnection connection, Query query)
{
var dialect = GetDialect(connection);
var mapping = dialect.GetTypeMap(typeof(T));

var q = dialect.RenderStatement(query);

var item = (T)query.Parsed.InsertItem;
mapping.Validate(item);

var q = dialect.RenderStatement(query);

using (var cmd = connection.CreateCommandWithParameters(q.Key))
{
Expand Down
92 changes: 90 additions & 2 deletions Ceen.Database/TableMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,22 @@ public class TableMapping
/// All columns that are primary keys
/// </summary>
public readonly ColumnMapping[] PrimaryKeys;

/// <summary>
/// The unique mappings
/// </summary>
public readonly UniqueMapping[] Uniques;
/// <summary>
/// The rules for validating table items prior to insert or update
/// </summary>
public readonly ValidationBaseAttribute[] ValidationRules;
/// <summary>
/// All the columns with validation rule(s) attached
/// </summary>
public readonly ColumnMapping[] AllColumnsWithValidationRules;
/// <summary>
/// Flag used to bypass validation code if it does not do anything
/// </summary>
public readonly bool HasValidationRules;

/// <summary>
/// Initializes a new instance of the <see cref="T:Ceen.Database.TableMapping"/> class.
Expand Down Expand Up @@ -177,6 +188,9 @@ public TableMapping(IDatabaseDialect dialect, Type type, string nameoverride = n
IsPrimaryKeyAutogenerated = PrimaryKeys.Any(x => x.AutoGenerateAction == AutoGenerateAction.DatabaseAutoID);
HasDatabaseGeneratedCreateTimestamp = ColumnsWithoutPrimaryKey.Any(x => x.AutoGenerateAction == AutoGenerateAction.DatabaseCreateTimestamp);
HasDatabaseGeneratedChangeTimestamp = ColumnsWithoutPrimaryKey.Any(x => x.AutoGenerateAction == AutoGenerateAction.DatabaseChangeTimestamp);
ValidationRules = type.GetCustomAttributes<ValidationBaseAttribute>(true).ToArray();
AllColumnsWithValidationRules = AllColumns.Where(x => x.ValidationRules.Length > 0).ToArray();
HasValidationRules = ValidationRules.Length + AllColumnsWithValidationRules.Length > 0;
}

/// <summary>
Expand All @@ -188,6 +202,76 @@ public string QuotedColumnName(string propertyname)
{
return Dialect.QuoteName(AllColumnsByMemberName[propertyname].ColumnName);
}

/// <summary>
/// Calls all validation methods on the item
/// </summary>
/// <param name="item">The item to validate, must be of the target type or a dictionary</param>
public void Validate(object item)
{
if (item == null)
throw new ArgumentNullException(nameof(item));

// Do not waste time testing empty validation rules
if (!HasValidationRules)
return;

if (item is Dictionary<string, object> dict)
{
// First validate the overall object
foreach (var v in ValidationRules)
v.Validate(item);

foreach (var c in dict)
{
if (!AllColumnsByMemberName.TryGetValue(c.Key, out var cl))
throw new ArgumentException($"No column named {c.Key} in {Type}");

foreach (var r in cl.ValidationRules)
{
try
{
r.Validate(c.Value);
}
catch (Exception ex)
{
// Add more context to the validation error
if (ex is ValidationException vex)
throw new ValidationException(Type, cl.Member, r, vex.Message, ex);
throw;
}
}
}
}
else if (item.GetType() == Type)
{
foreach (var v in ValidationRules)
v.Validate(item);

foreach (var cl in AllColumnsWithValidationRules)
{
var v = cl.GetValue(item);
foreach (var r in cl.ValidationRules)
{
try
{
r.Validate(v);
}
catch (Exception ex)
{
// Add more context to the validation error
if (ex is ValidationException vex)
throw new ValidationException(Type, cl.Member, r, vex.Message, ex);
throw;
}
}
}
}
else
{
throw new ArgumentException($"Item had type {item.GetType()} but it be either {Type} or a Dictionary<string, object>");
}
}
}

/// <summary>
Expand Down Expand Up @@ -228,6 +312,10 @@ public class ColumnMapping
/// The mapped type of the column
/// </summary>
public readonly string SqlType;
/// <summary>
/// The rules for validating values prior to insert or update
/// </summary>
public readonly ValidationBaseAttribute[] ValidationRules;

/// <summary>
/// Initializes a new instance of the <see cref="T:Ceen.Database.ColumnMapping"/> class.
Expand Down Expand Up @@ -265,7 +353,7 @@ private ColumnMapping(IDatabaseDialect dialect, MemberInfo member, Type memberTy
var sqlType = dialect.GetSqlColumnType(member);
SqlType = sqlType.Item1;
AutoGenerateAction = sqlType.Item2;

ValidationRules = member.GetCustomAttributes<ValidationBaseAttribute>(true).ToArray();
}

/// <summary>
Expand Down

0 comments on commit 2cec7e1

Please sign in to comment.