Skip to content

Commit 4934bc8

Browse files
author
Reng van Oord
committed
Added support for joining / including arrays with a mix of reference and non-reference objects
1 parent 90934f6 commit 4934bc8

File tree

6 files changed

+125
-79
lines changed

6 files changed

+125
-79
lines changed

demo/Sanity.Linq.Demo/Model/Post.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ public Post()
1717

1818
public SanityReference<Author> Author { get; set; }
1919

20+
[Include("author")]
21+
public Author DereferencedAuthor { get; set; }
22+
2023
public CommonTypes.SanityImage MainImage { get; set; }
2124

2225
public List<SanityReference<Category>> Categories { get; set; }

src/Sanity.Linq/Extensions/SanityDocumentSetExtensions.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,24 @@ public static IQueryable<TEntity> Include<TEntity, TProperty>(this IQueryable<TE
159159
}
160160
}
161161

162+
public static IQueryable<TEntity> Include<TEntity, TProperty>(this IQueryable<TEntity> source, Expression<Func<TEntity, TProperty>> property, string sourceName)
163+
{
164+
if (source == null)
165+
{
166+
throw new ArgumentNullException(nameof(source));
167+
}
168+
169+
if (source is SanityDocumentSet<TEntity> dbSet)
170+
{
171+
((SanityDocumentSet<TEntity>)source).Include(property, sourceName);
172+
return source;
173+
}
174+
else
175+
{
176+
throw new Exception("Queryable source must be a SanityDbSet<T>.");
177+
}
178+
}
179+
162180
public static SanityMutationBuilder<TDoc> Patch<TDoc>(this IQueryable<TDoc> source, Action<SanityPatch> patch)
163181
{
164182
if (source == null)

src/Sanity.Linq/QueryProvider/SanityExpressionParser.cs

Lines changed: 95 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,7 @@ public static string GetJoinProjection(string sourceName, string targetName, Typ
611611
var fieldRef = sourceName;
612612
if (sourceName != targetName && !string.IsNullOrEmpty(targetName))
613613
{
614-
fieldRef = $"\"{targetName}\": {sourceName}";
614+
fieldRef = $"\"{targetName}\":{sourceName}";
615615
}
616616

617617
// String or primative
@@ -638,7 +638,7 @@ public static string GetJoinProjection(string sourceName, string targetName, Typ
638638
var elementType = listOfSanityReferenceType.GetGenericArguments()[0].GetGenericArguments()[0];
639639
var fields = GetPropertyProjectionList(elementType);
640640
var fieldList = fields.Aggregate((c, n) => c + "," + n);
641-
projection = $"{fieldRef}[]->{{ {fieldList} }}";
641+
projection = $"{fieldRef}[]->{{{fieldList}}}";
642642
}
643643
else
644644
{
@@ -654,7 +654,7 @@ public static string GetJoinProjection(string sourceName, string targetName, Typ
654654

655655
// Nested Reference
656656
var fieldList = fields.Select(f => f.StartsWith("asset") ? $"asset->{(nestedFields.Count > 0 ? ("{" + nestedFields.Aggregate((a, b) => a + "," + b) + "}") : "")}" : f).Aggregate((c, n) => c + "," + n);
657-
projection = $"{fieldRef}{{ {fieldList} }}";
657+
projection = $"{fieldRef}{{{fieldList}}}";
658658
}
659659
else
660660
{
@@ -689,7 +689,7 @@ public static string GetJoinProjection(string sourceName, string targetName, Typ
689689

690690
// Nested Reference
691691
var fieldList = fields.Select(f => f == propertyName ? $"{propertyName}[]->{(nestedFields.Count > 0 ? ("{" + nestedFields.Aggregate((a, b) => a + "," + b) + "}") : "")}" : f).Aggregate((c, n) => c + "," + n);
692-
projection = $"{fieldRef}{{ {fieldList} }}";
692+
projection = $"{fieldRef}{{{fieldList}}}";
693693

694694
}
695695
else
@@ -705,8 +705,8 @@ public static string GetJoinProjection(string sourceName, string targetName, Typ
705705

706706

707707
// Nested Reference
708-
var fieldList = fields.Select(f => f.StartsWith("asset") ? $"asset->{{ ... }}" : f).Aggregate((c, n) => c + "," + n);
709-
projection = $"{fieldRef}[] {{ {fieldList} }}";
708+
var fieldList = fields.Select(f => f.StartsWith("asset") ? $"asset->{{{SanityConstants.SPREAD_OPERATOR}}}" : f).Aggregate((c, n) => c + "," + n);
709+
projection = $"{fieldRef}[]{{{fieldList}}}";
710710
}
711711
}
712712
}
@@ -727,7 +727,15 @@ public static string GetJoinProjection(string sourceName, string targetName, Typ
727727
{
728728
// Other strongly typed includes
729729
var fieldList = fields.Aggregate((c, n) => c + "," + n);
730-
projection = $"{fieldRef}[]->{{ {fieldList} }}";
730+
// projection = $"{fieldRef}[]->{{ {fieldList} }}";
731+
732+
// Include both references and inline objects:
733+
// E.g.
734+
// activities[] {
735+
// ...,
736+
// _type == 'reference' => @->{...}
737+
// },
738+
projection = $"{fieldRef}[]{{{fieldList},{SanityConstants.DEREFERENCING_SWITCH + "{" + fieldList + "}"}}}";
731739
}
732740
else
733741
{
@@ -742,12 +750,14 @@ public static string GetJoinProjection(string sourceName, string targetName, Typ
742750
{
743751
// Other strongly typed includes
744752
var fieldList = fields.Aggregate((c, n) => c + "," + n);
745-
projection = $"{fieldRef}->{{ {fieldList} }}";
753+
// projection = $"{fieldRef}->{{{fieldList}}}";
754+
projection = $"{fieldRef}{{{fieldList},{SanityConstants.DEREFERENCING_SWITCH + "{" + fieldList + "}"}}}";
746755
}
747756
else
748757
{
749758
// "object" without any fields defined
750-
projection = $"{fieldRef}->{{ ... }}";
759+
//projection = $"{fieldRef}->{{{SanityConstants.SPREAD_OPERATOR}}}";
760+
projection = $"{fieldRef}{{{SanityConstants.SPREAD_OPERATOR},{SanityConstants.DEREFERENCING_SWITCH + "{" + SanityConstants.SPREAD_OPERATOR + "}"}}}";
751761
}
752762
}
753763
}
@@ -756,6 +766,16 @@ public static string GetJoinProjection(string sourceName, string targetName, Typ
756766

757767
}
758768

769+
internal class SanityConstants
770+
{
771+
public const string ARRAY_INDICATOR = "[]";
772+
public const string DEREFERENCING_SWITCH = "_type=='reference'=>@->";
773+
public const string SPREAD_OPERATOR = "...";
774+
public const string STRING_DELIMITOR = "\"";
775+
public const string COLON = ":";
776+
public const string DEREFERENCING_OPERATOR = "->";
777+
}
778+
759779

760780
internal class SanityQueryBuilder
761781
{
@@ -846,7 +866,7 @@ public virtual string Build(bool includeProjections)
846866
if (!string.IsNullOrEmpty(projection))
847867
{
848868
projection = ExpandIncludesInProjection(projection, Includes);
849-
projection = projection.Replace("{...}", ""); // Remove redundant {...} to simplify query
869+
projection = projection.Replace($"{{{SanityConstants.SPREAD_OPERATOR}}}", ""); // Remove redundant {...} to simplify query
850870
sb.Append(projection);
851871
}
852872
}
@@ -887,6 +907,16 @@ public virtual string Build(bool includeProjections)
887907
return sb.ToString();
888908
}
889909

910+
private Dictionary<string, string> GroqTokens = new Dictionary<string, string>
911+
{
912+
{ SanityConstants.DEREFERENCING_SWITCH, "__0001__" },
913+
{ SanityConstants.DEREFERENCING_OPERATOR, "__0002__" },
914+
{ SanityConstants.STRING_DELIMITOR, "__0003__" },
915+
{ SanityConstants.COLON, "__0004__" },
916+
{ SanityConstants.SPREAD_OPERATOR, "__0005__" },
917+
{ SanityConstants.ARRAY_INDICATOR, "__0006__" },
918+
};
919+
890920
private string ExpandIncludesInProjection(string projection, Dictionary<string, string> includes)
891921
{
892922
// Finds and replaces includes in projection by converting projection (GROQ) to an equivelant JSON representation,
@@ -904,10 +934,11 @@ private string ExpandIncludesInProjection(string projection, Dictionary<string,
904934
var jObjectInclude = JsonConvert.DeserializeObject(jsonInclude) as JObject;
905935

906936
var pathParts = includeKey
907-
.Replace("\":", GroqTokens["\":"])
908-
.Replace("\"", GroqTokens["\""])
909-
.Replace("[]", GroqTokens["[]"])
910-
.Replace("->", ".")
937+
.Replace(SanityConstants.COLON, GroqTokens[SanityConstants.COLON])
938+
.Replace(SanityConstants.STRING_DELIMITOR, GroqTokens[SanityConstants.STRING_DELIMITOR])
939+
.Replace(SanityConstants.ARRAY_INDICATOR, GroqTokens[SanityConstants.ARRAY_INDICATOR])
940+
.Replace(SanityConstants.DEREFERENCING_SWITCH, GroqTokens[SanityConstants.DEREFERENCING_SWITCH])
941+
.Replace(SanityConstants.DEREFERENCING_OPERATOR, ".")
911942
.TrimEnd('.').Split('.');
912943

913944
JObject obj = jObjectProjection;
@@ -917,53 +948,56 @@ private string ExpandIncludesInProjection(string projection, Dictionary<string,
917948
bool isLast = i == pathParts.Length - 1;
918949
if (!isLast)
919950
{
920-
if (obj.ContainsKey(part))
921-
{
922-
obj = obj[part] as JObject;
923-
}
924-
else if (obj.ContainsKey(part + GroqTokens["->"]))
925-
{
926-
obj = obj[part + GroqTokens["->"]] as JObject;
927-
}
928-
else if (obj.ContainsKey(part + GroqTokens["[]"]))
929-
{
930-
obj = obj[part + GroqTokens["[]"]] as JObject;
931-
}
932-
else if (obj.ContainsKey(part + GroqTokens["[]"] + GroqTokens["->"]))
951+
// Traverse / construct path to property
952+
bool propertyExists = false;
953+
foreach (var property in obj)
933954
{
934-
obj = obj[part + GroqTokens["[]"] + GroqTokens["->"]] as JObject;
955+
if (property.Key == part
956+
|| property.Key.StartsWith($"{GroqTokens[SanityConstants.STRING_DELIMITOR]}{part}{GroqTokens[SanityConstants.STRING_DELIMITOR]}")
957+
|| property.Key.StartsWith(part + GroqTokens[SanityConstants.ARRAY_INDICATOR])
958+
|| property.Key.StartsWith(part + GroqTokens[SanityConstants.DEREFERENCING_OPERATOR]))
959+
{
960+
obj = obj[property.Key] as JObject;
961+
propertyExists = true;
962+
break;
963+
}
935964
}
936-
else
965+
if (!propertyExists)
937966
{
938967
obj[part] = new JObject();
939968
obj = obj[part] as JObject;
940969
}
941970
}
942971
else
943972
{
944-
if (obj.ContainsKey(part))
945-
{
946-
obj.Remove(part);
947-
}
948-
if (obj.ContainsKey(part + GroqTokens["[]"]))
973+
// Remove previous representations of field (typically without a projection)
974+
var fieldsToReplace = new List<string>();
975+
foreach (var property in obj)
949976
{
950-
obj.Remove(part + GroqTokens["[]"]);
951-
}
952-
if (jObjectInclude.ContainsKey(part))
953-
{
954-
obj[part] = jObjectInclude[part];
955-
}
956-
else if (jObjectInclude.ContainsKey(part + GroqTokens["[]"]))
957-
{
958-
obj[part + GroqTokens["[]"]] = jObjectInclude[part + GroqTokens["[]"]];
977+
if (property.Key == part
978+
|| property.Key.StartsWith($"{GroqTokens[SanityConstants.STRING_DELIMITOR]}{part}{GroqTokens[SanityConstants.STRING_DELIMITOR]}")
979+
|| property.Key.StartsWith(part + GroqTokens[SanityConstants.ARRAY_INDICATOR])
980+
|| property.Key.StartsWith(part + GroqTokens[SanityConstants.DEREFERENCING_OPERATOR]))
981+
{
982+
fieldsToReplace.Add(property.Key);
983+
}
959984
}
960-
else if (jObjectInclude.ContainsKey(part + GroqTokens["->"]))
985+
foreach (var key in fieldsToReplace)
961986
{
962-
obj[part + GroqTokens["->"]] = jObjectInclude[part + GroqTokens["->"]];
987+
obj.Remove(key);
963988
}
964-
else if (jObjectInclude.ContainsKey(part + GroqTokens["[]"] + GroqTokens["->"]))
989+
990+
// Set field to new projection
991+
foreach (var include in jObjectInclude)
965992
{
966-
obj[part + GroqTokens["[]"] + GroqTokens["->"]] = jObjectInclude[part + GroqTokens["[]"] + GroqTokens["->"]];
993+
if (include.Key == part
994+
|| include.Key.StartsWith($"{GroqTokens[SanityConstants.STRING_DELIMITOR]}{part}{GroqTokens[SanityConstants.STRING_DELIMITOR]}")
995+
|| include.Key.StartsWith(part + GroqTokens[SanityConstants.ARRAY_INDICATOR])
996+
|| include.Key.StartsWith(part + GroqTokens[SanityConstants.DEREFERENCING_OPERATOR]))
997+
{
998+
obj[include.Key] = include.Value;
999+
break;
1000+
}
9671001
}
9681002
}
9691003
}
@@ -977,26 +1011,15 @@ private string ExpandIncludesInProjection(string projection, Dictionary<string,
9771011
return projection;
9781012
}
9791013

980-
private Dictionary<string, string> GroqTokens = new Dictionary<string, string>
981-
{
982-
{ "\"", "VVV" },
983-
{ "\":", "WWW" },
984-
{ "...", "XXX" },
985-
{ "->", "YYY" },
986-
{ "[]", "ZZZ" },
987-
};
988-
9891014
private string GroqToJson(string groq)
9901015
{
991-
var json = groq
992-
.Replace(" ", "")
993-
.Replace("\":", GroqTokens["\":"])
994-
.Replace("\"", GroqTokens["\""])
995-
.Replace("{", ":{")
996-
.Replace("...", GroqTokens["..."])
997-
.Replace("->", GroqTokens["->"])
998-
.Replace("[]", GroqTokens["[]"])
999-
.TrimStart(':');
1016+
var json = groq.Replace(" ", "");
1017+
foreach (var token in GroqTokens.Keys.OrderBy(k => GroqTokens[k]))
1018+
{
1019+
json = json.Replace(token, GroqTokens[token]);
1020+
}
1021+
json = json.Replace("{", ":{")
1022+
.TrimStart(':');
10001023

10011024
// Replace variable names with valid json (e.g. convert myField to "myField":true)
10021025
var reVariables = new Regex("(,|{)([^\"}:,]+)(,|})");
@@ -1018,15 +1041,15 @@ private string GroqToJson(string groq)
10181041

10191042
private string JsonToGroq(string json)
10201043
{
1021-
return json
1022-
.Replace(GroqTokens["..."], "...")
1023-
.Replace(GroqTokens["->"], "->")
1044+
var groq = json
10241045
.Replace(":{", "{")
1025-
.Replace(GroqTokens["[]"], "[]")
10261046
.Replace(":true", "")
1027-
.Replace("\"", "")
1028-
.Replace(GroqTokens["\":"], "\":")
1029-
.Replace(GroqTokens["\""], "\"");
1047+
.Replace("\"", "");
1048+
foreach (var token in GroqTokens.Keys)
1049+
{
1050+
groq = groq.Replace(GroqTokens[token], token);
1051+
}
1052+
return groq;
10301053
}
10311054
}
10321055

src/Sanity.Linq/Sanity.Linq.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ This file is part of Sanity LINQ (https://github.com/oslofjord/sanity-linq).
2626
<Authors>Oslofjord Operations AS</Authors>
2727
<Company>Oslofjord Operations AS</Company>
2828
<Product>Sanity LINQ</Product>
29-
<Version>1.3.0</Version>
29+
<Version>1.3.1</Version>
3030
<Description>Strongly-typed .Net Client for Sanity CMS (https://sanity.io)</Description>
3131
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
3232
<Copyright>2019 Oslofjord Operations AS</Copyright>
@@ -35,11 +35,11 @@ This file is part of Sanity LINQ (https://github.com/oslofjord/sanity-linq).
3535
<RepositoryType>git</RepositoryType>
3636
<PackageTags>sanity cms dotnet linq client groq</PackageTags>
3737
<PackageLicenseUrl>https://raw.githubusercontent.com/oslofjord/sanity-linq/master/LICENSE</PackageLicenseUrl>
38-
<AssemblyVersion>1.3.0.0</AssemblyVersion>
38+
<AssemblyVersion>1.3.1.0</AssemblyVersion>
3939
<PackageId>Sanity.Linq</PackageId>
4040
<AssemblyName>Sanity.Linq</AssemblyName>
4141
<RootNamespace>Sanity.Linq</RootNamespace>
42-
<FileVersion>1.3.0.0</FileVersion>
42+
<FileVersion>1.3.1.0</FileVersion>
4343
<PackageReleaseNotes>1.0 - Sanity Linq library
4444
1.1 - BlockContent library
4545
1.1.1 - Improvements BlockContent

src/Sanity.Linq/SanityDocumentSet.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,16 +114,16 @@ public async Task<long> ExecuteLongCountAsync()
114114

115115
public SanityDocumentSet<TDoc> Include<TProperty>(Expression<Func<TDoc, TProperty>> property)
116116
{
117-
var includeMethod = typeof(SanityDocumentSetExtensions).GetMethod("Include").MakeGenericMethod(typeof(TDoc), typeof(TProperty));
117+
var includeMethod = typeof(SanityDocumentSetExtensions).GetMethods().FirstOrDefault(m => m.Name.StartsWith("Include") && m.GetParameters().Length == 2).MakeGenericMethod(typeof(TDoc), typeof(TProperty));
118118
var exp = Expression.Call(null, includeMethod, Expression, property);
119119
Expression = exp;
120120
return this;
121121
}
122122

123-
public SanityDocumentSet<TDoc> Include<TProperty>(Expression<Func<TDoc, TProperty>> property, string targetName)
123+
public SanityDocumentSet<TDoc> Include<TProperty>(Expression<Func<TDoc, TProperty>> property, string sourceName)
124124
{
125-
var includeMethod = typeof(SanityDocumentSetExtensions).GetMethod("Include").MakeGenericMethod(typeof(TDoc), typeof(TProperty), typeof(string));
126-
var exp = Expression.Call(null, includeMethod, Expression, property, Expression.Constant(targetName));
125+
var includeMethod = typeof(SanityDocumentSetExtensions).GetMethods().FirstOrDefault(m => m.Name.StartsWith("Include") && m.GetParameters().Length == 3).MakeGenericMethod(typeof(TDoc), typeof(TProperty));
126+
var exp = Expression.Call(null, includeMethod, Expression, property, Expression.Constant(sourceName));
127127
Expression = exp;
128128
return this;
129129

0 commit comments

Comments
 (0)