From b0bdb9d657f2eb8a1fdae19f5a09af86195d5313 Mon Sep 17 00:00:00 2001 From: Jacob Hilty Date: Fri, 4 May 2018 13:16:56 -0400 Subject: [PATCH] Closes #273 --- src/JsonApiDotNetCore/Builders/LinkBuilder.cs | 4 +- .../Internal/Query/QueryConstants.cs | 15 +++++ .../Services/QueryComposer.cs | 38 +++++++++++++ src/JsonApiDotNetCore/Services/QueryParser.cs | 39 +++++-------- .../Spec/DocumentTests/PagingTests.cs | 1 + test/UnitTests/Services/QueryComposerTests.cs | 57 +++++++++++++++++++ 6 files changed, 128 insertions(+), 26 deletions(-) create mode 100644 src/JsonApiDotNetCore/Internal/Query/QueryConstants.cs create mode 100644 src/JsonApiDotNetCore/Services/QueryComposer.cs create mode 100644 test/UnitTests/Services/QueryComposerTests.cs diff --git a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs index dced1225a9..42b82e6255 100644 --- a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs @@ -48,7 +48,9 @@ public string GetRelatedRelationLink(string parent, string parentId, string chil public string GetPageLink(int pageOffset, int pageSize) { - return $"{_context.BasePath}/{_context.RequestEntity.EntityName}?page[size]={pageSize}&page[number]={pageOffset}"; + var filterQueryComposer = new QueryComposer(); + var filters = filterQueryComposer.Compose(_context); + return $"{_context.BasePath}/{_context.RequestEntity.EntityName}?page[size]={pageSize}&page[number]={pageOffset}{filters}"; } } } diff --git a/src/JsonApiDotNetCore/Internal/Query/QueryConstants.cs b/src/JsonApiDotNetCore/Internal/Query/QueryConstants.cs new file mode 100644 index 0000000000..3117ff7cb3 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/QueryConstants.cs @@ -0,0 +1,15 @@ +namespace JsonApiDotNetCore.Internal.Query{ + public static class QueryConstants { + public const string FILTER = "filter"; + public const string SORT = "sort"; + public const string INCLUDE = "include"; + public const string PAGE = "page"; + public const string FIELDS = "fields"; + public const char OPEN_BRACKET = '['; + public const char CLOSE_BRACKET = ']'; + public const char COMMA = ','; + public const char COLON = ':'; + public const string COLON_STR = ":"; + + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Services/QueryComposer.cs b/src/JsonApiDotNetCore/Services/QueryComposer.cs new file mode 100644 index 0000000000..8fbf16339b --- /dev/null +++ b/src/JsonApiDotNetCore/Services/QueryComposer.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Internal.Query; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCore.Services +{ + public interface IQueryComposer + { + string Compose(IJsonApiContext jsonApiContext); + } + + public class QueryComposer : IQueryComposer + { + public string Compose(IJsonApiContext jsonApiContext) + { + string result = ""; + if(jsonApiContext != null && jsonApiContext.QuerySet != null) + { + List filterQueries = jsonApiContext.QuerySet.Filters; + if (filterQueries.Count > 0) + { + foreach (FilterQuery filter in filterQueries) + { + result += ComposeSingleFilter(filter); + } + } + } + return result; + } + + private string ComposeSingleFilter(FilterQuery query) + { + var result = "&filter"; + result += QueryConstants.OPEN_BRACKET + query.Attribute + QueryConstants.CLOSE_BRACKET + query.Operation + query.Value; + return result; + } + } +} diff --git a/src/JsonApiDotNetCore/Services/QueryParser.cs b/src/JsonApiDotNetCore/Services/QueryParser.cs index 5e705f4bc9..d7ac2d90bf 100644 --- a/src/JsonApiDotNetCore/Services/QueryParser.cs +++ b/src/JsonApiDotNetCore/Services/QueryParser.cs @@ -20,17 +20,6 @@ public class QueryParser : IQueryParser private readonly IControllerContext _controllerContext; private readonly JsonApiOptions _options; - private const string FILTER = "filter"; - private const string SORT = "sort"; - private const string INCLUDE = "include"; - private const string PAGE = "page"; - private const string FIELDS = "fields"; - private const char OPEN_BRACKET = '['; - private const char CLOSE_BRACKET = ']'; - private const char COMMA = ','; - private const char COLON = ':'; - private const string COLON_STR = ":"; - public QueryParser( IControllerContext controllerContext, JsonApiOptions options) @@ -46,35 +35,35 @@ public virtual QuerySet Parse(IQueryCollection query) foreach (var pair in query) { - if (pair.Key.StartsWith(FILTER)) + if (pair.Key.StartsWith(QueryConstants.FILTER)) { if (disabledQueries.HasFlag(QueryParams.Filter) == false) querySet.Filters.AddRange(ParseFilterQuery(pair.Key, pair.Value)); continue; } - if (pair.Key.StartsWith(SORT)) + if (pair.Key.StartsWith(QueryConstants.SORT)) { if (disabledQueries.HasFlag(QueryParams.Sort) == false) querySet.SortParameters = ParseSortParameters(pair.Value); continue; } - if (pair.Key.StartsWith(INCLUDE)) + if (pair.Key.StartsWith(QueryConstants.INCLUDE)) { if (disabledQueries.HasFlag(QueryParams.Include) == false) querySet.IncludedRelationships = ParseIncludedRelationships(pair.Value); continue; } - if (pair.Key.StartsWith(PAGE)) + if (pair.Key.StartsWith(QueryConstants.PAGE)) { if (disabledQueries.HasFlag(QueryParams.Page) == false) querySet.PageQuery = ParsePageQuery(querySet.PageQuery, pair.Key, pair.Value); continue; } - if (pair.Key.StartsWith(FIELDS)) + if (pair.Key.StartsWith(QueryConstants.FIELDS)) { if (disabledQueries.HasFlag(QueryParams.Fields) == false) querySet.Fields = ParseFieldsQuery(pair.Key, pair.Value); @@ -94,9 +83,9 @@ protected virtual List ParseFilterQuery(string key, string value) // expected input = filter[id]=eq:1 var queries = new List(); - var propertyName = key.Split(OPEN_BRACKET, CLOSE_BRACKET)[1]; + var propertyName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; - var values = value.Split(COMMA); + var values = value.Split(QueryConstants.COMMA); foreach (var val in values) { (var operation, var filterValue) = ParseFilterOperation(val); @@ -111,7 +100,7 @@ protected virtual (string operation, string value) ParseFilterOperation(string v if (value.Length < 3) return (string.Empty, value); - var operation = value.Split(COLON); + var operation = value.Split(QueryConstants.COLON); if (operation.Length == 1) return (string.Empty, value); @@ -121,7 +110,7 @@ protected virtual (string operation, string value) ParseFilterOperation(string v return (string.Empty, value); var prefix = operation[0]; - value = string.Join(COLON_STR, operation.Skip(1)); + value = string.Join(QueryConstants.COLON_STR, operation.Skip(1)); return (prefix, value); } @@ -132,7 +121,7 @@ protected virtual PageQuery ParsePageQuery(PageQuery pageQuery, string key, stri // page[number]=1 pageQuery = pageQuery ?? new PageQuery(); - var propertyName = key.Split(OPEN_BRACKET, CLOSE_BRACKET)[1]; + var propertyName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; const string SIZE = "size"; const string NUMBER = "number"; @@ -157,7 +146,7 @@ protected virtual List ParseSortParameters(string value) var sortParameters = new List(); const char DESCENDING_SORT_OPERATOR = '-'; - var sortSegments = value.Split(COMMA); + var sortSegments = value.Split(QueryConstants.COMMA); foreach (var sortSegment in sortSegments) { @@ -189,14 +178,14 @@ protected virtual List ParseIncludedRelationships(string value) throw new JsonApiException(400, "Deeply nested relationships are not supported"); return value - .Split(COMMA) + .Split(QueryConstants.COMMA) .ToList(); } protected virtual List ParseFieldsQuery(string key, string value) { // expected: fields[TYPE]=prop1,prop2 - var typeName = key.Split(OPEN_BRACKET, CLOSE_BRACKET)[1]; + var typeName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; const string ID = "Id"; var includedFields = new List { ID }; @@ -205,7 +194,7 @@ protected virtual List ParseFieldsQuery(string key, string value) if (string.Equals(typeName, _controllerContext.RequestEntity.EntityName, StringComparison.OrdinalIgnoreCase) == false) return includedFields; - var fields = value.Split(COMMA); + var fields = value.Split(QueryConstants.COMMA); foreach (var field in fields) { var attr = _controllerContext.RequestEntity diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/PagingTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/PagingTests.cs index 787c9f07a0..faae94a3d8 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/PagingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/PagingTests.cs @@ -23,6 +23,7 @@ public class PagingTests private Faker _personFaker; private Faker _todoItemFaker; private Faker _todoItemCollectionFaker; + private DateTime CurrentTime; public PagingTests(TestFixture fixture) { diff --git a/test/UnitTests/Services/QueryComposerTests.cs b/test/UnitTests/Services/QueryComposerTests.cs new file mode 100644 index 0000000000..e1bd892756 --- /dev/null +++ b/test/UnitTests/Services/QueryComposerTests.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Http; +using Moq; +using Xunit; + +namespace UnitTests.Services +{ + public class QueryComposerTests + { + private readonly Mock _jsonApiContext; + + public QueryComposerTests() + { + _jsonApiContext = new Mock(); + } + + [Fact] + public void Can_Compose_FilterStringForUrl() + { + // arrange + var filter = new FilterQuery("attribute", "value", "="); + var querySet = new QuerySet(); + List filters = new List(); + filters.Add(filter); + querySet.Filters=filters; + + _jsonApiContext + .Setup(m => m.QuerySet) + .Returns(querySet); + + var queryComposer = new QueryComposer(); + // act + var filterString = queryComposer.Compose(_jsonApiContext.Object); + // assert + Assert.Equal("&filter[attribute]=value", filterString); + } + + [Fact] + public void NoFilter_Compose_EmptyStringReturned() + { + // arrange + var querySet = new QuerySet(); + + _jsonApiContext + .Setup(m => m.QuerySet) + .Returns(querySet); + + var queryComposer = new QueryComposer(); + // act + var filterString = queryComposer.Compose(_jsonApiContext.Object); + // assert + Assert.Equal("", filterString); + } + } +} \ No newline at end of file