diff --git a/devdocs/specs/core-list-graph-datatable-filtering-paging.txt b/devdocs/specs/core-list-graph-datatable-filtering-paging.txt index 3167038c..4e861318 100644 --- a/devdocs/specs/core-list-graph-datatable-filtering-paging.txt +++ b/devdocs/specs/core-list-graph-datatable-filtering-paging.txt @@ -13,9 +13,21 @@ and also lists all the fields filterable, their type and the locale key to displ - e.g.: {list:"widget",fields:[{fld:"name",lt:"WidgetName",type:"text"},{fld:"dollarAmount",lt:"WidgetDollarAmount",type:"currency"}]} Certain types have extended abilities, for example dates have the classic floating AyaNova date ranges pre-defined or specific dates Filters are saved to the database: - - Filter: Name, OwnerId, Public, ListKey, Filter (Json string) (column names to be determined) - - i.e. "My widget filter", 1, true, "widget", "[{fld:"name",comparisonoperator:"Like",value:"Bob*"},{fld:"tags",comparisonoperator:"Eq",value:"[23,456,54]"}] - - means all widgets that start with the name "Bob" and are tagged with tags with id values 23, 456 and 54 + - Filter: Name, OwnerId, Public, ListKey, Filter (Json string) + - Filter format: + - Array [{fld:"fieldname",op:"See filtercomparisonoperator class",value:"compareValue"},...] these are all AND in sql, no OR + - fld=name of field to filter by + - Straight up field name like "name" + - Could be compound for joins like "table.name" (no a prepends the table name) + - Special indirect values such as "[TAGS]" which means cross filter with tags + - op=one of the values specified in the FilterComparisonOperator class in Biz namespace + - value= straight up direct comparison value + - If string then a string fragment, case is sensitive + - If date then iso style date + - could be whole number or decimal number + - Could be a special "macro" filter value like "[THIS_MONTH]" (always surrounded by square brackets, no need to disambiguate with a string because only applies to non string values) + - Could be a series of id values like this "[23,45,56,123]" as in tag id's or something related to that case. + Upon user selecting a filter to use the list query string has the regular paging info but also the filter id as a query parameter - Server loads the filter if it's public or has the user ID if it's personal only - If list not found then it will return a 404 instead of the list diff --git a/server/AyaNova/biz/DataFilterBiz.cs b/server/AyaNova/biz/DataFilterBiz.cs index 7bd0c526..80f95ae5 100644 --- a/server/AyaNova/biz/DataFilterBiz.cs +++ b/server/AyaNova/biz/DataFilterBiz.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.JsonPatch; using EnumsNET; +using Newtonsoft.Json.Linq; using AyaNova.Util; using AyaNova.Api.ControllerHelpers; using AyaNova.Biz; @@ -221,9 +222,9 @@ namespace AyaNova.Biz AddError(ValidationErrorType.RequiredPropertyEmpty, "OwnerId"); } - //Owner must be current user, there are no exceptions - if (inObj.OwnerId != UserId) - AddError(ValidationErrorType.InvalidValue, "OwnerId", "OwnerId must be current user Id"); + // //Owner must be current user, there are no exceptions + // if (inObj.OwnerId != UserId) + // AddError(ValidationErrorType.InvalidValue, "OwnerId", "OwnerId must be current user Id"); //Name required if (string.IsNullOrWhiteSpace(inObj.Name)) @@ -249,6 +250,30 @@ namespace AyaNova.Biz if (inObj.ListKey.Length > 255) AddError(ValidationErrorType.LengthExceeded, "ListKey", "255 max"); + //Filter json must parse + if (!string.IsNullOrWhiteSpace(inObj.Filter)) + { + try + { + var v = JArray.Parse(inObj.Filter); + for (int i = 0; i < v.Count; i++) + { + var filterItem = v[i]; + if (filterItem["fld"] == null) + AddError(ValidationErrorType.RequiredPropertyEmpty, "Filter", $"Filter array item {i}, object is missing required \"fld\" property "); + if (filterItem["op"] == null) + AddError(ValidationErrorType.RequiredPropertyEmpty, "Filter", $"Filter array item {i}, object is missing required \"op\" property "); + if (filterItem["value"] == null) + AddError(ValidationErrorType.RequiredPropertyEmpty, "Filter", $"Filter array item {i}, object is missing required \"value\" property "); + } + } + catch (Newtonsoft.Json.JsonReaderException ex) + { + AddError(ValidationErrorType.InvalidValue, "Filter", "Filter is not valid JSON string: " + ex.Message); + + } + } + return; } diff --git a/test/raven-integration/DataFilter/DataFilterCrud.cs b/test/raven-integration/DataFilter/DataFilterCrud.cs index 2a8f99be..a8321890 100644 --- a/test/raven-integration/DataFilter/DataFilterCrud.cs +++ b/test/raven-integration/DataFilter/DataFilterCrud.cs @@ -17,97 +17,91 @@ namespace raven_integration [Fact] public async void CRUD() { - /* - { - "id": 0, - "concurrencyToken": 0, - "ownerId": 0, - "name": "string", - "public": true, - "listKey": "string", - "filter": "string" -} - */ + //CREATE dynamic d = new JObject(); d.name = Util.Uniquify("Test DataFilter"); - d.ownerId = 1L; - d.listKey="Widget"; - d.filter=""; + // d.ownerId = 1L; + d["public"] = true; + d.listKey = "Widget"; - ApiResponse a = await Util.PostAsync("Tag", await Util.GetTokenAsync("BizAdminFull"), d.ToString()); + //"[{fld:"name",op:"!=",value:"Notequaltothis"},{fld:"tags",op:"Eq",value:"[23,456,54]"}] + dynamic dfilter = new JArray(); + dynamic df = new JObject(); + df.fld = "name"; + df.op = "%-"; + df.value = "Generic";//lots of seed widgets start with Generic + dfilter.Add(df); + + d.filter=dfilter.ToString();//it expects it to be a json string, not actual json + + ApiResponse a = await Util.PostAsync("DataFilter", await Util.GetTokenAsync("BizAdminFull"), d.ToString()); Util.ValidateDataReturnResponseOk(a); - long tagId = a.ObjectResponse["data"]["id"].Value(); - string tagName = a.ObjectResponse["data"]["name"].Value(); - tagName.Should().StartWith("testtag"); + + long Id = a.ObjectResponse["data"]["id"].Value(); + string Name = a.ObjectResponse["data"]["name"].Value(); + Name.Should().StartWith("Test DataFilter"); - //RETRIEVE - /* - { - "data": { - "id": 24, - "created": "2018-03-28T21:07:41.9703503Z", - "concurrencyToken": 9502, - "ownerId": 1, - "name": "یونی‌کُد چیست؟" - } - } - */ - //Get one - a = await Util.GetAsync("Tag/" + tagId.ToString(), await Util.GetTokenAsync("BizAdminFull")); - Util.ValidateDataReturnResponseOk(a); - a.ObjectResponse["data"]["name"].Value().Should().StartWith("testtag"); + // // //RETRIEVE + // // /* + // // { + // // "data": { + // // "id": 24, + // // "created": "2018-03-28T21:07:41.9703503Z", + // // "concurrencyToken": 9502, + // // "ownerId": 1, + // // "name": "یونی‌کُد چیست؟" + // // } + // // } + // // */ + // // //Get one + // // a = await Util.GetAsync("Tag/" + tagId.ToString(), await Util.GetTokenAsync("BizAdminFull")); + // // Util.ValidateDataReturnResponseOk(a); + // // a.ObjectResponse["data"]["name"].Value().Should().StartWith("testtag"); - //UPDATE + // // //UPDATE - //PUT - d.Id = tagId; - d.name = Util.Uniquify("PutTestTag"); - d.created = DateTime.UtcNow.ToString("s", System.Globalization.CultureInfo.InvariantCulture); - d.concurrencyToken = a.ObjectResponse["data"]["concurrencyToken"].Value(); - d.OwnerId = 1L; + // // //PUT + // // d.Id = tagId; + // // d.name = Util.Uniquify("PutTestTag"); + // // d.created = DateTime.UtcNow.ToString("s", System.Globalization.CultureInfo.InvariantCulture); + // // d.concurrencyToken = a.ObjectResponse["data"]["concurrencyToken"].Value(); + // // d.OwnerId = 1L; - ApiResponse PUTTestResponse = await Util.PutAsync("Tag/" + tagId.ToString(), await Util.GetTokenAsync("BizAdminFull"), d.ToString()); - Util.ValidateHTTPStatusCode(PUTTestResponse, 200); + // // ApiResponse PUTTestResponse = await Util.PutAsync("Tag/" + tagId.ToString(), await Util.GetTokenAsync("BizAdminFull"), d.ToString()); + // // Util.ValidateHTTPStatusCode(PUTTestResponse, 200); + + // // //check PUT worked + // // ApiResponse checkPUTWorked = await Util.GetAsync("Tag/" + tagId.ToString(), await Util.GetTokenAsync("BizAdminFull")); + // // Util.ValidateNoErrorInResponse(checkPUTWorked); + // // checkPUTWorked.ObjectResponse["data"]["name"].Value().Should().Be(d.name.ToString().ToLowerInvariant().Replace(" ", "-")); + // // uint concurrencyToken = PUTTestResponse.ObjectResponse["data"]["concurrencyToken"].Value(); + + // // //PATCH + // // var newName = Util.Uniquify("PatchUpdate"); + // // string patchJson = "[{\"value\": \"" + newName + "\",\"path\": \"/name\",\"op\": \"replace\"}]"; + // // ApiResponse PATCHTestResponse = await Util.PatchAsync("Tag/" + tagId.ToString() + "/" + concurrencyToken.ToString(), await Util.GetTokenAsync("BizAdminFull"), patchJson); + // // Util.ValidateHTTPStatusCode(PATCHTestResponse, 200); + + // // //check PATCH worked + // // ApiResponse checkPATCHWorked = await Util.GetAsync("Tag/" + tagId.ToString(), await Util.GetTokenAsync("BizAdminFull")); + // // Util.ValidateNoErrorInResponse(checkPATCHWorked); + // // checkPATCHWorked.ObjectResponse["data"]["name"].Value().Should().Be(newName.ToLowerInvariant().Replace(" ", "-")); + + // // // //DELETE + // // ApiResponse DELETETestResponse = await Util.DeleteAsync("Tag/" + tagId.ToString(), await Util.GetTokenAsync("BizAdminFull")); + // // Util.ValidateHTTPStatusCode(DELETETestResponse, 204); - //check PUT worked - ApiResponse checkPUTWorked = await Util.GetAsync("Tag/" + tagId.ToString(), await Util.GetTokenAsync("BizAdminFull")); - Util.ValidateNoErrorInResponse(checkPUTWorked); - checkPUTWorked.ObjectResponse["data"]["name"].Value().Should().Be(d.name.ToString().ToLowerInvariant().Replace(" ", "-")); - uint concurrencyToken = PUTTestResponse.ObjectResponse["data"]["concurrencyToken"].Value(); - //PATCH - var newName = Util.Uniquify("PatchUpdate"); - string patchJson = "[{\"value\": \"" + newName + "\",\"path\": \"/name\",\"op\": \"replace\"}]"; - ApiResponse PATCHTestResponse = await Util.PatchAsync("Tag/" + tagId.ToString() + "/" + concurrencyToken.ToString(), await Util.GetTokenAsync("BizAdminFull"), patchJson); - Util.ValidateHTTPStatusCode(PATCHTestResponse, 200); - //check PATCH worked - ApiResponse checkPATCHWorked = await Util.GetAsync("Tag/" + tagId.ToString(), await Util.GetTokenAsync("BizAdminFull")); - Util.ValidateNoErrorInResponse(checkPATCHWorked); - checkPATCHWorked.ObjectResponse["data"]["name"].Value().Should().Be(newName.ToLowerInvariant().Replace(" ", "-")); - // //DELETE - ApiResponse DELETETestResponse = await Util.DeleteAsync("Tag/" + tagId.ToString(), await Util.GetTokenAsync("BizAdminFull")); - Util.ValidateHTTPStatusCode(DELETETestResponse, 204); } - - - - - - - - - - - //================================================== }//eoc