Skip to content

Commit 8c438b6

Browse files
Enhance NoSqlQueryLayerComposer
1 parent 50bb3ec commit 8c438b6

File tree

2 files changed

+206
-53
lines changed

2 files changed

+206
-53
lines changed

src/JsonApiDotNetCore/Queries/INoSqlQueryLayerComposer.cs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,6 @@ public interface INoSqlQueryLayerComposer
2222
/// </summary>
2323
(QueryLayer QueryLayer, IncludeExpression Include) ComposeFromConstraintsForNoSql(ResourceType requestResourceType);
2424

25-
/// <summary>
26-
/// Composes a <see cref="QueryLayer" /> and an <see cref="IncludeExpression" /> from the constraints specified by the request. Used for primary
27-
/// resources.
28-
/// </summary>
29-
(QueryLayer QueryLayer, IncludeExpression Include) ComposeForGetByIdWithConstraintsForNoSql<TId>(TId id, ResourceType primaryResourceType,
30-
TopFieldSelection fieldSelection)
31-
where TId : notnull;
32-
33-
/// <summary>
34-
/// Composes a <see cref="QueryLayer" /> with a filter expression in the form "equals(id,'{stringId}')".
35-
/// </summary>
36-
QueryLayer ComposeForGetByIdForNoSql<TId>(TId id, ResourceType primaryResourceType)
37-
where TId : notnull;
38-
3925
/// <summary>
4026
/// Composes a <see cref="QueryLayer" /> from the constraints specified by the request and a filter expression in the form
4127
/// "equals({propertyName},'{propertyValue}')". Used for secondary or included resources.
@@ -59,6 +45,20 @@ QueryLayer ComposeForGetByIdForNoSql<TId>(TId id, ResourceType primaryResourceTy
5945
(QueryLayer QueryLayer, IncludeExpression Include) ComposeFromConstraintsForNoSql(ResourceType requestResourceType, string propertyName,
6046
string propertyValue, bool isIncluded);
6147

48+
/// <summary>
49+
/// Composes a <see cref="QueryLayer" /> and an <see cref="IncludeExpression" /> from the constraints specified by the request. Used for primary
50+
/// resources.
51+
/// </summary>
52+
(QueryLayer QueryLayer, IncludeExpression Include) ComposeForGetByIdWithConstraintsForNoSql<TId>(TId id, ResourceType primaryResourceType,
53+
TopFieldSelection fieldSelection)
54+
where TId : notnull;
55+
56+
/// <summary>
57+
/// Composes a <see cref="QueryLayer" /> with a filter expression in the form "equals(id,'{stringId}')".
58+
/// </summary>
59+
QueryLayer ComposeForGetByIdForNoSql<TId>(TId id, ResourceType primaryResourceType)
60+
where TId : notnull;
61+
6262
/// <summary>
6363
/// Builds a query that retrieves the primary resource, including all of its attributes and all targeted relationships, during a create/update/delete
6464
/// request.

src/JsonApiDotNetCore/Queries/NoSqlQueryLayerComposer.cs

Lines changed: 192 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Collections.Immutable;
34
using System.Linq;
5+
using System.Net;
46
using JetBrains.Annotations;
57
using JsonApiDotNetCore.Configuration;
8+
using JsonApiDotNetCore.Errors;
69
using JsonApiDotNetCore.Queries.Expressions;
710
using JsonApiDotNetCore.Queries.Internal;
811
using JsonApiDotNetCore.Resources;
912
using JsonApiDotNetCore.Resources.Annotations;
13+
using JsonApiDotNetCore.Serialization.Objects;
1014

1115
#pragma warning disable AV1551 // Method overload should call another overload
1216
#pragma warning disable AV2310 // Code block should not contain inline comment
@@ -52,48 +56,23 @@ public NoSqlQueryLayerComposer(IEnumerable<IQueryConstraintProvider> constraintP
5256
/// <inheritdoc />
5357
public FilterExpression? GetPrimaryFilterFromConstraintsForNoSql(ResourceType primaryResourceType)
5458
{
55-
return GetPrimaryFilterFromConstraints(primaryResourceType);
59+
return AssertFilterExpressionIsSimple(GetPrimaryFilterFromConstraints(primaryResourceType));
5660
}
5761

5862
/// <inheritdoc />
5963
public (QueryLayer QueryLayer, IncludeExpression Include) ComposeFromConstraintsForNoSql(ResourceType requestResourceType)
6064
{
6165
QueryLayer queryLayer = ComposeFromConstraints(requestResourceType);
62-
IncludeExpression include = queryLayer.Include ?? IncludeExpression.Empty;
6366

64-
queryLayer.Include = IncludeExpression.Empty;
65-
queryLayer.Projection = null;
66-
67-
return (queryLayer, include);
68-
}
69-
70-
/// <inheritdoc />
71-
public (QueryLayer QueryLayer, IncludeExpression Include) ComposeForGetByIdWithConstraintsForNoSql<TId>(TId id, ResourceType primaryResourceType,
72-
TopFieldSelection fieldSelection)
73-
where TId : notnull
74-
{
75-
QueryLayer queryLayer = ComposeForGetById(id, primaryResourceType, fieldSelection);
76-
IncludeExpression include = queryLayer.Include ?? IncludeExpression.Empty;
67+
IncludeExpression include = AssertIncludeExpressionIsSimple(queryLayer.Include);
7768

69+
queryLayer.Filter = AssertFilterExpressionIsSimple(queryLayer.Filter);
7870
queryLayer.Include = IncludeExpression.Empty;
7971
queryLayer.Projection = null;
8072

8173
return (queryLayer, include);
8274
}
8375

84-
/// <inheritdoc />
85-
public QueryLayer ComposeForGetByIdForNoSql<TId>(TId id, ResourceType primaryResourceType)
86-
where TId : notnull
87-
{
88-
return new QueryLayer(primaryResourceType)
89-
{
90-
Filter = new ComparisonExpression(ComparisonOperator.Equals,
91-
new ResourceFieldChainExpression(primaryResourceType.Fields.Single(field => field.Property.Name == nameof(IIdentifiable<TId>.Id))),
92-
new LiteralConstantExpression(id.ToString()!)),
93-
Include = IncludeExpression.Empty
94-
};
95-
}
96-
9776
/// <inheritdoc />
9877
public (QueryLayer QueryLayer, IncludeExpression Include) ComposeFromConstraintsForNoSql(ResourceType requestResourceType, string propertyName,
9978
string propertyValue, bool isIncluded)
@@ -104,27 +83,44 @@ public QueryLayer ComposeForGetByIdForNoSql<TId>(TId id, ResourceType primaryRes
10483
ComposeSecondaryResourceFilter(requestResourceType, propertyName, propertyValue)
10584
};
10685

86+
// @formatter:off
87+
10788
// Get the query expressions from the request.
108-
ExpressionInScope[] constraints = _constraintProviders.SelectMany(provider => provider.GetConstraints()).ToArray();
89+
ExpressionInScope[] constraints = _constraintProviders
90+
.SelectMany(provider => provider.GetConstraints())
91+
.ToArray();
10992

11093
bool IsQueryLayerConstraint(ExpressionInScope constraint)
11194
{
112-
return constraint.Expression is not IncludeExpression && (!isIncluded || (constraint.Scope is not null &&
113-
constraint.Scope.Fields.Any(field => field.PublicName == requestResourceType.PublicName)));
95+
return constraint.Expression is not IncludeExpression && (!isIncluded || IsResourceScoped(constraint));
96+
}
97+
98+
bool IsResourceScoped(ExpressionInScope constraint)
99+
{
100+
return constraint.Scope is not null &&
101+
constraint.Scope.Fields.Any(field => field.PublicName == requestResourceType.PublicName);
114102
}
115103

116-
IEnumerable<QueryExpression> requestQueryExpressions = constraints.Where(IsQueryLayerConstraint).Select(constraint => constraint.Expression);
104+
QueryExpression[] requestQueryExpressions = constraints
105+
.Where(IsQueryLayerConstraint)
106+
.Select(constraint => constraint.Expression)
107+
.ToArray();
117108

118-
// Combine the secondary resource filter and request query expressions and
119-
// create the query layer from the combined query expressions.
120-
QueryExpression[] queryExpressions = secondaryResourceFilterExpressions.Concat(requestQueryExpressions).ToArray();
109+
FilterExpression[] requestFilterExpressions = requestQueryExpressions
110+
.OfType<FilterExpression>()
111+
.Select(filterExpression => AssertFilterExpressionIsSimple(filterExpression)!)
112+
.ToArray();
113+
114+
FilterExpression[] combinedFilterExpressions = secondaryResourceFilterExpressions
115+
.Concat(requestFilterExpressions)
116+
.ToArray();
121117

122118
var queryLayer = new QueryLayer(requestResourceType)
123119
{
124120
Include = IncludeExpression.Empty,
125-
Filter = GetFilter(queryExpressions, requestResourceType),
126-
Sort = GetSort(queryExpressions, requestResourceType),
127-
Pagination = GetPagination(queryExpressions, requestResourceType)
121+
Filter = GetFilter(combinedFilterExpressions, requestResourceType),
122+
Sort = GetSort(requestQueryExpressions, requestResourceType),
123+
Pagination = GetPagination(requestQueryExpressions, requestResourceType)
128124
};
129125

130126
// Retrieve the IncludeExpression from the constraints collection.
@@ -133,7 +129,13 @@ bool IsQueryLayerConstraint(ExpressionInScope constraint)
133129
// into a single expression.
134130
IncludeExpression include = isIncluded
135131
? IncludeExpression.Empty
136-
: constraints.Select(constraint => constraint.Expression).OfType<IncludeExpression>().DefaultIfEmpty(IncludeExpression.Empty).Single();
132+
: AssertIncludeExpressionIsSimple(constraints
133+
.Select(constraint => constraint.Expression)
134+
.OfType<IncludeExpression>()
135+
.DefaultIfEmpty(IncludeExpression.Empty)
136+
.Single());
137+
138+
// @formatter:on
137139

138140
return (queryLayer, include);
139141
}
@@ -145,6 +147,35 @@ private static FilterExpression ComposeSecondaryResourceFilter(ResourceType reso
145147
new LiteralConstantExpression(properyValue));
146148
}
147149

150+
/// <inheritdoc />
151+
public (QueryLayer QueryLayer, IncludeExpression Include) ComposeForGetByIdWithConstraintsForNoSql<TId>(TId id, ResourceType primaryResourceType,
152+
TopFieldSelection fieldSelection)
153+
where TId : notnull
154+
{
155+
QueryLayer queryLayer = ComposeForGetById(id, primaryResourceType, fieldSelection);
156+
157+
IncludeExpression include = AssertIncludeExpressionIsSimple(queryLayer.Include);
158+
159+
queryLayer.Filter = AssertFilterExpressionIsSimple(queryLayer.Filter);
160+
queryLayer.Include = IncludeExpression.Empty;
161+
queryLayer.Projection = null;
162+
163+
return (queryLayer, include);
164+
}
165+
166+
/// <inheritdoc />
167+
public QueryLayer ComposeForGetByIdForNoSql<TId>(TId id, ResourceType primaryResourceType)
168+
where TId : notnull
169+
{
170+
return new QueryLayer(primaryResourceType)
171+
{
172+
Filter = new ComparisonExpression(ComparisonOperator.Equals,
173+
new ResourceFieldChainExpression(primaryResourceType.Fields.Single(field => field.Property.Name == nameof(IIdentifiable<TId>.Id))),
174+
new LiteralConstantExpression(id.ToString()!)),
175+
Include = IncludeExpression.Empty
176+
};
177+
}
178+
148179
public (QueryLayer QueryLayer, IncludeExpression Include) ComposeForUpdateForNoSql<TId>(TId id, ResourceType primaryResourceType)
149180
where TId : notnull
150181
{
@@ -171,5 +202,127 @@ private static AttrAttribute GetIdAttribute(ResourceType resourceType)
171202
{
172203
return resourceType.GetAttributeByPropertyName(nameof(Identifiable<object>.Id));
173204
}
205+
206+
private static FilterExpression? AssertFilterExpressionIsSimple(FilterExpression? filterExpression)
207+
{
208+
if (filterExpression is null)
209+
{
210+
return filterExpression;
211+
}
212+
213+
var visitor = new FilterExpressionVisitor();
214+
215+
return visitor.Visit(filterExpression, null)
216+
? filterExpression
217+
: throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest)
218+
{
219+
Title = "Unsupported filter expression",
220+
Detail = "Navigation of to-one or to-many relationships is not supported."
221+
});
222+
}
223+
224+
private static IncludeExpression AssertIncludeExpressionIsSimple(IncludeExpression? includeExpression)
225+
{
226+
if (includeExpression is null)
227+
{
228+
return IncludeExpression.Empty;
229+
}
230+
231+
return includeExpression.Elements.Any(element => element.Children.Any())
232+
? throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest)
233+
{
234+
Title = "Unsupported include expression",
235+
Detail = "Multi-level include expressions are not supported."
236+
})
237+
: includeExpression;
238+
}
239+
240+
private sealed class FilterExpressionVisitor : QueryExpressionVisitor<object?, bool>
241+
{
242+
private bool _isSimpleFilterExpression = true;
243+
244+
/// <inheritdoc />
245+
public override bool DefaultVisit(QueryExpression expression, object? argument)
246+
{
247+
return _isSimpleFilterExpression;
248+
}
249+
250+
/// <inheritdoc />
251+
public override bool VisitComparison(ComparisonExpression expression, object? argument)
252+
{
253+
return expression.Left.Accept(this, argument) && expression.Right.Accept(this, argument);
254+
}
255+
256+
/// <inheritdoc />
257+
public override bool VisitResourceFieldChain(ResourceFieldChainExpression expression, object? argument)
258+
{
259+
_isSimpleFilterExpression &= expression.Fields.All(IsFieldSupported);
260+
261+
return _isSimpleFilterExpression;
262+
}
263+
264+
private static bool IsFieldSupported(ResourceFieldAttribute field)
265+
{
266+
return field switch
267+
{
268+
AttrAttribute => true,
269+
HasManyAttribute hasMany when HasOwnsManyAttribute(hasMany) => true,
270+
_ => false
271+
};
272+
}
273+
274+
private static bool HasOwnsManyAttribute(ResourceFieldAttribute field)
275+
{
276+
return Attribute.GetCustomAttribute(field.Property, typeof(NoSqlOwnsManyAttribute)) is not null;
277+
}
278+
279+
/// <inheritdoc />
280+
public override bool VisitLogical(LogicalExpression expression, object? argument)
281+
{
282+
return expression.Terms.All(term => term.Accept(this, argument));
283+
}
284+
285+
/// <inheritdoc />
286+
public override bool VisitNot(NotExpression expression, object? argument)
287+
{
288+
return expression.Child.Accept(this, argument);
289+
}
290+
291+
/// <inheritdoc />
292+
public override bool VisitHas(HasExpression expression, object? argument)
293+
{
294+
return expression.TargetCollection.Accept(this, argument) && (expression.Filter is null || expression.Filter.Accept(this, argument));
295+
}
296+
297+
/// <inheritdoc />
298+
public override bool VisitSortElement(SortElementExpression expression, object? argument)
299+
{
300+
return expression.TargetAttribute is null || expression.TargetAttribute.Accept(this, argument);
301+
}
302+
303+
/// <inheritdoc />
304+
public override bool VisitSort(SortExpression expression, object? argument)
305+
{
306+
return expression.Elements.All(element => element.Accept(this, argument));
307+
}
308+
309+
/// <inheritdoc />
310+
public override bool VisitCount(CountExpression expression, object? argument)
311+
{
312+
return expression.TargetCollection.Accept(this, argument);
313+
}
314+
315+
/// <inheritdoc />
316+
public override bool VisitMatchText(MatchTextExpression expression, object? argument)
317+
{
318+
return expression.TargetAttribute.Accept(this, argument);
319+
}
320+
321+
/// <inheritdoc />
322+
public override bool VisitAny(AnyExpression expression, object? argument)
323+
{
324+
return expression.TargetAttribute.Accept(this, argument);
325+
}
326+
}
174327
}
175328
}

0 commit comments

Comments
 (0)