diff --git a/demo/Sanity.Linq.Demo/Sanity.Linq.Demo.csproj b/demo/Sanity.Linq.Demo/Sanity.Linq.Demo.csproj index 8ad2cef..d1bc360 100644 --- a/demo/Sanity.Linq.Demo/Sanity.Linq.Demo.csproj +++ b/demo/Sanity.Linq.Demo/Sanity.Linq.Demo.csproj @@ -1,7 +1,7 @@ - netstandard2.0 + net9.0 Sanity.Linq.Demo Sanity.Linq.Demo diff --git a/src/Sanity.Linq/QueryProvider/SanityExpressionParser.cs b/src/Sanity.Linq/QueryProvider/SanityExpressionParser.cs index 66635de..cfad8e1 100644 --- a/src/Sanity.Linq/QueryProvider/SanityExpressionParser.cs +++ b/src/Sanity.Linq/QueryProvider/SanityExpressionParser.cs @@ -51,10 +51,10 @@ public SanityExpressionParser(Expression expression, Type docType, int maxNestin public Type ResultType { get; } public string BuildQuery(bool includeProjections = true) - { + { //Initialize query builder QueryBuilder = new SanityQueryBuilder(); - + // Add contraint for root type QueryBuilder.DocType = DocType; QueryBuilder.ResultType = ResultType ?? DocType; @@ -70,7 +70,7 @@ public string BuildQuery(bool includeProjections = true) // Build query var query = QueryBuilder.Build(includeProjections, MaxNestingLevel); return query; - + } @@ -259,7 +259,7 @@ e.Arguments[0] is MemberExpression && return $"{memberName2} in {memberName1}"; } throw new Exception("'Contains' is only supported for simple expressions with non-null values."); - + } case "GetValue`1": case "GetValue": @@ -280,10 +280,6 @@ e.Arguments[0] is MemberExpression && { //Arg 0: Source var elementType = TypeSystem.GetElementType(e.Arguments[0].Type); - if (elementType != DocType) - { - throw new Exception("Where expressions are only supported on the root type."); - } Visit(e.Arguments[0]); //Arg 1: Query / lambda @@ -293,6 +289,13 @@ e.Arguments[0] is MemberExpression && QueryBuilder.Constraints.Add(constraint); return constraint; } + + if (e.Arguments[1] is LambdaExpression le) + { + var constraint = TransformOperand(le.Body); + return constraint; + } + throw new Exception("Syntax of Select expression not supported."); } case "Select": @@ -336,11 +339,36 @@ e.Arguments[0] is MemberExpression && sourceName = targetName; } } - var projection = GetJoinProjection(sourceName, targetName, propertyType, 0, MaxNestingLevel); + var projection = + GetJoinProjection(sourceName, targetName, propertyType, 0, MaxNestingLevel); QueryBuilder.Includes[fieldPath] = projection; return projection; } + else if (l.Body is MethodCallExpression n) + { + var fieldPath = TransformOperand(n.Arguments[0]); + var targetName = fieldPath.Split(new[] { '.', '>' }).LastOrDefault(); + var sourceName = targetName; + + // Arg 2: fieldName + if (e.Arguments.Count > 2 && e.Arguments[2] is ConstantExpression c) + { + sourceName = c.Value?.ToString(); + if (string.IsNullOrEmpty(sourceName)) + { + sourceName = targetName; + } + } + + var constraint = TransformOperand(l.Body); + + var projection = + GetJoinProjection(sourceName, targetName, l.Body.Type, 0, MaxNestingLevel, constraint); + QueryBuilder.Includes[fieldPath] = projection; + return constraint; + } } + throw new Exception("Joins can only be applied to properties."); } @@ -515,7 +543,7 @@ protected string TransformOperand(Expression e) { memberPath.Add(TransformOperand(m.Expression)); } - + return memberPath.Aggregate((a1, a2) => a1 != "->" && a2 != "->" ? $"{a2}.{a1}" : $"{a2}{a1}").Replace(".->","->").Replace("->.","->"); } @@ -625,7 +653,7 @@ protected static List GetPropertyProjectionList(Type type, int nestingLe } var result = new List(); - + // "Include all" primative types with a simple ... result.Add("..."); foreach (var prop in props) @@ -639,7 +667,7 @@ protected static List GetPropertyProjectionList(Type type, int nestingLe var fieldRef = targetName == sourceName ? sourceName : $"\"{targetName}\": {sourceName}"; bool isIncluded = includeAttr != null; if (isIncluded) - { + { // Add a join projection for [Include]d properties result.Add(GetJoinProjection(sourceName, targetName, prop.PropertyType, nestingLevel+1, maxNestingLevel)); } @@ -674,7 +702,7 @@ protected static List GetPropertyProjectionList(Type type, int nestingLe result.Add($"{fieldRef}{{{fieldList.Aggregate((c, n) => c + "," + n)}}}"); } } - } + } } return result; } @@ -686,7 +714,7 @@ protected static List GetPropertyProjectionList(Type type, int nestingLe /// /// /// - public static string GetJoinProjection(string sourceName, string targetName, Type propertyType, int nestingLevel, int maxNestingLevel) + public static string GetJoinProjection(string sourceName, string targetName, Type propertyType, int nestingLevel, int maxNestingLevel, string constraints = null) { string projection = ""; var fieldRef = sourceName; @@ -719,7 +747,7 @@ public static string GetJoinProjection(string sourceName, string targetName, Typ var elementType = listOfSanityReferenceType.GetGenericArguments()[0].GetGenericArguments()[0]; var fields = GetPropertyProjectionList(elementType, nestingLevel, maxNestingLevel); var fieldList = fields.Aggregate((c, n) => c + "," + n); - projection = $"{fieldRef}[]->{{{fieldList}}}"; + projection = $"{fieldRef}[{constraints}]->{{{fieldList}}}"; } else { @@ -747,7 +775,7 @@ public static string GetJoinProjection(string sourceName, string targetName, Typ var propertyName = nestedSanityReferenceProperty.GetCustomAttribute()?.PropertyName ?? nestedSanityReferenceProperty.Name.ToCamelCase(); var fields = GetPropertyProjectionList(propertyType, nestingLevel, maxNestingLevel); var elementType = nestedSanityReferenceProperty.PropertyType.GetGenericArguments()[0]; - var nestedFields = GetPropertyProjectionList(elementType, nestingLevel+1, maxNestingLevel); + var nestedFields = GetPropertyProjectionList(elementType, nestingLevel + 1, maxNestingLevel); // Nested Reference var fieldList = fields.Select(f => f == propertyName ? $"{propertyName}->{(nestedFields.Count > 0 ? ("{" + nestedFields.Aggregate((a, b) => a + "," + b) + "}") : "")}" : f).Aggregate((c, n) => c + "," + n); @@ -766,28 +794,28 @@ public static string GetJoinProjection(string sourceName, string targetName, Typ var fields = GetPropertyProjectionList(propertyType, nestingLevel, maxNestingLevel); var collectionType = nestedListOfSanityReferenceType.PropertyType.GetGenericArguments()[0]; var elementType = collectionType.GetGenericArguments()[0]; - var nestedFields = GetPropertyProjectionList(elementType, nestingLevel+1, maxNestingLevel); + var nestedFields = GetPropertyProjectionList(elementType, nestingLevel + 1, maxNestingLevel); // Nested Reference var fieldList = fields.Select(f => f == propertyName ? $"{propertyName}[]->{(nestedFields.Count > 0 ? ("{" + nestedFields.Aggregate((a, b) => a + "," + b) + "}") : "")}" : f).Aggregate((c, n) => c + "," + n); projection = $"{fieldRef}{{{fieldList}}}"; - } + } else { - + var listOfSanityImagesType = propertyType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>) && i.GetGenericArguments()[0].GetProperties().Any(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(SanityReference<>) && (p.Name.ToLower() == "asset" || ((p.GetCustomAttributes(true).FirstOrDefault())?.PropertyName?.Equals("asset")).GetValueOrDefault()))); bool isListOfSanityImages = listOfSanityImagesType != null; if (isListOfSanityImages) { // CASE 6: Array of objects with "asset" field (e.g. images) - var elementType = listOfSanityImagesType.GetGenericArguments()[0]; + var elementType = listOfSanityImagesType.GetGenericArguments()[0]; var fields = GetPropertyProjectionList(elementType, nestingLevel, maxNestingLevel); // Nested Reference var fieldList = fields.Select(f => f.StartsWith("asset") ? $"asset->{{{SanityConstants.SPREAD_OPERATOR}}}" : f).Aggregate((c, n) => c + "," + n); - projection = $"{fieldRef}[]{{{fieldList}}}"; + projection = $"{fieldRef}[{constraints}]{{{fieldList}}}"; } } } @@ -816,12 +844,12 @@ public static string GetJoinProjection(string sourceName, string targetName, Typ // ..., // _type == 'reference' => @->{...} // }, - projection = $"{fieldRef}[]{{{fieldList},{SanityConstants.DEREFERENCING_SWITCH + "{" + fieldList + "}"}}}"; + projection = $"{fieldRef}[{constraints}]{{{fieldList},{SanityConstants.DEREFERENCING_SWITCH + "{" + fieldList + "}"}}}"; } else { // "object" without any fields defined - projection = $"{fieldRef}[]->"; + projection = $"{fieldRef}[{constraints}]->"; } } else @@ -832,13 +860,13 @@ public static string GetJoinProjection(string sourceName, string targetName, Typ // Other strongly typed includes var fieldList = fields.Aggregate((c, n) => c + "," + n); // projection = $"{fieldRef}->{{{fieldList}}}"; - projection = $"{fieldRef}{{{fieldList},{SanityConstants.DEREFERENCING_SWITCH + "{" + fieldList + "}"}}}"; + projection = $"{fieldRef}[{constraints}]{{{fieldList},{SanityConstants.DEREFERENCING_SWITCH + "{" + fieldList + "}"}}}"; } else { // "object" without any fields defined //projection = $"{fieldRef}->{{{SanityConstants.SPREAD_OPERATOR}}}"; - projection = $"{fieldRef}{{{SanityConstants.SPREAD_OPERATOR},{SanityConstants.DEREFERENCING_SWITCH + "{" + SanityConstants.SPREAD_OPERATOR + "}"}}}"; + projection = $"{fieldRef}[{constraints}]{{{SanityConstants.SPREAD_OPERATOR},{SanityConstants.DEREFERENCING_SWITCH + "{" + SanityConstants.SPREAD_OPERATOR + "}"}}}"; } } } @@ -850,6 +878,8 @@ public static string GetJoinProjection(string sourceName, string targetName, Typ internal class SanityConstants { public const string ARRAY_INDICATOR = "[]"; + public const string ARRAY_INDICATOR_START = "["; + public const string ARRAY_INDICATOR_END = "]"; public const string DEREFERENCING_SWITCH = "_type=='reference'=>@->"; public const string SPREAD_OPERATOR = "..."; public const string STRING_DELIMITOR = "\""; @@ -946,8 +976,8 @@ public virtual string Build(bool includeProjections, int maxNestingLevel) // Add projection if (!string.IsNullOrEmpty(projection)) - { - projection = ExpandIncludesInProjection(projection, Includes); + { + projection = ExpandIncludesInProjection(projection, Includes); projection = projection.Replace($"{{{SanityConstants.SPREAD_OPERATOR}}}", ""); // Remove redundant {...} to simplify query if (projection != $"{{{SanityConstants.SPREAD_OPERATOR}}}") // Don't need to add an empty projection { @@ -1041,7 +1071,7 @@ private string ExpandIncludesInProjection(string projection, Dictionary(); foreach (var property in obj) { - if (property.Key == part - || property.Key.StartsWith($"{GroqTokens[SanityConstants.STRING_DELIMITOR]}{part}{GroqTokens[SanityConstants.STRING_DELIMITOR]}") - || property.Key.StartsWith(part + GroqTokens[SanityConstants.ARRAY_INDICATOR]) + if (property.Key == part + || property.Key.StartsWith($"{GroqTokens[SanityConstants.STRING_DELIMITOR]}{part}{GroqTokens[SanityConstants.STRING_DELIMITOR]}") + || property.Key.StartsWith(part + GroqTokens[SanityConstants.ARRAY_INDICATOR]) || property.Key.StartsWith(part + GroqTokens[SanityConstants.DEREFERENCING_OPERATOR])) { fieldsToReplace.Add(property.Key); @@ -1078,6 +1108,10 @@ private string ExpandIncludesInProjection(string projection, Dictionary $"{m.Groups[1]}\"{m.Groups[2].Value.Trim()}\"{m.Groups[3]}"); + return json; } diff --git a/src/Sanity.Linq/Sanity.Linq.csproj b/src/Sanity.Linq/Sanity.Linq.csproj index 08c420c..ef02c37 100644 --- a/src/Sanity.Linq/Sanity.Linq.csproj +++ b/src/Sanity.Linq/Sanity.Linq.csproj @@ -1,4 +1,5 @@ -