diff --git a/devdocs/specs/core-picklist-autocomplete-template-system.txt b/devdocs/specs/core-picklist-autocomplete-template-system.txt index 9358a5a3..03c5d707 100644 --- a/devdocs/specs/core-picklist-autocomplete-template-system.txt +++ b/devdocs/specs/core-picklist-autocomplete-template-system.txt @@ -30,19 +30,19 @@ AUTOCOMPLETE - Similar to DataListView but static, one for each type of object only - has default view if not overriden that is generally good enough for anyone - PickList Controller ROUTES - - GetList(AyaType, query) + - GET PickList/List(AyaType, query) - Uses cached view (fetches into cache if not fetched to keep cache small) - Use best practice caching for this so can extend to other things that need caching like BizRoles - Based on cached view constructs a query with relevant columns only and containing search query in each column - Also handles tags properly - - PutView(AyaType, viewJson) + - PUT PickList/Template (body AyaType, body viewJson) - Update a view to use a new default - updates cache when it updates db as well - - ResetView(AyaType) + - POST PickList/TemplateReset(AyaType) - for ui, resets the view back to it's default - - GetView(AyaType) + - GET PickList/Template (AyaType) - for ui picklistvieweditor to populate ui with current template (or default if not edited) - - GetViewList() + - GET PickList/TemplateList - for ui picklistview editor to switch lists - autocomplete / search involves all displayed template columns diff --git a/server/AyaNova/Controllers/PickListController.cs b/server/AyaNova/Controllers/PickListController.cs new file mode 100644 index 00000000..321345ad --- /dev/null +++ b/server/AyaNova/Controllers/PickListController.cs @@ -0,0 +1,116 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; +using AyaNova.Models; +using AyaNova.Api.ControllerHelpers; +using AyaNova.Biz; +//using AyaNova.PickList; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace AyaNova.Api.Controllers +{ + + [ApiController] + [ApiVersion("8.0")] + [Route("api/v{version:apiVersion}/[controller]")] + [Produces("application/json")] + [Authorize] + public class PickListController : ControllerBase + { + private readonly AyContext ct; + private readonly ILogger log; + private readonly ApiServerState serverState; + + + + /// + /// ctor + /// + /// + /// + /// + public PickListController(AyContext dbcontext, ILogger logger, ApiServerState apiServerState) + { + ct = dbcontext; + log = logger; + serverState = apiServerState; + } + + + + /// + /// Get picklist + /// + /// The AyaType object type to select from + /// The query to filter the returned list by + /// Filtered list (maximum 25 items are returned for any query) + [HttpGet("List")] + public async Task GetList([FromQuery]AyaType ayaType, [FromQuery]string query) + { + if (!serverState.IsOpen) + { + return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); + } + + //Instantiate the business object handler + PickListBiz biz = PickListBiz.GetBiz(ct, HttpContext); + + // //NOTE: This is the first check and often the only check but in some cases with some objects this will also need to check biz object rules + // if (!Authorized.HasReadFullRole(HttpContext.Items, biz.BizType)) + // return StatusCode(403, new ApiNotAuthorizedResponse()); + + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + + var o = await biz.GetPickListAsync(ayaType,query); + if (o == null) + return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); + + + return Ok(ApiOkResponse.Response(o, true)); + } + + + /// + /// List of all PickList templates + /// + /// List of strings + [HttpGet("TemplateList")] + public ActionResult GetTemplateList() + { + if (!serverState.IsOpen) + { + return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); + } + + return Ok(ApiOkResponse.Response(PickListFactory.GetListOfAllPickListKeyNames(), true)); + } + + + /// + /// List of all fields for data list key specified + /// + /// List of PickListFieldDefinition + [HttpGet("ListFields")] + public ActionResult GetPickListFields([FromQuery] string PickListKey) + { + if (!serverState.IsOpen) + { + return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); + } + + var PickList = PickListFactory.GetAyaPickList(PickListKey); + //was the name not found as a list? + if (PickList == null) + { + return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_FOUND, "PickListKey", $"PickList \"{PickListKey}\" specified does not exist")); + } + + return Ok(ApiOkResponse.Response(PickList.FieldDefinitions, true)); + } + + }//eoc +}//ens \ No newline at end of file diff --git a/server/AyaNova/biz/PickListBiz.cs b/server/AyaNova/biz/PickListBiz.cs new file mode 100644 index 00000000..17003cf4 --- /dev/null +++ b/server/AyaNova/biz/PickListBiz.cs @@ -0,0 +1,395 @@ +using System.Linq; +using System.Threading.Tasks; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json.Linq; +using AyaNova.Util; +using AyaNova.Api.ControllerHelpers; +using AyaNova.Models; +using AyaNova.DataList; + + +namespace AyaNova.Biz +{ + + + internal class PickListBiz : BizObject + { + + internal PickListBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles) + { + ct = dbcontext; + UserId = currentUserId; + UserTranslationId = userTranslationId; + CurrentUserRoles = UserRoles; + BizType = AyaType.DataListView; + } + + internal static PickListBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null) + { + if (httpContext != null) + return new PickListBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items)); + else + return new PickListBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdminFull); + } + + + + // //////////////////////////////////////////////////////////////////////////////////////////////// + // //EXISTS + // internal async Task ExistsAsync(long id) + // { + // return await ct.DataListView.AnyAsync(e => e.Id == id); + // } + + + + // //////////////////////////////////////////////////////////////////////////////////////////////// + // //CREATE + // internal async Task CreateAsync(DataListView inObj) + // { + // await ValidateAsync(inObj, true); + // if (HasErrors) + // return null; + // else + // { + // //do stuff with datafilter + // DataListView outObj = inObj; + // outObj.UserId = UserId; + + + // await ct.DataListView.AddAsync(outObj); + // await ct.SaveChangesAsync(); + + // //Handle child and associated items: + + // //EVENT LOG + // await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), ct); + + + + // return outObj; + + // } + // } + + + + + // //////////////////////////////////////////////////////////////////////////////////////////////// + // //DUPLICATE + // // + + // internal async Task DuplicateAsync(DataListView dbObj) + // { + + // DataListView outObj = new DataListView(); + // CopyObject.Copy(dbObj, outObj); + // //generate unique name + // string newUniqueName = string.Empty; + // bool NotUnique = true; + // long l = 1; + // do + // { + // newUniqueName = Util.StringUtil.UniqueNameBuilder(dbObj.Name, l++, 255); + // NotUnique = await ct.DataListView.AnyAsync(m => m.Name == newUniqueName); + // } while (NotUnique); + // outObj.Name = newUniqueName; + // outObj.Id = 0; + // outObj.ConcurrencyToken = 0; + + + + // await ct.DataListView.AddAsync(outObj); + // await ct.SaveChangesAsync(); + + // //Handle child and associated items: + // await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), ct); + + // return outObj; + + // } + + //////////////////////////////////////////////////////////////////////////////////////////////// + /// GET + + //Get one + internal async Task GetAsync(long fetchId, bool logTheGetEvent = true) + { + //This is simple so nothing more here, but often will be copying to a different output object or some other ops + var ret = await ct.DataListView.SingleOrDefaultAsync(m => m.Id == fetchId && (m.Public == true || m.UserId == UserId)); + if (logTheGetEvent && ret != null) + { + //Log + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, fetchId, BizType, AyaEvent.Retrieved), ct); + } + return ret; + } + + + + + //get picklist + internal async Task> GetPickListAsync(AyaType ayaType, string query) + { + //TODO: Use picklist objects to formulate and query and return results + List items = new List(); + // if (!string.IsNullOrWhiteSpace(listKey)) + // { + // items = await ct.DataListView + // .AsNoTracking() + // .Where(m => m.ListKey == listKey && (m.Public == true || m.UserId == UserId)) + // .OrderBy(m => m.Name) + // .Select(m => new NameIdItem() + // { + // Id = m.Id, + // Name = m.Name + // }).ToListAsync(); + + // } + return items; + } + + + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //UPDATE + // + + //put + internal async Task PutAsync(DataListView dbObj, DataListView inObj) + { + //preserve the owner ID if none was specified + if (inObj.UserId == 0) + inObj.UserId = dbObj.UserId; + + //Replace the db object with the PUT object + CopyObject.Copy(inObj, dbObj, "Id"); + //Set "original" value of concurrency token to input token + //this will allow EF to check it out + ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken; + + await ValidateAsync(dbObj, false); + if (HasErrors) + return false; + await ct.SaveChangesAsync(); + + //Log modification and save context + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct); + + + + return true; + } + + + // //////////////////////////////////////////////////////////////////////////////////////////////// + // //DELETE + // // + // internal async Task DeleteAsync(DataListView dbObj) + // { + // //Determine if the object can be deleted, do the deletion tentatively + // //Probably also in here deal with tags and associated search text etc + + // //FUTURE POSSIBLE NEED + // //ValidateCanDelete(dbObj); + + // if (HasErrors) + // return false; + // ct.DataListView.Remove(dbObj); + // await ct.SaveChangesAsync(); + + // //Delete sibling objects + + // //Event log process delete + // await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObj.Id, dbObj.Name, ct); + + + // //Delete search index + // //Search.ProcessDeletedObjectKeywords(dbObj.Id, BizType); + + + // return true; + // } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //VALIDATION + // + + //Can save or update? + private async Task ValidateAsync(DataListView inObj, bool isNew) + { + + //UserId required + if (!isNew) + { + if (inObj.UserId == 0) + AddError(ApiErrorCode.VALIDATION_REQUIRED, "UserId"); + } + + + + //Name required + if (string.IsNullOrWhiteSpace(inObj.Name)) + AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name"); + + //Name must be less than 255 characters + if (inObj.Name.Length > 255) + AddError(ApiErrorCode.VALIDATION_LENGTH_EXCEEDED, "Name", "255 max"); + + //If name is otherwise OK, check that name is unique + if (!PropertyHasErrors("Name")) + { + //Use Any command is efficient way to check existance, it doesn't return the record, just a true or false + if (await ct.DataListView.AnyAsync(m => m.Name == inObj.Name && m.Id != inObj.Id)) + { + AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name"); + } + } + + if (string.IsNullOrWhiteSpace(inObj.ListKey)) + AddError(ApiErrorCode.VALIDATION_REQUIRED, "ListKey"); + + + var DataList = DataListFactory.GetAyaDataList(inObj.ListKey); + + if (DataList == null) + { + AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "ListKey", $"ListKey \"{inObj.ListKey}\" DataListKey is not valid"); + } + + + + if (inObj.ListKey.Length > 255) + AddError(ApiErrorCode.VALIDATION_LENGTH_EXCEEDED, "ListKey", "255 max"); + + //Filter json must parse + //this is all automated normally so not going to do too much parsing here + //just ensure it's basically there + if (!string.IsNullOrWhiteSpace(inObj.ListView)) + { + try + { + var v = JArray.Parse(inObj.ListView); + for (int i = 0; i < v.Count; i++) + { + var filterItem = v[i]; + if (filterItem["fld"] == null) + AddError(ApiErrorCode.VALIDATION_REQUIRED, "ListView", $"ListView array item {i}, object is missing required \"fld\" property "); + else + { + var fld = filterItem["fld"].Value(); + if (string.IsNullOrWhiteSpace(fld)) + AddError(ApiErrorCode.VALIDATION_REQUIRED, "ListView", $"ListView array item {i}, \"fld\" property is empty and required"); + + //validate the field name if we can + if (DataList != null) + { + + var TheField = DataList.FieldDefinitions.SingleOrDefault(x => x.FieldKey.ToLowerInvariant() == fld); + + if (TheField == null) + { + AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "ListView", $"ListView array item {i}, fld property value \"{fld}\" is not a valid value for ListKey specified"); + } + + } + } + //This is the old filter validation code but at this point only going to validate that the fields are present and valid as the bare minimum + // if (filterItem["op"] == null) + // AddError(ApiErrorCode.VALIDATION_REQUIRED, "Filter", $"Filter array item {i}, object is missing required \"op\" property "); + // else + // { + // var opType = filterItem["op"].Value(); + // if (!DataListFilterComparisonOperator.Operators.Contains(opType)) + // AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Filter", $"Filter array item {i}, \"op\" property value of \"{opType}\" is not a valid FilterComparisonOperator type"); + // } + + // if (filterItem["value"] == null) + // AddError(ApiErrorCode.VALIDATION_REQUIRED, "Filter", $"Filter array item {i}, object is missing or is empty the required \"value\" property "); + // else + // { + // //check if the value is present, not what it is exactly, just that it's present + // //value also could contain relative date tokens, not that it checks them anyway but just noting it here + // if (filterItem["value"].Type == JTokenType.String && string.IsNullOrWhiteSpace(filterItem["value"].Value())) + // AddError(ApiErrorCode.VALIDATION_REQUIRED, "Filter", $"Filter array item {i}, object is missing or is empty the required \"value\" property "); + + // if (filterItem["value"].Type == JTokenType.Array && filterItem["value"].Count() == 0) + // AddError(ApiErrorCode.VALIDATION_REQUIRED, "Filter", $"Filter array item {i}, object is missing or is empty the required \"value\" property ARRAY "); + // } + + + //NOTE: value of nothing, null or empty is a valid value so no checking for it here + } + } + catch (Newtonsoft.Json.JsonReaderException ex) + { + AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "ListView", "ListView is not valid JSON string: " + ex.Message); + + } + } + + + // //VALIDATE SORT + // //Filter json must parse + // if (!string.IsNullOrWhiteSpace(inObj.Sort)) + // { + // try + // { + // var v = JArray.Parse(inObj.Sort); + // for (int i = 0; i < v.Count; i++) + // { + // var sortItem = v[i]; + // if (sortItem["fld"] == null) + // AddError(ApiErrorCode.VALIDATION_REQUIRED, "Sort", $"Sort array item {i}, object is missing required \"fld\" property "); + // else + // { + // var fld = sortItem["fld"].Value(); + // if (string.IsNullOrWhiteSpace(fld)) + // AddError(ApiErrorCode.VALIDATION_REQUIRED, "Sort", $"Sort array item {i}, \"fld\" property is empty and required"); + + // //validate the field name if we can + // if (DataList != null) + // { + + // if (!DataList.FieldDefinitions.Exists(x => x.FieldKey.ToLowerInvariant() == fld && x.IsFilterable)) + // { + // AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Sort", $"Sort array item {i}, fld property value \"{fld}\" is not a valid value for ListKey specified"); + // } + + // } + // } + // if (sortItem["dir"] == null) + // AddError(ApiErrorCode.VALIDATION_REQUIRED, "Sort", $"Sort array item {i}, object is missing required \"dir\" sort direction property "); + // else + // { + // var sortDir = sortItem["dir"].Value(); + // if (sortDir != "+" && sortDir != "-") + // AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Sort", $"Sort array item {i}, \"dir\" property value of \"{sortDir}\" is not a valid sort direction value, must be \"+\" or \"-\" only"); + // } + // //NOTE: value of nothing, null or empty is a valid value so no checking for it here + // } + // } + // catch (Newtonsoft.Json.JsonReaderException ex) + // { + // AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Sort", "Sort is not valid JSON string: " + ex.Message); + + // } + // } + + + + return; + } + + + + + ///////////////////////////////////////////////////////////////////// + + }//eoc + + +}//eons +