using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.JsonPatch; using EnumsNET; using AyaNova.Util; using AyaNova.Api.ControllerHelpers; using AyaNova.Biz; using AyaNova.Models; using System.Collections.Generic; using Microsoft.Extensions.Logging; namespace AyaNova.Biz { internal class WidgetBiz : BizObject, IJobObject { internal WidgetBiz(AyContext dbcontext, long currentUserId, long userLocaleId, AuthorizationRoles UserRoles) { ct = dbcontext; UserId = currentUserId; UserLocaleId = userLocaleId; CurrentUserRoles = UserRoles; BizType = AyaType.Widget; } internal static WidgetBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext) { return new WidgetBiz(ct, UserIdFromContext.Id(httpContext.Items), UserLocaleIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items)); } //Version for internal use internal static WidgetBiz GetBizInternal(AyContext ct) { return new WidgetBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID, AuthorizationRoles.BizAdminFull); } //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task ExistsAsync(long id) { return await ct.Widget.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.Widget.SingleOrDefaultAsync(m => m.Id == fetchId); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE internal async Task CreateAsync(Widget inObj) { Validate(inObj, true); if (HasErrors) return null; else { //do stuff with widget Widget outObj = inObj; outObj.OwnerId = UserId; //Test get serial id visible id number from generator outObj.Serial = ServerBootConfig.WIDGET_SERIAL.GetNext(); outObj.Tags = TagUtil.NormalizeTags(outObj.Tags); await ct.Widget.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 var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, outObj.Id, BizType, outObj.Name); SearchParams.AddWord(outObj.Notes).AddWord(outObj.Name).AddWord(outObj.Serial).AddWord(outObj.Tags); Search.ProcessNewObjectKeywords(SearchParams); return outObj; } } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE internal Widget Create(AyContext TempContext, Widget inObj) { Validate(inObj, true); if (HasErrors) return null; else { //do stuff with widget Widget outObj = inObj; outObj.OwnerId = UserId; //Test get serial id visible id number from generator outObj.Serial = ServerBootConfig.WIDGET_SERIAL.GetNext(); outObj.Tags = TagUtil.NormalizeTags(outObj.Tags); TempContext.Widget.Add(outObj); TempContext.SaveChanges(); //Handle child and associated items: //EVENT LOG EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), TempContext); //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, outObj.Id, BizType, outObj.Name); SearchParams.AddWord(outObj.Notes).AddWord(outObj.Name).AddWord(outObj.Serial).AddWord(outObj.Tags); Search.ProcessNewObjectKeywords(SearchParams); 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.Widget.SingleOrDefaultAsync(m => m.Id == fetchId); if (ret != null) { //Log EventLogProcessor.LogEventToDatabase(new Event(UserId, fetchId, BizType, AyaEvent.Retrieved), ct); } return ret; } public static FilterOptions FilterOptions(long localizeToLocaleId = 0) { //NOTE: All column names are lowercase to conform with Postgres AyaNova DB which uses lowercase for all identifiers //Also all list keys are lower case for consistency FilterOptions f = new FilterOptions("widget"); f. AddField("id", "ID", AyDataType.Integer). AddField("name", "WidgetName", AyDataType.Text). AddField("serial", "WidgetSerial", AyDataType.Integer). AddField("notes", "WidgetNotes", AyDataType.Text). AddField("dollaramount", "WidgetDollarAmount", AyDataType.Decimal). AddField("active", "CommonActive", AyDataType.Bool). AddField("startdate", "WidgetStartDate", AyDataType.Date). AddField("count", "WidgetCount", AyDataType.Integer). AddField("tags", "Tags", AyDataType.Tags). AddField("enddate", "WidgetEndDate", AyDataType.Date); if (localizeToLocaleId != 0) f.Localize(localizeToLocaleId); return f; } //get many (paged) internal async Task> GetManyAsync(IUrlHelper Url, string routeName, PagingOptions pagingOptions) { pagingOptions.Offset = pagingOptions.Offset ?? PagingOptions.DefaultOffset; pagingOptions.Limit = pagingOptions.Limit ?? PagingOptions.DefaultLimit; //BUILD THE QUERY //base query var q = "SELECT *, xmin FROM AWIDGET "; //GET THE FILTER / SORT if (pagingOptions.DataFilterId > 0) { var TheFilter = await ct.DataFilter.FirstOrDefaultAsync(x => x.Id == pagingOptions.DataFilterId); //BUILD WHERE AND APPEND IT q = q + FilterSqlCriteriaBuilder.DataFilterToSQLCriteria(TheFilter, WidgetBiz.FilterOptions(), UserId); //BUILD ORDER BY AND APPEND IT q = q + FilterSqlOrderByBuilder.DataFilterToSQLOrderBy(TheFilter); } else { //GET DEFAULT ORDER BY q = q + FilterSqlOrderByBuilder.DefaultGetManyOrderBy(); } #pragma warning disable EF1000 var items = await ct.Widget .AsNoTracking() .FromSql(q) .Skip(pagingOptions.Offset.Value) .Take(pagingOptions.Limit.Value) .ToArrayAsync(); var totalRecordCount = await ct.Widget .AsNoTracking() .FromSql(q) .CountAsync(); #pragma warning restore EF1000 var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, totalRecordCount).PagingLinksObject(); ApiPagedResponse pr = new ApiPagedResponse(items, pageLinks); return pr; } /// /// Get PickList /// /// /// /// /// internal ApiPagedResponse GetPickList(IUrlHelper Url, string routeName, PagingOptions pagingOptions) { pagingOptions.Offset = pagingOptions.Offset ?? PagingOptions.DefaultOffset; pagingOptions.Limit = pagingOptions.Limit ?? PagingOptions.DefaultLimit; var ret = PickListFetcher.GetPickList(ct, UserId, pagingOptions, "awidget"); var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, ret.TotalRecordCount).PagingLinksObject(); ApiPagedResponse pr = new ApiPagedResponse(ret.Items, pageLinks); return pr; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // //put internal bool Put(Widget dbObj, Widget inObj) { //preserve the owner ID if none was specified if (inObj.OwnerId == 0) inObj.OwnerId = dbObj.OwnerId; //Replace the db object with the PUT object CopyObject.Copy(inObj, dbObj, "Id,Serial"); dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags); //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.Notes, dbObj.Name, dbObj.Serial.ToString()); var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, dbObj.Id, BizType, dbObj.Name); SearchParams.AddWord(dbObj.Notes).AddWord(dbObj.Name).AddWord(dbObj.Serial).AddWord(dbObj.Tags); Search.ProcessNewObjectKeywords(SearchParams); return true; } //patch internal bool Patch(Widget dbObj, JsonPatchDocument objectPatch, uint concurrencyToken) { //Validate Patch is allowed //Note: Id, OwnerId and Serial are all checked for and disallowed in the validate code by default if (!ValidateJsonPatch.Validate(this, objectPatch)) return false; //Do the patching objectPatch.ApplyTo(dbObj); dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags); ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = 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.Notes, dbObj.Name, dbObj.Serial.ToString()); var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, dbObj.Id, BizType, dbObj.Name); SearchParams.AddWord(dbObj.Notes).AddWord(dbObj.Name).AddWord(dbObj.Serial).AddWord(dbObj.Tags); Search.ProcessNewObjectKeywords(SearchParams); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal bool Delete(Widget 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.Widget.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); // //TAGS // TagMapBiz.DeleteAllForObject(new AyaTypeId(BizType, dbObj.Id), ct); // ct.SaveChanges(); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // //Can save or update? private void Validate(Widget inObj, bool isNew) { //run validation and biz rules if (isNew) { // //NEW widgets must be active // if (inObj.Active == null || ((bool)inObj.Active) == false) // { // AddError(ValidationErrorType.InvalidValue, "Active", "New widget must be active"); // } } //OwnerId required if (!isNew) { if (inObj.OwnerId == 0) AddError(ValidationErrorType.RequiredPropertyEmpty, "OwnerId"); } //Name required if (string.IsNullOrWhiteSpace(inObj.Name)) AddError(ValidationErrorType.RequiredPropertyEmpty, "Name"); //Name must be less than 255 characters if (inObj.Name.Length > 255) AddError(ValidationErrorType.LengthExceeded, "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.Widget.Any(m => m.Name == inObj.Name && m.Id != inObj.Id)) { AddError(ValidationErrorType.NotUnique, "Name"); } } //Start date AND end date must both be null or both contain values if (inObj.StartDate == null && inObj.EndDate != null) AddError(ValidationErrorType.RequiredPropertyEmpty, "StartDate"); if (inObj.StartDate != null && inObj.EndDate == null) AddError(ValidationErrorType.RequiredPropertyEmpty, "EndDate"); //Start date before end date if (inObj.StartDate != null && inObj.EndDate != null) if (inObj.StartDate > inObj.EndDate) AddError(ValidationErrorType.StartDateMustComeBeforeEndDate, "StartDate"); //Enum is valid value if (!inObj.Roles.IsValid()) { AddError(ValidationErrorType.InvalidValue, "Roles"); } return; } //Can delete? private void ValidateCanDelete(Widget inObj) { //whatever needs to be check to delete this object } //////////////////////////////////////////////////////////////////////////////////////////////// //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.TestWidgetJob: await ProcessTestJobAsync(job); break; default: throw new System.ArgumentOutOfRangeException($"WidgetBiz.HandleJob-> Invalid job type{job.JobType.ToString()}"); } } /// /// /// Handle the test job /// /// private async Task ProcessTestJobAsync(OpsJob job) { var sleepTime = 30 * 1000; //Simulate a long running job here JobsBiz.UpdateJobStatus(job.GId, JobStatus.Running, ct); JobsBiz.LogJob(job.GId, $"WidgetBiz::ProcessTestJob started, sleeping for {sleepTime} seconds...", ct); //Uncomment this to test if the job prevents other routes from running //result is NO it doesn't prevent other requests, so we are a-ok for now await Task.Delay(sleepTime); JobsBiz.LogJob(job.GId, "WidgetBiz::ProcessTestJob done sleeping setting job to finished", ct); JobsBiz.UpdateJobStatus(job.GId, JobStatus.Completed, ct); } //Other job handlers here... ///////////////////////////////////////////////////////////////////// }//eoc }//eons