This commit is contained in:
@@ -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: 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 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.
|
- 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
|
- Docs: pagingOptions, sort and filter need to be documented for API
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace AyaNova.Biz
|
|||||||
//remove dupes, substitute dashes for spaces, lowercase and shorten if exceed 255 chars
|
//remove dupes, substitute dashes for spaces, lowercase and shorten if exceed 255 chars
|
||||||
public static List<string> NormalizeTags(List<string> inTags)
|
public static List<string> NormalizeTags(List<string> inTags)
|
||||||
{
|
{
|
||||||
if (inTags==null || inTags.Count == 0) return inTags;
|
if (inTags == null || inTags.Count == 0) return inTags;
|
||||||
|
|
||||||
List<string> outTags = new List<string>();
|
List<string> outTags = new List<string>();
|
||||||
foreach (var tag in inTags)
|
foreach (var tag in inTags)
|
||||||
@@ -40,11 +40,35 @@ namespace AyaNova.Biz
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static void ProcessTagsIntoRepository(AyContext ct, List<string> addTags, List<string> removeTags)
|
|
||||||
|
public static void ProcessDeleteTagsInRepository(AyContext ct, List<string> deleteTags)
|
||||||
{
|
{
|
||||||
|
if (deleteTags.Count == 0) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ProcessUpdateTagsInRepository(AyContext ct, List<string> newTags, List<string> originalTags = null)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (newTags.Count == 0 && (originalTags == null || originalTags.Count == 0)) return;
|
||||||
|
|
||||||
|
List<string> deleteTags=new List<string>();
|
||||||
|
List<string> addTags=new List<string>();
|
||||||
|
|
||||||
|
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
|
//Add / increase reference count for added tags
|
||||||
//remove / decrease reference count for removed 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
|
//https://stackoverflow.com/questions/10233298/increment-a-value-in-postgres
|
||||||
/*
|
/*
|
||||||
ONE SHOT WAY WHICH IS BOSS!!
|
ONE SHOT WAY WHICH IS BOSS!!
|
||||||
@@ -63,27 +87,27 @@ WHERE name = 'bill'
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
//WAY I PROBABLY SHOULD USE AND GROK for inventory later:
|
//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
|
//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
|
//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.
|
//maybe wrap that in a method I can re-use.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//Create table tags, word varchar 255, refcount longint, concurrency token
|
//Create table tags, word varchar 255, refcount longint, concurrency token
|
||||||
//so do this:
|
//so do this:
|
||||||
|
|
||||||
//ADD / INCREMENT TAGS
|
//ADD / INCREMENT TAGS
|
||||||
|
|
||||||
//START: Get tag word and concurrency token and count
|
//START: Get tag word and concurrency token and count
|
||||||
//if not present, then add it with a count of 0
|
//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:
|
//If that fails due to others added it or works, either way go back to START:
|
||||||
//UPDATE: INCREMENT the refcount and update the record
|
//UPDATE: INCREMENT the refcount and update the record
|
||||||
//If that fails due to a concurrency exception go to START:
|
//If that fails due to a concurrency exception go to START:
|
||||||
|
|
||||||
|
|
||||||
//REMOVE / DECREMENT TAGS
|
//REMOVE / DECREMENT TAGS
|
||||||
|
|
||||||
|
|
||||||
//Iterate remove tags
|
//Iterate remove tags
|
||||||
@@ -92,7 +116,8 @@ WHERE name = 'bill'
|
|||||||
//Fetch the tag if it exists and update it's ref count
|
//Fetch the tag if it exists and update it's ref count
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ namespace AyaNova.Biz
|
|||||||
//route linked version for external api access
|
//route linked version for external api access
|
||||||
internal async Task<Widget> CreateAsync(Widget inObj)
|
internal async Task<Widget> CreateAsync(Widget inObj)
|
||||||
{
|
{
|
||||||
Validate(inObj, true);
|
Validate(inObj, null);
|
||||||
if (HasErrors)
|
if (HasErrors)
|
||||||
return null;
|
return null;
|
||||||
else
|
else
|
||||||
@@ -98,7 +98,7 @@ namespace AyaNova.Biz
|
|||||||
//Handle child and associated items:
|
//Handle child and associated items:
|
||||||
EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), ct);
|
EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), ct);
|
||||||
SearchIndex(outObj, true);
|
SearchIndex(outObj, true);
|
||||||
|
TagUtil.ProcessUpdateTagsInRepository(ct, outObj.Tags, null);
|
||||||
return outObj;
|
return outObj;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,7 +107,7 @@ namespace AyaNova.Biz
|
|||||||
//Internal version for seeding
|
//Internal version for seeding
|
||||||
internal Widget Create(AyContext TempContext, Widget inObj)
|
internal Widget Create(AyContext TempContext, Widget inObj)
|
||||||
{
|
{
|
||||||
Validate(inObj, true);
|
Validate(inObj, null);
|
||||||
if (HasErrors)
|
if (HasErrors)
|
||||||
return null;
|
return null;
|
||||||
else
|
else
|
||||||
@@ -125,6 +125,7 @@ namespace AyaNova.Biz
|
|||||||
//Handle child and associated items:
|
//Handle child and associated items:
|
||||||
EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), TempContext);
|
EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), TempContext);
|
||||||
SearchIndex(outObj, true);
|
SearchIndex(outObj, true);
|
||||||
|
TagUtil.ProcessUpdateTagsInRepository(TempContext, outObj.Tags, null);
|
||||||
|
|
||||||
return outObj;
|
return outObj;
|
||||||
}
|
}
|
||||||
@@ -143,6 +144,10 @@ namespace AyaNova.Biz
|
|||||||
if (inObj.OwnerId == 0)
|
if (inObj.OwnerId == 0)
|
||||||
inObj.OwnerId = dbObj.OwnerId;
|
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
|
//Replace the db object with the PUT object
|
||||||
CopyObject.Copy(inObj, dbObj, "Id,Serial");
|
CopyObject.Copy(inObj, dbObj, "Id,Serial");
|
||||||
|
|
||||||
@@ -153,13 +158,14 @@ namespace AyaNova.Biz
|
|||||||
ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken;
|
ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken;
|
||||||
|
|
||||||
|
|
||||||
Validate(dbObj, false);
|
Validate(dbObj, SnapshotOfOriginalDBObj);
|
||||||
if (HasErrors)
|
if (HasErrors)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
//Associated items
|
//Associated items
|
||||||
EventLogProcessor.LogEventToDatabase(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct);
|
EventLogProcessor.LogEventToDatabase(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct);
|
||||||
SearchIndex(dbObj, false);
|
SearchIndex(dbObj, false);
|
||||||
|
TagUtil.ProcessUpdateTagsInRepository(ct, dbObj.Tags, SnapshotOfOriginalDBObj.Tags);
|
||||||
|
|
||||||
return true;
|
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
|
//Note: Id, OwnerId and Serial are all checked for and disallowed in the validate code by default
|
||||||
if (!ValidateJsonPatch<Widget>.Validate(this, objectPatch)) return false;
|
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
|
//Do the patching
|
||||||
objectPatch.ApplyTo(dbObj);
|
objectPatch.ApplyTo(dbObj);
|
||||||
|
|
||||||
dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags);
|
dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags);
|
||||||
|
|
||||||
ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = concurrencyToken;
|
ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = concurrencyToken;
|
||||||
Validate(dbObj, false);
|
Validate(dbObj, SnapshotOfOriginalDBObj);
|
||||||
if (HasErrors)
|
if (HasErrors)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -185,6 +195,8 @@ namespace AyaNova.Biz
|
|||||||
EventLogProcessor.LogEventToDatabase(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct);
|
EventLogProcessor.LogEventToDatabase(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct);
|
||||||
SearchIndex(dbObj, false);
|
SearchIndex(dbObj, false);
|
||||||
|
|
||||||
|
TagUtil.ProcessUpdateTagsInRepository(ct, dbObj.Tags, SnapshotOfOriginalDBObj.Tags);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +231,7 @@ namespace AyaNova.Biz
|
|||||||
EventLogProcessor.DeleteObject(UserId, BizType, dbObj.Id, dbObj.Name, ct);
|
EventLogProcessor.DeleteObject(UserId, BizType, dbObj.Id, dbObj.Name, ct);
|
||||||
ct.SaveChanges();
|
ct.SaveChanges();
|
||||||
Search.ProcessDeletedObjectKeywords(dbObj.Id, BizType);
|
Search.ProcessDeletedObjectKeywords(dbObj.Id, BizType);
|
||||||
|
TagUtil.ProcessDeleteTagsInRepository(ct, dbObj.Tags);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,60 +339,62 @@ namespace AyaNova.Biz
|
|||||||
//
|
//
|
||||||
|
|
||||||
//Can save or update?
|
//Can save or update?
|
||||||
private void Validate(Widget inObj, bool isNew)
|
private void Validate(Widget proposedObj, Widget currentObj)
|
||||||
{
|
{
|
||||||
//run validation and biz rules
|
//run validation and biz rules
|
||||||
if (isNew)
|
bool isNew = currentObj == null;
|
||||||
{
|
|
||||||
//WARNING: this is not really the "current" object, it's been modified already by caller
|
|
||||||
|
|
||||||
// //NEW widgets must be active
|
// if (isNew)
|
||||||
// if (inObj.Active == null || ((bool)inObj.Active) == false)
|
// {
|
||||||
// {
|
// //WARNING: this is not really the "current" object, it's been modified already by caller
|
||||||
// AddError(ValidationErrorType.InvalidValue, "Active", "New widget must be active");
|
|
||||||
// }
|
// // //NEW widgets must be active
|
||||||
}
|
// // if (inObj.Active == null || ((bool)inObj.Active) == false)
|
||||||
|
// // {
|
||||||
|
// // AddError(ValidationErrorType.InvalidValue, "Active", "New widget must be active");
|
||||||
|
// // }
|
||||||
|
// }
|
||||||
|
|
||||||
//OwnerId required
|
//OwnerId required
|
||||||
if (!isNew)
|
if (!isNew)
|
||||||
{
|
{
|
||||||
if (inObj.OwnerId == 0)
|
if (proposedObj.OwnerId == 0)
|
||||||
AddError(ValidationErrorType.RequiredPropertyEmpty, "OwnerId");
|
AddError(ValidationErrorType.RequiredPropertyEmpty, "OwnerId");
|
||||||
}
|
}
|
||||||
|
|
||||||
//Name required
|
//Name required
|
||||||
if (string.IsNullOrWhiteSpace(inObj.Name))
|
if (string.IsNullOrWhiteSpace(proposedObj.Name))
|
||||||
AddError(ValidationErrorType.RequiredPropertyEmpty, "Name");
|
AddError(ValidationErrorType.RequiredPropertyEmpty, "Name");
|
||||||
|
|
||||||
//Name must be less than 255 characters
|
//Name must be less than 255 characters
|
||||||
if (inObj.Name.Length > 255)
|
if (proposedObj.Name.Length > 255)
|
||||||
AddError(ValidationErrorType.LengthExceeded, "Name", "255 max");
|
AddError(ValidationErrorType.LengthExceeded, "Name", "255 max");
|
||||||
|
|
||||||
//If name is otherwise OK, check that name is unique
|
//If name is otherwise OK, check that name is unique
|
||||||
if (!PropertyHasErrors("Name"))
|
if (!PropertyHasErrors("Name"))
|
||||||
{
|
{
|
||||||
//Use Any command is efficient way to check existance, it doesn't return the record, just a true or false
|
//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");
|
AddError(ValidationErrorType.NotUnique, "Name");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Start date AND end date must both be null or both contain values
|
//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");
|
AddError(ValidationErrorType.RequiredPropertyEmpty, "StartDate");
|
||||||
|
|
||||||
if (inObj.StartDate != null && inObj.EndDate == null)
|
if (proposedObj.StartDate != null && proposedObj.EndDate == null)
|
||||||
AddError(ValidationErrorType.RequiredPropertyEmpty, "EndDate");
|
AddError(ValidationErrorType.RequiredPropertyEmpty, "EndDate");
|
||||||
|
|
||||||
//Start date before end date
|
//Start date before end date
|
||||||
if (inObj.StartDate != null && inObj.EndDate != null)
|
if (proposedObj.StartDate != null && proposedObj.EndDate != null)
|
||||||
if (inObj.StartDate > inObj.EndDate)
|
if (proposedObj.StartDate > proposedObj.EndDate)
|
||||||
AddError(ValidationErrorType.StartDateMustComeBeforeEndDate, "StartDate");
|
AddError(ValidationErrorType.StartDateMustComeBeforeEndDate, "StartDate");
|
||||||
|
|
||||||
//Enum is valid value
|
//Enum is valid value
|
||||||
|
|
||||||
if (!inObj.Roles.IsValid())
|
if (!proposedObj.Roles.IsValid())
|
||||||
{
|
{
|
||||||
AddError(ValidationErrorType.InvalidValue, "Roles");
|
AddError(ValidationErrorType.InvalidValue, "Roles");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user