Skip to content

Commit c8f3836

Browse files
Length operational, DateTime support nearly done
1 parent 8badfa7 commit c8f3836

File tree

9 files changed

+790
-198
lines changed

9 files changed

+790
-198
lines changed

Magic.IndexedDb/Helpers/PropertyMappingCache.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,24 @@ public static string GetCsharpPropertyName(string jsPropertyName, Type type)
357357
return jsPropertyName; // Fallback to original name if not found
358358
}
359359

360+
public static MagicPropertyEntry GetPropertyByCsharpName(this SearchPropEntry propCachee, string csharpName)
361+
{
362+
string errorMsg = $"Error retrieving C# property by the name of '{csharpName}'.";
363+
try
364+
{
365+
if (propCachee.propertyEntries.TryGetValue(csharpName, out var entry))
366+
{
367+
return entry;
368+
}
369+
}
370+
catch (Exception ex)
371+
{
372+
throw new Exception(errorMsg, ex);
373+
}
374+
375+
throw new Exception(errorMsg);
376+
}
377+
360378
public static string GetCsharpPropertyName(this SearchPropEntry propCachee, string jsPropertyName)
361379
{
362380
try

Magic.IndexedDb/LinqTranslation/Extensions/UniversalExpressionBuilder.cs

Lines changed: 300 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Magic.IndexedDb.Helpers;
22
using Magic.IndexedDb.LinqTranslation.Models;
3+
using Magic.IndexedDb.Models;
34
using Magic.IndexedDb.Models.UniversalOperations;
45
using System;
56
using System.Collections;
@@ -260,6 +261,11 @@ private FilterNode BuildComparisonLeaf(BinaryExpression bin, string? forceOperat
260261
{
261262
string operation = forceOperation ?? bin.NodeType.ToString();
262263

264+
if (TryRecognizeSpecialOperation(bin, operation, out var specialNode))
265+
{
266+
return specialNode;
267+
}
268+
263269
if (IsParameterMember(bin.Left) && !IsParameterMember(bin.Right))
264270
{
265271
var left = bin.Left as MemberExpression;
@@ -289,6 +295,282 @@ private FilterNode BuildComparisonLeaf(BinaryExpression bin, string? forceOperat
289295
throw new InvalidOperationException($"Unsupported binary expression: {bin}");
290296
}
291297

298+
private bool TryRecognizeSpecialOperation(BinaryExpression bin, string operation, out FilterNode node)
299+
{
300+
node = null!;
301+
302+
303+
if (TryRecognizeLengthProperty(bin, operation, out node))
304+
return true;
305+
306+
// Recognize things like: x => x.DateOfBirth.Value.Year >= 2020
307+
if (TryRecognizeDateProperty(bin, operation, out node))
308+
return true;
309+
310+
// Future recognizers:
311+
// if (TryRecognizeEnumFlag(bin, operation, out node)) return true;
312+
313+
return false;
314+
}
315+
316+
317+
private bool TryRecognizeLengthProperty(BinaryExpression bin, string operation, out FilterNode node)
318+
{
319+
node = null!;
320+
var memberPath = GetMemberAccessPath(bin.Left);
321+
322+
if (memberPath == null || memberPath.Count == 0)
323+
return false;
324+
325+
// Error if .Value is the last thing appended (illegal usage)
326+
if (memberPath[^1] == "Value")
327+
{
328+
throw new InvalidOperationException("You cannot end an expression with '.Value'. Only specific extensions like '.Length' are allowed.");
329+
}
330+
331+
// Only allow if the final segment is "Length"
332+
if (memberPath[^1] != "Length")
333+
return false;
334+
335+
// Must have at least 2 parts: root + Length
336+
if (memberPath.Count < 2)
337+
return false;
338+
339+
SearchPropEntry spe = PropertyMappingCache.GetTypeOfTProperties(typeof(T));
340+
341+
//string rootPropName = memberPath[memberPath.Count - 2]; // i.e., "Name"
342+
string rootPropName = ExtractRootProperty(memberPath);
343+
344+
MagicPropertyEntry mpe = spe.GetPropertyByCsharpName(rootPropName);
345+
string jsProp = mpe.JsPropertyName;
346+
347+
348+
var value = ToConst(bin.Right).Value;
349+
if (value == null || value is not int)
350+
return false;
351+
352+
string op = operation switch
353+
{
354+
"Equal" => "LengthEqual",
355+
"NotEqual" => "NotLengthEqual",
356+
"GreaterThan" => "LengthGreaterThan",
357+
"GreaterThanOrEqual" => "LengthGreaterThanOrEqual",
358+
"LessThan" => "LengthLessThan",
359+
"LessThanOrEqual" => "LengthLessThanOrEqual",
360+
_ => throw new InvalidOperationException($"Unsupported operator '{operation}' for .Length")
361+
};
362+
363+
// TODO: Replace this with your isString detection logic
364+
bool isString = mpe.Property.PropertyType == typeof(string);
365+
366+
node = new FilterNode
367+
{
368+
NodeType = FilterNodeType.Condition,
369+
Condition = new FilterCondition(
370+
jsProp,
371+
op,
372+
value,
373+
isString,
374+
false
375+
)
376+
};
377+
378+
return true;
379+
}
380+
381+
private static string ExtractRootProperty(List<string> path)
382+
{
383+
if (path.Count < 2)
384+
throw new InvalidOperationException("Invalid member access path.");
385+
386+
return path[^2] == "Value" && path.Count >= 3
387+
? path[^3]
388+
: path[^2];
389+
}
390+
391+
392+
private bool TryRecognizeDateProperty(BinaryExpression bin, string operation, out FilterNode node)
393+
{
394+
node = null!;
395+
396+
var memberPath = GetMemberAccessPath(bin.Left);
397+
if (memberPath == null || memberPath.Count < 2)
398+
return false;
399+
400+
var finalSegment = memberPath[^1];
401+
var rootSegment = memberPath[^2];
402+
403+
SearchPropEntry spe = PropertyMappingCache.GetTypeOfTProperties(typeof(T));
404+
405+
string rootPropName = ExtractRootProperty(memberPath);
406+
MagicPropertyEntry mpe = spe.GetPropertyByCsharpName(rootPropName);
407+
408+
string jsProp = mpe.JsPropertyName;
409+
410+
if (!IsDateType(mpe.Property.PropertyType))
411+
return false;
412+
413+
object? rawConst = ToConst(bin.Right).Value;
414+
415+
if (rawConst == null)
416+
return false;
417+
418+
switch (finalSegment)
419+
{
420+
case "Date":
421+
node = BuildDateEqualityRange(jsProp, rawConst, operation);
422+
return true;
423+
424+
case "Year":
425+
node = BuildDateYearNode(jsProp, rawConst, operation);
426+
return true;
427+
428+
case "Month":
429+
node = BuildComponentCondition(jsProp, rawConst, operation, "Month");
430+
return true;
431+
432+
case "Day":
433+
node = BuildComponentCondition(jsProp, rawConst, operation, "Day");
434+
return true;
435+
436+
case "DayOfYear":
437+
node = BuildComponentCondition(jsProp, rawConst, operation, "GetDayOfYear");
438+
return true;
439+
440+
case "DayOfWeek":
441+
node = BuildDayOfWeekNode(jsProp, bin.Right, operation);
442+
return true;
443+
444+
default:
445+
return false;
446+
}
447+
}
448+
449+
private FilterNode BuildDateYearNode(string jsProp, object value, string operation)
450+
{
451+
if (value is not int year)
452+
throw new InvalidOperationException("Expected integer constant for .Year comparison");
453+
454+
string finalOp = operation switch
455+
{
456+
"Equal" => "DateYearEqual",
457+
"NotEqual" => "NotDateYearEqual",
458+
"GreaterThan" => "DateYearGreaterThan",
459+
"GreaterThanOrEqual" => "DateYearGreaterThanOrEqual",
460+
"LessThan" => "DateYearLessThan",
461+
"LessThanOrEqual" => "DateYearLessThanOrEqual",
462+
_ => throw new InvalidOperationException($"Unsupported operator '{operation}' for .Year")
463+
};
464+
465+
return new FilterNode
466+
{
467+
NodeType = FilterNodeType.Condition,
468+
Condition = new FilterCondition(
469+
jsProp,
470+
finalOp,
471+
year,
472+
false,
473+
false
474+
)
475+
};
476+
}
477+
478+
479+
480+
private static bool IsDateType(Type type)
481+
{
482+
var actual = Nullable.GetUnderlyingType(type) ?? type;
483+
return actual == typeof(DateTime) || actual == typeof(DateOnly);
484+
}
485+
486+
487+
private FilterNode BuildDateEqualityRange(string jsProp, object rawConst, string op)
488+
{
489+
if (rawConst is not DateTime dt)
490+
throw new InvalidOperationException("Expected DateTime constant for .Date comparison");
491+
492+
DateTime startOfDay = dt.Date;
493+
DateTime nextDay = startOfDay.AddDays(1);
494+
495+
if (op is "Equal")
496+
{
497+
return new FilterNode
498+
{
499+
NodeType = FilterNodeType.Logical,
500+
Operator = FilterLogicalOperator.And,
501+
Children = new List<FilterNode>
502+
{
503+
new FilterNode
504+
{
505+
NodeType = FilterNodeType.Condition,
506+
Condition = new FilterCondition(jsProp, "GreaterThanOrEqual", startOfDay, false, false)
507+
},
508+
new FilterNode
509+
{
510+
NodeType = FilterNodeType.Condition,
511+
Condition = new FilterCondition(jsProp, "LessThan", nextDay, false, false)
512+
}
513+
}
514+
};
515+
}
516+
517+
// For <, <=, >, >=, NotEqual, etc.
518+
return new FilterNode
519+
{
520+
NodeType = FilterNodeType.Condition,
521+
Condition = new FilterCondition(jsProp, op, startOfDay, false, false)
522+
};
523+
}
524+
525+
526+
private FilterNode BuildComponentCondition(string jsProp, object value, string operation, string component)
527+
{
528+
string finalOp = operation switch
529+
{
530+
"Equal" => $"{component}Equal",
531+
"NotEqual" => $"Not{component}Equal",
532+
"GreaterThan" => $"{component}GreaterThan",
533+
"GreaterThanOrEqual" => $"{component}GreaterThanOrEqual",
534+
"LessThan" => $"{component}LessThan",
535+
"LessThanOrEqual" => $"{component}LessThanOrEqual",
536+
_ => throw new InvalidOperationException($"Unsupported operator '{operation}' for .{component}")
537+
};
538+
539+
return new FilterNode
540+
{
541+
NodeType = FilterNodeType.Condition,
542+
Condition = new FilterCondition(jsProp, finalOp, value, false, false)
543+
};
544+
}
545+
546+
547+
private FilterNode BuildDayOfWeekNode(string jsProp, Expression expr, string operation)
548+
{
549+
// Don't use ToConst because it wraps Convert(DayOfWeek.X) incorrectly
550+
object? result = Expression.Lambda(expr).Compile().DynamicInvoke();
551+
552+
if (result is not DayOfWeek enumVal)
553+
throw new InvalidOperationException("Expected DayOfWeek enum in .DayOfWeek comparison");
554+
555+
int jsDayOfWeek = (int)enumVal; // Sunday = 0 ... Saturday = 6
556+
557+
return BuildComponentCondition(jsProp, jsDayOfWeek, operation, "GetDayOfWeek");
558+
}
559+
560+
561+
private List<string>? GetMemberAccessPath(Expression expr)
562+
{
563+
var path = new List<string>();
564+
while (expr is MemberExpression memberExpr)
565+
{
566+
path.Insert(0, memberExpr.Member.Name);
567+
expr = memberExpr.Expression!;
568+
}
569+
570+
return expr is ParameterExpression ? path : null;
571+
}
572+
573+
292574

293575
// ------------------------------
294576
// "Contains" flattening logic:
@@ -375,7 +657,7 @@ private FilterCondition BuildConditionFromMemberAndConstant(
375657
if (memberExpr == null || constExpr == null)
376658
{
377659
throw new InvalidOperationException("Cannot build filter condition from null expressions.");
378-
}
660+
}
379661

380662
var propInfo = typeof(T).GetProperty(memberExpr.Member.Name);
381663
if (propInfo == null)
@@ -463,13 +745,28 @@ private static bool IsParameterMember(Expression expr)
463745

464746
private static ConstantExpression ToConst(Expression expr)
465747
{
748+
expr = StripConvert(expr); // <-- handle Convert wrappers
749+
466750
return expr switch
467751
{
468752
ConstantExpression c => c,
469-
MemberExpression m => Expression.Constant(Expression.Lambda(m).Compile().DynamicInvoke()),
753+
754+
// e.g., new DateTime(...) or anything not marked constant but compile-safe
755+
NewExpression or MemberExpression or MethodCallExpression =>
756+
Expression.Constant(Expression.Lambda(expr).Compile().DynamicInvoke()),
757+
470758
_ => throw new InvalidOperationException($"Unsupported or non-constant expression: {expr}")
471759
};
472760
}
473-
} // should stay as they are in their own files or namespaces.
761+
762+
private static Expression StripConvert(Expression expr)
763+
{
764+
while (expr is UnaryExpression unary && expr.NodeType == ExpressionType.Convert)
765+
{
766+
expr = unary.Operand;
767+
}
768+
return expr;
769+
}
770+
}
474771
}
475772

0 commit comments

Comments
 (0)