Skip to content

Commit

Permalink
Added support for joining / including arrays with a mix of reference …
Browse files Browse the repository at this point in the history
…and non-reference objects
  • Loading branch information
Reng van Oord committed Jul 10, 2019
1 parent 90934f6 commit 4934bc8
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 79 deletions.
3 changes: 3 additions & 0 deletions demo/Sanity.Linq.Demo/Model/Post.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public Post()

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

[Include("author")]
public Author DereferencedAuthor { get; set; }

public CommonTypes.SanityImage MainImage { get; set; }

public List<SanityReference<Category>> Categories { get; set; }
Expand Down
18 changes: 18 additions & 0 deletions src/Sanity.Linq/Extensions/SanityDocumentSetExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,24 @@ public static IQueryable<TEntity> Include<TEntity, TProperty>(this IQueryable<TE
}
}

public static IQueryable<TEntity> Include<TEntity, TProperty>(this IQueryable<TEntity> source, Expression<Func<TEntity, TProperty>> property, string sourceName)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}

if (source is SanityDocumentSet<TEntity> dbSet)
{
((SanityDocumentSet<TEntity>)source).Include(property, sourceName);
return source;
}
else
{
throw new Exception("Queryable source must be a SanityDbSet<T>.");
}
}

public static SanityMutationBuilder<TDoc> Patch<TDoc>(this IQueryable<TDoc> source, Action<SanityPatch> patch)
{
if (source == null)
Expand Down
167 changes: 95 additions & 72 deletions src/Sanity.Linq/QueryProvider/SanityExpressionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,7 @@ public static string GetJoinProjection(string sourceName, string targetName, Typ
var fieldRef = sourceName;
if (sourceName != targetName && !string.IsNullOrEmpty(targetName))
{
fieldRef = $"\"{targetName}\": {sourceName}";
fieldRef = $"\"{targetName}\":{sourceName}";
}

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

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

// 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} }}";
projection = $"{fieldRef}{{{fieldList}}}";

}
else
Expand All @@ -705,8 +705,8 @@ public static string GetJoinProjection(string sourceName, string targetName, Typ


// Nested Reference
var fieldList = fields.Select(f => f.StartsWith("asset") ? $"asset->{{ ... }}" : f).Aggregate((c, n) => c + "," + n);
projection = $"{fieldRef}[] {{ {fieldList} }}";
var fieldList = fields.Select(f => f.StartsWith("asset") ? $"asset->{{{SanityConstants.SPREAD_OPERATOR}}}" : f).Aggregate((c, n) => c + "," + n);
projection = $"{fieldRef}[]{{{fieldList}}}";
}
}
}
Expand All @@ -727,7 +727,15 @@ 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} }}";

// Include both references and inline objects:
// E.g.
// activities[] {
// ...,
// _type == 'reference' => @->{...}
// },
projection = $"{fieldRef}[]{{{fieldList},{SanityConstants.DEREFERENCING_SWITCH + "{" + fieldList + "}"}}}";
}
else
{
Expand All @@ -742,12 +750,14 @@ 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}}}";
projection = $"{fieldRef}{{{fieldList},{SanityConstants.DEREFERENCING_SWITCH + "{" + fieldList + "}"}}}";
}
else
{
// "object" without any fields defined
projection = $"{fieldRef}->{{ ... }}";
//projection = $"{fieldRef}->{{{SanityConstants.SPREAD_OPERATOR}}}";
projection = $"{fieldRef}{{{SanityConstants.SPREAD_OPERATOR},{SanityConstants.DEREFERENCING_SWITCH + "{" + SanityConstants.SPREAD_OPERATOR + "}"}}}";
}
}
}
Expand All @@ -756,6 +766,16 @@ public static string GetJoinProjection(string sourceName, string targetName, Typ

}

internal class SanityConstants
{
public const string ARRAY_INDICATOR = "[]";
public const string DEREFERENCING_SWITCH = "_type=='reference'=>@->";
public const string SPREAD_OPERATOR = "...";
public const string STRING_DELIMITOR = "\"";
public const string COLON = ":";
public const string DEREFERENCING_OPERATOR = "->";
}


internal class SanityQueryBuilder
{
Expand Down Expand Up @@ -846,7 +866,7 @@ public virtual string Build(bool includeProjections)
if (!string.IsNullOrEmpty(projection))
{
projection = ExpandIncludesInProjection(projection, Includes);
projection = projection.Replace("{...}", ""); // Remove redundant {...} to simplify query
projection = projection.Replace($"{{{SanityConstants.SPREAD_OPERATOR}}}", ""); // Remove redundant {...} to simplify query
sb.Append(projection);
}
}
Expand Down Expand Up @@ -887,6 +907,16 @@ public virtual string Build(bool includeProjections)
return sb.ToString();
}

private Dictionary<string, string> GroqTokens = new Dictionary<string, string>
{
{ SanityConstants.DEREFERENCING_SWITCH, "__0001__" },
{ SanityConstants.DEREFERENCING_OPERATOR, "__0002__" },
{ SanityConstants.STRING_DELIMITOR, "__0003__" },
{ SanityConstants.COLON, "__0004__" },
{ SanityConstants.SPREAD_OPERATOR, "__0005__" },
{ SanityConstants.ARRAY_INDICATOR, "__0006__" },
};

private string ExpandIncludesInProjection(string projection, Dictionary<string, string> includes)
{
// Finds and replaces includes in projection by converting projection (GROQ) to an equivelant JSON representation,
Expand All @@ -904,10 +934,11 @@ private string ExpandIncludesInProjection(string projection, Dictionary<string,
var jObjectInclude = JsonConvert.DeserializeObject(jsonInclude) as JObject;

var pathParts = includeKey
.Replace("\":", GroqTokens["\":"])
.Replace("\"", GroqTokens["\""])
.Replace("[]", GroqTokens["[]"])
.Replace("->", ".")
.Replace(SanityConstants.COLON, GroqTokens[SanityConstants.COLON])
.Replace(SanityConstants.STRING_DELIMITOR, GroqTokens[SanityConstants.STRING_DELIMITOR])
.Replace(SanityConstants.ARRAY_INDICATOR, GroqTokens[SanityConstants.ARRAY_INDICATOR])
.Replace(SanityConstants.DEREFERENCING_SWITCH, GroqTokens[SanityConstants.DEREFERENCING_SWITCH])
.Replace(SanityConstants.DEREFERENCING_OPERATOR, ".")
.TrimEnd('.').Split('.');

JObject obj = jObjectProjection;
Expand All @@ -917,53 +948,56 @@ private string ExpandIncludesInProjection(string projection, Dictionary<string,
bool isLast = i == pathParts.Length - 1;
if (!isLast)
{
if (obj.ContainsKey(part))
{
obj = obj[part] as JObject;
}
else if (obj.ContainsKey(part + GroqTokens["->"]))
{
obj = obj[part + GroqTokens["->"]] as JObject;
}
else if (obj.ContainsKey(part + GroqTokens["[]"]))
{
obj = obj[part + GroqTokens["[]"]] as JObject;
}
else if (obj.ContainsKey(part + GroqTokens["[]"] + GroqTokens["->"]))
// Traverse / construct path to property
bool propertyExists = false;
foreach (var property in obj)
{
obj = obj[part + GroqTokens["[]"] + GroqTokens["->"]] as JObject;
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]))
{
obj = obj[property.Key] as JObject;
propertyExists = true;
break;
}
}
else
if (!propertyExists)
{
obj[part] = new JObject();
obj = obj[part] as JObject;
}
}
else
{
if (obj.ContainsKey(part))
{
obj.Remove(part);
}
if (obj.ContainsKey(part + GroqTokens["[]"]))
// Remove previous representations of field (typically without a projection)
var fieldsToReplace = new List<string>();
foreach (var property in obj)
{
obj.Remove(part + GroqTokens["[]"]);
}
if (jObjectInclude.ContainsKey(part))
{
obj[part] = jObjectInclude[part];
}
else if (jObjectInclude.ContainsKey(part + GroqTokens["[]"]))
{
obj[part + GroqTokens["[]"]] = jObjectInclude[part + GroqTokens["[]"]];
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);
}
}
else if (jObjectInclude.ContainsKey(part + GroqTokens["->"]))
foreach (var key in fieldsToReplace)
{
obj[part + GroqTokens["->"]] = jObjectInclude[part + GroqTokens["->"]];
obj.Remove(key);
}
else if (jObjectInclude.ContainsKey(part + GroqTokens["[]"] + GroqTokens["->"]))

// Set field to new projection
foreach (var include in jObjectInclude)
{
obj[part + GroqTokens["[]"] + GroqTokens["->"]] = jObjectInclude[part + GroqTokens["[]"] + GroqTokens["->"]];
if (include.Key == part
|| include.Key.StartsWith($"{GroqTokens[SanityConstants.STRING_DELIMITOR]}{part}{GroqTokens[SanityConstants.STRING_DELIMITOR]}")
|| include.Key.StartsWith(part + GroqTokens[SanityConstants.ARRAY_INDICATOR])
|| include.Key.StartsWith(part + GroqTokens[SanityConstants.DEREFERENCING_OPERATOR]))
{
obj[include.Key] = include.Value;
break;
}
}
}
}
Expand All @@ -977,26 +1011,15 @@ private string ExpandIncludesInProjection(string projection, Dictionary<string,
return projection;
}

private Dictionary<string, string> GroqTokens = new Dictionary<string, string>
{
{ "\"", "VVV" },
{ "\":", "WWW" },
{ "...", "XXX" },
{ "->", "YYY" },
{ "[]", "ZZZ" },
};

private string GroqToJson(string groq)
{
var json = groq
.Replace(" ", "")
.Replace("\":", GroqTokens["\":"])
.Replace("\"", GroqTokens["\""])
.Replace("{", ":{")
.Replace("...", GroqTokens["..."])
.Replace("->", GroqTokens["->"])
.Replace("[]", GroqTokens["[]"])
.TrimStart(':');
var json = groq.Replace(" ", "");
foreach (var token in GroqTokens.Keys.OrderBy(k => GroqTokens[k]))
{
json = json.Replace(token, GroqTokens[token]);
}
json = json.Replace("{", ":{")
.TrimStart(':');

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

private string JsonToGroq(string json)
{
return json
.Replace(GroqTokens["..."], "...")
.Replace(GroqTokens["->"], "->")
var groq = json
.Replace(":{", "{")
.Replace(GroqTokens["[]"], "[]")
.Replace(":true", "")
.Replace("\"", "")
.Replace(GroqTokens["\":"], "\":")
.Replace(GroqTokens["\""], "\"");
.Replace("\"", "");
foreach (var token in GroqTokens.Keys)
{
groq = groq.Replace(GroqTokens[token], token);
}
return groq;
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/Sanity.Linq/Sanity.Linq.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ This file is part of Sanity LINQ (https://github.com/oslofjord/sanity-linq).
<Authors>Oslofjord Operations AS</Authors>
<Company>Oslofjord Operations AS</Company>
<Product>Sanity LINQ</Product>
<Version>1.3.0</Version>
<Version>1.3.1</Version>
<Description>Strongly-typed .Net Client for Sanity CMS (https://sanity.io)</Description>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<Copyright>2019 Oslofjord Operations AS</Copyright>
Expand All @@ -35,11 +35,11 @@ This file is part of Sanity LINQ (https://github.com/oslofjord/sanity-linq).
<RepositoryType>git</RepositoryType>
<PackageTags>sanity cms dotnet linq client groq</PackageTags>
<PackageLicenseUrl>https://raw.githubusercontent.com/oslofjord/sanity-linq/master/LICENSE</PackageLicenseUrl>
<AssemblyVersion>1.3.0.0</AssemblyVersion>
<AssemblyVersion>1.3.1.0</AssemblyVersion>
<PackageId>Sanity.Linq</PackageId>
<AssemblyName>Sanity.Linq</AssemblyName>
<RootNamespace>Sanity.Linq</RootNamespace>
<FileVersion>1.3.0.0</FileVersion>
<FileVersion>1.3.1.0</FileVersion>
<PackageReleaseNotes>1.0 - Sanity Linq library
1.1 - BlockContent library
1.1.1 - Improvements BlockContent
Expand Down
8 changes: 4 additions & 4 deletions src/Sanity.Linq/SanityDocumentSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,16 @@ public async Task<long> ExecuteLongCountAsync()

public SanityDocumentSet<TDoc> Include<TProperty>(Expression<Func<TDoc, TProperty>> property)
{
var includeMethod = typeof(SanityDocumentSetExtensions).GetMethod("Include").MakeGenericMethod(typeof(TDoc), typeof(TProperty));
var includeMethod = typeof(SanityDocumentSetExtensions).GetMethods().FirstOrDefault(m => m.Name.StartsWith("Include") && m.GetParameters().Length == 2).MakeGenericMethod(typeof(TDoc), typeof(TProperty));
var exp = Expression.Call(null, includeMethod, Expression, property);
Expression = exp;
return this;
}

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

Expand Down
Loading

0 comments on commit 4934bc8

Please sign in to comment.