diff --git a/server/AyaNova/Controllers/DataListTemplateController.cs b/server/AyaNova/Controllers/DataListTemplateController.cs new file mode 100644 index 00000000..ff35661d --- /dev/null +++ b/server/AyaNova/Controllers/DataListTemplateController.cs @@ -0,0 +1,224 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.JsonPatch; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +using AyaNova.Models; +using AyaNova.Api.ControllerHelpers; +using AyaNova.Biz; + + +namespace AyaNova.Api.Controllers +{ + + /// + /// + /// + [ApiController] + [ApiVersion("8.0")] + [Route("api/v{version:apiVersion}/[controller]")] + [Produces("application/json")] + [Authorize] + public class DataListTemplateController : ControllerBase + { + private readonly AyContext ct; + private readonly ILogger log; + private readonly ApiServerState serverState; + + + /// + /// ctor + /// + /// + /// + /// + public DataListTemplateController(AyContext dbcontext, ILogger logger, ApiServerState apiServerState) + { + ct = dbcontext; + log = logger; + serverState = apiServerState; + } + + + /// + /// Get full DataFilter object + /// + /// Required roles: + /// Any (for public filter), owned only for private filter + /// + /// + /// A single DataFilter + [HttpGet("{id}")] + public async Task GetDataFilter([FromRoute] long id) + { + if (serverState.IsClosed) + return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); + + //Instantiate the business object handler + DataListTemplateBiz biz = DataListTemplateBiz.GetBiz(ct, HttpContext); + + if (!Authorized.HasReadFullRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + + var o = await biz.GetAsync(id); + if (o == null) + return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); + + return Ok(ApiOkResponse.Response(o, !Authorized.HasModifyRole(HttpContext.Items, biz.BizType))); + } + + + + /// + /// Get DataFilter pick list + /// + /// Required roles: Any + /// + /// + /// List of public or owned data filters for listKey provided + [HttpGet("PickList", Name = nameof(DataFilterPickList))] + public async Task DataFilterPickList([FromQuery] string ListKey) + { + if (serverState.IsClosed) + return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); + + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + + //Instantiate the business object handler + DataListTemplateBiz biz = DataListTemplateBiz.GetBiz(ct, HttpContext); + + var l = await biz.GetPickListAsync(ListKey); + return Ok(ApiOkResponse.Response(l, true)); + + } + + + /// + /// Put (update) DataFilter + /// + /// Required roles: + /// Any (public filter) or owned only (private filter) + /// + /// + /// + /// + /// + [HttpPut("{id}")] + public async Task PutDataFilter([FromRoute] long id, [FromBody] DataListTemplate inObj) + { + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); + + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + + //Instantiate the business object handler + DataListTemplateBiz biz = DataListTemplateBiz.GetBiz(ct, HttpContext); + + var o = await biz.GetNoLogAsync(id); + if (o == null) + return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); + + if (!Authorized.HasModifyRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + + try + { + if (!biz.Put(o, inObj)) + return BadRequest(new ApiErrorResponse(biz.Errors)); + } + catch (DbUpdateConcurrencyException) + { + if (!await biz.ExistsAsync(id)) + return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); + else + return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT)); + } + return Ok(ApiOkResponse.Response(new { ConcurrencyToken = o.ConcurrencyToken }, true)); + } + + + /// + /// Post DataFilter + /// + /// Required roles: + /// BizAdminFull, InventoryFull, TechFull + /// + /// + /// Automatically filled from route path, no need to specify in body + /// + [HttpPost] + public async Task PostDataFilter([FromBody] DataListTemplate inObj, ApiVersion apiVersion) + { + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); + + //Instantiate the business object handler + DataListTemplateBiz biz = DataListTemplateBiz.GetBiz(ct, HttpContext); + + //If a user has change roles, or editOwnRoles then they can create, true is passed for isOwner since they are creating so by definition the owner + if (!Authorized.HasCreateRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + + //Create and validate + DataListTemplate o = await biz.CreateAsync(inObj); + if (o == null) + return BadRequest(new ApiErrorResponse(biz.Errors)); + else + return CreatedAtAction(nameof(DataListTemplateController.GetDataFilter), new { id = o.Id, version = apiVersion.ToString() }, new ApiCreatedResponse(o)); + + } + + + + /// + /// Delete DataFilter + /// + /// Required roles: + /// Any if public otherwise creator only + /// + /// + /// + /// Ok + [HttpDelete("{id}")] + public async Task DeleteDataFilter([FromRoute] long id) + { + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); + + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + + //Instantiate the business object handler + DataListTemplateBiz biz = DataListTemplateBiz.GetBiz(ct, HttpContext); + + var o = await biz.GetNoLogAsync(id); + if (o == null) + return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); + + if (!Authorized.HasDeleteRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + + if (!biz.Delete(o)) + return BadRequest(new ApiErrorResponse(biz.Errors)); + + return NoContent(); + } + + //------------ + + + }//eoc +}//eons \ No newline at end of file diff --git a/server/AyaNova/biz/DataListTemplateBiz.cs b/server/AyaNova/biz/DataListTemplateBiz.cs new file mode 100644 index 00000000..09a038c3 --- /dev/null +++ b/server/AyaNova/biz/DataListTemplateBiz.cs @@ -0,0 +1,424 @@ +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 DataListTemplateBiz : BizObject + { + + internal DataListTemplateBiz(AyContext dbcontext, long currentUserId, long userLocaleId, AuthorizationRoles UserRoles) + { + ct = dbcontext; + UserId = currentUserId; + UserLocaleId = userLocaleId; + CurrentUserRoles = UserRoles; + BizType = AyaType.DataFilter; + } + + internal static DataListTemplateBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext) + { + return new DataListTemplateBiz(ct, UserIdFromContext.Id(httpContext.Items), UserLocaleIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items)); + } + + //Version for internal use + internal static DataListTemplateBiz GetBizInternal(AyContext ct) + { + return new DataListTemplateBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID, AuthorizationRoles.BizAdminFull); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //EXISTS + internal async Task ExistsAsync(long id) + { + return await ct.DataListTemplate.AnyAsync(e => e.Id == id); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + /// GET + internal async Task GetNoLogAsync(long fetchId) + { + //This is simple so nothing more here, but often will be copying to a different output object or some other ops + return await ct.DataListTemplate.SingleOrDefaultAsync(m => m.Id == fetchId); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //CREATE + internal async Task CreateAsync(DataListTemplate inObj) + { + Validate(inObj, true); + if (HasErrors) + return null; + else + { + //do stuff with datafilter + DataListTemplate outObj = inObj; + outObj.UserId = UserId; + + + await ct.DataListTemplate.AddAsync(outObj); + await ct.SaveChangesAsync(); + + //Handle child and associated items: + + //EVENT LOG + EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), ct); + + //SEARCH INDEXING + // Search.ProcessNewObjectKeywords(UserLocaleId, outObj.Id, BizType, outObj.Name, outObj.Name); + + return outObj; + + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //CREATE + internal DataListTemplate Create(AyContext TempContext, DataListTemplate inObj) + { + Validate(inObj, true); + if (HasErrors) + return null; + else + { + //do stuff with datafilter + DataListTemplate outObj = inObj; + outObj.UserId = UserId; + + + TempContext.DataListTemplate.Add(outObj); + TempContext.SaveChanges(); + + //Handle child and associated items: + + //EVENT LOG + EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), TempContext); + + //SEARCH INDEXING + // Search.ProcessNewObjectKeywords(UserLocaleId, outObj.Id, BizType, outObj.Name, outObj.Name); + + return outObj; + + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + /// GET + + //Get one + internal async Task GetAsync(long fetchId) + { + //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.DataListTemplate.SingleOrDefaultAsync(m => m.Id == fetchId && (m.Public == true || m.UserId == UserId)); + if (ret != null) + { + //Log + EventLogProcessor.LogEventToDatabase(new Event(UserId, fetchId, BizType, AyaEvent.Retrieved), ct); + } + return ret; + } + + + //get picklist (NOT PAGED) + internal async Task> GetPickListAsync(string listKey) + { + List items = new List(); + if (!string.IsNullOrWhiteSpace(listKey)) + { + items = await ct.DataListTemplate + .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 bool Put(DataListTemplate dbObj, DataListTemplate 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; + + Validate(dbObj, false); + if (HasErrors) + return false; + + //Log modification + EventLogProcessor.LogEventToDatabase(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct); + + //Update keywords + // Search.ProcessUpdatedObjectKeywords(UserLocaleId, dbObj.Id, BizType, dbObj.Name, dbObj.Name); + + return true; + } + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //DELETE + // + internal bool Delete(DataListTemplate dbObj) + { + //Determine if the object can be deleted, do the deletion tentatively + //Probably also in here deal with tags and associated search text etc + + ValidateCanDelete(dbObj); + if (HasErrors) + return false; + ct.DataListTemplate.Remove(dbObj); + ct.SaveChanges(); + + //Delete sibling objects + + //Event log process delete + EventLogProcessor.DeleteObject(UserId, BizType, dbObj.Id, dbObj.Name, ct); + ct.SaveChanges(); + + //Delete search index + //Search.ProcessDeletedObjectKeywords(dbObj.Id, BizType); + + + return true; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //VALIDATION + // + + //Can save or update? + private void Validate(DataListTemplate 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 (ct.DataListTemplate.Any(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); + // List FieldList = null; + //if (!AyaFormFieldDefinitions.IsValidFormFieldDefinitionKey(inObj.ListKey)) + if (DataList == null) + { + AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "ListKey", $"ListKey \"{inObj.ListKey}\" DataListKey is not valid"); + } + // else + // { + // FieldList = AyaDataListFieldDefinition.AyaObjectFields(inObj.ListKey); + // } + + + if (inObj.ListKey.Length > 255) + AddError(ApiErrorCode.VALIDATION_LENGTH_EXCEEDED, "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(ApiErrorCode.VALIDATION_REQUIRED, "Filter", $"Filter array item {i}, object is missing required \"fld\" property "); + else + { + var fld = filterItem["fld"].Value(); + if (string.IsNullOrWhiteSpace(fld)) + AddError(ApiErrorCode.VALIDATION_REQUIRED, "Filter", $"Filter 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, "Filter", $"Filter array item {i}, fld property value \"{fld}\" is not a valid value for ListKey specified"); + } + else if (TheField.IsFilterable == false) + { + AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Filter", $"Filter array item {i}, fld property value \"{fld}\" is not filterable"); + } + + + + } + } + 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 (!DataListTemplateComparisonOperator.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 + { + 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, "Filter", "Filter 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; + } + + + //Can delete? + private void ValidateCanDelete(DataListTemplate inObj) + { + //Leaving this off for now + } + + + + // //////////////////////////////////////////////////////////////////////////////////////////////// + // //JOB / OPERATIONS + // // + // public async Task HandleJobAsync(OpsJob job) + // { + // //Hand off the particular job to the corresponding processing code + // //NOTE: If this code throws an exception the caller (JobsBiz::ProcessJobsAsync) will automatically set the job to failed and log the exeption so + // //basically any error condition during job processing should throw up an exception if it can't be handled + // switch (job.JobType) + // { + // case JobType.TestDataFilterJob: + // await ProcessTestJobAsync(job); + // break; + // default: + // throw new System.ArgumentOutOfRangeException($"DataFilterBiz.HandleJob-> Invalid job type{job.JobType.ToString()}"); + // } + // } + + + //Other job handlers here... + + + ///////////////////////////////////////////////////////////////////// + + }//eoc + + +}//eons +