From dd6d8f8b76c0bb8af5d5638d720db52e24b7265e Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Tue, 8 Dec 2020 23:50:54 +0000 Subject: [PATCH] --- docs/8.0/ayanova/docs/api-error-codes.md | 3 +- server/AyaNova/biz/ApiErrorCode.cs | 3 +- .../AyaNova/biz/ApiErrorCodeStockMessage.cs | 2 + server/AyaNova/biz/BizObject.cs | 10 +- server/AyaNova/biz/CustomerBiz.cs | 38 ++++++- server/AyaNova/biz/CustomerNoteBiz.cs | 60 ++++++----- server/AyaNova/biz/UserBiz.cs | 102 +++++++++--------- server/AyaNova/resource/de.json | 1 + server/AyaNova/resource/en.json | 1 + server/AyaNova/resource/es.json | 1 + server/AyaNova/resource/fr.json | 1 + 11 files changed, 139 insertions(+), 83 deletions(-) diff --git a/docs/8.0/ayanova/docs/api-error-codes.md b/docs/8.0/ayanova/docs/api-error-codes.md index b8a99154..29ac14ac 100644 --- a/docs/8.0/ayanova/docs/api-error-codes.md +++ b/docs/8.0/ayanova/docs/api-error-codes.md @@ -29,4 +29,5 @@ Here are all the API level error codes that can be returned by the API server: | 2206 | Validation error - A text property is required to be unique but an existing record with an identical value was found in the database | | 2207 | Validation error - The start date must be earlier than the end date | | 2208 | Validation error - Modifying the object (usually a delete) would break the link to other records in the database and operation was disallowed to preserve data integrity | -| 2209 | Validation error - Indicates the attempted property change is invalid because the value is fixed and cannot be changed | \ No newline at end of file +| 2209 | Validation error - Indicates the attempted property change is invalid because the value is fixed and cannot be changed | +| 2210 | Child object error - Indicates the attempted operation resulted in errors in linked child object | \ No newline at end of file diff --git a/server/AyaNova/biz/ApiErrorCode.cs b/server/AyaNova/biz/ApiErrorCode.cs index 99230e21..433c6ac3 100644 --- a/server/AyaNova/biz/ApiErrorCode.cs +++ b/server/AyaNova/biz/ApiErrorCode.cs @@ -27,7 +27,8 @@ namespace AyaNova.Biz VALIDATION_NOT_UNIQUE = 2206, VALIDATION_STARTDATE_AFTER_ENDDATE = 2207, VALIDATION_REFERENTIAL_INTEGRITY = 2208, - VALIDATION_NOT_CHANGEABLE = 2209 + VALIDATION_NOT_CHANGEABLE = 2209, + CHILD_OBJECT_ERROR = 2210 /* | 2000 | API closed - Server is running but access to the API has been closed to all users | diff --git a/server/AyaNova/biz/ApiErrorCodeStockMessage.cs b/server/AyaNova/biz/ApiErrorCodeStockMessage.cs index b4fed10a..5d420119 100644 --- a/server/AyaNova/biz/ApiErrorCodeStockMessage.cs +++ b/server/AyaNova/biz/ApiErrorCodeStockMessage.cs @@ -54,6 +54,8 @@ namespace AyaNova.Biz return "Modifying the object (usually a delete) would break the link to other records in the database and operation was disallowed to preserve data integrity"; case ApiErrorCode.VALIDATION_NOT_CHANGEABLE: return "the value is fixed and cannot be changed"; + case ApiErrorCode.CHILD_OBJECT_ERROR: + return "Errors in child object during operation"; diff --git a/server/AyaNova/biz/BizObject.cs b/server/AyaNova/biz/BizObject.cs index 698e5a08..1d33a929 100644 --- a/server/AyaNova/biz/BizObject.cs +++ b/server/AyaNova/biz/BizObject.cs @@ -13,7 +13,7 @@ namespace AyaNova.Biz public BizObject() { - + } @@ -27,7 +27,7 @@ namespace AyaNova.Biz internal long UserId { get; set; } internal long UserTranslationId { get; set; } internal AuthorizationRoles CurrentUserRoles { get; set; } - + #endregion @@ -62,6 +62,12 @@ namespace AyaNova.Biz _errors.Add(new ValidationError() { Code = errorCode, Message = errorMessage, Target = propertyName }); } + // //Add a bunch of errors, generally from a child object failed operastion + // public void AddErrors(List errors) + // { + // _errors.AddRange(errors); + // } + public string GetErrorsAsString() { if (!HasErrors) return string.Empty; diff --git a/server/AyaNova/biz/CustomerBiz.cs b/server/AyaNova/biz/CustomerBiz.cs index dd80af18..2044a030 100644 --- a/server/AyaNova/biz/CustomerBiz.cs +++ b/server/AyaNova/biz/CustomerBiz.cs @@ -151,7 +151,8 @@ namespace AyaNova.Biz try { Customer dbObject = await ct.Customer.SingleOrDefaultAsync(z => z.Id == id); - if (dbObject == null){ + if (dbObject == null) + { AddError(ApiErrorCode.NOT_FOUND); return false; } @@ -159,10 +160,38 @@ namespace AyaNova.Biz if (HasErrors) return false; - //todo: delete contacts and customer notes - //see workorder* collections and delete for transactional inclusion etc + + //DELETE DIRECT CHILD OBJECTS + //(note: the convention is to allow deletion of children created *in* the same UI area so this will delete contacts, customer notes, but not workorders of this customer for example) + //Will need to possibly display refintegrity error from the *linked* object delete attemp, i.e. Contact for here //so should test that out so that it flows to the UI so user understands they can't delete the Customer due to a Contact having links to other objects + { + var ContactIds = await ct.User.AsNoTracking().Where(z => z.CustomerId == id).Select(z => z.Id).ToListAsync(); + if (ContactIds.Count() > 0) + { + UserBiz b = new UserBiz(ct, UserId, UserTranslationId, CurrentUserRoles); + foreach (long ItemId in ContactIds) + if (!await b.DeleteAsync(ItemId, transaction)) + { + AddError(ApiErrorCode.VALIDATION_REQUIRED,b.GetErrorsAsString()); + return false; + } + } + } + { + var CustomerNoteIds = await ct.CustomerNote.AsNoTracking().Where(z => z.CustomerId == id).Select(z => z.Id).ToListAsync(); + if (CustomerNoteIds.Count() > 0) + { + CustomerNoteBiz b = new CustomerNoteBiz(ct, UserId, UserTranslationId, CurrentUserRoles); + foreach (long ItemId in CustomerNoteIds) + if (!await b.DeleteAsync(ItemId, transaction)) + { + AddErrors(b.Errors); + return false; + } + } + } ct.Customer.Remove(dbObject); await ct.SaveChangesAsync(); @@ -177,6 +206,7 @@ namespace AyaNova.Biz } catch { + //NOTE: no need to rollback the transaction, it will auto-rollback if not committed and it is disposed when it goes out of scope either way //Just re-throw for now, let exception handler deal, but in future may want to deal with this more here throw; @@ -216,7 +246,7 @@ namespace AyaNova.Biz { //skip validation if seeding if (ServerBootConfig.SEEDING) return; - + bool isNew = currentObj == null; //Name required diff --git a/server/AyaNova/biz/CustomerNoteBiz.cs b/server/AyaNova/biz/CustomerNoteBiz.cs index ac37a31b..b776c194 100644 --- a/server/AyaNova/biz/CustomerNoteBiz.cs +++ b/server/AyaNova/biz/CustomerNoteBiz.cs @@ -109,39 +109,45 @@ namespace AyaNova.Biz //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // - internal async Task DeleteAsync(long id) + internal async Task DeleteAsync(long id, Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction parentTransaction = null) { - using (var transaction = await ct.Database.BeginTransactionAsync()) + //this may be part of a larger delete operation involving other objects (e.g. Customer delete and remove contacts) + //if so then there will be a parent transaction otherwise we make our own + Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction transaction = null; + if (parentTransaction == null) + transaction = await ct.Database.BeginTransactionAsync(); + try { - try + CustomerNote dbObject = await ct.CustomerNote.SingleOrDefaultAsync(m => m.Id == id); + if (dbObject == null) { - CustomerNote dbObject = await ct.CustomerNote.SingleOrDefaultAsync(m => m.Id == id); - if (dbObject == null){ - AddError(ApiErrorCode.NOT_FOUND); - return false; - } - if (HasErrors) - return false; - if (HasErrors) - return false; - ct.CustomerNote.Remove(dbObject); - await ct.SaveChangesAsync(); + AddError(ApiErrorCode.NOT_FOUND); + return false; + } + if (HasErrors) + return false; + if (HasErrors) + return false; + ct.CustomerNote.Remove(dbObject); + await ct.SaveChangesAsync(); - await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObject.Id, "CustomerNote", ct); - await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, BizType, ct); - await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); - await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); + await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObject.Id, "CustomerNote", ct); + await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, BizType, ct); + await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); + await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); + //all good do the commit if it's ours + if (parentTransaction == null) await transaction.CommitAsync(); - // await NotifyEventProcessor.HandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); - } - catch - { - //Just re-throw for now, let exception handler deal, but in future may want to deal with this more here - throw; - } - return true; } + catch + { + //Just re-throw for now, let exception handler deal, but in future may want to deal with this more here + throw; + } + + return true; + } //////////////////////////////////////////////////////////////////////////////////////////////// @@ -183,7 +189,7 @@ namespace AyaNova.Biz var orderedList = from id in batch join z in batchResults on id equals z.Id select z; foreach (CustomerNote w in orderedList) { - var jo = JObject.FromObject(w); + var jo = JObject.FromObject(w); ReportData.Add(jo); } } diff --git a/server/AyaNova/biz/UserBiz.cs b/server/AyaNova/biz/UserBiz.cs index e2a98ef2..c876605a 100644 --- a/server/AyaNova/biz/UserBiz.cs +++ b/server/AyaNova/biz/UserBiz.cs @@ -442,65 +442,71 @@ namespace AyaNova.Biz //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // - internal async Task DeleteAsync(long id) + internal async Task DeleteAsync(long id, Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction parentTransaction = null) { - - using (var transaction = await ct.Database.BeginTransactionAsync()) + //this may be part of a larger delete operation involving other objects (e.g. Customer delete and remove contacts) + //if so then there will be a parent transaction otherwise we make our own + Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction transaction = null; + if (parentTransaction == null) + transaction = await ct.Database.BeginTransactionAsync(); + try { - try + User dbObject = await ct.User.SingleOrDefaultAsync(z => z.Id == id); + if (dbObject == null) { - User dbObject = await ct.User.SingleOrDefaultAsync(z => z.Id == id); - if (dbObject == null) - { - AddError(ApiErrorCode.NOT_FOUND); - return false; - } + AddError(ApiErrorCode.NOT_FOUND); + return false; + } - //Also used for Contacts (customer type user or ho type user) - //by users with no User right but with Customer rights so need to double check here - if ( - (dbObject.IsOutsideUser && !Authorized.HasDeleteRole(CurrentUserRoles, AyaType.Customer)) || - (!dbObject.IsOutsideUser && !Authorized.HasDeleteRole(CurrentUserRoles, AyaType.User)) - ) - { - AddError(ApiErrorCode.NOT_AUTHORIZED); - return false; - } + //Also used for Contacts (customer type user or ho type user) + //by users with no User right but with Customer rights so need to double check here + if ( + (dbObject.IsOutsideUser && !Authorized.HasDeleteRole(CurrentUserRoles, AyaType.Customer)) || + (!dbObject.IsOutsideUser && !Authorized.HasDeleteRole(CurrentUserRoles, AyaType.User)) + ) + { + AddError(ApiErrorCode.NOT_AUTHORIZED); + return false; + } - await ValidateCanDelete(dbObject); - if (HasErrors) - return false; + await ValidateCanDelete(dbObject); + if (HasErrors) + return false; - //Delete sibling objects - //USEROPTIONS - await ct.Database.ExecuteSqlInterpolatedAsync($"delete from auseroptions where userid = {dbObject.Id}"); - //NOTIFY SUBSCRIPTIONS - await ct.Database.ExecuteSqlInterpolatedAsync($"delete from anotifysubscription where userid = {dbObject.Id}"); - //personal datalistview - await ct.Database.ExecuteSqlInterpolatedAsync($"delete from adatalistview where public = {false} and userid = {dbObject.Id}"); - //Dashboard view - await ct.Database.ExecuteSqlInterpolatedAsync($"delete from adashboardview where userid = {dbObject.Id}"); + //Delete sibling objects + //USEROPTIONS + await ct.Database.ExecuteSqlInterpolatedAsync($"delete from auseroptions where userid = {dbObject.Id}"); + //NOTIFY SUBSCRIPTIONS + await ct.Database.ExecuteSqlInterpolatedAsync($"delete from anotifysubscription where userid = {dbObject.Id}"); + //personal datalistview + await ct.Database.ExecuteSqlInterpolatedAsync($"delete from adatalistview where public = {false} and userid = {dbObject.Id}"); + //Dashboard view + await ct.Database.ExecuteSqlInterpolatedAsync($"delete from adashboardview where userid = {dbObject.Id}"); - await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObject.Id, dbObject.Name, ct); - await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, BizType, ct); - await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); - await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); + //Remove the object + ct.User.Remove(dbObject); + await ct.SaveChangesAsync(); - //Remove the object - ct.User.Remove(dbObject); - await ct.SaveChangesAsync(); + await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObject.Id, dbObject.Name, ct); + await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, BizType, ct); + await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); + await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); + //all good do the commit if it's ours + if (parentTransaction == null) await transaction.CommitAsync(); - await NotifyEventProcessor.HandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); - } - catch - { - //Just re-throw for now, let exception handler deal, but in future may want to deal with this more here - throw; - } - return true; + await NotifyEventProcessor.HandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); } + catch + { + //Just re-throw for now, let exception handler deal, but in future may want to deal with this more here + //no need to rollback, the transaction will rollback automatically if it's disposed without committing + throw; + } + //Note: Transaction does not need to be disposed it will automatically when it goes out of scope due to Using statement + return true; + } @@ -862,7 +868,7 @@ namespace AyaNova.Biz if (SaveIt) { - + o = await PutAsync(o); if (o == null) await JobsBiz.LogJobAsync(job.GId, $"Error processing item {id}: {GetErrorsAsString()}"); diff --git a/server/AyaNova/resource/de.json b/server/AyaNova/resource/de.json index b4f410b1..16631541 100644 --- a/server/AyaNova/resource/de.json +++ b/server/AyaNova/resource/de.json @@ -1616,6 +1616,7 @@ "ErrorAPI2207": "Das Startdatum muss vor dem Enddatum liegen", "ErrorAPI2208": "Dieses Objekt ist mit anderen verknüpft und kann auf diese Weise nicht geändert werden", "ErrorAPI2209": "Dieser Wert kann nicht geändert werden", + "ErrorAPI2210": "Untergeordneter Objektfehler", "ErrorServerUnresponsive": "Der Server reagiert nicht (E17)", "ErrorUserNotAuthenticated": "Nicht authentifiziert (E16)", "ErrorUserNotAuthorized": "Nicht autorisiert", diff --git a/server/AyaNova/resource/en.json b/server/AyaNova/resource/en.json index eb6724a5..58fdcfff 100644 --- a/server/AyaNova/resource/en.json +++ b/server/AyaNova/resource/en.json @@ -1616,6 +1616,7 @@ "ErrorAPI2207": "Start date must be before end date", "ErrorAPI2208": "This object is linked to others and can not be changed this way", "ErrorAPI2209": "This value cannot be changed", + "ErrorAPI2210": "Child object error", "ErrorServerUnresponsive": "Server not responding (E17)", "ErrorUserNotAuthenticated": "Not authenticated (E16)", "ErrorUserNotAuthorized": "Not authorized", diff --git a/server/AyaNova/resource/es.json b/server/AyaNova/resource/es.json index 1265e1ee..b061281e 100644 --- a/server/AyaNova/resource/es.json +++ b/server/AyaNova/resource/es.json @@ -1616,6 +1616,7 @@ "ErrorAPI2207": "La fecha de inicio debe ser anterior a la fecha de finalización", "ErrorAPI2208": "Este objeto está vinculado a otros y no puede ser cambiado de esta manera", "ErrorAPI2209": "Este valor no puede ser cambiado", + "ErrorAPI2210": "Error de objeto secundario", "ErrorServerUnresponsive": "El servidor no responde (E17)", "ErrorUserNotAuthenticated": "No autenticado (E16)", "ErrorUserNotAuthorized": "No autorizado", diff --git a/server/AyaNova/resource/fr.json b/server/AyaNova/resource/fr.json index 162c5a65..f6c0ed59 100644 --- a/server/AyaNova/resource/fr.json +++ b/server/AyaNova/resource/fr.json @@ -1616,6 +1616,7 @@ "ErrorAPI2207": "La date de début doit être antérieure à la date de fin", "ErrorAPI2208": "Cet objet est lié à d'autres et ne peut pas être modifié de cette façon", "ErrorAPI2209": "Cette valeur ne peut pas être changée", + "ErrorAPI2210": "Erreur d'objet enfant", "ErrorServerUnresponsive": "Le serveur ne répond pas (E17)", "ErrorUserNotAuthenticated": "Non authentifié (E16)", "ErrorUserNotAuthorized": "Non autorisé",