diff --git a/server/AyaNova/Controllers/CustomerController.cs b/server/AyaNova/Controllers/CustomerController.cs new file mode 100644 index 00000000..69dfe7c4 --- /dev/null +++ b/server/AyaNova/Controllers/CustomerController.cs @@ -0,0 +1,270 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.JsonPatch; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using AyaNova.Models; +using AyaNova.Api.ControllerHelpers; +using AyaNova.Biz; + + +namespace AyaNova.Api.Controllers +{ + + [ApiController] + [ApiVersion("8.0")] + [Route("api/v{version:apiVersion}/[controller]")] + [Produces("application/json")] + [Authorize] + public class CustomerController : ControllerBase + { + private readonly AyContext ct; + private readonly ILogger log; + private readonly ApiServerState serverState; + + + /// + /// ctor + /// + /// + /// + /// + public CustomerController(AyContext dbcontext, ILogger logger, ApiServerState apiServerState) + { + ct = dbcontext; + log = logger; + serverState = apiServerState; + } + + + /// + /// Get full Customer object + /// + /// + /// A single Customer + [HttpGet("{id}")] + public async Task GetCustomer([FromRoute] long id) + { + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + + //Instantiate the business object handler + CustomerBiz biz = CustomerBiz.GetBiz(ct, HttpContext); + + //NOTE: This is the first check and often the only check but in some cases with some objects this will also need to check biz object rules + if (!Authorized.HasReadFullRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + + var o = await biz.GetAsync(id); + if (o == null) + return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); + + // NOTE: HERE would be the second check of biz rules before returning the object + // in cases where there is also a business rule to affect retrieval on top of basic rights + + return Ok(ApiOkResponse.Response(o, !Authorized.HasModifyRole(HttpContext.Items, biz.BizType))); + } + + + + /// + /// Put (update) Customer + /// + /// + /// + /// + [HttpPut("{id}")] + public async Task PutCustomer([FromRoute] long id, [FromBody] Customer inObj) + { + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + + //Instantiate the business object handler + CustomerBiz biz = CustomerBiz.GetBiz(ct, HttpContext); + + var o = await biz.GetAsync(id, false); + if (o == null) + return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); + + if (!Authorized.HasModifyRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + + try + { + if (!await biz.PutAsync(o, inObj)) + return BadRequest(new ApiErrorResponse(biz.Errors)); + } + catch (DbUpdateConcurrencyException) + { + if (!await biz.ExistsAsync(id)) + return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); + else + return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT)); + } + return Ok(ApiOkResponse.Response(new { ConcurrencyToken = o.ConcurrencyToken }, true)); + } + + + + /// + /// Patch (update) Customer + /// + /// + /// + /// + /// + [HttpPatch("{id}/{concurrencyToken}")] + public async Task PatchCustomer([FromRoute] long id, [FromRoute] uint concurrencyToken, [FromBody]JsonPatchDocument objectPatch) + { + //https://dotnetcoretutorials.com/2017/11/29/json-patch-asp-net-core/ + + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + + //Instantiate the business object handler + CustomerBiz biz = CustomerBiz.GetBiz(ct, HttpContext); + + var o = await biz.GetAsync(id, false); + if (o == null) + return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); + + if (!Authorized.HasModifyRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + + try + { + //patch and validate + if (!await biz.PatchAsync(o, objectPatch, concurrencyToken)) + return BadRequest(new ApiErrorResponse(biz.Errors)); + } + catch (DbUpdateConcurrencyException) + { + if (!await biz.ExistsAsync(id)) + return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); + else + return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT)); + } + return Ok(ApiOkResponse.Response(new { ConcurrencyToken = o.ConcurrencyToken }, true)); + } + + + /// + /// Post Customer + /// + /// + /// Automatically filled from route path, no need to specify in body + /// + [HttpPost] + public async Task PostCustomer([FromBody] Customer inObj, ApiVersion apiVersion) + { + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + + //Instantiate the business object handler + CustomerBiz biz = CustomerBiz.GetBiz(ct, HttpContext); + + //If a user has change roles + if (!Authorized.HasCreateRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + + //Create and validate + Customer o = await biz.CreateAsync(inObj); + if (o == null) + return BadRequest(new ApiErrorResponse(biz.Errors)); + else + return CreatedAtAction(nameof(CustomerController.GetCustomer), new { id = o.Id, version = apiVersion.ToString() }, new ApiCreatedResponse(o)); + + + } + + /// + /// Duplicate Customer + /// + /// Create a duplicate of this items id + /// Automatically filled from route path, no need to specify in body + /// + [HttpPost("duplicate/{id}")] + public async Task DuplicateCustomer([FromRoute] long id, ApiVersion apiVersion) + { + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + + //Instantiate the business object handler + CustomerBiz biz = CustomerBiz.GetBiz(ct, HttpContext); + + //If a user has change roles + if (!Authorized.HasCreateRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + + var oSrc = await biz.GetAsync(id, false); + if (oSrc == null) + return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); + + //Create and validate + Customer o = await biz.DuplicateAsync(oSrc); + if (o == null) + return BadRequest(new ApiErrorResponse(biz.Errors)); + else + return CreatedAtAction(nameof(CustomerController.GetCustomer), new { id = o.Id, version = apiVersion.ToString() }, new ApiCreatedResponse(o)); + + } + + + + /// + /// Delete Customer + /// + /// + /// Ok + [HttpDelete("{id}")] + public async Task DeleteCustomer([FromRoute] long id) + { + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + + //Instantiate the business object handler + CustomerBiz biz = CustomerBiz.GetBiz(ct, HttpContext); + + var o = await biz.GetAsync(id, false); + if (o == null) + return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); + + if (!Authorized.HasDeleteRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + + if (!await biz.DeleteAsync(o)) + return BadRequest(new ApiErrorResponse(biz.Errors)); + + return NoContent(); + } + + + + + + //------------ + + + }//eoc +}//eons \ No newline at end of file diff --git a/server/AyaNova/PickList/PickListFactory.cs b/server/AyaNova/PickList/PickListFactory.cs index 09468aba..4c9292fd 100644 --- a/server/AyaNova/PickList/PickListFactory.cs +++ b/server/AyaNova/PickList/PickListFactory.cs @@ -16,8 +16,10 @@ namespace AyaNova.PickList //CoreBizObject add here case AyaType.Widget: return new WidgetPickList() as IAyaPickList; - case AyaType.User: + case AyaType.User: return new UserPickList() as IAyaPickList; + case AyaType.Customer: + throw new System.NotImplementedException("PICKLIST NOT IMPLEMENTED"); } return null; } diff --git a/server/AyaNova/biz/AyaFormFieldDefinitions.cs b/server/AyaNova/biz/AyaFormFieldDefinitions.cs index 99e6c1ec..5cf68216 100644 --- a/server/AyaNova/biz/AyaFormFieldDefinitions.cs +++ b/server/AyaNova/biz/AyaFormFieldDefinitions.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System; namespace AyaNova.Biz { @@ -9,21 +10,23 @@ namespace AyaNova.Biz public static class AyaFormFieldDefinitions { - - //DEFINE VALID KEYS HERE - - // public const string WIDGET_KEY = "widget"; - // public const string USER_KEY = "user"; - - + public static List AyaFormFieldDefinitionKeys { get { - //CoreBizObject add here - List l = new List{ - AyaType.Widget.ToString(),AyaType.User.ToString() - }; + //return the names of all AyaTypes that have the corebizobject attribute + List l = new List(); + var values = Enum.GetValues(typeof(AyaType)); + foreach (AyaType t in values) + { + if (t.HasAttribute(typeof(CoreBizObjectAttribute))) + { + l.Add(t.ToString()); + } + } + + return l; } } diff --git a/server/AyaNova/biz/AyaType.cs b/server/AyaNova/biz/AyaType.cs index 5602adb5..ab296b8a 100644 --- a/server/AyaNova/biz/AyaType.cs +++ b/server/AyaNova/biz/AyaType.cs @@ -18,8 +18,8 @@ namespace AyaNova.Biz //CoreBizObject add here //Search for that IN SERVER AND CLIENT CODE and you will see all areas that need coding for the new object - //***IMPORTANT: Also need to add translations for any new biz objects added that don't match exactly the name here in the key - //because enumlist gets it that way, i.e. "Global" would be the expected key + //***IMPORTANT: Also need to add translations for any new biz objects added that don't match exactly the name here in the key + //because enumlist gets it that way, i.e. "Global" would be the expected key NoType = 0, Global = 1, @@ -34,7 +34,8 @@ namespace AyaNova.Biz License = 5, LogFile = 6, PickListTemplate = 7, - DEPRECATED_REUSELATER_08 = 8, + [CoreBizObject] + Customer = 8, ServerJob = 9, DEPRECATED_10 = 10, TrialSeeder = 11, @@ -55,7 +56,7 @@ namespace AyaNova.Biz //AyaNova.Biz.BizObjectNameFetcherDIRECT //and in the CLIENT in ayatype.js - //and need TRANSLATION KEYS because any type could show in the event log at teh client end + //and need TRANSLATION KEYS because any type could show in the event log at teh client end } diff --git a/server/AyaNova/biz/BizObjectExistsInDatabase.cs b/server/AyaNova/biz/BizObjectExistsInDatabase.cs index 51c777af..f0644083 100644 --- a/server/AyaNova/biz/BizObjectExistsInDatabase.cs +++ b/server/AyaNova/biz/BizObjectExistsInDatabase.cs @@ -31,6 +31,8 @@ namespace AyaNova.Biz switch (aytype) { //CoreBizObject add here + case AyaType.Customer: + return await ct.Customer.AnyAsync(m => m.Id == id); case AyaType.User: return await ct.User.AnyAsync(m => m.Id == id); case AyaType.Widget: diff --git a/server/AyaNova/biz/BizObjectFactory.cs b/server/AyaNova/biz/BizObjectFactory.cs index 26b6c393..a9dce204 100644 --- a/server/AyaNova/biz/BizObjectFactory.cs +++ b/server/AyaNova/biz/BizObjectFactory.cs @@ -19,11 +19,14 @@ namespace AyaNova.Biz //Returns the biz object class that corresponds to the type presented + //Used by SEARCH and objects with JOBS internal static BizObject GetBizObject(AyaType aytype, AyContext dbcontext, long userId = 1, AuthorizationRoles roles = AuthorizationRoles.All) { switch (aytype) { //CoreBizObject add here + case AyaType.Customer: + return new CustomerBiz(dbcontext, userId, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, roles); case AyaType.User: return new UserBiz(dbcontext, userId, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, roles); case AyaType.Widget: diff --git a/server/AyaNova/biz/BizObjectNameFetcherDirect.cs b/server/AyaNova/biz/BizObjectNameFetcherDirect.cs index 4043ddf0..752fab46 100644 --- a/server/AyaNova/biz/BizObjectNameFetcherDirect.cs +++ b/server/AyaNova/biz/BizObjectNameFetcherDirect.cs @@ -3,6 +3,7 @@ namespace AyaNova.Biz //Turn a type and ID into a displayable name //this version uses a direct DataReader for performance in tight loops (search) + //Used by search internal static class BizObjectNameFetcherDirect { @@ -21,15 +22,11 @@ namespace AyaNova.Biz } string TABLE = string.Empty; string COLUMN = "name"; - //CoreBizObject add here + //CoreBizObject add here BUT ONLY ADD IF AYATYPE NAME DIFFERS FROM TABLE NAME OR NO NAME FIELD AS PRIMARY NAME-LIKE COLUMN + switch (aytype) { - case AyaType.User: - TABLE = "auser"; - break; - case AyaType.Widget: - TABLE = "awidget"; - break; + //Oddballs only, otherwise let default handle it case AyaType.FileAttachment: TABLE = "afileattachment"; @@ -42,9 +39,10 @@ namespace AyaNova.Biz TABLE = "aformcustom"; COLUMN = "formkey"; break; - default: - throw new System.NotSupportedException($"AyaNova.BLL.BizObjectNameFetcher::Name type {aytype.ToString()} is not supported"); + TABLE = "a" + aytype.ToString().ToLowerInvariant(); + break; + } cmd.CommandText = $"SELECT m.{COLUMN} FROM {TABLE} AS m WHERE m.id = {id} LIMIT 1"; using (var dr = cmd.ExecuteReader()) diff --git a/server/AyaNova/biz/BizRoles.cs b/server/AyaNova/biz/BizRoles.cs index 31cc35c0..60b53e32 100644 --- a/server/AyaNova/biz/BizRoles.cs +++ b/server/AyaNova/biz/BizRoles.cs @@ -31,6 +31,26 @@ namespace AyaNova.Biz #region All roles initialization //CoreBizObject add here + + //TODO: BIZ objects, fine tune this stuff, best guess first pass here + + //////////////////////////////////////////////////////////// + //CUSTOMER + // + roles.Add(AyaType.Customer, new BizRoleSet() + { + Change = AuthorizationRoles.BizAdminFull | AuthorizationRoles.DispatchFull | AuthorizationRoles.SalesFull | AuthorizationRoles.TechFull | AuthorizationRoles.AccountingFull, + ReadFullRecord = AuthorizationRoles.BizAdminLimited | AuthorizationRoles.DispatchLimited | AuthorizationRoles.SalesLimited | AuthorizationRoles.TechLimited, + Select = AuthorizationRoles.All + }); + + + + + + + + //////////////////////////////////////////////////////////// //GLOBAL BIZ SETTINGS // diff --git a/server/AyaNova/biz/CustomerBiz.cs b/server/AyaNova/biz/CustomerBiz.cs new file mode 100644 index 00000000..8bd1d3e0 --- /dev/null +++ b/server/AyaNova/biz/CustomerBiz.cs @@ -0,0 +1,305 @@ +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.JsonPatch; +using AyaNova.Util; +using AyaNova.Api.ControllerHelpers; +using AyaNova.Models; + +namespace AyaNova.Biz +{ + + internal class CustomerBiz : BizObject, ISearchAbleObject + { + + internal CustomerBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles) + { + ct = dbcontext; + UserId = currentUserId; + UserTranslationId = userTranslationId; + CurrentUserRoles = UserRoles; + BizType = AyaType.Customer; + } + + internal static CustomerBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null) + { + + if (httpContext != null) + return new CustomerBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items)); + else//when called internally for internal ops there will be no context so need to set default values for that + return new CustomerBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdminFull); + } + + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //EXISTS + internal async Task ExistsAsync(long id) + { + return await ct.Customer.AnyAsync(e => e.Id == id); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + /// GET + /// + /// + + internal async Task GetAsync(long fetchId, bool logTheGetEvent = true) + { + //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.Customer.SingleOrDefaultAsync(m => m.Id == fetchId); + if (logTheGetEvent && ret != null) + { + //Log + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, fetchId, BizType, AyaEvent.Retrieved), ct); + } + return ret; + } + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //CREATE + + //Called from route and also seeder + internal async Task CreateAsync(Customer inObj) + { + await ValidateAsync(inObj, null); + if (HasErrors) + return null; + else + { + //do stuff with Customer + Customer outObj = inObj; + + outObj.Tags = TagUtil.NormalizeTags(outObj.Tags); + outObj.CustomFields = JsonUtil.CompactJson(outObj.CustomFields); + //Save to db + await ct.Customer.AddAsync(outObj); + await ct.SaveChangesAsync(); + //Handle child and associated items: + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), ct); + await SearchIndexAsync(outObj, true); + await TagUtil.ProcessUpdateTagsInRepositoryAsync(ct, outObj.Tags, null); + + return outObj; + } + } + + + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //DUPLICATE + // + + internal async Task DuplicateAsync(Customer dbObj) + { + + Customer outObj = new Customer(); + CopyObject.Copy(dbObj, outObj, "Wiki"); + // outObj.Name = Util.StringUtil.NameUniquify(outObj.Name, 255); + //generate unique name + string newUniqueName = string.Empty; + bool NotUnique = true; + long l = 1; + do + { + newUniqueName = Util.StringUtil.UniqueNameBuilder(dbObj.Name, l++, 255); + NotUnique = await ct.Customer.AnyAsync(m => m.Name == newUniqueName); + } while (NotUnique); + + outObj.Name = newUniqueName; + + + outObj.Id = 0; + outObj.ConcurrencyToken = 0; + + await ct.Customer.AddAsync(outObj); + await ct.SaveChangesAsync(); + + //Handle child and associated items: + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), ct); + await SearchIndexAsync(outObj, true); + await TagUtil.ProcessUpdateTagsInRepositoryAsync(ct, outObj.Tags, null); + return outObj; + + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //UPDATE + // + + //put + internal async Task PutAsync(Customer dbObj, Customer inObj) + { + + //make a snapshot of the original for validation but update the original to preserve workflow + Customer SnapshotOfOriginalDBObj = new Customer(); + CopyObject.Copy(dbObj, SnapshotOfOriginalDBObj); + + //Replace the db object with the PUT object + CopyObject.Copy(inObj, dbObj, "Id"); + + 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; + + + await ValidateAsync(dbObj, SnapshotOfOriginalDBObj); + if (HasErrors) + return false; + + //Log event and save context + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct); + await SearchIndexAsync(dbObj, false); + await TagUtil.ProcessUpdateTagsInRepositoryAsync(ct, dbObj.Tags, SnapshotOfOriginalDBObj.Tags); + + return true; + } + + //patch + internal async Task PatchAsync(Customer dbObj, JsonPatchDocument objectPatch, uint concurrencyToken) + { + //Validate Patch is allowed + if (!ValidateJsonPatch.Validate(this, objectPatch)) return false; + + //make a snapshot of the original for validation but update the original to preserve workflow + Customer SnapshotOfOriginalDBObj = new Customer(); + 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; + await ValidateAsync(dbObj, SnapshotOfOriginalDBObj); + if (HasErrors) + return false; + + //Log event and save context + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct); + await SearchIndexAsync(dbObj, false); + + await TagUtil.ProcessUpdateTagsInRepositoryAsync(ct, dbObj.Tags, SnapshotOfOriginalDBObj.Tags); + + return true; + } + + + private async Task SearchIndexAsync(Customer obj, bool isNew) + { + //SEARCH INDEXING + var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType); + SearchParams.AddText(obj.Notes).AddText(obj.Name).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields); + + if (isNew) + await Search.ProcessNewObjectKeywordsAsync(SearchParams); + else + await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); + } + + public async Task GetSearchResultSummary(long id) + { + var obj = await ct.Customer.SingleOrDefaultAsync(m => m.Id == id); + var SearchParams = new Search.SearchIndexProcessObjectParameters(); + if (obj != null) + SearchParams.AddText(obj.Notes).AddText(obj.Name).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields); + return SearchParams; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //DELETE + // + internal async Task DeleteAsync(Customer dbObj) + { + //Determine if the object can be deleted, do the deletion tentatively + //Probably also in here deal with tags and associated search text etc + + //NOT REQUIRED NOW BUT IF IN FUTURE ValidateCanDelete(dbObj); + if (HasErrors) + return false; + ct.Customer.Remove(dbObj); + await ct.SaveChangesAsync(); + + //Log event + await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObj.Id, dbObj.Name, ct); + await Search.ProcessDeletedObjectKeywordsAsync(dbObj.Id, BizType); + await TagUtil.ProcessDeleteTagsInRepositoryAsync(ct, dbObj.Tags); + return true; + } + + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //VALIDATION + // + + //Can save or update? + private async Task ValidateAsync(Customer proposedObj, Customer currentObj) + { + + //run validation and biz rules + bool isNew = currentObj == null; + + //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 (await ct.Customer.AnyAsync(m => m.Name == proposedObj.Name && m.Id != proposedObj.Id)) + { + AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name"); + } + } + + + //Any form customizations to validate? + var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(x => x.FormKey == AyaType.Customer.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); + + //validate custom fields + CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); + } + + } + + + //Can delete? + // private void ValidateCanDelete(Customer inObj) + // { + // //whatever needs to be check to delete this object + // } + + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //JOB / OPERATIONS + // + + + //Other job handlers here... + + + ///////////////////////////////////////////////////////////////////// + + }//eoc + + +}//eons + diff --git a/server/AyaNova/models/AyContext.cs b/server/AyaNova/models/AyContext.cs index 6fee3aeb..3e169306 100644 --- a/server/AyaNova/models/AyContext.cs +++ b/server/AyaNova/models/AyContext.cs @@ -9,10 +9,8 @@ namespace AyaNova.Models public virtual DbSet Event { get; set; } public virtual DbSet SearchDictionary { get; set; } public virtual DbSet SearchKey { get; set; } - public virtual DbSet User { get; set; } public virtual DbSet UserOptions { get; set; } public virtual DbSet License { get; set; } - public virtual DbSet Widget { get; set; } public virtual DbSet FileAttachment { get; set; } public virtual DbSet OpsJob { get; set; } public virtual DbSet OpsJobLog { get; set; } @@ -21,7 +19,11 @@ namespace AyaNova.Models public virtual DbSet DataListView { get; set; } public virtual DbSet Tag { get; set; } public virtual DbSet FormCustom { get; set; } - public virtual DbSet PickListTemplate { get; set; } + public virtual DbSet PickListTemplate { get; set; } + + public virtual DbSet Widget { get; set; } + public virtual DbSet User { get; set; } + public virtual DbSet Customer { get; set; } //Note: had to add this constructor to work with the code in startup.cs that gets the connection string from the appsettings.json file diff --git a/server/AyaNova/models/Customer.cs b/server/AyaNova/models/Customer.cs new file mode 100644 index 00000000..0a61c8cd --- /dev/null +++ b/server/AyaNova/models/Customer.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using AyaNova.Biz; +using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; + +namespace AyaNova.Models +{ + //NOTE: Any non required field (nullable in DB) sb nullable here, i.e. decimal? not decimal, + //otherwise the server will call it an invalid record if the field isn't sent from client + + public partial class Customer + { + public long Id { get; set; } + public uint ConcurrencyToken { get; set; } + + [Required] + public string Name { get; set; } + public bool Active { get; set; } + public string Notes { get; set; } + public string Wiki {get;set;} + public string CustomFields { get; set; } + public List Tags { get; set; } + + + public Customer() + { + Tags = new List(); + } + + }//eoc + +}//eons diff --git a/server/AyaNova/util/AySchema.cs b/server/AyaNova/util/AySchema.cs index 84bdc4be..5b95534a 100644 --- a/server/AyaNova/util/AySchema.cs +++ b/server/AyaNova/util/AySchema.cs @@ -315,6 +315,28 @@ namespace AyaNova.Util } + +////////////////////////////////////////////////// + //MULTIPLE BIZ OBJECT tables + if (currentSchema < 11) + { + LogUpdateMessage(log); + + //CUSTOMER + await ExecQueryAsync("CREATE TABLE acustomer (id BIGSERIAL PRIMARY KEY, name varchar(255) not null unique, active bool, " + + "notes text NULL, wiki text null, customfields text NULL, tags varchar(255) ARRAY NULL)"); + await ExecQueryAsync("CREATE UNIQUE INDEX acustomer_name_id_idx ON acustomer (id, name);"); + await ExecQueryAsync("CREATE INDEX acustomer_tags ON acustomer using GIN(tags)"); + + + + + + await SetSchemaLevelAsync(++currentSchema); + } + + + // ////////////////////////////////////////////////// // // WikiPage table // if (currentSchema < 11)