Files
raven/server/AyaNova/biz/WidgetBiz.cs
2020-01-15 20:31:23 +00:00

590 lines
24 KiB
C#

using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.JsonPatch;
using Newtonsoft.Json.Linq;
using EnumsNET;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Models;
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<bool> ExistsAsync(long id)
{
return await ct.Widget.AnyAsync(e => e.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// GET
///
///
//Get without logging
internal async Task<Widget> 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);
}
//Get one with logging
internal async Task<Widget> 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;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//route linked version for external api access
internal async Task<Widget> CreateAsync(Widget inObj)
{
Validate(inObj, null);
if (HasErrors)
return null;
else
{
//do stuff with widget
Widget outObj = inObj;
//Test get serial id visible id number from generator
outObj.Serial = ServerBootConfig.WIDGET_SERIAL.GetNext();
outObj.Tags = TagUtil.NormalizeTags(outObj.Tags);
outObj.CustomFields = JsonUtil.CompactJson(outObj.CustomFields);
await ct.Widget.AddAsync(outObj);
await ct.SaveChangesAsync();
//Handle child and associated items:
EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), ct);
SearchIndex(outObj, true);
TagUtil.ProcessUpdateTagsInRepository(ct, outObj.Tags, null);
return outObj;
}
}
//Internal version for seeding
internal Widget Create(AyContext TempContext, Widget inObj)
{
Validate(inObj, null);
if (HasErrors)
return null;
else
{
//do stuff with widget
Widget outObj = inObj;
//Test get serial id visible id number from generator
outObj.Serial = ServerBootConfig.WIDGET_SERIAL.GetNext();
outObj.Tags = TagUtil.NormalizeTags(outObj.Tags);
outObj.CustomFields = JsonUtil.CompactJson(outObj.CustomFields);
TempContext.Widget.Add(outObj);
TempContext.SaveChanges();
//Handle child and associated items:
EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), TempContext);
SearchIndex(outObj, true);
TagUtil.ProcessUpdateTagsInRepository(TempContext, outObj.Tags, null);
return outObj;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DUPLICATE
//
internal async Task<Widget> DuplicateAsync(Widget dbObj)
{
Widget outObj = new Widget();
CopyObject.Copy(dbObj, outObj);
outObj.Name = Util.StringUtil.NameUniquify(outObj.Name, 255);
outObj.Id = 0;
outObj.ConcurrencyToken = 0;
//Test get serial id visible id number from generator
outObj.Serial = ServerBootConfig.WIDGET_SERIAL.GetNext();
await ct.Widget.AddAsync(outObj);
await ct.SaveChangesAsync();
//Handle child and associated items:
EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), ct);
SearchIndex(outObj, true);
TagUtil.ProcessUpdateTagsInRepository(ct, outObj.Tags, null);
return outObj;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
//put
internal bool Put(Widget dbObj, Widget inObj)
{
//make a snapshot of the original for validation but update the original to preserve workflow
Widget SnapshotOfOriginalDBObj = new Widget();
CopyObject.Copy(dbObj, SnapshotOfOriginalDBObj);
//Replace the db object with the PUT object
CopyObject.Copy(inObj, dbObj, "Id,Serial");
dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags);
dbObj.CustomFields = JsonUtil.CompactJson(dbObj.CustomFields);
//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, SnapshotOfOriginalDBObj);
if (HasErrors)
return false;
//Associated items
EventLogProcessor.LogEventToDatabase(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct);
SearchIndex(dbObj, false);
TagUtil.ProcessUpdateTagsInRepository(ct, dbObj.Tags, SnapshotOfOriginalDBObj.Tags);
return true;
}
//patch
internal bool Patch(Widget dbObj, JsonPatchDocument<Widget> objectPatch, uint concurrencyToken)
{
//Validate Patch is allowed
//Note: Id and Serial are all checked for and disallowed in the validate code by default
if (!ValidateJsonPatch<Widget>.Validate(this, objectPatch)) return false;
//make a snapshot of the original for validation but update the original to preserve workflow
Widget SnapshotOfOriginalDBObj = new Widget();
CopyObject.Copy(dbObj, SnapshotOfOriginalDBObj);
//Do the patching
objectPatch.ApplyTo(dbObj);
dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags);
dbObj.CustomFields = JsonUtil.CompactJson(dbObj.CustomFields);
ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = concurrencyToken;
Validate(dbObj, SnapshotOfOriginalDBObj);
if (HasErrors)
return false;
//Associated items
EventLogProcessor.LogEventToDatabase(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct);
SearchIndex(dbObj, false);
TagUtil.ProcessUpdateTagsInRepository(ct, dbObj.Tags, SnapshotOfOriginalDBObj.Tags);
return true;
}
private void SearchIndex(Widget obj, bool isNew)
{
//SEARCH INDEXING
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, obj.Id, BizType, obj.Name);
SearchParams.AddText(obj.Notes).AddText(obj.Name).AddText(obj.Serial).AddText(obj.Tags).AddCustomFields(obj.CustomFields);
if (isNew)
Search.ProcessNewObjectKeywords(SearchParams);
else
Search.ProcessUpdatedObjectKeywords(SearchParams);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//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();
//Associated items
EventLogProcessor.DeleteObject(UserId, BizType, dbObj.Id, dbObj.Name, ct);
ct.SaveChanges();
Search.ProcessDeletedObjectKeywords(dbObj.Id, BizType);
TagUtil.ProcessDeleteTagsInRepository(ct, dbObj.Tags);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
// LISTS
//
//get many (paged)
internal async Task<ApiPagedResponse> GetList(IUrlHelper Url, string routeName, ListOptions listOptions)
{
listOptions.Offset = listOptions.Offset ?? ListOptions.DefaultOffset;
listOptions.Limit = listOptions.Limit ?? ListOptions.DefaultLimit;
//BUILD THE QUERY
//base query
var q = "SELECT *, xmin FROM AWIDGET ";
//GET THE FILTER / SORT
if (listOptions.DataFilterId > 0)
{
var TheFilter = await ct.DataFilter.FirstOrDefaultAsync(x => x.Id == listOptions.DataFilterId);
//BUILD WHERE AND APPEND IT
q = q + FilterSqlCriteriaBuilder.DataFilterToSQLCriteria(TheFilter, ObjectFields.ObjectFieldsList(ObjectFields.WIDGET_KEY), 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
//GET THE FULL LIST OF ITEMS
var items = await ct.Widget
.FromSqlRaw(q)
.AsNoTracking()
.Skip(listOptions.Offset.Value)
.Take(listOptions.Limit.Value)
.ToArrayAsync();
//GET THE RECORD COUNT
var totalRecordCount = await ct.Widget
.FromSqlRaw(q)
.AsNoTracking()
.CountAsync();
#pragma warning restore EF1000
//BUILD THE PAGING LINKS PORTION
var pageLinks = new PaginationLinkBuilder(Url, routeName, null, listOptions, totalRecordCount).PagingLinksObject();
//BUILD THE RETURN BASED ON TEMPLATE and MINI CONDITIONAL FORMAT
//TODO: Get template (MOCKED FOR NOW UNTIL PROOF OF CONCEPT)
var MOCK_WIDGET_DISPLAY_TEMPLATE_JSON = @"
{
""full"":[""WidgetName"",""WidgetSerial"",""WidgetDollarAmount"",""WidgetRoles"",""WidgetStartDate"",""Active""],
""mini"":[""WidgetName"",""WidgetSerial""]
}
";
//TODO: BUILD THE COLUMNS RETURN PROPERTY JSON FRAGMENT
//for MINI format we just use a static column definition built in to this list object
//for FULL we build it from the template automatically
//eg: columns:{[ {name:"lt_client_name",datatype:text,ayatype:client},{name:"lt_client_notes",datatype:text},{name:"lt_last_workorder",datatype:number,ayatype:workorder}]}
//if FULL Pass the template to the ObjectFields which will build the return JSON for here and will also ensure the fields are correct, if any are unknown it will just ignore them but maybe log it?
string ColumnsJSON = string.Empty;
if (listOptions.Mini)
{
//all mini lists will have an id so include the type to open
ColumnsJSON = $"[ {{\"cm\":\"Widget\",\"dt\":{(int)AyaDataType.Text},\"ay\":{(int)AyaType.Widget}}}]";
//"[{\"cm\":\"WidgetName\",\"dt\":4},{\"cm\":\"WidgetSerial\",\"dt\":5},{\"cm\":\"WidgetDollarAmount\",\"dt\":8},{\"cm\":\"WidgetRoles\",\"dt\":10},{\"cm\":\"WidgetStartDate\",\"dt\":1},{\"cm\":\"Active\",\"dt\":6}]"
}
else
{
//TODO: need at least one column to be the openable object here or else nothing will open
ColumnsJSON = ObjectFields.GenerateListColumnJSONFromTemplate(ObjectFields.WIDGET_KEY, MOCK_WIDGET_DISPLAY_TEMPLATE_JSON);
}
//TODO: BUILD THE RETURN LIST OF DATA ITEMS
//If mini format all desired columns in order into the single mini return display (and set the only other return field which is ID)
//If wide then format the fields in oder chosen (grid sort and filter template has already done that part above)
//TODO: Genericize the above block of building return when it's working as this code needs to be central and optimized as much as possible
//New return code
// dynamic ret = new JObject();
// ret.items = new JArray.fromobj(items);
// ret.pageLinks = pageLinks;
// ret.columns=ColumnsJSON;
// return ret.ToString();
ApiPagedResponse pr = new ApiPagedResponse(items, pageLinks, ColumnsJSON);
return pr;
}
// //get many (paged)
// internal async Task<ApiPagedResponse<Widget>> GetManyAsync(IUrlHelper Url, string routeName, ListOptions pagingOptions)
// {
// pagingOptions.Offset = pagingOptions.Offset ?? ListOptions.DefaultOffset;
// pagingOptions.Limit = pagingOptions.Limit ?? ListOptions.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
// .FromSqlRaw(q)
// .AsNoTracking()
// .Skip(pagingOptions.Offset.Value)
// .Take(pagingOptions.Limit.Value)
// .ToArrayAsync();
// var totalRecordCount = await ct.Widget
// .FromSqlRaw(q)
// .AsNoTracking()
// .CountAsync();
// #pragma warning restore EF1000
// var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, totalRecordCount).PagingLinksObject();
// ApiPagedResponse<Widget> pr = new ApiPagedResponse<Widget>(items, pageLinks);
// return pr;
// }
// /// <summary>
// /// Get PickList
// /// </summary>
// /// <param name="Url"></param>
// /// <param name="routeName"></param>
// /// <param name="pagingOptions"></param>
// /// <returns></returns>
// internal ApiPagedResponse<NameIdItem> GetPickList(IUrlHelper Url, string routeName, ListOptions pagingOptions)
// {
// pagingOptions.Offset = pagingOptions.Offset ?? ListOptions.DefaultOffset;
// pagingOptions.Limit = pagingOptions.Limit ?? ListOptions.DefaultLimit;
// var ret = PickListFetcher.GetPickList(ct, UserId, pagingOptions, FilterOptions(), "awidget");
// var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, ret.TotalRecordCount).PagingLinksObject();
// ApiPagedResponse<NameIdItem> pr = new ApiPagedResponse<NameIdItem>(ret.Items, pageLinks);
// return pr;
// }
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private void Validate(Widget proposedObj, Widget currentObj)
{
//NOTE: In DB schema only name and serial are not nullable
//run validation and biz rules
bool isNew = currentObj == null;
// if (isNew)
// {
// //WARNING: this is not really the "current" object, it's been modified already by caller
// // //NEW widgets must be active
// // if (inObj.Active == null || ((bool)inObj.Active) == false)
// // {
// // AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Active", "New widget must be active");
// // }
// }
//Name required
if (string.IsNullOrWhiteSpace(proposedObj.Name))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name");
//Name must be less than 255 characters
if (proposedObj.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.Widget.Any(m => m.Name == proposedObj.Name && m.Id != proposedObj.Id))
{
AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name");
}
}
//Start date AND end date must both be null or both contain values
if (proposedObj.StartDate == null && proposedObj.EndDate != null)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "StartDate");
if (proposedObj.StartDate != null && proposedObj.EndDate == null)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "EndDate");
//Start date before end date
if (proposedObj.StartDate != null && proposedObj.EndDate != null)
if (proposedObj.StartDate > proposedObj.EndDate)
AddError(ApiErrorCode.VALIDATION_STARTDATE_AFTER_ENDDATE, "StartDate");
//Enum is valid value
//Note: because of the way the flags work as powers of 2 and with the 1 being the first flag value, basically any value up to the "All" value will be valid
//because you can make any number from 0 to all using any combination of the flags so the only thing that will fail is less than zero or greater than All
if (!proposedObj.Roles.IsValid())
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Roles");
}
//Any form customizations to validate?
var FormCustomization = ct.FormCustom.SingleOrDefault(x => x.FormKey == ObjectFields.WIDGET_KEY);
if (FormCustomization != null)
{
//Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required
//validate users choices for required non custom fields
RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);
//validate custom fields
CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
//for debug purposes
#if (DEBUG)
//TESTING
//make a fake server error for ui testing purposes
// if (proposedObj.Count == 666)
// {
// AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Count", "Test field server error");
// }
//removed because seeding huge kept triggering this
// if (proposedObj.DollarAmount == 666.66M)
// {
// AddError(ApiErrorCode.INVALID_OPERATION, null, "This is a test of a general server error");
// }
//TESTING
#endif
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()}");
}
}
/// <summary>
/// /// Handle the test job
/// </summary>
/// <param name="job"></param>
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