diff --git a/devdocs/todo.txt b/devdocs/todo.txt index c29bd1aa..c9b55387 100644 --- a/devdocs/todo.txt +++ b/devdocs/todo.txt @@ -89,6 +89,7 @@ TODO SERVER STUFF - TAG ROUTE: LIST route to fetch tags in Alphabetical order by name based on starts with (or maybe contains) used for quickly autofilling a list at UI level - TAG ROUTE: CLOUD route to fetch tags in order of refcount decreasing + - TAG repo tests: ensure updates delete and add properly, ensure new adds properly, ensure lists work properly - Boot server and seed with debug log turned on, see what is being tracked by EF that doesn't need to, seems some of that shit is being tracked. - Docs: pagingOptions, sort and filter need to be documented for API diff --git a/server/AyaNova/biz/TagUtil.cs b/server/AyaNova/biz/TagUtil.cs index 6aa61986..9521dab1 100644 --- a/server/AyaNova/biz/TagUtil.cs +++ b/server/AyaNova/biz/TagUtil.cs @@ -12,7 +12,7 @@ namespace AyaNova.Biz //remove dupes, substitute dashes for spaces, lowercase and shorten if exceed 255 chars public static List NormalizeTags(List inTags) { - if (inTags==null || inTags.Count == 0) return inTags; + if (inTags == null || inTags.Count == 0) return inTags; List outTags = new List(); foreach (var tag in inTags) @@ -40,11 +40,35 @@ namespace AyaNova.Biz } - public static void ProcessTagsIntoRepository(AyContext ct, List addTags, List removeTags) + + public static void ProcessDeleteTagsInRepository(AyContext ct, List deleteTags) { + if (deleteTags.Count == 0) return; + } + + public static void ProcessUpdateTagsInRepository(AyContext ct, List newTags, List originalTags = null) + { + + if (newTags.Count == 0 && (originalTags == null || originalTags.Count == 0)) return; + +List deleteTags=new List(); +List addTags=new List(); + + if(originalTags!=null){ + //Update + //This logic is supposed to only come up with CHANGES, if the item is in both lists then it should disappear and not need to be dealt with + //testing will validate it + deleteTags=originalTags.Except(newTags).ToList(); + addTags=newTags.Except(originalTags).ToList(); + HERE + + }else{ + //Add + } + //Add / increase reference count for added tags //remove / decrease reference count for removed tags - // var v=ct.Event.Any(x=>x.Textra=="word"); + // var v=ct.Event.Any(x=>x.Textra=="word"); //https://stackoverflow.com/questions/10233298/increment-a-value-in-postgres /* ONE SHOT WAY WHICH IS BOSS!! @@ -63,27 +87,27 @@ WHERE name = 'bill' */ -//WAY I PROBABLY SHOULD USE AND GROK for inventory later: -//https://stackoverflow.com/questions/14718929/best-practice-to-lock-a-record-for-editing-while-using-entity-framework + //WAY I PROBABLY SHOULD USE AND GROK for inventory later: + //https://stackoverflow.com/questions/14718929/best-practice-to-lock-a-record-for-editing-while-using-entity-framework -//Catch the concurrency exception, refetch and try again a certain number of times maximum until it's resolved -//maybe wrap that in a method I can re-use. + //Catch the concurrency exception, refetch and try again a certain number of times maximum until it's resolved + //maybe wrap that in a method I can re-use. -//Create table tags, word varchar 255, refcount longint, concurrency token -//so do this: + //Create table tags, word varchar 255, refcount longint, concurrency token + //so do this: -//ADD / INCREMENT TAGS + //ADD / INCREMENT TAGS -//START: Get tag word and concurrency token and count -//if not present, then add it with a count of 0 - //If that fails due to others added it or works, either way go back to START: -//UPDATE: INCREMENT the refcount and update the record - //If that fails due to a concurrency exception go to START: + //START: Get tag word and concurrency token and count + //if not present, then add it with a count of 0 + //If that fails due to others added it or works, either way go back to START: + //UPDATE: INCREMENT the refcount and update the record + //If that fails due to a concurrency exception go to START: -//REMOVE / DECREMENT TAGS + //REMOVE / DECREMENT TAGS //Iterate remove tags @@ -92,7 +116,8 @@ WHERE name = 'bill' //Fetch the tag if it exists and update it's ref count - + + } diff --git a/server/AyaNova/biz/WidgetBiz.cs b/server/AyaNova/biz/WidgetBiz.cs index f71c168d..f093b171 100644 --- a/server/AyaNova/biz/WidgetBiz.cs +++ b/server/AyaNova/biz/WidgetBiz.cs @@ -78,7 +78,7 @@ namespace AyaNova.Biz //route linked version for external api access internal async Task CreateAsync(Widget inObj) { - Validate(inObj, true); + Validate(inObj, null); if (HasErrors) return null; else @@ -98,7 +98,7 @@ namespace AyaNova.Biz //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; } } @@ -107,7 +107,7 @@ namespace AyaNova.Biz //Internal version for seeding internal Widget Create(AyContext TempContext, Widget inObj) { - Validate(inObj, true); + Validate(inObj, null); if (HasErrors) return null; else @@ -125,6 +125,7 @@ namespace AyaNova.Biz //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; } @@ -143,6 +144,10 @@ namespace AyaNova.Biz if (inObj.OwnerId == 0) inObj.OwnerId = dbObj.OwnerId; + //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"); @@ -153,13 +158,14 @@ namespace AyaNova.Biz ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken; - Validate(dbObj, false); + 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; } @@ -171,13 +177,17 @@ namespace AyaNova.Biz //Note: Id, OwnerId and Serial are all checked for and disallowed in the validate code by default if (!ValidateJsonPatch.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); ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = concurrencyToken; - Validate(dbObj, false); + Validate(dbObj, SnapshotOfOriginalDBObj); if (HasErrors) return false; @@ -185,6 +195,8 @@ namespace AyaNova.Biz EventLogProcessor.LogEventToDatabase(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct); SearchIndex(dbObj, false); + TagUtil.ProcessUpdateTagsInRepository(ct, dbObj.Tags, SnapshotOfOriginalDBObj.Tags); + return true; } @@ -219,7 +231,7 @@ namespace AyaNova.Biz EventLogProcessor.DeleteObject(UserId, BizType, dbObj.Id, dbObj.Name, ct); ct.SaveChanges(); Search.ProcessDeletedObjectKeywords(dbObj.Id, BizType); - + TagUtil.ProcessDeleteTagsInRepository(ct, dbObj.Tags); return true; } @@ -327,60 +339,62 @@ namespace AyaNova.Biz // //Can save or update? - private void Validate(Widget inObj, bool isNew) + private void Validate(Widget proposedObj, Widget currentObj) { //run validation and biz rules - if (isNew) - { - //WARNING: this is not really the "current" object, it's been modified already by caller + bool isNew = currentObj == null; - // //NEW widgets must be active - // if (inObj.Active == null || ((bool)inObj.Active) == false) - // { - // AddError(ValidationErrorType.InvalidValue, "Active", "New widget must be active"); - // } - } + // 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(ValidationErrorType.InvalidValue, "Active", "New widget must be active"); + // // } + // } //OwnerId required if (!isNew) { - if (inObj.OwnerId == 0) + if (proposedObj.OwnerId == 0) AddError(ValidationErrorType.RequiredPropertyEmpty, "OwnerId"); } //Name required - if (string.IsNullOrWhiteSpace(inObj.Name)) + if (string.IsNullOrWhiteSpace(proposedObj.Name)) AddError(ValidationErrorType.RequiredPropertyEmpty, "Name"); //Name must be less than 255 characters - if (inObj.Name.Length > 255) + if (proposedObj.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)) + if (ct.Widget.Any(m => m.Name == proposedObj.Name && m.Id != proposedObj.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) + if (proposedObj.StartDate == null && proposedObj.EndDate != null) AddError(ValidationErrorType.RequiredPropertyEmpty, "StartDate"); - if (inObj.StartDate != null && inObj.EndDate == null) + if (proposedObj.StartDate != null && proposedObj.EndDate == null) AddError(ValidationErrorType.RequiredPropertyEmpty, "EndDate"); //Start date before end date - if (inObj.StartDate != null && inObj.EndDate != null) - if (inObj.StartDate > inObj.EndDate) + if (proposedObj.StartDate != null && proposedObj.EndDate != null) + if (proposedObj.StartDate > proposedObj.EndDate) AddError(ValidationErrorType.StartDateMustComeBeforeEndDate, "StartDate"); //Enum is valid value - if (!inObj.Roles.IsValid()) + if (!proposedObj.Roles.IsValid()) { AddError(ValidationErrorType.InvalidValue, "Roles"); }