diff --git a/server/AyaNova/biz/CustomerBiz.cs b/server/AyaNova/biz/CustomerBiz.cs index af164ae9..621ce438 100644 --- a/server/AyaNova/biz/CustomerBiz.cs +++ b/server/AyaNova/biz/CustomerBiz.cs @@ -138,7 +138,7 @@ namespace AyaNova.Biz AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } - await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, BizType, AyaEvent.Modified), ct); + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, BizType, AyaEvent.Modified), ct); await SearchIndexAsync(putObject, false); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags); await HandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); diff --git a/server/AyaNova/biz/WorkOrderBiz.cs b/server/AyaNova/biz/WorkOrderBiz.cs index de050234..16de0a86 100644 --- a/server/AyaNova/biz/WorkOrderBiz.cs +++ b/server/AyaNova/biz/WorkOrderBiz.cs @@ -23,6 +23,8 @@ namespace AyaNova.Biz I guess just implementing a very simple portion like from wo down to item down to a single grandchild makes the most sense + todo: Don't all *child items require a transaction to be passed for *any* crud op, i.e. including put and etc? + As they might be called from a parent transaction? */ @@ -627,7 +629,7 @@ namespace AyaNova.Biz */ -use this as the basis for the children below, it's using latest methodology + #region WorkOrderState level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS @@ -841,10 +843,8 @@ use this as the basis for the children below, it's using latest methodology //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // - internal async Task ItemCreateAsync(WorkOrderItem dtNewObject) + internal async Task ItemCreateAsync(WorkOrderItem newObject) { - WorkOrderItem newObject = new WorkOrderItem(); - CopyObject.Copy(dtNewObject, newObject); await ItemValidateAsync(newObject, null); if (HasErrors) return null; @@ -874,17 +874,17 @@ use this as the basis for the children below, it's using latest methodology //https://docs.microsoft.com/en-us/ef/core/querying/related-data //docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections var ret = - await ct.WorkOrderItem - .Include(wi => wi.Expenses) - .Include(wi => wi.Labors) - .Include(wi => wi.Loans) - .Include(wi => wi.Parts) - .Include(wi => wi.PartRequests) - .Include(wi => wi.ScheduledUsers) - .Include(wi => wi.Tasks) - .Include(wi => wi.Travels) - .Include(wi => wi.Units) - .SingleOrDefaultAsync(z => z.Id == id); + await ct.WorkOrderItem.AsNoTracking() + .Include(wi => wi.Expenses) + .Include(wi => wi.Labors) + .Include(wi => wi.Loans) + .Include(wi => wi.Parts) + .Include(wi => wi.PartRequests) + .Include(wi => wi.ScheduledUsers) + .Include(wi => wi.Tasks) + .Include(wi => wi.Travels) + .Include(wi => wi.Units) + .SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderItem, AyaEvent.Retrieved), ct); return ret; @@ -894,48 +894,42 @@ use this as the basis for the children below, it's using latest methodology //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // - internal async Task ItemPutAsync(WorkOrderItem dtPutObject) + internal async Task ItemPutAsync(WorkOrderItem putObject) { - - WorkOrderItem dbObject = await ct.WorkOrderItem.SingleOrDefaultAsync(z => z.Id == dtPutObject.Id); + var dbObject = await ItemGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } - - // make a snapshot of the original for validation but update the original to preserve workflow - WorkOrderItem SnapshotOfOriginalDBObj = new WorkOrderItem(); - CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); - - //Replace the db object with the PUT object - CopyObject.Copy(dtPutObject, dbObject, "Id"); - - dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags); - dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); - - ct.Entry(dbObject).OriginalValues["Concurrency"] = dtPutObject.Concurrency; - await ItemValidateAsync(dbObject, SnapshotOfOriginalDBObj); - if (HasErrors) + if (dbObject.Concurrency != putObject.Concurrency) + { + AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; + } + putObject.Tags = TagBiz.NormalizeTags(putObject.Tags); + putObject.CustomFields = JsonUtil.CompactJson(putObject.CustomFields); + await ItemValidateAsync(putObject, dbObject); + if (HasErrors) return null; + ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { - if (!await ItemExistsAsync(dtPutObject.Id)) + if (!await ItemExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } - await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, AyaType.WorkOrderItem, AyaEvent.Modified), ct); - await ItemSearchIndexAsync(dbObject, false); - await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags); - await ItemHandlePotentialNotificationEvent(AyaEvent.Modified, dbObject, SnapshotOfOriginalDBObj); - return dbObject; + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, AyaType.WorkOrderItem, AyaEvent.Modified), ct); + await ItemSearchIndexAsync(putObject, false); + await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags); + await ItemHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); + return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// @@ -948,7 +942,12 @@ use this as the basis for the children below, it's using latest methodology transaction = await ct.Database.BeginTransactionAsync(); try { - WorkOrderItem dbObject = await ct.WorkOrderItem.SingleOrDefaultAsync(z => z.Id == id); + var dbObject = await ItemGetAsync(id, false); + if (dbObject == null) + { + AddError(ApiErrorCode.NOT_FOUND); + return false; + } ItemValidateCanDelete(dbObject); if (HasErrors) return false; @@ -966,38 +965,38 @@ use this as the basis for the children below, it's using latest methodology //Delete children foreach (long ItemId in ExpenseIds) - if (!await ExpenseDeleteAsync(ItemId)) + if (!await ExpenseDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in LaborIds) - if (!await LaborDeleteAsync(ItemId)) + if (!await LaborDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in LoanIds) - if (!await LoanDeleteAsync(ItemId)) + if (!await LoanDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in PartIds) - if (!await PartDeleteAsync(ItemId)) + if (!await PartDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in PartRequestIds) - if (!await PartRequestDeleteAsync(ItemId)) + if (!await PartRequestDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in ScheduledUserIds) - if (!await ScheduledUserDeleteAsync(ItemId)) + if (!await ScheduledUserDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in TaskIds) - if (!await TaskDeleteAsync(ItemId)) + if (!await TaskDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in TravelIds) - if (!await TravelDeleteAsync(ItemId)) + if (!await TravelDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in UnitIds) - if (!await UnitDeleteAsync(ItemId)) + if (!await UnitDeleteAsync(ItemId, transaction)) return false; ct.WorkOrderItem.Remove(dbObject); await ct.SaveChangesAsync(); //Log event - await EventLogProcessor.DeleteObjectLogAsync(UserId, AyaType.WorkOrderItem, dbObject.Id, "wo:" + dbObject.WorkOrderId.ToString(), ct); + await EventLogProcessor.DeleteObjectLogAsync(UserId, AyaType.WorkOrderItem, dbObject.Id, "wo:" + dbObject.WorkOrderId.ToString(), ct);//FIX wo?? Not sure what is best here; revisit await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, AyaType.WorkOrderItem, ct); await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); @@ -1012,14 +1011,29 @@ use this as the basis for the children below, it's using latest methodology //Just re-throw for now, let exception handler deal, but in future may want to deal with this more here throw; } - finally - { - if (parentTransaction == null) - await transaction.DisposeAsync(); - } return true; } + private async Task ItemSearchIndexAsync(WorkOrderItem obj, bool isNew) + { + //SEARCH INDEXING + var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, AyaType.WorkOrderItem); + SearchParams.AddText(obj.Notes).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields); + + if (isNew) + await Search.ProcessNewObjectKeywordsAsync(SearchParams); + else + await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); + } + + public async Task ItemGetSearchResultSummary(long id) + { + var obj = await ct.WorkOrderItem.SingleOrDefaultAsync(z => z.Id == id); + var SearchParams = new Search.SearchIndexProcessObjectParameters(); + if (obj != null) + SearchParams.AddText(obj.Notes).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields); + return SearchParams; + } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION @@ -1069,27 +1083,6 @@ use this as the basis for the children below, it's using latest methodology } - private async Task ItemSearchIndexAsync(WorkOrderItem obj, bool isNew) - { - //SEARCH INDEXING - var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, AyaType.WorkOrderItem); - SearchParams.AddText(obj.Notes).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields); - - if (isNew) - await Search.ProcessNewObjectKeywordsAsync(SearchParams); - else - await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); - } - - public async Task ItemGetSearchResultSummary(long id) - { - var obj = await ct.WorkOrderItem.SingleOrDefaultAsync(z => z.Id == id); - var SearchParams = new Search.SearchIndexProcessObjectParameters(); - if (obj != null) - SearchParams.AddText(obj.Notes).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields); - return SearchParams; - } - //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // @@ -1180,36 +1173,31 @@ use this as the basis for the children below, it's using latest methodology //https://docs.microsoft.com/en-us/ef/core/querying/related-data //docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections - var ret = - await ct.WorkOrderItemExpense - .SingleOrDefaultAsync(z => z.Id == id); + var ret = await ct.WorkOrderItemExpense.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) - await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderItemExpense, AyaEvent.Retrieved), ct); + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // - internal async Task ExpensePutAsync(WorkOrderItemExpense dtPutObject) + internal async Task ExpensePutAsync(WorkOrderItemExpense putObject) { - WorkOrderItemExpense dbObject = await ct.WorkOrderItemExpense.SingleOrDefaultAsync(z => z.Id == dtPutObject.Id); + var dbObject = await ExpenseGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } - - // make a snapshot of the original for validation but update the original to preserve workflow - WorkOrderItemExpense SnapshotOfOriginalDBObj = new WorkOrderItemExpense(); - CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); - CopyObject.Copy(dtPutObject, dbObject, "Id"); - + if (dbObject.Concurrency != putObject.Concurrency) + { + AddError(ApiErrorCode.CONCURRENCY_CONFLICT); + return null; + } // dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags); // dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); - - ct.Entry(dbObject).OriginalValues["Concurrency"] = dtPutObject.Concurrency; - await ExpenseValidateAsync(dbObject, SnapshotOfOriginalDBObj); + await ExpenseValidateAsync(putObject, dbObject); if (HasErrors) return null; try { @@ -1217,86 +1205,53 @@ use this as the basis for the children below, it's using latest methodology } catch (DbUpdateConcurrencyException) { - if (!await ExpenseExistsAsync(dtPutObject.Id)) + if (!await ExpenseExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } - await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, AyaType.WorkOrderItemExpense, AyaEvent.Modified), ct); - await ExpenseSearchIndexAsync(dbObject, false); - //await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags); - await ExpenseHandlePotentialNotificationEvent(AyaEvent.Modified, dbObject, SnapshotOfOriginalDBObj); - return dbObject; + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct); + await ExpenseSearchIndexAsync(putObject, false); + //await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags); + await ExpenseHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); + return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // - internal async Task ExpenseDeleteAsync(long id) + internal async Task ExpenseDeleteAsync(long id, Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction parentTransaction = null) { - WorkOrderItemExpense dbObject = await ct.WorkOrderItemExpense.SingleOrDefaultAsync(z => z.Id == id); - ExpenseValidateCanDelete(dbObject); - if (HasErrors) - return false; - ct.WorkOrderItemExpense.Remove(dbObject); - await ct.SaveChangesAsync(); + Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction transaction = null; + if (parentTransaction == null) + transaction = await ct.Database.BeginTransactionAsync(); + try + { + WorkOrderItemExpense dbObject = await ct.WorkOrderItemExpense.SingleOrDefaultAsync(z => z.Id == id); + ExpenseValidateCanDelete(dbObject); + if (HasErrors) + return false; + ct.WorkOrderItemExpense.Remove(dbObject); + await ct.SaveChangesAsync(); - //Log event - await EventLogProcessor.DeleteObjectLogAsync(UserId, AyaType.WorkOrderItemExpense, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct); - await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, AyaType.WorkOrderItemExpense, ct); - //await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); - await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); - await ExpenseHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); + //Log event + await EventLogProcessor.DeleteObjectLogAsync(UserId, AyaType.WorkOrderItemExpense, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct); + await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, AyaType.WorkOrderItemExpense, ct); + //await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); + await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); + if (parentTransaction == null) + await transaction.CommitAsync(); + await ExpenseHandlePotentialNotificationEvent(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; } - //////////////////////////////////////////////////////////////////////////////////////////////// - //VALIDATION - // - private async Task ExpenseValidateAsync(WorkOrderItemExpense proposedObj, WorkOrderItemExpense currentObj) - { - //run validation and biz rules - bool isNew = currentObj == null; - - if (proposedObj.WorkOrderItemId == 0) - AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); - else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) - { - AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); - } - - //Any form customizations to validate? - var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemExpense.ToString()); - 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);//note: this is passed only to add errors - - //validate custom fields - // CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); - } - } - - - private void ExpenseValidateCanDelete(WorkOrderItemExpense obj) - { - if (obj == null) - { - AddError(ApiErrorCode.NOT_FOUND, "id"); - return; - } - - //re-check rights here necessary due to traversal delete from Principle object - if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemExpense)) - { - AddError(ApiErrorCode.NOT_AUTHORIZED); - return; - } - } - ////////////////////////////////////////////// //INDEXING // @@ -1314,13 +1269,61 @@ use this as the basis for the children below, it's using latest methodology public async Task ExpenseGetSearchResultSummary(long id) { - var obj = await ct.WorkOrderItemExpense.SingleOrDefaultAsync(z => z.Id == id); + var obj = await ExpenseGetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Description).AddText(obj.Name); return SearchParams; } + //////////////////////////////////////////////////////////////////////////////////////////////// + //VALIDATION + // + private async Task ExpenseValidateAsync(WorkOrderItemExpense proposedObj, WorkOrderItemExpense currentObj) + { + //skip validation if seeding + // if (ServerBootConfig.SEEDING) return; + + //run validation and biz rules + bool isNew = currentObj == null; + + if (proposedObj.WorkOrderItemId == 0) + AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); + else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) + AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); + + //Any form customizations to validate? + var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemExpense.ToString()); + 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);//note: this is passed only to add errors + + //validate custom fields + // CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); + } + } + + private void ExpenseValidateCanDelete(WorkOrderItemExpense obj) + { + if (obj == null) + { + AddError(ApiErrorCode.NOT_FOUND, "id"); + return; + } + + //re-check rights here necessary due to traversal delete from Principle object + if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemExpense)) + { + AddError(ApiErrorCode.NOT_AUTHORIZED); + return; + } + } + + + //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING //