diff --git a/server/AyaNova/Controllers/NotifySubscriptionController.cs b/server/AyaNova/Controllers/NotifySubscriptionController.cs new file mode 100644 index 00000000..810d70ba --- /dev/null +++ b/server/AyaNova/Controllers/NotifySubscriptionController.cs @@ -0,0 +1,155 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Logging; +using AyaNova.Models; +using AyaNova.Api.ControllerHelpers; +using AyaNova.Biz; +using System.Linq; +using Microsoft.EntityFrameworkCore; + +namespace AyaNova.Api.Controllers +{ + [ApiController] + [ApiVersion("8.0")] + [Route("api/v{version:apiVersion}/notify")] + [Produces("application/json")] + [Authorize] + public class NotifySubscriptionController : ControllerBase + { + private readonly AyContext ct; + private readonly ILogger log; + private readonly ApiServerState serverState; + + /// + /// ctor + /// + /// + /// + /// + public NotifySubscriptionController(AyContext dbcontext, ILogger logger, ApiServerState apiServerState) + { + ct = dbcontext; + log = logger; + serverState = apiServerState; + } + + /// + /// Create NotifySubscription + /// + /// + /// From route path + /// + [HttpPost] + public async Task PostNotifySubscription([FromBody] NotifySubscription newObject, ApiVersion apiVersion) + { + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + NotifySubscriptionBiz biz = NotifySubscriptionBiz.GetBiz(ct, HttpContext); + if (!Authorized.HasCreateRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + NotifySubscription o = await biz.CreateAsync(newObject); + if (o == null) + return BadRequest(new ApiErrorResponse(biz.Errors)); + else + return CreatedAtAction(nameof(NotifySubscriptionController.GetNotifySubscription), new { id = o.Id, version = apiVersion.ToString() }, new ApiCreatedResponse(o)); + } + + /// + /// Duplicate NotifySubscription + /// (Wiki and Attachments are not duplicated) + /// + /// Source object id + /// From route path + /// NotifySubscription + [HttpPost("duplicate/{id}")] + public async Task DuplicateNotifySubscription([FromRoute] long id, ApiVersion apiVersion) + { + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + NotifySubscriptionBiz biz = NotifySubscriptionBiz.GetBiz(ct, HttpContext); + if (!Authorized.HasCreateRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + NotifySubscription o = await biz.DuplicateAsync(id); + if (o == null) + return BadRequest(new ApiErrorResponse(biz.Errors)); + else + return CreatedAtAction(nameof(NotifySubscriptionController.GetNotifySubscription), new { id = o.Id, version = apiVersion.ToString() }, new ApiCreatedResponse(o)); + } + + /// + /// Get NotifySubscription + /// + /// + /// NotifySubscription + [HttpGet("{id}")] + public async Task GetNotifySubscription([FromRoute] long id) + { + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + NotifySubscriptionBiz biz = NotifySubscriptionBiz.GetBiz(ct, HttpContext); + 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)); + return Ok(ApiOkResponse.Response(o)); + } + + /// + /// Put (update) NotifySubscription + /// + /// + /// + [HttpPut] + public async Task PutNotifySubscription([FromBody] NotifySubscription updatedObject) + { + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + NotifySubscriptionBiz biz = NotifySubscriptionBiz.GetBiz(ct, HttpContext); + if (!Authorized.HasModifyRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + var o = await biz.PutAsync(updatedObject);//In future may need to return entire object, for now just concurrency token + if (o == null) + { + if (biz.Errors.Exists(z => z.Code == ApiErrorCode.CONCURRENCY_CONFLICT)) + return StatusCode(409, new ApiErrorResponse(biz.Errors)); + else + return BadRequest(new ApiErrorResponse(biz.Errors)); + } + return Ok(ApiOkResponse.Response(new { Concurrency = o.Concurrency })); + } + + /// + /// Delete NotifySubscription + /// + /// + /// NoContent + [HttpDelete("{id}")] + public async Task DeleteNotifySubscription([FromRoute] long id) + { + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + NotifySubscriptionBiz biz = NotifySubscriptionBiz.GetBiz(ct, HttpContext); + if (!Authorized.HasDeleteRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + if (!await biz.DeleteAsync(id)) + return BadRequest(new ApiErrorResponse(biz.Errors)); + return NoContent(); + } + + //------------ + + + }//eoc +}//eons \ No newline at end of file diff --git a/server/AyaNova/biz/NotifySubscriptionBiz.cs b/server/AyaNova/biz/NotifySubscriptionBiz.cs new file mode 100644 index 00000000..dcfb41e7 --- /dev/null +++ b/server/AyaNova/biz/NotifySubscriptionBiz.cs @@ -0,0 +1,322 @@ +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using System.Linq; +using EnumsNET; +using AyaNova.Util; +using AyaNova.Api.ControllerHelpers; +using AyaNova.Models; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; + +namespace AyaNova.Biz +{ + internal class NotifySubscriptionBiz : BizObject//, IJobObject, ISearchAbleObject + { + internal NotifySubscriptionBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles) + { + ct = dbcontext; + UserId = currentUserId; + UserTranslationId = userTranslationId; + CurrentUserRoles = UserRoles; + BizType = AyaType.NotifySubscription; + } + + internal static NotifySubscriptionBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null) + { + if (httpContext != null) + return new NotifySubscriptionBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items)); + else + return new NotifySubscriptionBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdminFull); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //EXISTS + internal async Task ExistsAsync(long id) + { + return await ct.NotifySubscription.AnyAsync(z => z.Id == id); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //CREATE + // + internal async Task CreateAsync(NotifySubscription newObject) + { + await ValidateAsync(newObject); + if (HasErrors) + return null; + else + { + newObject.InTags = TagBiz.NormalizeTags(newObject.InTags); + newObject.OutTags = TagBiz.NormalizeTags(newObject.OutTags); + + await ct.NotifySubscription.AddAsync(newObject); + await ct.SaveChangesAsync(); + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct); + // await SearchIndexAsync(newObject, true); + return newObject; + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //DUPLICATE + // + internal async Task DuplicateAsync(long id) + { + throw new System.NotImplementedException("NotifySubscriptionBiz::Duplicateasync NOT IMPLEMENTED YET"); + NotifySubscription dbObject = await GetAsync(id, false); + if (dbObject == null) + { + AddError(ApiErrorCode.NOT_FOUND, "id"); + return null; + } + NotifySubscription newObject = new NotifySubscription(); + CopyObject.Copy(dbObject, newObject, "Wiki,Serial"); + + newObject.Id = 0; + newObject.Concurrency = 0; + await ct.NotifySubscription.AddAsync(newObject); + await ct.SaveChangesAsync(); + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct); + //await SearchIndexAsync(newObject, true); + // await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); + return newObject; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + // GET + // + internal async Task GetAsync(long id, bool logTheGetEvent = true) + { + var ret = await ct.NotifySubscription.SingleOrDefaultAsync(z => z.Id == id && z.UserId == UserId); + if (logTheGetEvent && ret != null) + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, AyaEvent.Retrieved), ct); + return ret; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //UPDATE + // + internal async Task PutAsync(NotifySubscription putObject) + { + NotifySubscription dbObject = await ct.NotifySubscription.SingleOrDefaultAsync(z => z.Id == putObject.Id); + if (dbObject == null) + { + AddError(ApiErrorCode.NOT_FOUND, "id"); + return null; + } + NotifySubscription SnapshotOfOriginalDBObj = new NotifySubscription(); + CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); + CopyObject.Copy(putObject, dbObject, "Id");//can update serial + dbObject.InTags = TagBiz.NormalizeTags(dbObject.InTags); + dbObject.OutTags = TagBiz.NormalizeTags(dbObject.OutTags); + + ct.Entry(dbObject).OriginalValues["Concurrency"] = putObject.Concurrency; + await ValidateAsync(dbObject); + if (HasErrors) return null; + try + { + await ct.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!await ExistsAsync(putObject.Id)) + AddError(ApiErrorCode.NOT_FOUND); + else + AddError(ApiErrorCode.CONCURRENCY_CONFLICT); + return null; + } + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, BizType, AyaEvent.Modified), ct); + + return dbObject; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //DELETE + // + internal async Task DeleteAsync(long id) + { + throw new System.NotImplementedException("NotifySubscriptionBiz::DeleteAsync NOT IMPLEMENTED YET"); + //needs to delete notifyevent and also notifications records + using (var transaction = await ct.Database.BeginTransactionAsync()) + { + try + { + NotifySubscription dbObject = await ct.NotifySubscription.SingleOrDefaultAsync(z => z.Id == id); + ValidateCanDelete(dbObject); + if (HasErrors) + return false; + if (HasErrors) + return false; + ct.NotifySubscription.Remove(dbObject); + await ct.SaveChangesAsync(); + + //Log event + await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObject.Id, dbObject.EventType.ToString(), ct); + await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, BizType, ct); + + await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); + //all good do the commit + await transaction.CommitAsync(); + } + 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 ValidateAsync(NotifySubscription proposedObj) + { + //NOTE: In DB schema only name and serial are not nullable + + //run validation and biz rules + + // //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.NotifySubscription.AnyAsync(z => z.Name == proposedObj.Name && z.Id != proposedObj.Id)) + // { + // AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name"); + // } + // } + + // //Start date AND end date must both be null or both contain values + // if (proposedObj.StartDate == null && proposedObj.EndDate != null) + // AddError(ApiErrorCode.VALIDATION_REQUIRED, "StartDate"); + + // if (proposedObj.StartDate != null && proposedObj.EndDate == null) + // AddError(ApiErrorCode.VALIDATION_REQUIRED, "EndDate"); + + // //Start date before end date + // if (proposedObj.StartDate != null && proposedObj.EndDate != null) + // if (proposedObj.StartDate > proposedObj.EndDate) + // AddError(ApiErrorCode.VALIDATION_STARTDATE_AFTER_ENDDATE, "StartDate"); + + // //Enum is valid value + // if (!proposedObj.UserType.IsValid()) + // { + // AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "UserType"); + // } + + // //Any form customizations to validate? + // var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.NotifySubscription.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); + // } + } + + private void ValidateCanDelete(NotifySubscription inObj) + { + //whatever needs to be check to delete this object + } + + + + // //////////////////////////////////////////////////////////////////////////////////////////////// + // //JOB / OPERATIONS + // // + // public async Task HandleJobAsync(OpsJob job) + // { + // //Hand off the particular job to the corresponding processing code + // //NOTE: If this code throws an exception the caller (JobsBiz::ProcessJobsAsync) will automatically set the job to failed and log the exeption so + // //basically any error condition during job processing should throw up an exception if it can't be handled + // switch (job.JobType) + // { + // case JobType.BulkCoreBizObjectOperation: + // await ProcessBulkJobAsync(job); + // break; + // default: + // throw new System.ArgumentOutOfRangeException($"NotifySubscriptionBiz.HandleJob-> Invalid job type{job.JobType.ToString()}"); + // } + // } + + + + // private async Task ProcessBulkJobAsync(OpsJob job) + // { + // await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running); + // await JobsBiz.LogJobAsync(job.GId, $"Bulk job {job.SubType} started..."); + // List idList = new List(); + // long ProcessedObjectCount = 0; + // JObject jobData = JObject.Parse(job.JobInfo); + // if (jobData.ContainsKey("idList")) + // idList = ((JArray)jobData["idList"]).ToObject>(); + // else + // idList = await ct.NotifySubscription.Select(z => z.Id).ToListAsync(); + // bool SaveIt = false; + // foreach (long id in idList) + // { + // try + // { + // SaveIt = false; + // ClearErrors(); + // var o = await GetAsync(id, false); + // switch (job.SubType) + // { + // case JobSubType.TagAddAny: + // case JobSubType.TagAdd: + // case JobSubType.TagRemoveAny: + // case JobSubType.TagRemove: + // case JobSubType.TagReplaceAny: + // case JobSubType.TagReplace: + // SaveIt = TagBiz.ProcessBulkTagOperation(o.Tags, (string)jobData["tag"], jobData.ContainsKey("toTag") ? (string)jobData["toTag"] : null, job.SubType); + // break; + // default: + // throw new System.ArgumentOutOfRangeException($"ProcessBulkJob -> Invalid job Subtype{job.SubType}"); + // } + // if (SaveIt) + // { + // o = await PutAsync(o); + // if (o == null) + // await JobsBiz.LogJobAsync(job.GId, $"Error processing item {id}: {GetErrorsAsString()}"); + // else + // ProcessedObjectCount++; + // } + // else + // ProcessedObjectCount++; + // } + // catch (Exception ex) + // { + // await JobsBiz.LogJobAsync(job.GId, $"Error processing item {id}"); + // await JobsBiz.LogJobAsync(job.GId, ExceptionUtil.ExtractAllExceptionMessages(ex)); + // } + // } + // await JobsBiz.LogJobAsync(job.GId, $"Bulk job {job.SubType} processed {ProcessedObjectCount} of {idList.Count}"); + // await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed); + // } + + + ///////////////////////////////////////////////////////////////////// + + }//eoc + + +}//eons + diff --git a/server/AyaNova/util/AySchema.cs b/server/AyaNova/util/AySchema.cs index f3c6e3a8..a60ccaaa 100644 --- a/server/AyaNova/util/AySchema.cs +++ b/server/AyaNova/util/AySchema.cs @@ -22,7 +22,7 @@ namespace AyaNova.Util //!!!!WARNING: BE SURE TO UPDATE THE DbUtil::EmptyBizDataFromDatabaseForSeedingOrImporting WHEN NEW TABLES ADDED!!!! private const int DESIRED_SCHEMA_LEVEL = 12; - internal const long EXPECTED_COLUMN_COUNT = 379; + internal const long EXPECTED_COLUMN_COUNT = 380; internal const long EXPECTED_INDEX_COUNT = 139; //!!!!WARNING: BE SURE TO UPDATE THE DbUtil::EmptyBizDataFromDatabaseForSeedingOrImporting WHEN NEW TABLES ADDED!!!!