This commit is contained in:
2022-12-16 06:01:23 +00:00
parent 26c2ae5cc9
commit effd96143f
310 changed files with 48715 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
namespace Sockeye.Biz
{
public enum ApiErrorCode : int
{
/*
DON'T FORGET TO UPDATE THE API-ERROR-CODES.MD DOCUMENTATION
AND UPDATE THE ApiErrorCodeStockMessage.cs
*/
API_CLOSED = 2000,
API_OPS_ONLY = 2001,
API_SERVER_ERROR = 2002,
AUTHENTICATION_FAILED = 2003,
NOT_AUTHORIZED = 2004,
CONCURRENCY_CONFLICT = 2005,
NOT_FOUND = 2010,
PUT_ID_MISMATCH = 2020,
INVALID_OPERATION = 2030,
INSUFFICIENT_INVENTORY = 2040,
VALIDATION_FAILED = 2200,
VALIDATION_REQUIRED = 2201,
VALIDATION_LENGTH_EXCEEDED = 2202,
VALIDATION_INVALID_VALUE = 2203,
VALIDATION_CUSTOM_REQUIRED_EMPTY = 2204,
VALIDATION_MISSING_PROPERTY = 2205,
VALIDATION_NOT_UNIQUE = 2206,
VALIDATION_STARTDATE_AFTER_ENDDATE = 2207,
VALIDATION_REFERENTIAL_INTEGRITY = 2208,
VALIDATION_NOT_CHANGEABLE = 2209,
CHILD_OBJECT_ERROR = 2210,
VALIDATION_REQUIRED_CUSTOM = 2211,
VALIDATION_WO_MULTIPLE_CONTRACTED_UNITS = 2212,
/*
| 2000 | API closed - Server is running but access to the API has been closed to all users |
| 2001 | API closed all non OPS routes - Server is running but access to the API has been restricted to only server maintenance operations related functionality |
| 2002 | Internal error from the API server, details in [server log](ops-log.md) file |
| 2003 | Authentication failed, bad login or password, user not found |
| 2004 | Not authorized - current user is not authorized for operation attempted on the resource (insufficient rights) |
| 2005 | Object was changed by another user since retrieval (concurrency token mismatch). A record was attempted to be saved but another user has just modified it so it's invalid. (first save "wins") |
| 2010 | Object not found - API could not find the object requested |
| 2020 | PUT Id mismatch - object Id does not match route Id |
| 2030 | Invalid operation - operation could not be completed, not valid, details in message property |
| 2200 | Validation error - general issue with object overall not valid, specifics in "details" property |
| 2201 | Validation error - Field is required but is empty or null |
| 2202 | Validation error - Field length exceeded. The limit will be returned in the `message` property of the validation error |
| 2203 | Validation error - invalid value. Usually an type mismatch or a logical or business rule mismatch (i.e. only certain values are valid for current state of object) |
| 2204 | Validation error - Customized form property is set to required but has an empty value |
| 2205 | Validation error - Required property is missing entirely. Usually a development or communications error |
| 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 - When an object requires a start and end date 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 | */
}
}//eons

View File

@@ -0,0 +1,95 @@
using System;
using Microsoft.Extensions.Logging;
using Sockeye.Models;
using System.Linq;
using System.Collections.Generic;
using Sockeye.Biz;
namespace Sockeye.Biz
{
internal static class ApiErrorCodeStockMessage
{
internal static string GetTranslationCodeForApiErrorCode(ApiErrorCode code)
{
return $"ErrorAPI"+((int)code).ToString();
// switch (code)
// {
// case ApiErrorCode.API_CLOSED:
// return "API Closed";
// case ApiErrorCode.API_OPS_ONLY:
// return "API Closed to non operations routes";
// case ApiErrorCode.API_SERVER_ERROR:
// return "Server internal error, details in server log file";
// case ApiErrorCode.AUTHENTICATION_FAILED:
// return "Authentication failed";
// case ApiErrorCode.NOT_AUTHORIZED:
// return "User not authorized for this resource operation (insufficient rights)";
// case ApiErrorCode.CONCURRENCY_CONFLICT:
// return "Object was changed by another user since retrieval (concurrency token mismatch)";
// case ApiErrorCode.NOT_FOUND:
// return "Object not found";
// case ApiErrorCode.PUT_ID_MISMATCH:
// return "Update failed: ID mismatch - route ID doesn't match object id";
// case ApiErrorCode.INVALID_OPERATION:
// return "An attempt was made to perform an invalid operation";
// case ApiErrorCode.VALIDATION_FAILED:
// return "Object did not pass validation";
// case ApiErrorCode.VALIDATION_REQUIRED:
// return "Required field empty";
// case ApiErrorCode.VALIDATION_LENGTH_EXCEEDED:
// return "Field too long";
// case ApiErrorCode.VALIDATION_INVALID_VALUE:
// return "Field is set to a non allowed value";
// case ApiErrorCode.VALIDATION_CUSTOM_REQUIRED_EMPTY:
// return "Customized form property is set to required but has an empty value";
// case ApiErrorCode.VALIDATION_MISSING_PROPERTY:
// return "Required property is missing entirel";
// case ApiErrorCode.VALIDATION_NOT_UNIQUE:
// return "Field is required to be unique but an existing record with an identical value was found in the database";
// case ApiErrorCode.VALIDATION_STARTDATE_AFTER_ENDDATE:
// return "The start date must be earlier than the end date";
// case ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY:
// 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";
// default:
// return null;
// }
}
/*
API_CLOSED = 2000,
API_OPS_ONLY = 2001,
API_SERVER_ERROR = 2002,
AUTHENTICATION_FAILED = 2003,
NOT_AUTHORIZED = 2004,
CONCURRENCY_CONFLICT=2005,
NOT_FOUND = 2010,
PUT_ID_MISMATCH = 2020,
INVALID_OPERATION = 2030,
VALIDATION_FAILED = 2200,
VALIDATION_REQUIRED = 2201,
VALIDATION_LENGTH_EXCEEDED = 2202,
VALIDATION_INVALID_VALUE = 2203,
VALIDATION_CUSTOM_REQUIRED_EMPTY = 2204,
VALIDATION_MISSING_PROPERTY = 2205,
VALIDATION_NOT_UNIQUE = 2206,
VALIDATION_STARTDATE_AFTER_ENDDATE = 2207,
VALIDATION_REFERENTIAL_INTEGRITY = 2208,
VALIDATION_NOT_CHANGEABLE = 2209
*/
}
}//eons

248
server/biz/AttachmentBiz.cs Normal file
View File

@@ -0,0 +1,248 @@
using System.Threading.Tasks;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
using Microsoft.Extensions.Logging;
using System;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Collections.Generic;
namespace Sockeye.Biz
{
/// <summary>
/// Handle attachment file related cleanup and checking
/// </summary>
internal class AttachmentBiz : BizObject, IJobObject
{
internal AttachmentBiz(AyContext dbcontext, long currentUserId, AuthorizationRoles userRoles)
{
ct = dbcontext;
UserId = currentUserId;
CurrentUserRoles = userRoles;
BizType = SockType.FileAttachment;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//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
//There might be future other job types so doing it like this for all biz job handlers for now
switch (job.JobType)
{
case JobType.AttachmentMaintenance:
await ProcessAttachmentMaintenanceAsync(job);
break;
default:
throw new System.ArgumentOutOfRangeException($"AttachmentBiz.HandleJobAsync -> Invalid job type{job.JobType.ToString()}");
}
}
/// <summary>
/// Handle the job
/// </summary>
/// <param name="job"></param>
private async Task ProcessAttachmentMaintenanceAsync(OpsJob job)
{
ILogger log = Sockeye.Util.ApplicationLogging.CreateLogger("AttachmentMaintenanceJob");
ApiServerState apiServerState = (ApiServerState)ServiceProviderProvider.Provider.GetService(typeof(ApiServerState));
//get the current server state so can set back to it later
ApiServerState.ServerState wasServerState = apiServerState.GetState();
string wasReason = apiServerState.Reason;
try
{
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running);
await JobsBiz.LogJobAsync(job.GId, $"LT:StartJob ");
apiServerState.SetOpsOnly("Attachment file maintenance");
//get a list of all attachment files currently on disk
var AllAttachmentFilesOnDisk = FileUtil.GetAllAttachmentFilePaths();
List<string> AllDBFileFullPath = new List<string>();
// EXISTENCE CHECK
//iterate all records in chunks, update the existence bool field if it's incorrect only
bool moreRecords = true;
int skip = 0;
int chunkSize = 100;
do
{
var chunk = await ct.FileAttachment.AsNoTracking().OrderBy(z => z.Id).Skip(skip).Take(chunkSize).ToListAsync();
if (chunk.Count < chunkSize)
{
//we've reached the end
moreRecords = false;
}
skip += chunkSize;
foreach (var i in chunk)
{
var FullPathName = FileUtil.GetPermanentAttachmentFilePath(i.StoredFileName);
AllDBFileFullPath.Add(FullPathName);
var FileExistsInReality = AllAttachmentFilesOnDisk.Contains(FullPathName);
var ParentBizObjectExistsInReality = await BizObjectExistsInDatabase.ExistsAsync(i.AttachToAType, i.AttachToObjectId, ct);
//does the db record reflect the same status as reality?
if (FileExistsInReality != i.Exists || !ParentBizObjectExistsInReality)
{
var f = await ct.FileAttachment.FirstOrDefaultAsync(z => z.Id == i.Id);
if (f != null)
{
f.Exists = FileExistsInReality;
if (!ParentBizObjectExistsInReality)
{
//switch it to notype
f.AttachToAType = SockType.NoType;
f.AttachToObjectId = 0;
}
await ct.SaveChangesAsync();
}
}
//DOES THE PARENT OBJECT EXIST?
}
} while (moreRecords);
//I kept this block because I did a lot of work to figure it out but in the end I don't need it because
//a user will be moving attachments so they would no longer be existing on their old NOTYPE orphan location anyway
// //DE-ORPHANIZE ACTION (clean up former orphans)
// //people can attach an orphan to another record so this cleans that up
// //also, potentiallly
// //iterate orphaned file attachments to NOTHING type, if found to be attached to any other object remove the orphaned object attachment record in db
// //but keeping the physical file since it's attached to something else
// var AllOrphansInDb = await ct.FileAttachment.Where(z => z.AttachToAType == SockType.NoType).ToListAsync();
// foreach (FileAttachment OrphanInDb in AllOrphansInDb)
// {
// if (await ct.FileAttachment.AnyAsync(z => z.StoredFileName==OrphanInDb.StoredFileName && z.AttachToAType != SockType.NoType))
// {
// //It is also attached to something else so remove it from the nothing type
// ct.FileAttachment.Remove(OrphanInDb);
// await ct.SaveChangesAsync();
// }
// }
// ORPHANED FILES CHECK
var FilesOnDiskNotInDb = AllAttachmentFilesOnDisk.Except(AllDBFileFullPath);
if (FilesOnDiskNotInDb.Count() > 0)
await JobsBiz.LogJobAsync(job.GId, $"Found {FilesOnDiskNotInDb.Count()} physical files not known to be existing Attachments, creating attachment records tied to 'NoType' so they show in UI");
// FOREIGN FILES placed in folder directly outside of attachment system
// user thinks they can just drop them in or accidentally copies them here
// Or, user renames a folder for some reason?
// This is a good reason not to delete them, because they can just un-rename them to fix it
// SWEEPER JOB
// I think it should delete them outright, but maybe that's a bad idea, not sure
// ID them to see if they *should* be one of ours by the file name I guess since it's the only identifying characteristic
// is it in the correct folder which is named based on it's hash?
// Is it X characters long (they all are or not?)
// Does it have an extension? None of ours have an extension
//Vet the file and see if it has the characteristics of an attachment before re-attaching it, if not, compile into a list then log it and notify
//Attach any found into the NOTHING object type with id 0 so they will be represented in attachment list for being dealt with
List<string> ForeignFilesNotLikelyAttachmentsFoundInAttachmentsFolder = new List<string>();
foreach (string orphan in FilesOnDiskNotInDb)
{
if (FileUtil.AppearsToBeAnOrphanedAttachment(orphan))
{
FileAttachment fa = new FileAttachment();
fa.AttachToObjectId = 0;
fa.AttachToAType = SockType.NoType;
fa.ContentType = "application/octet-stream";//most generic type, we don't know what it is
fa.DisplayFileName = "FOUND" + FileUtil.GetSafeDateFileName();
fa.LastModified = DateTime.UtcNow;
fa.Notes = "Found in attachments folder not linked to an object";
fa.StoredFileName = System.IO.Path.GetFileName(orphan);
await ct.FileAttachment.AddAsync(fa);
await ct.SaveChangesAsync();
}
else
{
log.LogDebug($"Foreign file found in attachments folder but doesn't appear to belong {orphan}");
ForeignFilesNotLikelyAttachmentsFoundInAttachmentsFolder.Add(orphan);
}
}
//ok, these files could be important and shouldn't be here so notify and log as much as possible
var ffcount = ForeignFilesNotLikelyAttachmentsFoundInAttachmentsFolder.Count;
if (ffcount > 0)
{
string msg = string.Empty;
if (ffcount < 25)
{
//we can list them all
msg = $"{ffcount} files found in attachments folder that don't appear to belong there:";
}
else
{
msg = $"{ffcount} files found in attachments folder that don't appear to belong there, here are the first 25";
}
log.LogDebug(msg);
await JobsBiz.LogJobAsync(job.GId, msg);
await NotifyEventHelper.AddOpsProblemEvent($"Attachments issue:{msg}");
var outList = ForeignFilesNotLikelyAttachmentsFoundInAttachmentsFolder.Take(25).ToList();
foreach (string s in outList)
{
log.LogDebug(s);
await JobsBiz.LogJobAsync(job.GId, s);
}
}
await JobsBiz.LogJobAsync(job.GId, "Finished.");
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed);
}
catch (Exception ex)
{
log.LogError(ex, "AttachmentMaintenanceJob error during ops");
await JobsBiz.LogJobAsync(job.GId, $"AttachmentMaintenanceJob error during ops\r\n{ex.Message}");
throw;
}
finally
{
log.LogInformation($"AttachmentMaintenanceJob: setting server state back to {wasServerState.ToString()}");
apiServerState.SetState(wasServerState, wasReason);
}
}
//Other job handlers here...
////////////////////////////////////////////////////////////////////////////////////////////////
//DUPLICATE ATTACHMENTS TO NEW OBJECT
//
internal static async Task DuplicateAttachments(SockTypeId aSource, SockTypeId aDest, AyContext ct)
{
var sources = await ct.FileAttachment.AsNoTracking()
.Where(z => z.AttachToAType == aSource.SockType && z.AttachToObjectId == aSource.ObjectId)
.ToListAsync();
if (sources.Count > 0)
{
foreach (var src in sources)
{
ct.FileAttachment.Add(new FileAttachment { AttachToObjectId = aDest.ObjectId, AttachToAType = aDest.SockType, StoredFileName = src.StoredFileName, DisplayFileName = src.DisplayFileName, ContentType = src.ContentType, LastModified = src.LastModified, Notes = src.Notes, Exists = src.Exists, Size = src.Size });
}
await ct.SaveChangesAsync();
}
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,621 @@
using System;
namespace Sockeye.Biz
{
/// <summary>
/// Authorization roles
/// </summary>
[Flags]
public enum AuthorizationRoles : int
{
//https://stackoverflow.com/questions/8447/what-does-the-flags-enum-attribute-mean-in-c
//MAX 31 (2147483647)!!! or will overflow int and needs to be turned into a long
//Must be a power of two: https://en.wikipedia.org/wiki/Power_of_two
///<summary>No role set</summary>
NoRole = 0,
///<summary>BizAdminRestricted</summary>
BizAdminRestricted = 1,
///<summary>BizAdmin</summary>
BizAdmin = 2,
///<summary>ServiceRestricted</summary>
ServiceRestricted = 4,
///<summary>Service</summary>
Service = 8,
///<summary>InventoryRestricted</summary>
InventoryRestricted = 16,
///<summary>Inventory</summary>
Inventory = 32,
///<summary>Accounting</summary>
Accounting = 64,//No limited role, not sure if there is a need
///<summary>TechRestricted</summary>
TechRestricted = 128,
///<summary>Tech</summary>
Tech = 256,
///<summary>SubContractorRestricted</summary>
SubContractorRestricted = 512, //same as tech but restricted by further business rules (more fine grained)
///<summary>SubContractor</summary>
SubContractor = 1024,//same as tech limited but restricted by further business rules (more fine grained)
///<summary>ClientRestricted</summary>
CustomerRestricted = 2048,
///<summary>Client</summary>
Customer = 4096,
///<summary>OpsAdminRestricted</summary>
OpsAdminRestricted = 8192,
///<summary>OpsAdmin</summary>
OpsAdmin = 16384,
///<summary>Sales</summary>
Sales = 32768,
///<summary>SalesRestricted</summary>
SalesRestricted = 65536,
///<summary>Anyone of any role</summary>
All = BizAdminRestricted | BizAdmin | ServiceRestricted | Service | InventoryRestricted |
Inventory | Accounting | TechRestricted | Tech | SubContractorRestricted |
SubContractor | CustomerRestricted | Customer | OpsAdminRestricted | OpsAdmin | Sales | SalesRestricted
// ,AllInsideUserRoles = BizAdminRestricted | BizAdmin | ServiceRestricted | Service | InventoryRestricted |
// Inventory | Accounting | TechRestricted | Tech | SubContractorRestricted |
// SubContractor | Sales | SalesRestricted | OpsAdminRestricted | OpsAdmin
}//end AuthorizationRoles
//, 65536, 131072, 262144, 524288, 1,048,576
}//end namespace GZTW.Sockeye.BLL
/*
### INFO FOR DOCS ####
official names for docs
"AuthorizationRoles": "Authorization roles",
"AuthorizationRoleNoRole": "No role",
"AuthorizationRoleBizAdminRestricted": "Business administration - restricted",
"AuthorizationRoleBizAdmin": "Business administration",
"AuthorizationRoleServiceRestricted": "Service - restricted",
"AuthorizationRoleService": "Service",
"AuthorizationRoleInventoryRestricted": "Inventory - restricted",
"AuthorizationRoleInventory": "Inventory",
"AuthorizationRoleAccounting": "Accounting",
"AuthorizationRoleTechRestricted": "Service technician - restricted",
"AuthorizationRoleTech": "Service technician",
"AuthorizationRoleSubContractorRestricted": "Subcontractor - restricted",
"AuthorizationRoleSubContractor": "Subcontractor",
"AuthorizationRoleCustomerRestricted": "Customer user - restricted",
"AuthorizationRoleCustomer": "Customer user",
"AuthorizationRoleOpsAdminRestricted": "System operations - restricted",
"AuthorizationRoleOpsAdmin": "System operations",
"AuthorizationRoleSalesRestricted": "Sales - restricted",
"AuthorizationRoleSales": "Sales",
v8-beta-0.10 rights by role
{
"data": {
"typeroles": [
{
"sockType": "Backup",
"change": "OpsAdmin",
"readFullRecord": "BizAdminRestricted, BizAdmin, OpsAdminRestricted",
"select": "NoRole"
},
{
"sockType": "BizMetrics",
"change": "BizAdmin",
"readFullRecord": "BizAdminRestricted, Accounting, Sales, SalesRestricted",
"select": "NoRole"
},
{
"sockType": "Contract",
"change": "BizAdmin, Service, Accounting, Tech, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "Customer",
"change": "BizAdmin, Service, Accounting, Tech, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "CustomerNote",
"change": "BizAdmin, Service, Accounting, Tech, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "CustomerServiceRequest",
"change": "BizAdmin, Service, Customer",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, Tech, CustomerRestricted",
"select": "All"
},
{
"sockType": "DataListSavedFilter",
"change": "BizAdmin",
"readFullRecord": "All",
"select": "NoRole"
},
{
"sockType": "FileAttachment",
"change": "BizAdmin",
"readFullRecord": "BizAdminRestricted, BizAdmin",
"select": "NoRole"
},
{
"sockType": "FormCustom",
"change": "BizAdmin",
"readFullRecord": "All",
"select": "NoRole"
},
{
"sockType": "FormUserOptions",
"change": "All",
"readFullRecord": "All",
"select": "NoRole"
},
{
"sockType": "Global",
"change": "BizAdmin",
"readFullRecord": "BizAdminRestricted",
"select": "NoRole"
},
{
"sockType": "GlobalOps",
"change": "OpsAdmin",
"readFullRecord": "OpsAdminRestricted",
"select": "NoRole"
},
{
"sockType": "HeadOffice",
"change": "BizAdmin, Service, Accounting, Tech, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "License",
"change": "BizAdmin",
"readFullRecord": "BizAdminRestricted",
"select": "NoRole"
},
{
"sockType": "LoanUnit",
"change": "BizAdmin, Service, Accounting, Tech, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "LogFile",
"change": "NoRole",
"readFullRecord": "OpsAdminRestricted, OpsAdmin",
"select": "NoRole"
},
{
"sockType": "Memo",
"change": "BizAdminRestricted, BizAdmin, ServiceRestricted, Service, InventoryRestricted, Inventory, Accounting, TechRestricted, Tech, SubContractorRestricted, SubContractor, OpsAdminRestricted, OpsAdmin, Sales, SalesRestricted",
"readFullRecord": "BizAdminRestricted, BizAdmin, ServiceRestricted, Service, InventoryRestricted, Inventory, Accounting, TechRestricted, Tech, SubContractorRestricted, SubContractor, OpsAdminRestricted, OpsAdmin, Sales, SalesRestricted",
"select": "BizAdminRestricted, BizAdmin, ServiceRestricted, Service, InventoryRestricted, Inventory, Accounting, TechRestricted, Tech, SubContractorRestricted, SubContractor, OpsAdminRestricted, OpsAdmin, Sales, SalesRestricted"
},
{
"sockType": "Notification",
"change": "All",
"readFullRecord": "All",
"select": "NoRole"
},
{
"sockType": "NotifySubscription",
"change": "All",
"readFullRecord": "All",
"select": "NoRole"
},
{
"sockType": "OpsNotificationSettings",
"change": "OpsAdmin",
"readFullRecord": "BizAdminRestricted, BizAdmin, OpsAdminRestricted",
"select": "NoRole"
},
{
"sockType": "Part",
"change": "BizAdmin, Inventory, Accounting",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, Service, InventoryRestricted",
"select": "All"
},
{
"sockType": "PartAssembly",
"change": "BizAdmin, Inventory, Accounting",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, Service, InventoryRestricted",
"select": "All"
},
{
"sockType": "PartInventory",
"change": "BizAdmin, Inventory, Accounting",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, Service, InventoryRestricted",
"select": "All"
},
{
"sockType": "PartInventoryDataList",
"change": "BizAdmin, Inventory, Accounting",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, Service, InventoryRestricted",
"select": "All"
},
{
"sockType": "PartInventoryRequest",
"change": "BizAdmin, Inventory, Accounting",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, Service, InventoryRestricted",
"select": "All"
},
{
"sockType": "PartInventoryRequestDataList",
"change": "BizAdmin, Inventory, Accounting",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, Service, InventoryRestricted",
"select": "All"
},
{
"sockType": "PartInventoryRestock",
"change": "BizAdmin, Inventory, Accounting",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, Service, InventoryRestricted",
"select": "All"
},
{
"sockType": "PartWarehouse",
"change": "BizAdmin, Inventory, Accounting",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, Service, InventoryRestricted",
"select": "All"
},
{
"sockType": "PickListTemplate",
"change": "BizAdmin",
"readFullRecord": "All",
"select": "NoRole"
},
{
"sockType": "PM",
"change": "BizAdmin, Service",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "PMItem",
"change": "BizAdmin, Service",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "PMItemExpense",
"change": "BizAdmin, Service",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "PMItemLabor",
"change": "BizAdmin, Service",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "PMItemLoan",
"change": "BizAdmin, Service",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "PMItemOutsideService",
"change": "BizAdmin, Service",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "PMItemPart",
"change": "BizAdmin, Service",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "PMItemScheduledUser",
"change": "BizAdmin, Service",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "PMItemTask",
"change": "BizAdmin, Service",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "PMItemTravel",
"change": "BizAdmin, Service",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "PMItemUnit",
"change": "BizAdmin, Service",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "Project",
"change": "BizAdmin, Service, Accounting, Tech, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "PurchaseOrder",
"change": "BizAdmin, Inventory, Accounting",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, Service, InventoryRestricted",
"select": "All"
},
{
"sockType": "Quote",
"change": "BizAdmin, Service, Accounting, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "QuoteItem",
"change": "BizAdmin, Service, Accounting, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "QuoteItemExpense",
"change": "BizAdmin, Service, Accounting, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "QuoteItemLabor",
"change": "BizAdmin, Service, Accounting, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "QuoteItemLoan",
"change": "BizAdmin, Service, Accounting, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "QuoteItemOutsideService",
"change": "BizAdmin, Service, Accounting, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "QuoteItemPart",
"change": "BizAdmin, Service, Accounting, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "QuoteItemScheduledUser",
"change": "BizAdmin, Service, Accounting, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "QuoteItemTask",
"change": "BizAdmin, Service, Accounting, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "QuoteItemTravel",
"change": "BizAdmin, Service, Accounting, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "QuoteItemUnit",
"change": "BizAdmin, Service, Accounting, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "QuoteStatus",
"change": "BizAdmin, Service, Sales",
"readFullRecord": "All",
"select": "All"
},
{
"sockType": "Reminder",
"change": "BizAdminRestricted, BizAdmin, ServiceRestricted, Service, InventoryRestricted, Inventory, Accounting, TechRestricted, Tech, SubContractorRestricted, SubContractor, OpsAdminRestricted, OpsAdmin, Sales, SalesRestricted",
"readFullRecord": "BizAdminRestricted, BizAdmin, ServiceRestricted, Service, InventoryRestricted, Inventory, Accounting, TechRestricted, Tech, SubContractorRestricted, SubContractor, OpsAdminRestricted, OpsAdmin, Sales, SalesRestricted",
"select": "BizAdminRestricted, BizAdmin, ServiceRestricted, Service, InventoryRestricted, Inventory, Accounting, TechRestricted, Tech, SubContractorRestricted, SubContractor, OpsAdminRestricted, OpsAdmin, Sales, SalesRestricted"
},
{
"sockType": "Report",
"change": "BizAdminRestricted, BizAdmin",
"readFullRecord": "All",
"select": "All"
},
{
"sockType": "Review",
"change": "BizAdminRestricted, BizAdmin, ServiceRestricted, Service, InventoryRestricted, Inventory, Accounting, TechRestricted, Tech, SubContractorRestricted, SubContractor, OpsAdminRestricted, OpsAdmin, Sales, SalesRestricted",
"readFullRecord": "BizAdminRestricted, BizAdmin, ServiceRestricted, Service, InventoryRestricted, Inventory, Accounting, TechRestricted, Tech, SubContractorRestricted, SubContractor, OpsAdminRestricted, OpsAdmin, Sales, SalesRestricted",
"select": "BizAdminRestricted, BizAdmin, ServiceRestricted, Service, InventoryRestricted, Inventory, Accounting, TechRestricted, Tech, SubContractorRestricted, SubContractor, OpsAdminRestricted, OpsAdmin, Sales, SalesRestricted"
},
{
"sockType": "ServerJob",
"change": "OpsAdmin",
"readFullRecord": "BizAdminRestricted, BizAdmin, OpsAdminRestricted",
"select": "NoRole"
},
{
"sockType": "ServerMetrics",
"change": "OpsAdmin",
"readFullRecord": "OpsAdminRestricted, OpsAdmin",
"select": "NoRole"
},
{
"sockType": "ServerState",
"change": "OpsAdmin",
"readFullRecord": "All",
"select": "NoRole"
},
{
"sockType": "ServiceRate",
"change": "BizAdmin, Service, Accounting",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, Service, Tech, Sales",
"select": "All"
},
{
"sockType": "TaskGroup",
"change": "BizAdmin, Service",
"readFullRecord": "All",
"select": "All"
},
{
"sockType": "TaxCode",
"change": "BizAdmin, Accounting",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, Service, TechRestricted, Tech, Sales, SalesRestricted",
"select": "All"
},
{
"sockType": "Translation",
"change": "BizAdmin",
"readFullRecord": "BizAdminRestricted",
"select": "All"
},
{
"sockType": "TravelRate",
"change": "BizAdmin, Service, Accounting",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, Service, Tech, Sales",
"select": "All"
},
{
"sockType": "TrialSeeder",
"change": "BizAdmin, OpsAdmin",
"readFullRecord": "BizAdminRestricted, OpsAdminRestricted",
"select": "NoRole"
},
{
"sockType": "Unit",
"change": "BizAdmin, Service, Accounting, Tech, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "UnitMeterReading",
"change": "BizAdmin, Service, Accounting, Tech, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "UnitModel",
"change": "BizAdmin, Service, Accounting, Tech, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "User",
"change": "BizAdmin",
"readFullRecord": "BizAdminRestricted",
"select": "All"
},
{
"sockType": "UserOptions",
"change": "BizAdmin",
"readFullRecord": "BizAdminRestricted",
"select": "NoRole"
},
{
"sockType": "Vendor",
"change": "BizAdmin, Service, Inventory, Accounting, Tech, Sales",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SalesRestricted",
"select": "All"
},
{
"sockType": "WorkOrder",
"change": "BizAdmin, Service, Accounting, TechRestricted, Tech, SubContractorRestricted, SubContractor",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SubContractorRestricted, Sales, SalesRestricted",
"select": "All"
},
{
"sockType": "WorkOrderItem",
"change": "BizAdmin, Service, Accounting, Tech",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SubContractorRestricted, Sales, SalesRestricted",
"select": "All"
},
{
"sockType": "WorkOrderItemExpense",
"change": "BizAdmin, Service, Accounting, TechRestricted, Tech",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SubContractorRestricted, Sales, SalesRestricted",
"select": "All"
},
{
"sockType": "WorkOrderItemLabor",
"change": "BizAdmin, Service, Accounting, TechRestricted, Tech, SubContractorRestricted, SubContractor",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SubContractorRestricted, Sales, SalesRestricted",
"select": "All"
},
{
"sockType": "WorkOrderItemLoan",
"change": "BizAdmin, Service, Accounting, Tech",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SubContractor, Sales, SalesRestricted",
"select": "All"
},
{
"sockType": "WorkOrderItemOutsideService",
"change": "BizAdmin, Service, Accounting, Tech",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, Sales, SalesRestricted",
"select": "All"
},
{
"sockType": "WorkOrderItemPart",
"change": "BizAdmin, Service, Accounting, Tech",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SubContractor, Sales, SalesRestricted",
"select": "All"
},
{
"sockType": "WorkOrderItemPartRequest",
"change": "BizAdmin, Service, Accounting, Tech",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SubContractor, Sales, SalesRestricted",
"select": "All"
},
{
"sockType": "WorkOrderItemPriority",
"change": "BizAdmin, Service, Accounting, Tech, SubContractor",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SubContractorRestricted, Sales, SalesRestricted",
"select": "All"
},
{
"sockType": "WorkOrderItemScheduledUser",
"change": "BizAdmin, Service, Accounting, Tech",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SubContractorRestricted, SubContractor, Sales, SalesRestricted",
"select": "All"
},
{
"sockType": "WorkOrderItemStatus",
"change": "BizAdmin, Service, Accounting, Tech, SubContractor",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SubContractorRestricted, Sales, SalesRestricted",
"select": "All"
},
{
"sockType": "WorkOrderItemTask",
"change": "BizAdmin, Service, Accounting, TechRestricted, Tech, SubContractorRestricted, SubContractor",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SubContractorRestricted, Sales, SalesRestricted",
"select": "All"
},
{
"sockType": "WorkOrderItemTravel",
"change": "BizAdmin, Service, Accounting, TechRestricted, Tech, SubContractorRestricted, SubContractor",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SubContractorRestricted, Sales, SalesRestricted",
"select": "All"
},
{
"sockType": "WorkOrderItemUnit",
"change": "BizAdmin, Service, Accounting, Tech",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SubContractor, Sales, SalesRestricted",
"select": "All"
},
{
"sockType": "WorkOrderStatus",
"change": "BizAdmin, Service, Accounting, Tech, SubContractor",
"readFullRecord": "BizAdminRestricted, ServiceRestricted, TechRestricted, SubContractorRestricted, Sales, SalesRestricted",
"select": "All"
}
]
}
}
*/

101
server/biz/BizObject.cs Normal file
View File

@@ -0,0 +1,101 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Sockeye.Biz
{
/// <summary>
/// Business object base class
/// </summary>
internal abstract class BizObject : IBizObject
{
public BizObject()
{
}
#region common props
internal SockType BizType { get; set; }
internal Sockeye.Models.AyContext ct { get; set; }
internal long UserId { get; set; }
internal long UserTranslationId { get; set; }
internal AuthorizationRoles CurrentUserRoles { get; set; }
#endregion
internal async Task<string> Translate(string key)
{
return await TranslationBiz.GetTranslationStaticAsync(key, UserTranslationId, ct);
}
#region Error handling
private readonly List<ValidationError> _errors = new List<ValidationError>();
public List<ValidationError> Errors => _errors;
public bool HasErrors => _errors.Any();
public void ClearErrors() => _errors.Clear();
// public void AddvalidationError(ValidationError validationError)
// {
// _errors.Add(validationError);
// }
public bool PropertyHasErrors(string propertyName)
{
if (_errors.Count == 0) return false;
var v = _errors.FirstOrDefault(m => m.Target == propertyName);
return (v != null);
}
public void AddError(ApiErrorCode errorCode, string propertyName = "generalerror", string errorMessage = null)
{
//if Target is generalerror that means show in UI in general error box of form
_errors.Add(new ValidationError() { Code = errorCode, Message = errorMessage, Target = propertyName });
}
//TODO: CHILD COLLECTION MOD add error version for indexed child
// //Add a bunch of errors, generally from a child object failed operastion
// public void AddErrors(List<ValidationError> errors)
// {
// _errors.AddRange(errors);
// }
public string GetErrorsAsString()
{
if (!HasErrors) return string.Empty;
StringBuilder sb = new StringBuilder();
// sb.AppendLine("LT:Errors - ");
foreach (ValidationError e in _errors)
{
var msg = $"LT:{ApiErrorCodeStockMessage.GetTranslationCodeForApiErrorCode(e.Code)}";
if (!string.IsNullOrWhiteSpace(e.Message))
msg += $", {e.Message}";
if (!string.IsNullOrWhiteSpace(e.Target) && e.Target != "generalerror")
msg += $", field: {e.Target}";
sb.AppendLine(msg);
}
return sb.ToString();
}
#endregion error handling
}//eoc
}//eons

View File

@@ -0,0 +1,75 @@
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Sockeye.Models;
namespace Sockeye.Biz
{
//THIS IS USED BY THE ATTACHMENT CONTROLLER
//IN THEORY WE ONLY NEED TO CHECK FOR ATTACHABLE TYPES, BUT I CAN SEE IT'S POTENTIAL USEFULNESS DOWN THE ROAD FOR OTHER THINGS
internal static class BizObjectExistsInDatabase
{
//Returns existance status of object type and id specified in database
internal static async Task<bool> ExistsAsync(SockType aType, long id, AyContext ct)
{
//new up a context??
switch (aType)
{
//CoreBizObject add here
case SockType.NoType://no type always exists and this is used by orphaned attachments
return true;
case SockType.FileAttachment:
return await ct.FileAttachment.AnyAsync(z => z.Id == id);
case SockType.DataListSavedFilter:
return await ct.DataListSavedFilter.AnyAsync(z => z.Id == id);
case SockType.FormCustom:
return await ct.FormCustom.AnyAsync(z => z.Id == id);
case SockType.User:
return await ct.User.AnyAsync(z => z.Id == id);
case SockType.Customer:
return await ct.Customer.AnyAsync(z => z.Id == id);
case SockType.CustomerNote:
return await ct.CustomerNote.AnyAsync(z => z.Id == id);
case SockType.HeadOffice:
return await ct.HeadOffice.AnyAsync(z => z.Id == id);
case SockType.Memo:
return await ct.Memo.AnyAsync(z => z.Id == id);
case SockType.Report:
return await ct.Report.AnyAsync(z => z.Id == id);
case SockType.Reminder:
return await ct.Reminder.AnyAsync(z => z.Id == id);
case SockType.Review:
return await ct.Review.AnyAsync(z => z.Id == id);
case SockType.CustomerNotifySubscription:
return await ct.CustomerNotifySubscription.AnyAsync(z => z.Id == id);
case SockType.Integration:
return await ct.Integration.AnyAsync(z => z.Id == id);
default:
throw new System.NotSupportedException($"Sockeye.Biz.BizObjectExistsInDatabase::ExistsAsync type {aType.ToString()} is not supported");
}
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,70 @@
using Sockeye.Util;
using Sockeye.Models;
namespace Sockeye.Biz
{
internal static class BizObjectFactory
{
//Returns the biz object class that corresponds to the type presented
//Used by SEARCH, REPORTING and objects with JOBS
internal static BizObject GetBizObject(SockType sockType,
AyContext ct,
long userId,
AuthorizationRoles roles,
long translationId = 0)
{
if (translationId == 0)
translationId = ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID;
switch (sockType)
{
//CoreBizObject add here
case SockType.ServerJob:
return new JobOperationsBiz(ct, userId, roles);
case SockType.Translation:
return new TranslationBiz(ct, userId, translationId, roles);
case SockType.DataListSavedFilter:
return new DataListSavedFilterBiz(ct, userId, translationId, roles);
case SockType.FormCustom:
return new FormCustomBiz(ct, userId, translationId, roles);
case SockType.FileAttachment:
return new AttachmentBiz(ct, userId, roles);
case SockType.Customer:
return new CustomerBiz(ct, userId, translationId, roles);
case SockType.CustomerNote:
return new CustomerNoteBiz(ct, userId, translationId, roles);
case SockType.User:
return new UserBiz(ct, userId, translationId, roles);
case SockType.Memo:
return new MemoBiz(ct, userId, translationId, roles);
case SockType.HeadOffice:
return new HeadOfficeBiz(ct, userId, translationId, roles);
case SockType.Reminder:
return new ReminderBiz(ct, userId, translationId, roles);
case SockType.Review:
return new ReviewBiz(ct, userId, translationId, roles);
case SockType.CustomerNotifySubscription:
return new CustomerNotifySubscriptionBiz(ct, userId, translationId, roles);
case SockType.Report:
return new ReportBiz(ct, userId, translationId, roles);
default:
throw new System.NotSupportedException($"Sockeye.BLL.BizObjectFactory::GetBizObject type {sockType.ToString()} is not supported");
}
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,54 @@
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
namespace Sockeye.Biz
{
//Turn a type and ID into a displayable name
//Used by search and eventlog processor
internal static class BizObjectNameFetcherDirect
{
internal static string Name(SockType sockType, long id, long translationid, System.Data.Common.DbCommand cmd)
{
try
{
string ret;
cmd.CommandText = $"select PUBLIC.AYGETNAME({id}, {(int)sockType}, {translationid}) as m";
using (var dr = cmd.ExecuteReader())
{
if (dr.Read())
{
if (dr.IsDBNull(0))
ret = $"?? type:{sockType},id:{id}";
else
ret = dr.GetString(0);
}
else
{
ret = "-";
}
//return dr.Read() ? dr.GetString(0) : "-";
}
return ret;
}
catch
{
((ILogger)Sockeye.Util.ApplicationLogging.CreateLogger("BizObjectNameFetcherDirect")).LogError($"### Error fetching for type {sockType}");
throw;
}
}
//warning: use the above in a loop, not this one
internal static string Name(SockType sockType, long id, long translationId, Sockeye.Models.AyContext ct)
{
using (var command = ct.Database.GetDbConnection().CreateCommand())
{
ct.Database.OpenConnection();
return Name(sockType, id,translationId, command);
}
}
}//eoc
}//eons

15
server/biz/BizRoleSet.cs Normal file
View File

@@ -0,0 +1,15 @@
namespace Sockeye.Biz
{
/// <summary>
/// This is a set of roles to be stored in the central BizRoles with a key for each object type
/// </summary>
public class BizRoleSet
{
public AuthorizationRoles Change { get; set; }
public AuthorizationRoles ReadFullRecord { get; set; }
public AuthorizationRoles Select { get; set; }
}//eoc
}//eons

581
server/biz/BizRoles.cs Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
using System;
namespace Sockeye.Biz
{
/// <summary>
/// Marker attribute indicating that an object is a core business object
/// In other words a real world business relevant object and not a utility or internal type of object
/// something that would be worked with by users in the UI for business purposes
/// This is used to indicate that an object supports these features:
/// PickList selectable (has picklist)
/// Attachable (can attach to it)
/// Reviewable (can set a review on it)
/// ETC
/// Used in <see cref="SockType"/>
/// </summary>
[AttributeUsage(AttributeTargets.All)]
public class CoreBizObjectAttribute : Attribute
{
//No code required, it's just a marker
//https://docs.microsoft.com/en-us/dotnet/standard/attributes/writing-custom-attributes
}
}//eons

View File

@@ -0,0 +1,40 @@
using System.Collections.Generic;
namespace Sockeye.Biz
{
public static class CustomFieldType
{
private static List<int> _validCustomFieldTypes = new List<int>();
static CustomFieldType()
{
//v7 custom field types:
// - Currency
// - DateAndTime
// - TimeOnly
// - DateOnly
// - Number
// - Text
// - Bool
_validCustomFieldTypes.Add((int)UiFieldDataType.Currency);
_validCustomFieldTypes.Add((int)UiFieldDataType.Date);
_validCustomFieldTypes.Add((int)UiFieldDataType.Time);
_validCustomFieldTypes.Add((int)UiFieldDataType.DateTime);
_validCustomFieldTypes.Add((int)UiFieldDataType.Text);
_validCustomFieldTypes.Add((int)UiFieldDataType.Decimal);
_validCustomFieldTypes.Add((int)UiFieldDataType.Integer);
_validCustomFieldTypes.Add((int)UiFieldDataType.Bool);
}
public static List<int> ValidCustomFieldTypes
{
get
{
return _validCustomFieldTypes;
}
}
}
}

View File

@@ -0,0 +1,121 @@
using Sockeye.Models;
using System.Linq;
using Newtonsoft.Json.Linq;
namespace Sockeye.Biz
{
internal static class CustomFieldsValidator
{
internal static void Validate(BizObject biz, FormCustom formCustom, string customFields)
{
bool hasCustomData = !string.IsNullOrWhiteSpace(customFields);
//No form custom = no template to check against so nothing to do
if (formCustom == null)
return;
var FormTemplate = JArray.Parse(formCustom.Template);
var ThisFormCustomFieldsList = FormFieldOptionalCustomizableReference.FormFieldReferenceList(formCustom.FormKey).Where(z => z.IsCustomField == true).Select(z => z.TKey).ToList();
//If the customFields string is empty then only validation is if any of the fields are required to be filled in
if (!hasCustomData)
{
//iterate the template
for (int i = 0; i < FormTemplate.Count; i++)
{
//get the field customization
var fldKey = FormTemplate[i]["fld"].Value<string>();
var fldRequired = FormTemplate[i]["required"].Value<bool>();
//Check if this is an expected custom field and that it was set to required
if (ThisFormCustomFieldsList.Contains(fldKey) && fldRequired == true)
{
//Ok, this field is required but custom fields are all empty so add this error
biz.AddError(ApiErrorCode.VALIDATION_CUSTOM_REQUIRED_EMPTY, fldKey);
}
}
return;
}
//here we have both a bunch of custom fields presumeably and a form customization so let's get cracking...
//parse the custom fields, it should contain an object with 16 keys
//NOTE: to save bandwidth the actual custom fields look like this:
// - {c1:"blah",c2:"blah",c3:"blah".....c16:"blah"}
//However the LT field names might be WidgetCustom1 or UserCustom16 so we need to translate by EndsWith
//Top level object is a Object not an array when it comes to custom fields and the key names are the custom fields abbreviated
var CustomFieldData = JObject.Parse(customFields);
//make sure all the *required* keys are present
foreach (string iFldKey in ThisFormCustomFieldsList)
{
//Translate the LT field key to the actual customFieldData field key
var InternalCustomFieldName = FormFieldOptionalCustomizableReference.TranslateLTCustomFieldToInternalCustomFieldName(iFldKey);
//Check if it's set to required
var isRequired = CustomFieldIsSetToRequired(FormTemplate, iFldKey);
//if it's not required then we don't care, jump to the next item...
if (!isRequired)
continue;
//It's required, make sure the key is present and contains data
if (CustomFieldData.ContainsKey(InternalCustomFieldName))
{
//validate for now that the custom fields set as required have data in them. Note that we are not validating the sanity of the values, only that they exist
//trying to build in slack for when users inevitably change the TYPE of the custom field
//Maybe in future this will be handled more thoroughly here but for now just make sure it's been filled in
//validate it
string CurrentValue = CustomFieldData[InternalCustomFieldName].Value<string>();
if (string.IsNullOrWhiteSpace(CurrentValue))
{
biz.AddError(ApiErrorCode.VALIDATION_CUSTOM_REQUIRED_EMPTY, iFldKey);
}
// foreach (JObject jo in FormTemplate)
// {
// if (jo["fld"].Value<string>() == iFldKey)
// {
// var fldRequired = jo["required"].Value<bool>();
// if (fldRequired && string.IsNullOrWhiteSpace(CurrentValue))
// {
// biz.AddError(ApiErrorCode.VALIDATION_CUSTOM_REQUIRED_EMPTY, iFldKey);
// }
// break;
// }
// }
}
else
{
//This is a serious issue and invalidates all
biz.AddError(ApiErrorCode.VALIDATION_MISSING_PROPERTY, iFldKey);
}
}
}
/// <summary>
/// Check if field is required
/// </summary>
/// <param name="FormTemplate"></param>
/// <param name="FieldKey"></param>
/// <returns></returns>
private static bool CustomFieldIsSetToRequired(JArray FormTemplate, string FieldKey)
{
foreach (JObject jo in FormTemplate)
{
if (jo["fld"].Value<string>() == FieldKey)
{
return jo["required"].Value<bool>();
}
}
return false;
}
}//eoc
}//ens

668
server/biz/CustomerBiz.cs Normal file
View File

@@ -0,0 +1,668 @@
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.Linq;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Sockeye.Biz
{
internal class CustomerBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject, IImportAbleObject, INotifiableObject
{
internal CustomerBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = SockType.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
return new CustomerBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExistsAsync(long id)
{
return await ct.Customer.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<Customer> CreateAsync(Customer newObject)
{
await ValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.Customer.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, SockEvent.Created), ct);
await SearchIndexAsync(newObject, true);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await HandlePotentialNotificationEvent(SockEvent.Created, newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//GET
//
internal async Task<Customer> GetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.Customer.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, SockEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<Customer> PutAsync(Customer putObject)
{
var dbObject = await GetAsync(putObject.Id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
putObject.Tags = TagBiz.NormalizeTags(putObject.Tags);
putObject.CustomFields = JsonUtil.CompactJson(putObject.CustomFields);
await ValidateAsync(putObject, dbObject);
if (HasErrors) return null;
ct.Replace(dbObject, putObject);
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, putObject.Id, BizType, SockEvent.Modified), ct);
await SearchIndexAsync(putObject, false);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
await HandlePotentialNotificationEvent(SockEvent.Modified, putObject, dbObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> DeleteAsync(long id)
{
using (var transaction = await ct.Database.BeginTransactionAsync())
{
Customer dbObject = await GetAsync(id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND);
return false;
}
await ValidateCanDeleteAsync(dbObject);
if (HasErrors)
return false;
//DELETE DIRECT CHILD / RELATED 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)
//Also these are done through their biz objects as there are notification, search and other concerns to be handled
{
var IDList = await ct.User.AsNoTracking().Where(z => z.CustomerId == id).Select(z => z.Id).ToListAsync();
if (IDList.Count() > 0)
{
UserBiz b = new UserBiz(ct, UserId, UserTranslationId, CurrentUserRoles);
foreach (long ItemId in IDList)
if (!await b.DeleteAsync(ItemId, transaction))
{
AddError(ApiErrorCode.CHILD_OBJECT_ERROR, null, $"CustomerContact [{ItemId}]: {b.GetErrorsAsString()}");
return false;
}
}
}
{
var IDList = await ct.CustomerNote.AsNoTracking().Where(z => z.CustomerId == id).Select(z => z.Id).ToListAsync();
if (IDList.Count() > 0)
{
CustomerNoteBiz b = new CustomerNoteBiz(ct, UserId, UserTranslationId, CurrentUserRoles);
foreach (long ItemId in IDList)
if (!await b.DeleteAsync(ItemId, transaction))
{
AddError(ApiErrorCode.CHILD_OBJECT_ERROR, null, $"CustomerNote [{ItemId}]: {b.GetErrorsAsString()}");
return false;
}
}
}
{
var IDList = await ct.Review.AsNoTracking().Where(x => x.SockType == SockType.Customer && x.ObjectId == id).Select(x => x.Id).ToListAsync();
if (IDList.Count() > 0)
{
ReviewBiz b = new ReviewBiz(ct, UserId, UserTranslationId, CurrentUserRoles);
foreach (long ItemId in IDList)
if (!await b.DeleteAsync(ItemId, transaction))
{
AddError(ApiErrorCode.CHILD_OBJECT_ERROR, null, $"Review [{ItemId}]: {b.GetErrorsAsString()}");
return false;
}
}
}
ct.Customer.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
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);
await transaction.CommitAsync();
await HandlePotentialNotificationEvent(SockEvent.Deleted, dbObject);
return true;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//GET LIST FOR QBI MAPPING
//
internal async Task<List<NameIdActiveItem>> GetNameIdActiveItemsAsync()
{
return await ct.Customer.AsNoTracking().OrderBy(x => x.Name).Select(x => new NameIdActiveItem { Name = x.Name, Id = x.Id, Active = x.Active }).ToListAsync();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//SEARCH
//
private async Task SearchIndexAsync(Customer obj, bool isNew)
{
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType);
DigestSearchText(obj, SearchParams);
if (isNew)
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
else
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
}
public async Task<Search.SearchIndexProcessObjectParameters> GetSearchResultSummary(long id, SockType specificType)
{
var obj = await GetAsync(id, false);
var SearchParams = new Search.SearchIndexProcessObjectParameters();
DigestSearchText(obj, SearchParams);
return SearchParams;
}
public void DigestSearchText(Customer obj, Search.SearchIndexProcessObjectParameters searchParams)
{
if (obj != null)
searchParams.AddText(obj.Notes)
.AddText(obj.Name)
.AddText(obj.Wiki)
.AddText(obj.Tags)
.AddText(obj.WebAddress)
.AddText(obj.AlertNotes)
.AddText(obj.TechNotes)
.AddText(obj.AccountNumber)
.AddText(obj.Phone1)
.AddText(obj.Phone2)
.AddText(obj.Phone3)
.AddText(obj.Phone4)
.AddText(obj.Phone5)
.AddText(obj.EmailAddress)
.AddText(obj.PostAddress)
.AddText(obj.PostCity)
.AddText(obj.PostRegion)
.AddText(obj.PostCountry)
.AddText(obj.PostCode)
.AddText(obj.Address)
.AddText(obj.City)
.AddText(obj.Region)
.AddText(obj.Country)
.AddText(obj.AddressPostal)
.AddCustomFields(obj.CustomFields);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task ValidateAsync(Customer proposedObj, Customer currentObj)
{
bool isNew = currentObj == null;
//Name required
if (string.IsNullOrWhiteSpace(proposedObj.Name))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name");
//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(z => z.Name == proposedObj.Name && z.Id != proposedObj.Id))
{
AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name");
}
}
if (proposedObj.BillHeadOffice && (proposedObj.HeadOfficeId == null || proposedObj.HeadOfficeId == 0))
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "HeadOfficeId");
}
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == SockType.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);
}
}
private async Task ValidateCanDeleteAsync(Customer inObj)
{
//## NOTE: contact isn't so important, this could be changed to check only more important things like workorders etc
//and just attempt to delete all the contacts if possible, but for now....
//The plan is anything that is "in" the same form is automatically deleted (unless really critical)
//However, anything that you select on "another" form is not automatically deleted and is allowed to trigger a ref integrity check
//So, for a customer, the Customer Notes collection is accessed from within the Customer form, even though it's actually external but user doesn't see it that way
//so they would be deleted automatically, same goes for Contacts (if they can be deleted and don't have connections elsewhere)
//Workorders and things that you select a Customer for would trigger an error and NOT be deleted automatically
//The Mass delete Extension will be used in those cases to clear out all the workorders etc
//FOREIGN KEY CHECKS
if (await ct.User.AnyAsync(m => m.CustomerId == inObj.Id))
AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("User"));
// await Task.CompletedTask;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//REPORTING
//
public async Task<JArray> GetReportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
{
var idList = dataListSelectedRequest.SelectedRowIds;
JArray ReportData = new JArray();
while (idList.Any())
{
var batch = idList.Take(IReportAbleObject.REPORT_DATA_BATCH_SIZE);
idList = idList.Skip(IReportAbleObject.REPORT_DATA_BATCH_SIZE).ToArray();
//query for this batch, comes back in db natural order unfortunately
var batchResults = await ct.Customer.AsNoTracking().Where(z => batch.Contains(z.Id)).ToArrayAsync();
//order the results back into original
//What is happening here:
//for performance the query is batching a bunch at once by fetching a block of items from the sql server
//however it's returning in db order which is often not the order the id list is in
//so it needs to be sorted back into the same order as the ide list
//This would not be necessary if just fetching each one at a time individually (like in workorder get report data)
var orderedList = from id in batch join z in batchResults on id equals z.Id select z;
batchResults = null;
foreach (Customer w in orderedList)
{
if (!ReportRenderManager.KeepGoing(jobId)) return null;
await PopulateVizFields(w);
var jo = JObject.FromObject(w);
if (!JsonUtil.JTokenIsNullOrEmpty(jo["CustomFields"]))
jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]);
ReportData.Add(jo);
}
orderedList = null;
}
vc.Clear();
return ReportData;
}
private VizCache vc = new VizCache();
//populate viz fields from provided object
private async Task PopulateVizFields(Customer o)
{
if (o.HeadOfficeId != null)
{
if (!vc.Has("headoffice", o.HeadOfficeId))
{
vc.Add(await ct.HeadOffice.AsNoTracking().Where(x => x.Id == o.HeadOfficeId).Select(x => x.Name).FirstOrDefaultAsync(), "headoffice", o.HeadOfficeId);
}
o.HeadOfficeViz = vc.Get("headoffice", o.HeadOfficeId);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// IMPORT EXPORT
//
public async Task<JArray> GetExportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
{
return await GetReportData(dataListSelectedRequest, jobId);
}
public async Task<List<string>> ImportData(AyImportData importData)
{
List<string> ImportResult = new List<string>();
string ImportTag = ImportUtil.GetImportTag();
//ignore these fields
var jsset = JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = new Sockeye.Util.JsonUtil.ShouldSerializeContractResolver(new string[] { "Concurrency", "Id", "CustomFields" }) });
foreach (JObject j in importData.Data)
{
try
{
long? ImportHeadOfficeId = -1;
if (j["HeadOfficeViz"] != null)
{
ImportHeadOfficeId = null;
if (!JsonUtil.JTokenIsNullOrEmpty(j["HeadOfficeViz"]))
{
ImportHeadOfficeId = await ct.HeadOffice.AsNoTracking().Where(z => z.Name == (string)j["HeadOfficeViz"]).Select(x => x.Id).FirstOrDefaultAsync();
if (ImportHeadOfficeId == 0)
AddError(ApiErrorCode.NOT_FOUND, "HeadOfficeViz", $"'{(string)j["HeadOfficeViz"]}'");
}
}
long existingId = await ct.Customer.AsNoTracking().Where(z => z.Name == (string)j["Name"]).Select(x => x.Id).FirstOrDefaultAsync();
if (existingId == 0)
{
if (importData.DoImport)
{
//import this record
var Target = j.ToObject<Customer>(jsset);
Target.Tags.Add(ImportTag);
//Set linked objects
if (Target.BillHeadOffice)
{
if (ImportHeadOfficeId != -1)
Target.HeadOfficeId = ImportHeadOfficeId;
else
AddError(ApiErrorCode.VALIDATION_REQUIRED, "HeadOfficeViz", "(BillHeadOffice=true)");
}
var res = await CreateAsync(Target);
if (res == null)
{
ImportResult.Add($"❌ {Target.Name}\r\n{this.GetErrorsAsString()}");
this.ClearErrors();
}
else
{
ImportResult.Add($"✔️ {Target.Name}");
}
}
}
else
{
if (importData.DoUpdate)
{
//update this record with any data provided
//load existing record
var Target = await GetAsync((long)existingId);
var Source = j.ToObject<Customer>(jsset);
var propertiesToUpdate = j.Properties().Select(p => p.Name).ToList();
propertiesToUpdate.Remove("Name");
ImportUtil.Update(Source, Target, propertiesToUpdate);
if (Target.BillHeadOffice)
{
if (ImportHeadOfficeId != -1)
Target.HeadOfficeId = ImportHeadOfficeId;
else
AddError(ApiErrorCode.VALIDATION_REQUIRED, "HeadOfficeViz", "(BillHeadOffice=true)");
}
var res = await PutAsync(Target);
if (res == null)
{
ImportResult.Add($"❌ {Target.Name} - {this.GetErrorsAsString()}");
this.ClearErrors();
}
else
{
ImportResult.Add($"✔️ {Target.Name}");
}
}
}
}
catch (Exception ex)
{
ImportResult.Add($"❌ Exception processing import\n record:{j.ToString()}\nError:{ex.Message}\nSource:{ex.Source}\nStack:{ex.StackTrace.ToString()}");
}
}
return ImportResult;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//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.BatchCoreObjectOperation:
await ProcessBatchJobAsync(job);
break;
default:
throw new System.ArgumentOutOfRangeException($"CustomerBiz.HandleJob-> Invalid job type{job.JobType.ToString()}");
}
}
private async Task ProcessBatchJobAsync(OpsJob job)
{
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running);
await JobsBiz.LogJobAsync(job.GId, $"LT:StartJob {job.SubType}");
List<long> idList = new List<long>();
long FailedObjectCount = 0;
JObject jobData = JObject.Parse(job.JobInfo);
if (jobData.ContainsKey("idList"))
idList = ((JArray)jobData["idList"]).ToObject<List<long>>();
else
idList = await ct.Customer.AsNoTracking().Select(z => z.Id).ToListAsync();
bool SaveIt = false;
//---------------------------------
//case 4192
TimeSpan ProgressAndCancelCheckSpan = new TimeSpan(0, 0, ServerBootConfig.JOB_PROGRESS_UPDATE_AND_CANCEL_CHECK_SECONDS);
DateTime LastProgressCheck = DateTime.UtcNow.Subtract(new TimeSpan(1, 1, 1, 1, 1));
var TotalRecords = idList.LongCount();
long CurrentRecord = -1;
//---------------------------------
foreach (long id in idList)
{
try
{
//--------------------------------
//case 4192
//Update progress / cancel requested?
CurrentRecord++;
if (DateUtil.IsAfterDuration(LastProgressCheck, ProgressAndCancelCheckSpan))
{
await JobsBiz.UpdateJobProgressAsync(job.GId, $"{CurrentRecord}/{TotalRecords}");
if (await JobsBiz.GetJobStatusAsync(job.GId) == JobStatus.CancelRequested)
break;
LastProgressCheck = DateTime.UtcNow;
}
//---------------------------------
SaveIt = false;
ClearErrors();
Customer o = null;
//save a fetch if it's a delete
if (job.SubType != JobSubType.Delete)
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.ProcessBatchTagOperation(o.Tags, (string)jobData["tag"], jobData.ContainsKey("toTag") ? (string)jobData["toTag"] : null, job.SubType);
break;
case JobSubType.Delete:
if (!await DeleteAsync(id))
{
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}");
FailedObjectCount++;
}
break;
default:
throw new System.ArgumentOutOfRangeException($"ProcessBatchJobAsync -> Invalid job Subtype{job.SubType}");
}
if (SaveIt)
{
o = await PutAsync(o);
if (o == null)
{
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}");
FailedObjectCount++;
}
}
//delay so we're not tying up all the resources in a tight loop
await Task.Delay(Sockeye.Util.ServerBootConfig.JOB_OBJECT_HANDLE_BATCH_JOB_LOOP_DELAY);
}
catch (Exception ex)
{
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors id({id})");
await JobsBiz.LogJobAsync(job.GId, ExceptionUtil.ExtractAllExceptionMessages(ex));
}
}
//---------------------------------
//case 4192
await JobsBiz.UpdateJobProgressAsync(job.GId, $"{++CurrentRecord}/{TotalRecords}");
//---------------------------------
await JobsBiz.LogJobAsync(job.GId, $"LT:BatchJob {job.SubType} {idList.Count}{(FailedObjectCount > 0 ? " - LT:Failed " + FailedObjectCount : "")}");
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed);
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task HandlePotentialNotificationEvent(SockEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = Sockeye.Util.ApplicationLogging.CreateLogger<CustomerBiz>();
log.LogDebug($"HandlePotentialNotificationEvent processing: [SockType:{this.BizType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
Customer o = (Customer)proposedObj;
//## DELETED EVENTS
//any event added below needs to be removed, so
//just blanket remove any event for this object of eventtype that would be added below here
//do it regardless any time there's an update and then
//let this code below handle the refreshing addition that could have changes
//await NotifyEventHelper.ClearPriorEventsForObject(ct, SockType.Customer, o.Id, NotifyEventType.ContractExpiring);
//## CREATED / MODIFIED EVENTS
if (ayaEvent == SockEvent.Created || ayaEvent == SockEvent.Modified)
{
}
}//end of process notifications
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,303 @@
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
namespace Sockeye.Biz
{
internal class CustomerNoteBiz : BizObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject, INotifiableObject
{
internal CustomerNoteBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = SockType.CustomerNote;
}
internal static CustomerNoteBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new CustomerNoteBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new CustomerNoteBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExistsAsync(long id)
{
return await ct.CustomerNote.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<CustomerNote> CreateAsync(CustomerNote newObject)
{
//await ValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
await ct.CustomerNote.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, SockEvent.Created), ct);
await SearchIndexAsync(newObject, true);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//GET
//
internal async Task<CustomerNote> GetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.CustomerNote.AsNoTracking().SingleOrDefaultAsync(m => m.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, SockEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<CustomerNote> PutAsync(CustomerNote putObject)
{
CustomerNote dbObject = await GetAsync(putObject.Id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
putObject.Tags = TagBiz.NormalizeTags(putObject.Tags);
//no validate required
if (HasErrors) return null;
ct.Replace(dbObject, putObject);
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, SockEvent.Modified), ct);
await SearchIndexAsync(putObject, false);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> DeleteAsync(long id, Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction parentTransaction = null)
{
//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();
CustomerNote dbObject = await GetAsync(id, false);
if (dbObject == null)
{
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);
//all good do the commit if it's ours
if (parentTransaction == null)
await transaction.CommitAsync();
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//SEARCH
//
private async Task SearchIndexAsync(CustomerNote obj, bool isNew)
{
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType);
DigestSearchText(obj, SearchParams);
if (isNew)
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
else
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
}
public async Task<Search.SearchIndexProcessObjectParameters> GetSearchResultSummary(long id, SockType specificType)
{
var obj = await GetAsync(id);
var SearchParams = new Search.SearchIndexProcessObjectParameters();
DigestSearchText(obj, SearchParams);
return SearchParams;
}
public void DigestSearchText(CustomerNote obj, Search.SearchIndexProcessObjectParameters searchParams)
{
if (obj != null)
searchParams.AddText(obj.Notes)
.AddText(obj.Tags);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//REPORTING
//
public async Task<JArray> GetReportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
{
var idList = dataListSelectedRequest.SelectedRowIds;
JArray ReportData = new JArray();
while (idList.Any())
{
var batch = idList.Take(IReportAbleObject.REPORT_DATA_BATCH_SIZE);
idList = idList.Skip(IReportAbleObject.REPORT_DATA_BATCH_SIZE).ToArray();
//query for this batch, comes back in db natural order unfortunately
var batchResults = await ct.CustomerNote.AsNoTracking().Where(z => batch.Contains(z.Id)).ToArrayAsync();
//order the results back into original
var orderedList = from id in batch join z in batchResults on id equals z.Id select z;
batchResults = null;
foreach (CustomerNote w in orderedList)
{
if (!ReportRenderManager.KeepGoing(jobId)) return null;
await PopulateVizFields(w);
var jo = JObject.FromObject(w);
ReportData.Add(jo);
}
orderedList = null;
}
vc.Clear();
return ReportData;
}
//populate viz fields from provided object
private async Task PopulateVizFields(CustomerNote o)
{
if (!vc.Has("customer", o.CustomerId))
vc.Add(await ct.Customer.AsNoTracking().Where(x => x.Id == o.CustomerId).Select(x => x.Name).FirstOrDefaultAsync(), "customer", o.CustomerId);
o.CustomerViz = vc.Get("customer", o.CustomerId);
if (!vc.Has("user", o.UserId))
vc.Add(await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(), "user", o.UserId);
o.UserViz = vc.Get("user", o.UserId);
}
private VizCache vc = new VizCache();
////////////////////////////////////////////////////////////////////////////////////////////////
// IMPORT EXPORT
//
public async Task<JArray> GetExportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
{
//for now just re-use the report data code
//this may turn out to be the pattern for most biz object types but keeping it seperate allows for custom usage from time to time
return await GetReportData(dataListSelectedRequest, jobId);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
// private async Task ValidateAsync(CustomerNote proposedObj, CustomerNote currentObj)
// {
// // bool isNew = currentObj == null;
// // //Any form customizations to validate?
// // var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(x => x.FormKey == SockType.CustomerNote.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(CustomerNote inObj)
// {
// //whatever needs to be check to delete this object
// }
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task HandlePotentialNotificationEvent(SockEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = Sockeye.Util.ApplicationLogging.CreateLogger<CustomerNoteBiz>();
log.LogDebug($"HandlePotentialNotificationEvent processing: [SockType:{this.BizType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
var oProposed = (CustomerNote)proposedObj;
oProposed.CustomerViz = await ct.Customer.AsNoTracking().Where(x => x.Id == oProposed.CustomerId).Select(x => x.Name).FirstOrDefaultAsync();
proposedObj.Name = oProposed.CustomerViz;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
////////////////////////////////////////////////////////////////////////////////////////////////
//JOB / OPERATIONS
//
//Other job handlers here...
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,187 @@
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
using System.Linq;
namespace Sockeye.Biz
{
internal class CustomerNotifySubscriptionBiz : BizObject//, IJobObject, ISearchAbleObject
{
internal CustomerNotifySubscriptionBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = SockType.CustomerNotifySubscription;
}
internal static CustomerNotifySubscriptionBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new CustomerNotifySubscriptionBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new CustomerNotifySubscriptionBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExistsAsync(long id)
{
return await ct.CustomerNotifySubscription.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<CustomerNotifySubscription> CreateAsync(CustomerNotifySubscription newObject)
{
ValidateAsync(newObject);
if (HasErrors)
return null;
else
{
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
newObject.CustomerTags = TagBiz.NormalizeTags(newObject.CustomerTags);
await ct.CustomerNotifySubscription.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, SockEvent.Created), ct);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.CustomerTags, null);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<CustomerNotifySubscription> GetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.CustomerNotifySubscription.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, SockEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<CustomerNotifySubscription> PutAsync(CustomerNotifySubscription putObject)
{
//TODO: Must remove all prior events and replace them
var dbObject = await GetAsync(putObject.Id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
putObject.CustomerTags = TagBiz.NormalizeTags(putObject.CustomerTags);
putObject.Tags = TagBiz.NormalizeTags(putObject.Tags);
ValidateAsync(putObject);
if (HasErrors) return null;
ct.Replace(dbObject, putObject);
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, SockEvent.Modified), ct);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.CustomerTags, dbObject.CustomerTags);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> DeleteAsync(long id)
{
using (var transaction = await ct.Database.BeginTransactionAsync())
{
var dbObject = await GetAsync(id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND);
return false;
}
//ValidateCanDelete(dbObject);
if (HasErrors)
return false;
{
var IDList = await ct.Review.AsNoTracking().Where(x => x.SockType == SockType.CustomerNotifySubscription && x.ObjectId == id).Select(x => x.Id).ToListAsync();
if (IDList.Count() > 0)
{
ReviewBiz b = new ReviewBiz(ct, UserId, UserTranslationId, CurrentUserRoles);
foreach (long ItemId in IDList)
if (!await b.DeleteAsync(ItemId, transaction))
{
AddError(ApiErrorCode.CHILD_OBJECT_ERROR, null, $"Review [{ItemId}]: {b.GetErrorsAsString()}");
return false;
}
}
}
ct.CustomerNotifySubscription.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObject.Id, dbObject.EventType.ToString(), ct);
await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.CustomerTags);
await transaction.CommitAsync();
return true;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private void ValidateAsync(CustomerNotifySubscription proposedObj)
{
if (string.IsNullOrWhiteSpace(proposedObj.CurrencyName))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "CurrencyName");
if (string.IsNullOrWhiteSpace(proposedObj.LanguageOverride))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "LanguageOverride");
if (string.IsNullOrWhiteSpace(proposedObj.TimeZoneOverride))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "TimeZoneOverride");
if (proposedObj.TranslationId == 0)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "TranslationId");
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,122 @@
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
namespace Sockeye.Biz
{
//Only one dashboard view per user and there is always one so no delete method, not create method
internal class DashboardViewBiz : BizObject
{
internal DashboardViewBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = SockType.DashboardView;
}
internal static DashboardViewBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new DashboardViewBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new DashboardViewBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExistsAsync()
{
return await ct.DashboardView.AnyAsync(z => z.UserId == UserId);
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// GET
//Get one - will create if there isn't one to get
internal async Task<DashboardView> GetAsync()
{
//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.DashboardView.SingleOrDefaultAsync(z => z.UserId == UserId);
if (ret == null)
{
ret = new DashboardView();
ret.UserId = UserId;
ct.DashboardView.Add(ret);
await ct.SaveChangesAsync();
}
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
//put
internal async Task<bool> PutAsync(DashboardView dbObject, string theView)
{
//todo: check this, is it correct to not be using the standard PUT methodology
dbObject.View = theView;
Validate(dbObject, false);
if (HasErrors)
return false;
await ct.SaveChangesAsync();
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private void Validate(DashboardView inObj, bool isNew)
{
//Filter json must parse
//this is all automated normally so not going to do too much parsing here
//just ensure it's basically there
if (!string.IsNullOrWhiteSpace(inObj.View))
{
try
{
var v = JArray.Parse(inObj.View);
}
catch (Newtonsoft.Json.JsonReaderException ex)
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "DashboardView", "DashboardView is not valid JSON string: " + ex.Message);
}
}
return;
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,196 @@
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
using Sockeye.DataList;
namespace Sockeye.Biz
{
internal class DataListColumnViewBiz : BizObject
{
internal DataListColumnViewBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = SockType.DataListColumnView;
}
internal static DataListColumnViewBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new DataListColumnViewBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new DataListColumnViewBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExistsAsync(long id)
{
return await ct.DataListColumnView.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<DataListColumnView> CreateAsync(DataListColumnView newObject)
{
ValidateAsync(newObject);
if (HasErrors)
return null;
else
{
//delete the existing one in favor of this one
var dbObject = await GetAsync(newObject.UserId, newObject.ListKey, false);
if (dbObject != null)
{
ct.DataListColumnView.Remove(dbObject);
await ct.SaveChangesAsync();
}
await ct.DataListColumnView.AddAsync(newObject);
await ct.SaveChangesAsync();
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//SET SORT
//
internal async Task<bool> SetSort(Sockeye.Models.DataListSortRequest newObject)
{
//Get the current column view (in an updateable manner) and update it's sort property and save
var dbObject = await ct.DataListColumnView.SingleOrDefaultAsync(z => z.UserId == UserId && z.ListKey == newObject.ListKey);
if (dbObject == null)
dbObject = await CreateDefaultColumnView(newObject.ListKey);
//convert arrays to proper format for saving
Dictionary<string, string> newSort = new Dictionary<string, string>();
for (int x = 0; x < newObject.sortBy.Length; x++)
newSort.Add(newObject.sortBy[x], (newObject.sortDesc[x] ? "-" : "+"));
dbObject.Sort = JsonConvert.SerializeObject(newSort);
//columnview is always replace
await ct.SaveChangesAsync();
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//GET
//
//
internal async Task<DataListColumnView> GetAsync(long userId, string listKey, bool createDefaultIfNecessary)
{
var ret = await ct.DataListColumnView.AsNoTracking().SingleOrDefaultAsync(z => z.UserId == userId && z.ListKey == listKey);
if (ret == null && createDefaultIfNecessary)
{
return await CreateDefaultColumnView(listKey);
}
return ret;
}
internal async Task<DataListColumnView> CreateDefaultColumnView(string listKey)
{
if (!DataListFactory.ListKeyIsValid(listKey))
{
throw new System.ArgumentOutOfRangeException($"ListKey '{listKey}' is not a valid DataListKey");
}
var ret = new DataListColumnView();
ret.UserId = UserId;
ret.ListKey = listKey;
var dataList = DataListFactory.GetAyaDataList(listKey, 0);
ret.Columns = JsonConvert.SerializeObject(dataList.DefaultColumns);
ret.Sort = JsonConvert.SerializeObject(dataList.DefaultSortBy);
return await CreateAsync(ret);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE (RESET)
//
internal async Task<DataListColumnView> DeleteAsync(long userId, string listKey)
{
//this is effectively the RESET route handler
//so it can be called any time even if there is no default list and it's a-ok
//because a new default will be created if needed
var dbObject = await GetAsync(userId, listKey, false);
if (dbObject != null)
{
ct.DataListColumnView.Remove(dbObject);
await ct.SaveChangesAsync();
dbObject = await GetAsync(userId, listKey, true);
}
return dbObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private void ValidateAsync(DataListColumnView inObj)
{
if (inObj.UserId != UserId)
AddError(ApiErrorCode.NOT_AUTHORIZED, "UserId", "Only own view can be modified");
if (string.IsNullOrWhiteSpace(inObj.ListKey))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "ListKey");
if (!DataListFactory.ListKeyIsValid(inObj.ListKey))
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "ListKey", $"ListKey \"{inObj.ListKey}\" DataListKey is not valid");
//Validate Sort JSON
try
{
JsonConvert.DeserializeObject<Dictionary<string, string>>(inObj.Sort);
}
catch (System.Exception ex)
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Sort", "Sort is not valid JSON string, can't convert to valid Dictionary<string, string> SortBy, error: " + ex.Message);
}
//Validate Columns JSON
try
{
JsonConvert.DeserializeObject<List<string>>(inObj.Columns);
}
catch (System.Exception ex)
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Sort", "Sort is not valid JSON string, can't convert to valid Dictionary<string, string> SortBy, error: " + ex.Message);
}
return;
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,261 @@
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
using Sockeye.DataList;
namespace Sockeye.Biz
{
internal class DataListSavedFilterBiz : BizObject
{
internal DataListSavedFilterBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = SockType.DataListSavedFilter;
}
internal static DataListSavedFilterBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new DataListSavedFilterBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new DataListSavedFilterBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExistsAsync(long id)
{
return await ct.DataListSavedFilter.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
internal async Task<DataListSavedFilter> CreateAsync(DataListSavedFilter inObj)
{
Validate(inObj, true);
if (HasErrors)
return null;
else
{
//do stuff with datafilter
DataListSavedFilter outObj = inObj;
outObj.UserId = UserId;
await ct.DataListSavedFilter.AddAsync(outObj);
await ct.SaveChangesAsync();
return outObj;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// GET
//Get one
internal async Task<DataListSavedFilter> GetAsync(long fetchId)
{
//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.DataListSavedFilter.AsNoTracking().SingleOrDefaultAsync(z => z.Id == fetchId && (z.Public == true || z.UserId == UserId));
return ret;
}
//GET FILTERLIST
internal async Task<List<NameIdDefaultItem>> GetViewListAsync(string listKey)
{
await EnsureDefaultAsync(listKey);
return await ct.DataListSavedFilter
.AsNoTracking()
.Where(z => z.ListKey == listKey && (z.Public == true || z.UserId == UserId))
.OrderBy(z => z.Name)
.Select(z => new NameIdDefaultItem(z.Id, z.Name, z.DefaultFilter)).ToListAsync();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE Default if it doesn't exist already
internal async Task EnsureDefaultAsync(string listKey)
{
if (!await ct.DataListSavedFilter.AnyAsync(z => z.UserId == UserId && z.ListKey == listKey && z.DefaultFilter == true))
{
if (!DataListFactory.ListKeyIsValid(listKey))
{
throw new System.ArgumentOutOfRangeException($"ListKey '{listKey}' is not a valid DataListKey");
}
//var dataList = DataListFactory.GetAyaDataList(listKey,0);
DataListSavedFilter d = new DataListSavedFilter();
d.ListKey = listKey;
d.Name = "-";
d.DefaultFilter = true;
d.Public = false;
d.UserId = UserId;
d.Filter = "[]";//empty array is default becuase it would be filter:[{column:"PartName",any:true/false,items:[{op: "=",value: "400735"}]}]
await ct.DataListSavedFilter.AddAsync(d);
await ct.SaveChangesAsync();
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
//put
internal async Task<DataListSavedFilter> PutAsync(DataListSavedFilter putObject)
{
var dbObject = await GetAsync(putObject.Id);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
//preserve the owner ID if none was specified
if (putObject.UserId == 0)
putObject.UserId = dbObject.UserId;
Validate(dbObject, false);
if (HasErrors)
return null;
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await ExistsAsync(putObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> DeleteAsync(DataListSavedFilter dbObject)
{
ValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.DataListSavedFilter.Remove(dbObject);
await ct.SaveChangesAsync();
//was it a "reset" of default?
if (dbObject.DefaultFilter)
await EnsureDefaultAsync(dbObject.ListKey);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private void ValidateCanDelete(DataListSavedFilter inObj)
{
if (inObj.UserId != UserId)
{
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror", "Can't delete another users filter");
}
}
//Can save or update?
private void Validate(DataListSavedFilter inObj, bool isNew)
{
//UserId required
if (!isNew)
{
if (inObj.UserId == 0)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "UserId");
}
//can't save a filter for someone else
if (inObj.UserId != UserId && inObj.Public == false)
{
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror", "Can only save private filters for self not other UserId's");
return;
}
//technically not "validation" but always ensure default filter has - name
if (inObj.DefaultFilter)
{
inObj.Name = "-";
}
//Name required
if (string.IsNullOrWhiteSpace(inObj.Name))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name");
if (string.IsNullOrWhiteSpace(inObj.Filter))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Filter");
if (string.IsNullOrWhiteSpace(inObj.ListKey))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "ListKey");
if (!DataListFactory.ListKeyIsValid(inObj.ListKey))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "ListKey", $"ListKey \"{inObj.ListKey}\" DataListKey is not valid");
}
if (inObj.ListKey.Length > 255)
AddError(ApiErrorCode.VALIDATION_LENGTH_EXCEEDED, "ListKey", "255 max");
//check if filter can be reconstructed into a C# filter object
try
{
JsonConvert.DeserializeObject<List<DataListFilterOption>>(inObj.Filter);
}
catch (System.Exception ex)
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Filter", "Filter is not valid JSON string, can't convert to List<DataListFilterOption>, error: " + ex.Message);
}
return;
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,157 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Sockeye.Models;
using System;
namespace Sockeye.Biz
{
internal static class EventLogProcessor
{
private const int DEFAULT_EVENT_LIMIT = 20;
/// <summary>
/// Add an entry to the log
///
/// </summary>
/// <param name="newEvent"></param>
/// <param name="ct"></param>
/// <returns></returns>
internal static async Task LogEventToDatabaseAsync(Event newEvent, AyContext ct)
{
await ct.Event.AddAsync(newEvent);
await ct.SaveChangesAsync();
}
/// <summary>
/// Handle delete
/// remove all prior entries for object, add one deleted entry
/// </summary>
/// <param name="userId"></param>
/// <param name="sockType"></param>
/// <param name="sockId"></param>
/// <param name="textra"></param>
/// <param name="ct"></param>
internal static async Task DeleteObjectLogAsync(long userId, SockType sockType, long sockId, string textra, AyContext ct)
{
await ct.Database.ExecuteSqlInterpolatedAsync($"delete from aevent where socktype = {sockType} and sockid={sockId}");
await ct.Event.AddAsync(new Event(userId, sockId, sockType, SockEvent.Deleted, $"{textra} (id {sockId})"));
await ct.SaveChangesAsync();
}
/// <summary>
/// Get the event log for a specified object
/// Presentation is the client's responsibility (localization internationalization etc)
/// </summary>
internal static async Task<Sockeye.Api.Controllers.EventLogController.ObjectEventLog> GetLogForObjectAsync(Sockeye.Api.Controllers.EventLogController.EventLogOptions opt, long translationId, AyContext ct)
{
Sockeye.Api.Controllers.EventLogController.ObjectEventLog ret = new Api.Controllers.EventLogController.ObjectEventLog();
var limit = opt.Limit ?? DEFAULT_EVENT_LIMIT;
var offset = opt.Offset ?? 0;
//Set up the query
var q = ct.Event.Select(z => z).AsNoTracking();
q = q.Where(z => z.SockId == opt.AyId && z.SockType == opt.SockType);
q = q.OrderByDescending(z => z.Created);
q = q.Skip(offset).Take(limit);
//Execute the query
var EventItems = await q.ToArrayAsync();
//convert the Event array to the correct return type array
using (var command = ct.Database.GetDbConnection().CreateCommand())
{
ct.Database.OpenConnection();
ret.Events = EventItems.Select(z => new Sockeye.Api.Controllers.EventLogController.ObjectEventLogItem()
{
Date = z.Created,
UserId = z.UserId,
Event = z.SockEvent,
Textra = z.Textra,
Name = BizObjectNameFetcherDirect.Name(SockType.User, z.UserId, translationId, command)
}).ToArray();
ret.Name = BizObjectNameFetcherDirect.Name(opt.SockType, opt.AyId, translationId, command);
return ret;
}
}
/// <summary>
/// Get the event log for a specified User
/// Presentation is the client's responsibility (localization internationalization etc)
/// </summary>
internal static async Task<Sockeye.Api.Controllers.EventLogController.UserEventLog> GetLogForUserAsync(Sockeye.Api.Controllers.EventLogController.UserEventLogOptions opt, long translationId, AyContext ct)
{
Sockeye.Api.Controllers.EventLogController.UserEventLog ret = new Api.Controllers.EventLogController.UserEventLog();
var limit = opt.Limit ?? DEFAULT_EVENT_LIMIT;
var offset = opt.Offset ?? 0;
//Set up the query
var q = ct.Event.Select(z => z).AsNoTracking();
q = q.Where(z => z.UserId == opt.UserId);
q = q.OrderByDescending(z => z.Created);
q = q.Skip(offset).Take(limit);
//Execute the query
var EventItems = await q.ToArrayAsync();
using (var command = ct.Database.GetDbConnection().CreateCommand())
{
ct.Database.OpenConnection();
//convert the Event array to the correct return type array
ret.Events = EventItems.Select(z => new Sockeye.Api.Controllers.EventLogController.UserEventLogItem()
{
Date = z.Created,
SockType = z.SockType,
ObjectId = z.SockId,
Event = z.SockEvent,
Textra = z.Textra,
Name = BizObjectNameFetcherDirect.Name(z.SockType, z.SockId, translationId, command)
}).ToArray();
ret.Name = BizObjectNameFetcherDirect.Name(SockType.User, opt.UserId, translationId, command);
return ret;
}
}
/// <summary>
/// V7 export handler
/// Exporter needs to fixup event log CREATED entry to show original created and last modified times
/// and users
/// </summary>
internal static async Task V7_Modify_LogAsync(Sockeye.Api.Controllers.EventLogController.V7Event ev, AyContext ct)
{
//delete the automatically created entry from the exported object
await ct.Database.ExecuteSqlInterpolatedAsync($"delete from aevent where socktype = {ev.SockType} and ayid={ev.AyId}");
//Now create the entries to reflect the original data from v7
//CREATED
await EventLogProcessor.LogEventToDatabaseAsync(new Event(ev.Creator, ev.AyId, ev.SockType, SockEvent.Created, ev.Created, null), ct);
//MODIFIED
await EventLogProcessor.LogEventToDatabaseAsync(new Event(ev.Modifier, ev.AyId, ev.SockType, SockEvent.Modified, ev.Modified, null), ct);
await ct.SaveChangesAsync();
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

334
server/biz/FormCustomBiz.cs Normal file
View File

@@ -0,0 +1,334 @@
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
namespace Sockeye.Biz
{
//## NOTE this is a *GLOBAL* form custom that applies to all users as configured by someone with rights to do so
//this is *not* a personal customization system
internal class FormCustomBiz : BizObject
{
internal FormCustomBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = SockType.FormCustom;
}
internal static FormCustomBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new FormCustomBiz(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 FormCustomBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExistsAsync(string formKey)
{
return await ct.FormCustom.AnyAsync(z => z.FormKey == formKey);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
internal async Task<FormCustom> CreateAsync(FormCustom inObj)
{
await ValidateAsync(inObj, true);
if (HasErrors)
return null;
else
{
//
FormCustom outObj = inObj;
outObj.Template = JsonUtil.CompactJson(outObj.Template);
await ct.FormCustom.AddAsync(outObj);
await ct.SaveChangesAsync();
//Handle child and associated items:
//EVENT LOG
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, outObj.Id, BizType, SockEvent.Created), ct);
return outObj;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// GET
//Get one
internal async Task<FormCustom> GetAsync(string formKey)
{
//Step 1: check if exists, if it does then just return it
if (await ExistsAsync(formKey))
{
return await ct.FormCustom.SingleOrDefaultAsync(z => z.FormKey == formKey);
}
//If it doesn't exist, vet the form key name is ok by checking with this list
if (!FormFieldOptionalCustomizableReference.FormFieldKeys.Contains(formKey))
{
//Nope, whatever it is, it's not valid
return null;
}
// Name is valid, make a new formcustom for the name specified, save to db and then return it
//NOTE: This assumes that the client will make it a legal form custom and so it doesn't include any pre-defined stock fields that are required etc
//this just makes an empty template suitable for the client to work with
var fc = new FormCustom()
{
FormKey = formKey,
Template = @"[]"
};
//Create and save to db
return await CreateAsync(fc);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
//put
internal async Task<FormCustom> PutAsync(FormCustom putObject)
{
var dbObject = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == putObject.FormKey);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "formKey");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
dbObject.Template = JsonUtil.CompactJson(putObject.Template);
putObject.Id=dbObject.Id;//weird workaround needed because ID is not sent with the putobject for...reasons 🤷?
await ValidateAsync(putObject, false);
if (HasErrors)
return null;
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await ExistsAsync(putObject.FormKey))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
//Log modification and save context
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, BizType, SockEvent.Modified), ct);
return putObject;
// //todo: replace with new put methodology
// //Replace the db object with the PUT object
// CopyObject.Copy(putObject, dbObject, "Id");
// //Set "original" value of concurrency token to input token
// //this will allow EF to check it out
// ct.Entry(dbObject).OriginalValues["Concurrency"] = putObject.Concurrency;
// await ValidateAsync(dbObject, false);
// if (HasErrors)
// return false;
// dbObject.Template = JsonUtil.CompactJson(dbObject.Template);
// await ct.SaveChangesAsync();
// //Log modification and save context
// await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, BizType, AyaEvent.Modified), ct);
// return true;
}
//NO DELETE, ONLY EDIT
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private async Task ValidateAsync(FormCustom inObj, bool isNew)
{
//FormKey required and must be valid
if (string.IsNullOrWhiteSpace(inObj.FormKey))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "FormKey");
else
{
if (!FormFieldOptionalCustomizableReference.IsValidFormFieldKey(inObj.FormKey))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "FormKey");
}
}
//FormKey must be less than 255 characters
if (inObj.FormKey.Length > 255)
AddError(ApiErrorCode.VALIDATION_LENGTH_EXCEEDED, "FormKey", "255 max");
//If name is otherwise OK, check that name is unique
if (!PropertyHasErrors("FormKey") && isNew)
{
//Use Any command is efficient way to check existance, it doesn't return the record, just a true or false
if (await ct.FormCustom.AnyAsync(z => z.FormKey == inObj.FormKey && z.Id != inObj.Id))
{
AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "FormKey");
}
}
//Template json must parse
if ((!PropertyHasErrors("FormKey") && !string.IsNullOrWhiteSpace(inObj.Template)))
{
var ValidCustomFieldTypes = CustomFieldType.ValidCustomFieldTypes;
var ValidFormFields = FormFieldOptionalCustomizableReference.FormFieldReferenceList(inObj.FormKey);
try
{
//Parse the json, expecting something like this:
//[{fld:"ltkeyfieldname",hide:"true/false",required:"true/false", type:"bool"},{fld:"ltkeyfieldname",hide:"true/false",required:"true/false", type:"text"]
//Array at root is valid json and saves a bit of bandwidth so minimal is best
//Only Custom fields that are to be displayed need to be in this fragment.
//fields that are not chosen to display don't need to be in the fragment to be valid
var v = JArray.Parse(inObj.Template);
for (int i = 0; i < v.Count; i++)
{
FormField MasterFormField = null;
var formFieldItem = v[i];
if (formFieldItem["fld"] == null)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Template", $"Template array item {i}, object is missing required \"fld\" property ");
else
{
var fldKey = formFieldItem["fld"].Value<string>();
if (string.IsNullOrWhiteSpace(fldKey))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Template", $"Template array item {i}, \"fld\" property exists but is empty, a value is required");
//validate the field name if we can
if (ValidFormFields != null)
{
if (!ValidFormFields.Exists(z => z.FieldKey == fldKey))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Template", $"Template array item {i}, fld property value \"{fldKey}\" is not a valid form field value for formKey specified");
}
else
{
MasterFormField = ValidFormFields.FirstOrDefault(z => z.FieldKey == fldKey);
}
}
}
if (MasterFormField != null)
{
//removed due to removal of hidden property in ff reference since only customizable fields are hideable by default
// if (formFieldItem["hide"] != null)
// {
// var fieldHideValue = formFieldItem["hide"].Value<bool>();
// if (!MasterFormField.Hideable && fieldHideValue == true)
// AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Template", $"Template array item {i} (\"{MasterFormField.FieldKey}\"), \"hide\" property value of \"{fieldHideValue}\" is not valid, this field is core and cannot be hidden");
// }
//validate if it's a custom field that it has a type specified
if (MasterFormField.IsCustomField && formFieldItem["type"] == null)
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Template", $"Template array item {i} (\"{MasterFormField.FieldKey}\"), \"type\" property value is MISSING for custom field, Custom fields MUST have types specified");
}
if (formFieldItem["type"] != null)
{
if (!MasterFormField.IsCustomField)
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Template", $"Template array item {i} (\"{MasterFormField.FieldKey}\"), \"type\" property value is not valid, only Custom fields can have types specified");
else
{//It is a custom field, is it a valid type value
var templateFieldCustomTypeValue = formFieldItem["type"].Value<int>();
if (!ValidCustomFieldTypes.Contains(templateFieldCustomTypeValue))
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Template", $"Template array item {i} (\"{MasterFormField.FieldKey}\"), \"type\" property value of \"{templateFieldCustomTypeValue}\" is not a valid custom field type");
}
}
}
//other code depends on seeing the required value even if it's not set to true
if (formFieldItem["required"] == null)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Template", $"Template array item {i}, object is missing \"required\" property. All items must contain this property. ");
//NOTE: value of nothing, null or empty is a valid value so no checking for it here
}
}
catch (Newtonsoft.Json.JsonReaderException ex)
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Template", "Template is not valid JSON string: " + ex.Message);
}
}
return;
}
// //Can delete?
// private void ValidateCanDelete(FormCustom inObj)
// {
// //Leaving this off for now
// }
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,383 @@
using System.Collections.Generic;
using System;
namespace Sockeye.Biz
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// This contains all the **OPTIONAL** fields that can be customized by user to be required or hide
// on all object edit forms
// it is used for both validation and driving the UI
// it does *NOT* need to contain every field on the form, just user customizable ones
// and should not have mandatory fields since they are not customizable by end user
//See the DataList folder / namespace for LIST related similar class
public static class FormFieldOptionalCustomizableReference
{
private static Dictionary<string, List<FormField>> _formFields;
private static List<string> _formFieldKeys = null;
public static List<string> FormFieldKeys
{
get
{
if (_formFieldKeys == null)
{
_formFieldKeys = new List<string>();
var values = Enum.GetValues(typeof(SockType));
foreach (SockType t in values)
{
if (t.HasAttribute(typeof(CoreBizObjectAttribute)))
{
_formFieldKeys.Add(t.ToString());
}
}
//No type / not corebiz form keys:
_formFieldKeys.Add("Contact");
}
return _formFieldKeys;
}
}
public static bool IsValidFormFieldKey(string key)
{
return FormFieldKeys.Contains(key);
}
public static List<FormField> FormFieldReferenceList(string key)
{
//Initialize the static list here on first retrieval
if (_formFields == null)
{
_formFields = new Dictionary<string, List<FormField>>();
/* ***************************** WARNING: Be careful here, if a standard field is hideable and also it's DB SCHEMA is set to NON NULLABLE then the CLIENT end needs to set a default
***************************** Otherwise the hidden field can't be set and the object can't be saved EVER
*/
#region USER_KEY
{
List<FormField> l = new List<FormField>();
l.Add(new FormField { TKey = "UserEmployeeNumber", FieldKey = "EmployeeNumber" });
l.Add(new FormField { TKey = "LastLogin", FieldKey = "LastLogin" });
l.Add(new FormField { TKey = "UserNotes", FieldKey = "Notes" });
l.Add(new FormField { TKey = "Tags", FieldKey = "Tags" });
l.Add(new FormField { TKey = "Wiki", FieldKey = "Wiki" });
l.Add(new FormField { TKey = "Attachments", FieldKey = "Attachments", Requireable = false });
l.Add(new FormField { TKey = "UserCustom1", FieldKey = "UserCustom1", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom2", FieldKey = "UserCustom2", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom3", FieldKey = "UserCustom3", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom4", FieldKey = "UserCustom4", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom5", FieldKey = "UserCustom5", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom6", FieldKey = "UserCustom6", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom7", FieldKey = "UserCustom7", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom8", FieldKey = "UserCustom8", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom9", FieldKey = "UserCustom9", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom10", FieldKey = "UserCustom10", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom11", FieldKey = "UserCustom11", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom12", FieldKey = "UserCustom12", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom13", FieldKey = "UserCustom13", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom14", FieldKey = "UserCustom14", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom15", FieldKey = "UserCustom15", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom16", FieldKey = "UserCustom16", IsCustomField = true });
_formFields.Add(SockType.User.ToString(), l);
}
#endregion
#region CONTACT_KEY
{
List<FormField> l = new List<FormField>();
l.Add(new FormField { TKey = "UserEmployeeNumber", FieldKey = "EmployeeNumber" });
l.Add(new FormField { TKey = "LastLogin", FieldKey = "LastLogin" });
l.Add(new FormField { TKey = "UserNotes", FieldKey = "Notes" });
l.Add(new FormField { TKey = "Tags", FieldKey = "Tags" });
l.Add(new FormField { TKey = "Wiki", FieldKey = "Wiki" });
l.Add(new FormField { TKey = "Attachments", FieldKey = "Attachments", Requireable = false });
l.Add(new FormField { TKey = "UserCustom1", FieldKey = "UserCustom1", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom2", FieldKey = "UserCustom2", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom3", FieldKey = "UserCustom3", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom4", FieldKey = "UserCustom4", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom5", FieldKey = "UserCustom5", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom6", FieldKey = "UserCustom6", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom7", FieldKey = "UserCustom7", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom8", FieldKey = "UserCustom8", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom9", FieldKey = "UserCustom9", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom10", FieldKey = "UserCustom10", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom11", FieldKey = "UserCustom11", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom12", FieldKey = "UserCustom12", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom13", FieldKey = "UserCustom13", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom14", FieldKey = "UserCustom14", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom15", FieldKey = "UserCustom15", IsCustomField = true });
l.Add(new FormField { TKey = "UserCustom16", FieldKey = "UserCustom16", IsCustomField = true });
_formFields.Add("Contact", l);
}
#endregion
#region Customer
{
List<FormField> l = new List<FormField>();
l.Add(new FormField { TKey = "CustomerAccountNumber", FieldKey = "AccountNumber" });
l.Add(new FormField { TKey = "WebAddress", FieldKey = "WebAddress" });
l.Add(new FormField { TKey = "CustomerEmail", FieldKey = "EmailAddress" });
l.Add(new FormField { TKey = "CustomerPhone1", FieldKey = "Phone1" });
l.Add(new FormField { TKey = "CustomerPhone2", FieldKey = "Phone2" });
l.Add(new FormField { TKey = "CustomerPhone3", FieldKey = "Phone3" });
l.Add(new FormField { TKey = "CustomerPhone4", FieldKey = "Phone4" });
l.Add(new FormField { TKey = "CustomerPhone5", FieldKey = "Phone5" });
l.Add(new FormField { TKey = "CustomerBillHeadOffice", FieldKey = "BillHeadOffice" });
l.Add(new FormField { TKey = "HeadOffice", FieldKey = "HeadOfficeId" });
l.Add(new FormField { TKey = "Contract", FieldKey = "ContractId" });
l.Add(new FormField { TKey = "ContractExpires", FieldKey = "ContractExpires" });
//l.Add(new FormField { TKey = "UsesBanking", FieldKey = "UsesBanking" });
l.Add(new FormField { TKey = "CustomerNotes", FieldKey = "Notes" });
l.Add(new FormField { TKey = "CustomerTechNotes", FieldKey = "TechNotes" });
l.Add(new FormField { TKey = "AlertNotes", FieldKey = "AlertNotes" });
l.Add(new FormField { TKey = "Tags", FieldKey = "Tags" });
l.Add(new FormField { TKey = "Wiki", FieldKey = "Wiki" });
l.Add(new FormField { TKey = "Attachments", FieldKey = "Attachments", Requireable = false });
l.Add(new FormField { TKey = "AddressDeliveryAddress", FieldKey = "Address" });
l.Add(new FormField { TKey = "AddressCity", FieldKey = "City" });
l.Add(new FormField { TKey = "AddressStateProv", FieldKey = "Region" });
l.Add(new FormField { TKey = "AddressCountry", FieldKey = "Country" });
l.Add(new FormField { TKey = "AddressPostal", FieldKey = "AddressPostal" });
l.Add(new FormField { TKey = "AddressLatitude", FieldKey = "Latitude" });
l.Add(new FormField { TKey = "AddressLongitude", FieldKey = "Longitude" });
l.Add(new FormField { TKey = "AddressPostalDeliveryAddress", FieldKey = "PostAddress" });
l.Add(new FormField { TKey = "AddressPostalCity", FieldKey = "PostCity" });
l.Add(new FormField { TKey = "AddressPostalStateProv", FieldKey = "PostRegion" });
l.Add(new FormField { TKey = "AddressPostalCountry", FieldKey = "PostCountry" });
l.Add(new FormField { TKey = "AddressPostalPostal", FieldKey = "PostCode" });
l.Add(new FormField { TKey = "CustomerCustom1", FieldKey = "CustomerCustom1", IsCustomField = true });
l.Add(new FormField { TKey = "CustomerCustom2", FieldKey = "CustomerCustom2", IsCustomField = true });
l.Add(new FormField { TKey = "CustomerCustom3", FieldKey = "CustomerCustom3", IsCustomField = true });
l.Add(new FormField { TKey = "CustomerCustom4", FieldKey = "CustomerCustom4", IsCustomField = true });
l.Add(new FormField { TKey = "CustomerCustom5", FieldKey = "CustomerCustom5", IsCustomField = true });
l.Add(new FormField { TKey = "CustomerCustom6", FieldKey = "CustomerCustom6", IsCustomField = true });
l.Add(new FormField { TKey = "CustomerCustom7", FieldKey = "CustomerCustom7", IsCustomField = true });
l.Add(new FormField { TKey = "CustomerCustom8", FieldKey = "CustomerCustom8", IsCustomField = true });
l.Add(new FormField { TKey = "CustomerCustom9", FieldKey = "CustomerCustom9", IsCustomField = true });
l.Add(new FormField { TKey = "CustomerCustom10", FieldKey = "CustomerCustom10", IsCustomField = true });
l.Add(new FormField { TKey = "CustomerCustom11", FieldKey = "CustomerCustom11", IsCustomField = true });
l.Add(new FormField { TKey = "CustomerCustom12", FieldKey = "CustomerCustom12", IsCustomField = true });
l.Add(new FormField { TKey = "CustomerCustom13", FieldKey = "CustomerCustom13", IsCustomField = true });
l.Add(new FormField { TKey = "CustomerCustom14", FieldKey = "CustomerCustom14", IsCustomField = true });
l.Add(new FormField { TKey = "CustomerCustom15", FieldKey = "CustomerCustom15", IsCustomField = true });
l.Add(new FormField { TKey = "CustomerCustom16", FieldKey = "CustomerCustom16", IsCustomField = true });
_formFields.Add(SockType.Customer.ToString(), l);
}
#endregion
#region HeadOffice
{
List<FormField> l = new List<FormField>();
l.Add(new FormField { TKey = "HeadOfficeAccountNumber", FieldKey = "AccountNumber" });
l.Add(new FormField { TKey = "WebAddress", FieldKey = "WebAddress" });
l.Add(new FormField { TKey = "HeadOfficeEmail", FieldKey = "EmailAddress" });
l.Add(new FormField { TKey = "HeadOfficePhone1", FieldKey = "Phone1" });
l.Add(new FormField { TKey = "HeadOfficePhone2", FieldKey = "Phone2" });
l.Add(new FormField { TKey = "HeadOfficePhone3", FieldKey = "Phone3" });
l.Add(new FormField { TKey = "HeadOfficePhone4", FieldKey = "Phone4" });
l.Add(new FormField { TKey = "HeadOfficePhone5", FieldKey = "Phone5" });
l.Add(new FormField { TKey = "Contract", FieldKey = "ContractId" });
l.Add(new FormField { TKey = "ContractExpires", FieldKey = "ContractExpires" });
//l.Add(new FormField { TKey = "UsesBanking", FieldKey = "UsesBanking" });
l.Add(new FormField { TKey = "HeadOfficeNotes", FieldKey = "Notes" });
l.Add(new FormField { TKey = "Tags", FieldKey = "Tags" });
l.Add(new FormField { TKey = "Wiki", FieldKey = "Wiki" });
l.Add(new FormField { TKey = "Attachments", FieldKey = "Attachments", Requireable = false });
l.Add(new FormField { TKey = "AddressDeliveryAddress", FieldKey = "Address" });
l.Add(new FormField { TKey = "AddressCity", FieldKey = "City" });
l.Add(new FormField { TKey = "AddressStateProv", FieldKey = "Region" });
l.Add(new FormField { TKey = "AddressCountry", FieldKey = "Country" });
l.Add(new FormField { TKey = "AddressPostal", FieldKey = "AddressPostal" });
l.Add(new FormField { TKey = "AddressLatitude", FieldKey = "Latitude" });
l.Add(new FormField { TKey = "AddressLongitude", FieldKey = "Longitude" });
l.Add(new FormField { TKey = "AddressPostalDeliveryAddress", FieldKey = "PostAddress" });
l.Add(new FormField { TKey = "AddressPostalCity", FieldKey = "PostCity" });
l.Add(new FormField { TKey = "AddressPostalStateProv", FieldKey = "PostRegion" });
l.Add(new FormField { TKey = "AddressPostalCountry", FieldKey = "PostCountry" });
l.Add(new FormField { TKey = "AddressPostalPostal", FieldKey = "PostCode" });
l.Add(new FormField { TKey = "HeadOfficeCustom1", FieldKey = "HeadOfficeCustom1", IsCustomField = true });
l.Add(new FormField { TKey = "HeadOfficeCustom2", FieldKey = "HeadOfficeCustom2", IsCustomField = true });
l.Add(new FormField { TKey = "HeadOfficeCustom3", FieldKey = "HeadOfficeCustom3", IsCustomField = true });
l.Add(new FormField { TKey = "HeadOfficeCustom4", FieldKey = "HeadOfficeCustom4", IsCustomField = true });
l.Add(new FormField { TKey = "HeadOfficeCustom5", FieldKey = "HeadOfficeCustom5", IsCustomField = true });
l.Add(new FormField { TKey = "HeadOfficeCustom6", FieldKey = "HeadOfficeCustom6", IsCustomField = true });
l.Add(new FormField { TKey = "HeadOfficeCustom7", FieldKey = "HeadOfficeCustom7", IsCustomField = true });
l.Add(new FormField { TKey = "HeadOfficeCustom8", FieldKey = "HeadOfficeCustom8", IsCustomField = true });
l.Add(new FormField { TKey = "HeadOfficeCustom9", FieldKey = "HeadOfficeCustom9", IsCustomField = true });
l.Add(new FormField { TKey = "HeadOfficeCustom10", FieldKey = "HeadOfficeCustom10", IsCustomField = true });
l.Add(new FormField { TKey = "HeadOfficeCustom11", FieldKey = "HeadOfficeCustom11", IsCustomField = true });
l.Add(new FormField { TKey = "HeadOfficeCustom12", FieldKey = "HeadOfficeCustom12", IsCustomField = true });
l.Add(new FormField { TKey = "HeadOfficeCustom13", FieldKey = "HeadOfficeCustom13", IsCustomField = true });
l.Add(new FormField { TKey = "HeadOfficeCustom14", FieldKey = "HeadOfficeCustom14", IsCustomField = true });
l.Add(new FormField { TKey = "HeadOfficeCustom15", FieldKey = "HeadOfficeCustom15", IsCustomField = true });
l.Add(new FormField { TKey = "HeadOfficeCustom16", FieldKey = "HeadOfficeCustom16", IsCustomField = true });
_formFields.Add(SockType.HeadOffice.ToString(), l);
}
#endregion
#region Memo
{
List<FormField> l = new List<FormField>();
l.Add(new FormField { TKey = "Tags", FieldKey = "Tags" });
l.Add(new FormField { TKey = "Wiki", FieldKey = "Wiki" });
l.Add(new FormField { TKey = "Attachments", FieldKey = "Attachments", Requireable = false });
l.Add(new FormField { TKey = "MemoCustom1", FieldKey = "MemoCustom1", IsCustomField = true });
l.Add(new FormField { TKey = "MemoCustom2", FieldKey = "MemoCustom2", IsCustomField = true });
l.Add(new FormField { TKey = "MemoCustom3", FieldKey = "MemoCustom3", IsCustomField = true });
l.Add(new FormField { TKey = "MemoCustom4", FieldKey = "MemoCustom4", IsCustomField = true });
l.Add(new FormField { TKey = "MemoCustom5", FieldKey = "MemoCustom5", IsCustomField = true });
l.Add(new FormField { TKey = "MemoCustom6", FieldKey = "MemoCustom6", IsCustomField = true });
l.Add(new FormField { TKey = "MemoCustom7", FieldKey = "MemoCustom7", IsCustomField = true });
l.Add(new FormField { TKey = "MemoCustom8", FieldKey = "MemoCustom8", IsCustomField = true });
l.Add(new FormField { TKey = "MemoCustom9", FieldKey = "MemoCustom9", IsCustomField = true });
l.Add(new FormField { TKey = "MemoCustom10", FieldKey = "MemoCustom10", IsCustomField = true });
l.Add(new FormField { TKey = "MemoCustom11", FieldKey = "MemoCustom11", IsCustomField = true });
l.Add(new FormField { TKey = "MemoCustom12", FieldKey = "MemoCustom12", IsCustomField = true });
l.Add(new FormField { TKey = "MemoCustom13", FieldKey = "MemoCustom13", IsCustomField = true });
l.Add(new FormField { TKey = "MemoCustom14", FieldKey = "MemoCustom14", IsCustomField = true });
l.Add(new FormField { TKey = "MemoCustom15", FieldKey = "MemoCustom15", IsCustomField = true });
l.Add(new FormField { TKey = "MemoCustom16", FieldKey = "MemoCustom16", IsCustomField = true });
_formFields.Add(SockType.Memo.ToString(), l);
}
#endregion
#region Reminder
{
List<FormField> l = new List<FormField>();
l.Add(new FormField { TKey = "ReminderColor", FieldKey = "Color" });
l.Add(new FormField { TKey = "Tags", FieldKey = "Tags" });
l.Add(new FormField { TKey = "Wiki", FieldKey = "Wiki" });
l.Add(new FormField { TKey = "Attachments", FieldKey = "Attachments", Requireable = false });
l.Add(new FormField { TKey = "ReminderCustom1", FieldKey = "ReminderCustom1", IsCustomField = true });
l.Add(new FormField { TKey = "ReminderCustom2", FieldKey = "ReminderCustom2", IsCustomField = true });
l.Add(new FormField { TKey = "ReminderCustom3", FieldKey = "ReminderCustom3", IsCustomField = true });
l.Add(new FormField { TKey = "ReminderCustom4", FieldKey = "ReminderCustom4", IsCustomField = true });
l.Add(new FormField { TKey = "ReminderCustom5", FieldKey = "ReminderCustom5", IsCustomField = true });
l.Add(new FormField { TKey = "ReminderCustom6", FieldKey = "ReminderCustom6", IsCustomField = true });
l.Add(new FormField { TKey = "ReminderCustom7", FieldKey = "ReminderCustom7", IsCustomField = true });
l.Add(new FormField { TKey = "ReminderCustom8", FieldKey = "ReminderCustom8", IsCustomField = true });
l.Add(new FormField { TKey = "ReminderCustom9", FieldKey = "ReminderCustom9", IsCustomField = true });
l.Add(new FormField { TKey = "ReminderCustom10", FieldKey = "ReminderCustom10", IsCustomField = true });
l.Add(new FormField { TKey = "ReminderCustom11", FieldKey = "ReminderCustom11", IsCustomField = true });
l.Add(new FormField { TKey = "ReminderCustom12", FieldKey = "ReminderCustom12", IsCustomField = true });
l.Add(new FormField { TKey = "ReminderCustom13", FieldKey = "ReminderCustom13", IsCustomField = true });
l.Add(new FormField { TKey = "ReminderCustom14", FieldKey = "ReminderCustom14", IsCustomField = true });
l.Add(new FormField { TKey = "ReminderCustom15", FieldKey = "ReminderCustom15", IsCustomField = true });
l.Add(new FormField { TKey = "ReminderCustom16", FieldKey = "ReminderCustom16", IsCustomField = true });
_formFields.Add(SockType.Reminder.ToString(), l);
}
#endregion
#region Review
{
List<FormField> l = new List<FormField>();
l.Add(new FormField { TKey = "Tags", FieldKey = "Tags" });
l.Add(new FormField { TKey = "Wiki", FieldKey = "Wiki" });
l.Add(new FormField { TKey = "Attachments", FieldKey = "Attachments", Requireable = false });
l.Add(new FormField { TKey = "ReviewCustom1", FieldKey = "ReviewCustom1", IsCustomField = true });
l.Add(new FormField { TKey = "ReviewCustom2", FieldKey = "ReviewCustom2", IsCustomField = true });
l.Add(new FormField { TKey = "ReviewCustom3", FieldKey = "ReviewCustom3", IsCustomField = true });
l.Add(new FormField { TKey = "ReviewCustom4", FieldKey = "ReviewCustom4", IsCustomField = true });
l.Add(new FormField { TKey = "ReviewCustom5", FieldKey = "ReviewCustom5", IsCustomField = true });
l.Add(new FormField { TKey = "ReviewCustom6", FieldKey = "ReviewCustom6", IsCustomField = true });
l.Add(new FormField { TKey = "ReviewCustom7", FieldKey = "ReviewCustom7", IsCustomField = true });
l.Add(new FormField { TKey = "ReviewCustom8", FieldKey = "ReviewCustom8", IsCustomField = true });
l.Add(new FormField { TKey = "ReviewCustom9", FieldKey = "ReviewCustom9", IsCustomField = true });
l.Add(new FormField { TKey = "ReviewCustom10", FieldKey = "ReviewCustom10", IsCustomField = true });
l.Add(new FormField { TKey = "ReviewCustom11", FieldKey = "ReviewCustom11", IsCustomField = true });
l.Add(new FormField { TKey = "ReviewCustom12", FieldKey = "ReviewCustom12", IsCustomField = true });
l.Add(new FormField { TKey = "ReviewCustom13", FieldKey = "ReviewCustom13", IsCustomField = true });
l.Add(new FormField { TKey = "ReviewCustom14", FieldKey = "ReviewCustom14", IsCustomField = true });
l.Add(new FormField { TKey = "ReviewCustom15", FieldKey = "ReviewCustom15", IsCustomField = true });
l.Add(new FormField { TKey = "ReviewCustom16", FieldKey = "ReviewCustom16", IsCustomField = true });
_formFields.Add(SockType.Review.ToString(), l);
}
#endregion
//******************************************************
}
if (!_formFields.ContainsKey(key))
throw new System.ArgumentOutOfRangeException($"FormFieldReferenceList: {key} is not valid");
return _formFields[key];
}
public static string TranslateLTCustomFieldToInternalCustomFieldName(string lTCustomFieldName)
{
var i = System.Convert.ToInt32(System.Text.RegularExpressions.Regex.Replace(
lTCustomFieldName, // Our input
"[^0-9]", // Select everything that is not in the range of 0-9
"" // Replace that with an empty string.
));
return $"c{i}";
}
}//eoc ObjectFields
public class FormField
{
private string tKey;
//CLIENT / SERVER Unique identifier used at BOTH client and server
//MUST MATCH MODEL PROPERTY NAME EXACTLY UNLESS ModelProperty is set OR REQUIRED FIELD VALIDATION WON"T WORK
//The model name is used for validation and the fieldKey sometimes is not the model name in big forms with repeating model names in which case
//the fieldkey will be unique and the ModelProperty will be set instead
public string FieldKey { get; set; }
//This exists to handle scenario of repeated identical model property multiple times on workorder quote pm forms
//e.g. need to use a unique fieldkey but it can't match the model property becuase then
//it would need to be "Tags" but there is already a "Tags" on the workorder header and in units
//so here we can specify an exact property tag to check. RequiredfieldsValidator will use this instead when set to issue errors
public string ModelProperty { get; set; } = null;
//CLIENT Use only for display in customization form, translation key to show translated name on UI customize form
public string TKey
{
get => tKey;
set
{
tKey = value;
if (this.FieldKey == null)//save having to type out fieldkey when it's identical to tkey
{
this.FieldKey = value;
}
}
}
//CLIENT Use only for display in customization form to disambiguate things like
//Tags in main workorder and Tags in Workorder Item and Tags in Unit (all on same form)
public string TKeySection { get; set; } = null;
//CLIENT form customization
public bool Hideable { get; set; }
//CLIENT form customization
public bool Requireable { get; set; }
//CLIENT / SERVER - client display server validation purposes
public bool IsCustomField { get; set; }
public FormField()
{
//most common defaults
Hideable = true;
Requireable = true;
IsCustomField = false;
}
}//eoc
}//ens

View File

@@ -0,0 +1,158 @@
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
namespace Sockeye.Biz
{
//## This class manages personal form settings for users
internal class FormUserOptionsBiz : BizObject
{
internal FormUserOptionsBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = SockType.FormUserOptions;
}
internal static FormUserOptionsBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new FormUserOptionsBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new FormUserOptionsBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExistsAsync(long id)
{
return await ct.FormUserOptions.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<FormUserOptions> UpsertAsync(FormUserOptions newObject)
{
//Validate(newObject, null);
newObject.UserId=UserId;//always defaults to currently logged in user
if (HasErrors)
return null;
else
{
//remove any prior version that might exist (or might not)
await DeleteAsync(newObject.FormKey);
newObject.Options = JsonUtil.CompactJson(newObject.Options);
await ct.FormUserOptions.AddAsync(newObject);
await ct.SaveChangesAsync();
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//GET
//
internal async Task<FormUserOptions> GetAsync(string formKey)
{
var ret = await ct.FormUserOptions.AsNoTracking().SingleOrDefaultAsync(m => m.FormKey == formKey && m.UserId == UserId);
return ret;
}
// ////////////////////////////////////////////////////////////////////////////////////////////////
// //UPDATE
// //
// internal async Task<FormUserOptions> PutAsync(FormUserOptions putObject)
// {
// var dbObject = await GetAsync(putObject.FormKey);
// if (dbObject == null)
// {
// AddError(ApiErrorCode.NOT_FOUND, "formKey");
// return null;
// }
// if (dbObject.Concurrency != putObject.Concurrency)
// {
// AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
// return null;
// }
// putObject.Options = JsonUtil.CompactJson(putObject.Options);
// Validate(putObject, dbObject);
// if (HasErrors) return null;
// ct.Replace(dbObject, putObject);
// try
// {
// await ct.SaveChangesAsync();
// }
// catch (DbUpdateConcurrencyException)
// {
// if (!await ExistsAsync(putObject.Id))
// AddError(ApiErrorCode.NOT_FOUND);
// else
// AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
// return null;
// }
// return putObject;
// }
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> DeleteAsync(string formKey)
{
// using (var transaction = await ct.Database.BeginTransactionAsync())
// {
var dbObject = await GetAsync(formKey);
if (dbObject == null)
{
return true;
}
// ValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.FormUserOptions.Remove(dbObject);
await ct.SaveChangesAsync();
// await transaction.CommitAsync();
return true;
// }
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
// private void Validate(FormUserOptions proposedObj, FormUserOptions currentObj)
// {
// if (proposedObj.UserId != UserId)
// {
// AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror", "A user can only modify their own personal form settings. UserId does not match current api user logged in.");
// }
// }
// private void ValidateCanDelete(FormUserOptions inObj)
// {
// if (inObj.UserId != UserId)
// {
// AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror", "A user can only modify their own personal form settings. UserId does not match current api user logged in.");
// }
// }
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,121 @@
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
using System.Collections.Generic;
namespace Sockeye.Biz
{
internal class GlobalBizSettingsBiz : BizObject
{
internal GlobalBizSettingsBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = SockType.Global;
}
internal static GlobalBizSettingsBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new GlobalBizSettingsBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new GlobalBizSettingsBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// GET
//Get one
internal async Task<GlobalBizSettings> GetAsync(bool logTheGetEvent = true)
{
//first try to fetch from db
var ret = await ct.GlobalBizSettings.AsNoTracking().SingleOrDefaultAsync(m => m.Id == 1);
if (logTheGetEvent && ret != null)
{
//Log
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, 1, BizType, SockEvent.Retrieved), ct);
}
//not in db then get the default
if (ret == null)
{
throw new System.Exception("GlobalBizSettingsBiz::GetAsync -> Global settings object not found in database!!");
}
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<GlobalBizSettings> PutAsync(GlobalBizSettings putObject)
{
var dbObject = await GetAsync(false);
if (dbObject == null)
throw new System.Exception("GlobalBizSettingsBiz::PutAsync -> Global settings object not found in database. Contact support immediately!");
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
Validate(putObject, dbObject);
if (HasErrors) return null;
List<string> originalTags = dbObject.AllTags();
List<string> newTags = putObject.AllTags();
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
//Update cache
ServerGlobalBizSettings.Initialize(putObject, null);
//Log modification and save context
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, 1, BizType, SockEvent.Modified), ct);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newTags, originalTags);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private void Validate(GlobalBizSettings proposedObj, GlobalBizSettings currentObj)
{
//currently nothing to validate
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,128 @@
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
namespace Sockeye.Biz
{
internal class GlobalOpsBackupSettingsBiz : BizObject
{
internal GlobalOpsBackupSettingsBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = SockType.Backup;
}
internal static GlobalOpsBackupSettingsBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new GlobalOpsBackupSettingsBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new GlobalOpsBackupSettingsBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// GET
//Get one
internal async Task<GlobalOpsBackupSettings> GetAsync(bool logTheGetEvent = true)
{
//first try to fetch from db
var ret = await ct.GlobalOpsBackupSettings.SingleOrDefaultAsync(m => m.Id == 1);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, 1, BizType, SockEvent.Retrieved), ct);
//expected to exists because it's created on boot if not present
if (ret == null)
throw new System.Exception("GlobalOpsBackupSettings::GetAsync -> Backup settings object not found in database!!");
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
//put
internal async Task<GlobalOpsBackupSettings> PutAsync(GlobalOpsBackupSettings putObject)
{
//todo: replace with new put methodology?
var dbObject = await ct.GlobalOpsBackupSettings.FirstOrDefaultAsync(m => m.Id == 1);
if (dbObject == null)
throw new System.Exception("GlobalOpsBackupSettings::PutAsync -> Global settings object not found in database!!");
// //testing UTC fuckiness
// var utcNow = DateTime.UtcNow;
// var desiredBackupTime = new DateTime(2020, 5, 23, 23, 55, 0).ToUniversalTime();
// var NextBackup = new DateTime(utcNow.Year, utcNow.Month, utcNow.Day, putObject.BackupTime.Hour, putObject.BackupTime.Minute, 0, DateTimeKind.Utc);
// if (NextBackup < utcNow) NextBackup = NextBackup.AddDays(1);
// //theory if nexxtbacup at the end of the adjustment is in the past then add a day to it
//If backup time has changed then reset last backup as well as it might block from taking effect
var ResetLastBackup = (putObject.BackupTime.Hour != dbObject.BackupTime.Hour || putObject.BackupTime.Minute != dbObject.BackupTime.Minute);
CopyObject.Copy(putObject, dbObject, "Id");
ct.Entry(dbObject).OriginalValues["Concurrency"] = putObject.Concurrency;
Validate(dbObject);
if (HasErrors)
return null;
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, 1, BizType, SockEvent.Modified), ct);
//Update the static copy for the server
ServerGlobalOpsSettingsCache.Backup = dbObject;
if (ResetLastBackup)
{
ServerGlobalOpsSettingsCache.NextBackup = DateTime.MinValue;
ServerGlobalOpsSettingsCache.SetNextBackup();
}
return dbObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private void Validate(GlobalOpsBackupSettings inObj)
{
//currently nothing to validate
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,110 @@
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
namespace Sockeye.Biz
{
internal class GlobalOpsNotificationSettingsBiz : BizObject
{
internal GlobalOpsNotificationSettingsBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = SockType.OpsNotificationSettings;
}
internal static GlobalOpsNotificationSettingsBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new GlobalOpsNotificationSettingsBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new GlobalOpsNotificationSettingsBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// GET
//Get one
internal async Task<GlobalOpsNotificationSettings> GetAsync(bool logTheGetEvent = true)
{
//first try to fetch from db
var ret = await ct.GlobalOpsNotificationSettings.SingleOrDefaultAsync(m => m.Id == 1);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, 1, BizType, SockEvent.Retrieved), ct);
//expected to exists because it's created on boot if not present
if (ret == null)
throw new System.Exception("GlobalOpsNotificationSettings::GetAsync -> Settings object not found in database!!");
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
//put
internal async Task<GlobalOpsNotificationSettings> PutAsync(GlobalOpsNotificationSettings putObject)
{
//todo: replace with new put methodology
var dbObject = await ct.GlobalOpsNotificationSettings.FirstOrDefaultAsync(m => m.Id == 1);
if (dbObject == null)
throw new System.Exception("GlobalOpsNotificationSettings::PutAsync -> Settings object not found in database!!");
CopyObject.Copy(putObject, dbObject, "Id");
ct.Entry(dbObject).OriginalValues["Concurrency"] = putObject.Concurrency;
Validate(dbObject);
if (HasErrors)
return null;
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, 1, BizType, SockEvent.Modified), ct);
//Update the static copy for the server
ServerGlobalOpsSettingsCache.Notify = dbObject;
return dbObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private void Validate(GlobalOpsNotificationSettings inObj)
{
//currently nothing to validate
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

597
server/biz/HeadOfficeBiz.cs Normal file
View File

@@ -0,0 +1,597 @@
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.Linq;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Sockeye.Biz
{
internal class HeadOfficeBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject, IImportAbleObject, INotifiableObject
{
internal HeadOfficeBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = SockType.HeadOffice;
}
internal static HeadOfficeBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new HeadOfficeBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new HeadOfficeBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExistsAsync(long id)
{
return await ct.HeadOffice.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<HeadOffice> CreateAsync(HeadOffice newObject)
{
await ValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.HeadOffice.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, SockEvent.Created), ct);
await SearchIndexAsync(newObject, true);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await HandlePotentialNotificationEvent(SockEvent.Created, newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//GET
//
internal async Task<HeadOffice> GetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.HeadOffice.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, SockEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<HeadOffice> PutAsync(HeadOffice putObject)
{
var dbObject = await GetAsync(putObject.Id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
putObject.Tags = TagBiz.NormalizeTags(putObject.Tags);
putObject.CustomFields = JsonUtil.CompactJson(putObject.CustomFields);
await ValidateAsync(putObject, dbObject);
if (HasErrors) return null;
ct.Replace(dbObject, putObject);
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, SockEvent.Modified), ct);
await SearchIndexAsync(putObject, false);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
await HandlePotentialNotificationEvent(SockEvent.Modified, putObject, dbObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> DeleteAsync(long id)
{
using (var transaction = await ct.Database.BeginTransactionAsync())
{
var dbObject = await GetAsync(id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND);
return false;
}
await ValidateCanDeleteAsync(dbObject);
if (HasErrors)
return false;
//DELETE DIRECT CHILD OBJECTS
{
var ContactIds = await ct.User.AsNoTracking().Where(z => z.HeadOfficeId == 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.CHILD_OBJECT_ERROR, null, $"HeadOfficeContact [{ItemId}]: {b.GetErrorsAsString()}");
return false;
}
}
{
var IDList = await ct.Review.AsNoTracking().Where(x => x.SockType == SockType.HeadOffice && x.ObjectId == id).Select(x => x.Id).ToListAsync();
if (IDList.Count() > 0)
{
ReviewBiz b = new ReviewBiz(ct, UserId, UserTranslationId, CurrentUserRoles);
foreach (long ItemId in IDList)
if (!await b.DeleteAsync(ItemId, transaction))
{
AddError(ApiErrorCode.CHILD_OBJECT_ERROR, null, $"Review [{ItemId}]: {b.GetErrorsAsString()}");
return false;
}
}
}
}
ct.HeadOffice.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
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);
await transaction.CommitAsync();
await HandlePotentialNotificationEvent(SockEvent.Deleted, dbObject);
return true;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//SEARCH
//
private async Task SearchIndexAsync(HeadOffice obj, bool isNew)
{
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType);
DigestSearchText(obj, SearchParams);
if (isNew)
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
else
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
}
public async Task<Search.SearchIndexProcessObjectParameters> GetSearchResultSummary(long id, SockType specificType)
{
var obj = await GetAsync(id, false);
var SearchParams = new Search.SearchIndexProcessObjectParameters();
DigestSearchText(obj, SearchParams);
return SearchParams;
}
public void DigestSearchText(HeadOffice obj, Search.SearchIndexProcessObjectParameters searchParams)
{
if (obj != null)
searchParams.AddText(obj.Notes)
.AddText(obj.Name)
.AddText(obj.Wiki)
.AddText(obj.Tags)
.AddText(obj.WebAddress)
.AddText(obj.AccountNumber)
.AddText(obj.Phone1)
.AddText(obj.Phone2)
.AddText(obj.Phone3)
.AddText(obj.Phone4)
.AddText(obj.Phone5)
.AddText(obj.EmailAddress)
.AddText(obj.PostAddress)
.AddText(obj.PostCity)
.AddText(obj.PostRegion)
.AddText(obj.PostCountry)
.AddText(obj.PostCode)
.AddText(obj.Address)
.AddText(obj.City)
.AddText(obj.Region)
.AddText(obj.Country)
.AddText(obj.AddressPostal)
.AddCustomFields(obj.CustomFields);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task ValidateAsync(HeadOffice proposedObj, HeadOffice currentObj)
{
bool isNew = currentObj == null;
//Name required
if (string.IsNullOrWhiteSpace(proposedObj.Name))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name");
//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.HeadOffice.AnyAsync(z => z.Name == proposedObj.Name && z.Id != proposedObj.Id))
{
AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name");
}
}
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == SockType.HeadOffice.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 async Task ValidateCanDeleteAsync(HeadOffice inObj)
{
//Referential integrity
//FOREIGN KEY CHECKS
if (await ct.User.AnyAsync(m => m.HeadOfficeId == inObj.Id))
AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("User"));
if (await ct.Customer.AnyAsync(z => z.HeadOfficeId == inObj.Id) == true)
AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("Customer"));
}
////////////////////////////////////////////////////////////////////////////////////////////////
//REPORTING
//
public async Task<JArray> GetReportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
{
var idList = dataListSelectedRequest.SelectedRowIds;
JArray ReportData = new JArray();
while (idList.Any())
{
var batch = idList.Take(IReportAbleObject.REPORT_DATA_BATCH_SIZE);
idList = idList.Skip(IReportAbleObject.REPORT_DATA_BATCH_SIZE).ToArray();
//query for this batch, comes back in db natural order unfortunately
var batchResults = await ct.HeadOffice.AsNoTracking().Where(z => batch.Contains(z.Id)).ToArrayAsync();
//order the results back into original
var orderedList = from id in batch join z in batchResults on id equals z.Id select z;
batchResults = null;
foreach (HeadOffice w in orderedList)
{
if (!ReportRenderManager.KeepGoing(jobId)) return null;
// await PopulateVizFields(w);
var jo = JObject.FromObject(w);
if (!JsonUtil.JTokenIsNullOrEmpty(jo["CustomFields"]))
jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]);
ReportData.Add(jo);
}
orderedList = null;
}
vc.Clear();
return ReportData;
}
private VizCache vc = new VizCache();
// //populate viz fields from provided object
// private async Task PopulateVizFields(HeadOffice o)
// {
// }
////////////////////////////////////////////////////////////////////////////////////////////////
// IMPORT EXPORT
//
public async Task<JArray> GetExportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
{
//for now just re-use the report data code
//this may turn out to be the pattern for most biz object types but keeping it seperate allows for custom usage from time to time
return await GetReportData(dataListSelectedRequest, jobId);
}
public async Task<List<string>> ImportData(AyImportData importData)
{
List<string> ImportResult = new List<string>();
string ImportTag = ImportUtil.GetImportTag();
//ignore these fields
var jsset = JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = new Sockeye.Util.JsonUtil.ShouldSerializeContractResolver(new string[] { "Concurrency", "Id", "CustomFields" }) });
foreach (JObject j in importData.Data)
{
try
{
long existingId = await ct.HeadOffice.AsNoTracking().Where(z => z.Name == (string)j["Name"]).Select(x => x.Id).FirstOrDefaultAsync();
if (existingId == 0)
{
if (importData.DoImport)
{
//import this record
var Target = j.ToObject<HeadOffice>(jsset);
Target.Tags.Add(ImportTag);
var res = await CreateAsync(Target);
if (res == null)
{
ImportResult.Add($"❌ {Target.Name}\r\n{this.GetErrorsAsString()}");
this.ClearErrors();
}
else
{
ImportResult.Add($"✔️ {Target.Name}");
}
}
}
else
{
if (importData.DoUpdate)
{
//update this record with any data provided
//load existing record
var Target = await GetAsync((long)existingId);
var Source = j.ToObject<HeadOffice>(jsset);
var propertiesToUpdate = j.Properties().Select(p => p.Name).ToList();
propertiesToUpdate.Remove("Name");
ImportUtil.Update(Source, Target, propertiesToUpdate);
var res = await PutAsync(Target);
if (res == null)
{
ImportResult.Add($"❌ {Target.Name} - {this.GetErrorsAsString()}");
this.ClearErrors();
}
else
{
ImportResult.Add($"✔️ {Target.Name}");
}
}
}
}
catch (Exception ex)
{
ImportResult.Add($"❌ Exception processing import\n record:{j.ToString()}\nError:{ex.Message}\nSource:{ex.Source}\nStack:{ex.StackTrace.ToString()}");
}
}
return ImportResult;
}
// public async Task<List<string>> ImportData(AyImportData importData)
// {
// List<string> ImportResult = new List<string>();
// string ImportTag = $"imported-{FileUtil.GetSafeDateFileName()}";
// var jsset = JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = new Sockeye.Util.JsonUtil.ShouldSerializeContractResolver(new string[] { "Concurrency", "Id", "CustomFields" }) });
// foreach (JObject j in importData.Data)
// {
// var w = j.ToObject<HeadOffice>(jsset);
// if (j["CustomFields"] != null)
// w.CustomFields = j["CustomFields"].ToString();
// w.Tags.Add(ImportTag);//so user can find them all and revert later if necessary
// var res = await CreateAsync(w);
// if (res == null)
// {
// ImportResult.Add($"* {w.Name} - {this.GetErrorsAsString()}");
// this.ClearErrors();
// }
// else
// {
// ImportResult.Add($"{w.Name} - ok");
// }
// }
// return ImportResult;
// }
////////////////////////////////////////////////////////////////////////////////////////////////
//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.BatchCoreObjectOperation:
await ProcessBatchJobAsync(job);
break;
default:
throw new System.ArgumentOutOfRangeException($"HeadOfficeBiz.HandleJob-> Invalid job type{job.JobType.ToString()}");
}
}
private async Task ProcessBatchJobAsync(OpsJob job)
{
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running);
await JobsBiz.LogJobAsync(job.GId, $"LT:StartJob {job.SubType}");
List<long> idList = new List<long>();
long FailedObjectCount = 0;
JObject jobData = JObject.Parse(job.JobInfo);
if (jobData.ContainsKey("idList"))
idList = ((JArray)jobData["idList"]).ToObject<List<long>>();
else
idList = await ct.HeadOffice.AsNoTracking().Select(z => z.Id).ToListAsync();
bool SaveIt = false;
//---------------------------------
//case 4192
TimeSpan ProgressAndCancelCheckSpan = new TimeSpan(0, 0, ServerBootConfig.JOB_PROGRESS_UPDATE_AND_CANCEL_CHECK_SECONDS);
DateTime LastProgressCheck = DateTime.UtcNow.Subtract(new TimeSpan(1, 1, 1, 1, 1));
var TotalRecords = idList.LongCount();
long CurrentRecord = -1;
//---------------------------------
foreach (long id in idList)
{
try
{
//--------------------------------
//case 4192
//Update progress / cancel requested?
CurrentRecord++;
if (DateUtil.IsAfterDuration(LastProgressCheck, ProgressAndCancelCheckSpan))
{
await JobsBiz.UpdateJobProgressAsync(job.GId, $"{CurrentRecord}/{TotalRecords}");
if (await JobsBiz.GetJobStatusAsync(job.GId) == JobStatus.CancelRequested)
break;
LastProgressCheck = DateTime.UtcNow;
}
//---------------------------------
SaveIt = false;
ClearErrors();
HeadOffice o = null;
//save a fetch if it's a delete
if (job.SubType != JobSubType.Delete)
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.ProcessBatchTagOperation(o.Tags, (string)jobData["tag"], jobData.ContainsKey("toTag") ? (string)jobData["toTag"] : null, job.SubType);
break;
case JobSubType.Delete:
if (!await DeleteAsync(id))
{
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}");
FailedObjectCount++;
}
break;
default:
throw new System.ArgumentOutOfRangeException($"ProcessBatchJobAsync -> Invalid job Subtype{job.SubType}");
}
if (SaveIt)
{
o = await PutAsync(o);
if (o == null)
{
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}");
FailedObjectCount++;
}
}
//delay so we're not tying up all the resources in a tight loop
await Task.Delay(Sockeye.Util.ServerBootConfig.JOB_OBJECT_HANDLE_BATCH_JOB_LOOP_DELAY);
}
catch (Exception ex)
{
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors id({id})");
await JobsBiz.LogJobAsync(job.GId, ExceptionUtil.ExtractAllExceptionMessages(ex));
}
}
//---------------------------------
//case 4192
await JobsBiz.UpdateJobProgressAsync(job.GId, $"{++CurrentRecord}/{TotalRecords}");
//---------------------------------
await JobsBiz.LogJobAsync(job.GId, $"LT:BatchJob {job.SubType} {idList.Count}{(FailedObjectCount > 0 ? " - LT:Failed " + FailedObjectCount : "")}");
await JobsBiz.UpdateJobStatusAsync(job.GId, FailedObjectCount == 0 ? JobStatus.Completed : JobStatus.Failed);
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task HandlePotentialNotificationEvent(SockEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = Sockeye.Util.ApplicationLogging.CreateLogger<HeadOfficeBiz>();
log.LogDebug($"HandlePotentialNotificationEvent processing: [SockType:{this.BizType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
HeadOffice o = (HeadOffice)proposedObj;
//## DELETED EVENTS
//any event added below needs to be removed, so
//just blanket remove any event for this object of eventtype that would be added below here
//do it regardless any time there's an update and then
//let this code below handle the refreshing addition that could have changes
// await NotifyEventHelper.ClearPriorEventsForObject(ct, SockType.HeadOffice, o.Id, NotifyEventType.ContractExpiring);
//## CREATED / MODIFIED EVENTS
if (ayaEvent == SockEvent.Created || ayaEvent == SockEvent.Modified)
{
}
}//end of process notifications
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

51
server/biz/IBizObject.cs Normal file
View File

@@ -0,0 +1,51 @@
using System.Collections.Generic;
namespace Sockeye.Biz
{
/// <summary>
///
/// </summary>
internal interface IBizObject
{
//validate via validation attributes
//https://stackoverflow.com/questions/36330981/is-the-validationresult-class-suitable-when-validating-the-state-of-an-object
/// <summary>
/// Contains list of errors
/// </summary>
List<ValidationError> Errors { get; }
/// <summary>
/// Is true if there are errors
/// </summary>
bool HasErrors { get; }
/// <summary>
/// Is true if the field specified exists in the list
/// </summary>
bool PropertyHasErrors(string propertyName);
/// <summary>
///
/// </summary>
/// <param name="errorCode"></param>
/// <param name="errorMessage"></param>
/// <param name="propertyName"></param>
void AddError(ApiErrorCode errorCode, string propertyName = null, string errorMessage = null);
// /// <summary>
// ///
// /// </summary>
// /// <param name="validationError"></param>
// void AddvalidationError(ValidationError validationError);
}
}

View File

@@ -0,0 +1,21 @@
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Sockeye.Models;
using System;
namespace Sockeye.Biz
{
/// <summary>
/// Interface for biz objects that support exporting
/// </summary>
internal interface IExportAbleObject
{
//Get items indicated in id list in exportable format
//called by ExportBiz rendering code
Task<JArray> GetExportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId);
const int EXPORT_DATA_BATCH_SIZE = 100;
}
}

View File

@@ -0,0 +1,15 @@
using System.Threading.Tasks;
using System.Collections.Generic;
using Sockeye.Models;
namespace Sockeye.Biz
{
/// <summary>
/// Interface for biz objects that support importing from JSON
/// </summary>
internal interface IImportAbleObject
{
Task<List<string>> ImportData(AyImportData importData);
}
}

22
server/biz/IJobObject.cs Normal file
View File

@@ -0,0 +1,22 @@
using Sockeye.Models;
namespace Sockeye.Biz
{
/// <summary>
/// Interface for biz objects that support jobs / long running operations
/// </summary>
internal interface IJobObject
{
/// <summary>
/// Start and process an operation
/// 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
/// </summary>
/// <param name="job"></param>
System.Threading.Tasks.Task HandleJobAsync(OpsJob job);
}
}

View File

@@ -0,0 +1,12 @@
using System.Threading.Tasks;
using Sockeye.Models;
namespace Sockeye.Biz
{
/// <summary>
/// Interface for biz objects that support notification
/// </summary>
internal interface INotifiableObject
{
Task HandlePotentialNotificationEvent(SockEvent ayaEvent, ICoreBizObjectModel newObject, ICoreBizObjectModel originalObject = null);
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Threading.Tasks;
using Sockeye.Models;
using Newtonsoft.Json.Linq;
namespace Sockeye.Biz
{
/// <summary>
/// Interface for biz objects that support reporting
/// </summary>
internal interface IReportAbleObject
{
//Get items indicated in id list in report format
//called by ReportBiz rendering code
Task<JArray> GetReportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId);
const int REPORT_DATA_BATCH_SIZE = 100;
}
}

View File

@@ -0,0 +1,16 @@
using System.Threading.Tasks;
namespace Sockeye.Biz
{
/// <summary>
/// Interface for biz objects that support searching
/// </summary>
internal interface ISearchAbleObject
{
//get all text for the object that would have been indexed for search
//called by search::GetInfoAsync as a result of a user requesting a search result sumary
Task<Search.SearchIndexProcessObjectParameters> GetSearchResultSummary(long id, SockType specificType);
}
}

View File

@@ -0,0 +1,15 @@
using System;
namespace Sockeye.Biz
{
/// <summary>
/// Marker attribute indicating that an object is a importable type
/// Used in <see cref="SockType"/>
/// </summary>
[AttributeUsage(AttributeTargets.All)]
public class ImportableBizObjectAttribute : Attribute
{
//No code required, it's just a marker
//https://docs.microsoft.com/en-us/dotnet/standard/attributes/writing-custom-attributes
}
}//eons

View File

@@ -0,0 +1,253 @@
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
using System.Linq;
using System.Collections.Generic;
namespace Sockeye.Biz
{
internal class IntegrationBiz : BizObject
{
/*
todo: needs code to back routes for logging and fetching log to view
*/
internal IntegrationBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = SockType.Integration;
}
internal static IntegrationBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new IntegrationBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new IntegrationBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExistsAsync(long id)
{
return await ct.Integration.AnyAsync(z => z.Id == id);
}
internal async Task<bool> ExistsByIntegrationAppIdAsync(Guid IntegrationAppId)
{
return await ct.Integration.AnyAsync(z => z.IntegrationAppId == IntegrationAppId);
}
///////////////////////////////////////////////////////
//APPID FROM dbID
internal async Task<Guid> AppIdFromDbIdAsync(long id)
{
return await ct.Integration.AsNoTracking().Where(z => z.Id == id).Select(z => z.IntegrationAppId).SingleOrDefaultAsync();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<Integration> CreateAsync(Integration newObject)
{
await ValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
await ct.Integration.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, SockEvent.Created), ct);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//GET
//
internal async Task<Integration> GetAsync(long id, bool logTheGetEvent = true)
{
return await GetAsync(await AppIdFromDbIdAsync(id), logTheGetEvent);
}
internal async Task<Integration> GetAsync(Guid IntegrationAppId, bool logTheGetEvent = true)
{
var ret = await ct.Integration.AsNoTracking().Include(z => z.Items).SingleOrDefaultAsync(m => m.IntegrationAppId == IntegrationAppId);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, ret.Id, BizType, SockEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<Integration> PutAsync(Integration putObject)
{
//Get the db object with no tracking as about to be replaced not updated
Integration dbObject = await GetAsync(putObject.IntegrationAppId, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "IntegrationAppId");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await ValidateAsync(putObject, dbObject);
if (HasErrors) return null;
ct.Replace(dbObject, putObject);
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, SockEvent.Modified), ct);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> DeleteAsync(long id)
{
return await DeleteAsync(await AppIdFromDbIdAsync(id));
}
internal async Task<bool> DeleteAsync(Guid IntegrationAppId)
{
using (var transaction = await ct.Database.BeginTransactionAsync())
{
Integration dbObject = await GetAsync(IntegrationAppId, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND);
return false;
}
ValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.Integration.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObject.Id, dbObject.Name, ct);
await transaction.CommitAsync();
return true;
}
}
/////////////////////////////////////////////////////////////////////////////////
//LOG to integration log
//
internal async Task<bool> LogAsync(NameIdItem logItem)
{
if (string.IsNullOrWhiteSpace(logItem.Name))
{
AddError(ApiErrorCode.NOT_FOUND, "name", "The log text message (name) is empty, nothing to log");
return false;
}
if (!await ExistsAsync(logItem.Id))
{
AddError(ApiErrorCode.NOT_FOUND, "id", "The integration id specified was not found, remember this is the internal id (integer), not the application specific id (Guid)");
return false;
}
await ct.IntegrationLog.AddAsync(new IntegrationLog { IntegrationId = logItem.Id, StatusText = logItem.Name });
await ct.SaveChangesAsync();
return true;
}
//GET LOG
internal async Task<List<IntegrationLog>> GetLogAsync(long id)
{
return await ct.IntegrationLog.AsNoTracking().Where(z=>z.IntegrationId==id).OrderByDescending(z=>z.Created).ToListAsync();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task ValidateAsync(Integration proposedObj, Integration currentObj)
{
bool isNew = currentObj == null;
//Name required
if (string.IsNullOrWhiteSpace(proposedObj.Name))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name");
//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.Integration.AnyAsync(m => m.Name == proposedObj.Name && m.Id != proposedObj.Id))
{
AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name");
}
}
//Name required
if (proposedObj.IntegrationAppId == Guid.Empty)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "IntegrationAppId");
//If name is otherwise OK, check that name is unique
if (!PropertyHasErrors("IntegrationAppId"))
{
//Use Any command is efficient way to check existance, it doesn't return the record, just a true or false
if (await ct.Integration.AnyAsync(m => m.IntegrationAppId == proposedObj.IntegrationAppId && m.Id != proposedObj.Id))
{
AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "IntegrationAppId");
}
}
}
private void ValidateCanDelete(Integration inObj)
{
//whatever needs to be check to delete this object
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,145 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Sockeye.Models;
namespace Sockeye.Biz
{
internal class JobOperationsBiz : BizObject, IJobObject
{
internal JobOperationsBiz(AyContext dbcontext, long currentUserId, AuthorizationRoles userRoles)
{
ct = dbcontext;
UserId = currentUserId;
CurrentUserRoles = userRoles;
BizType = SockType.ServerJob;
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<List<JobOperationsFetchInfo>> GetJobListAsync()
{
List<JobOperationsFetchInfo> ret = new List<JobOperationsFetchInfo>();
var jobitems = await ct.OpsJob
.OrderBy(z => z.Created)
.ToListAsync();
foreach (OpsJob i in jobitems)
{
//fetch the most recent log time for each job
var mostRecentLogItem = await ct.OpsJobLog
.Where(z => z.JobId == i.GId)
.OrderByDescending(z => z.Created)
.FirstOrDefaultAsync();
JobOperationsFetchInfo o = new JobOperationsFetchInfo();
if (mostRecentLogItem != null)
o.LastAction = mostRecentLogItem.Created;
else
o.LastAction = i.Created;
o.Created = i.Created;
o.GId = i.GId;
o.JobStatus = i.JobStatus.ToString();
o.Name = i.Name;
ret.Add(o);
}
return ret;
}
//Get list of logs for job
internal async Task<List<JobOperationsLogInfoItem>> GetJobLogListAsync(Guid jobId)
{
List<JobOperationsLogInfoItem> ret = new List<JobOperationsLogInfoItem>();
var l = await ct.OpsJobLog
.Where(z => z.JobId == jobId)
.OrderBy(z => z.Created)
.ToListAsync();
foreach (OpsJobLog i in l)
{
JobOperationsLogInfoItem o = new JobOperationsLogInfoItem();
o.Created = i.Created;
o.StatusText = i.StatusText;
o.JobId = jobId;
ret.Add(o);
}
return ret;
}
///////////////////////////////////////////////////////////////////////////////
//GET LOG OF ALL JOBS FOR CLIENT
//
internal async Task<List<JobOperationsLogInfoItem>> GetAllJobsLogsListAsync()
{
List<JobOperationsLogInfoItem> ret = new List<JobOperationsLogInfoItem>();
var l = await ct.OpsJobLog
.OrderByDescending(z => z.Created)
.ToListAsync();
foreach (OpsJobLog i in l)
{
JobOperationsLogInfoItem o = new JobOperationsLogInfoItem();
o.Created = i.Created;
o.StatusText = i.StatusText;
o.JobId = i.JobId;
ret.Add(o);
}
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//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.TestJob:
await ProcessTestJobAsync(job);
break;
default:
throw new System.ArgumentOutOfRangeException($"JobOperationsBiz.HandleJob-> Invalid job type{job.JobType.ToString()}");
}
}
private async Task ProcessTestJobAsync(OpsJob job)
{
var sleepTime = 30 * 1000;
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running);
await JobsBiz.LogJobAsync(job.GId, $"LT:StartJob");
await Task.Delay(sleepTime);
await JobsBiz.LogJobAsync(job.GId, "LT:JobCompleted");
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed);
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

19
server/biz/JobStatus.cs Normal file
View File

@@ -0,0 +1,19 @@
namespace Sockeye.Biz
{
/// <summary>
/// Job status for opsjobs
/// </summary>
public enum JobStatus : int
{
Absent = 0,
Sleeping = 1,
Running = 2,
Completed = 3,
Failed = 4,
CancelRequested = 5
}
}//eons

37
server/biz/JobType.cs Normal file
View File

@@ -0,0 +1,37 @@
namespace Sockeye.Biz
{
/// <summary>
/// All Sockeye Job types, used by OpsJob and biz objects for long running processes
/// </summary>
public enum JobType : int
{
NotSet = 0,
TestJob = 1,//test job for unit and OPS admin testing
CoreJobSweeper = 2,
SeedTestData = 4,
BatchCoreObjectOperation = 5,
Backup = 6,
AttachmentMaintenance = 7,
RenderReport=8,
ExportData=9
}
/// <summary>
/// SubTypes for jobs that have further distinctions
/// </summary>
public enum JobSubType : int
{
NotSet = 0,
TagAdd = 1,
TagAddAny = 2,
TagRemove = 3,
TagRemoveAny = 4,
TagReplace = 5,
TagReplaceAny = 6,
Delete = 7
}
}//eons

412
server/biz/JobsBiz.cs Normal file
View File

@@ -0,0 +1,412 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Sockeye.Models;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
namespace Sockeye.Biz
{
internal static class JobsBiz
{
private static ILogger log = Sockeye.Util.ApplicationLogging.CreateLogger("JobsBiz");
#region JOB OPS
/// <summary>
/// Get a non tracked list of jobs that are ready to process and exclusive only
/// </summary>
/// <returns></returns>
internal static async Task<List<OpsJob>> GetReadyJobsExclusiveOnlyAsync()
{
return await GetReadyJobsAsync(true);
}
/// <summary>
/// Get a non tracked list of jobs that are ready to process and exclusive only
/// </summary>
/// <returns></returns>
internal static async Task<List<OpsJob>> GetReadyJobsNotExlusiveOnlyAsync()
{
return await GetReadyJobsAsync(false);
}
/// <summary>
/// Get a non tracked list of jobs filtered by exclusivity
/// </summary>
/// <returns></returns>
private static async Task<List<OpsJob>> GetReadyJobsAsync(bool exclusiveOnly)
{
using (AyContext ct = ServiceProviderProvider.DBContext)
{
var ret = await ct.OpsJob
.AsNoTracking()
.Where(z => z.StartAfter < System.DateTime.UtcNow && z.Exclusive == exclusiveOnly && z.JobStatus == JobStatus.Sleeping)
.OrderBy(z => z.Created)
.ToListAsync();
return ret;
}
}
/// <summary>
/// Add a new job to the database
/// </summary>
/// <param name="newJob"></param>
internal static async Task AddJobAsync(OpsJob newJob)
{
using (AyContext ct = ServiceProviderProvider.DBContext)
{
log.LogDebug($"Adding new job:{newJob.ToString()}");
await LogJobAsync(newJob.GId, $"LT:JobCreated \"{newJob.Name}\"");
await ct.OpsJob.AddAsync(newJob);
await ct.SaveChangesAsync();
}
}
/// <summary>
/// Request the cancellation of a job, not all jobs honour this
/// </summary>
/// <param name="jobId"></param>
internal static async Task RequestCancelAsync(Guid jobId)
{
await UpdateJobStatusAsync(jobId, JobStatus.CancelRequested);
await LogJobAsync(jobId, "LT:Cancel");
}
/// <summary>
/// Remove the job and it's logs
/// </summary>
/// <param name="jobIdToBeDeleted"></param>
internal static async Task RemoveJobAndLogsAsync(Guid jobIdToBeDeleted)
{
using (AyContext ct = ServiceProviderProvider.DBContext)
using (var transaction = await ct.Database.BeginTransactionAsync())
{
try
{
log.LogDebug($"RemoveJobAndLogs for job id:{jobIdToBeDeleted}");
//delete logs
await ct.Database.ExecuteSqlInterpolatedAsync($"delete from aopsjoblog where jobid = {jobIdToBeDeleted}");
//delete the job
await ct.Database.ExecuteSqlInterpolatedAsync($"delete from aopsjob where gid = {jobIdToBeDeleted}");
// Commit transaction if all commands succeed, transaction will auto-rollback
// when disposed if either commands fails
await transaction.CommitAsync();
}
catch
{
throw;
}
}
}
/// <summary>
/// Make a log entry for a job
/// </summary>
/// <param name="jobId">(NOTE: Guid.empty indicates internal job)</param>
/// <param name="statusText"></param>
internal static async Task LogJobAsync(Guid jobId, string statusText)
{
using (AyContext ct = ServiceProviderProvider.DBContext)
{
if (string.IsNullOrWhiteSpace(statusText))
statusText = "No status provided";
OpsJobLog newObj = new OpsJobLog();
newObj.JobId = jobId;
newObj.StatusText = statusText;
await ct.OpsJobLog.AddAsync(newObj);
await ct.SaveChangesAsync();
}
}
/// <summary>
/// Update the status of a job
/// </summary>
/// <param name="jobId"></param>
/// <param name="newStatus"></param>
internal static async Task UpdateJobStatusAsync(Guid jobId, JobStatus newStatus)
{
using (AyContext ct = ServiceProviderProvider.DBContext)
{
var oFromDb = await ct.OpsJob.SingleOrDefaultAsync(z => z.GId == jobId);
if (oFromDb == null) return;
oFromDb.JobStatus = newStatus;
await ct.SaveChangesAsync();
}
}
/// <summary>
/// Get the status of a job
/// </summary>
/// <param name="jobId"></param>
internal static async Task<JobStatus> GetJobStatusAsync(Guid jobId)
{
using (AyContext ct = ServiceProviderProvider.DBContext)
{
var o = await ct.OpsJob.AsNoTracking().SingleOrDefaultAsync(z => z.GId == jobId);
if (o == null) return JobStatus.Absent;
return o.JobStatus;
}
}
/// <summary>
/// Update the progress of a job
/// </summary>
/// <param name="jobId"></param>
/// <param name="progress"></param>
internal static async Task UpdateJobProgressAsync(Guid jobId, string progress)
{
using (AyContext ct = ServiceProviderProvider.DBContext)
{
var oFromDb = await ct.OpsJob.SingleOrDefaultAsync(z => z.GId == jobId);
if (oFromDb == null) return;
oFromDb.Progress = progress;
await ct.SaveChangesAsync();
}
}
/// <summary>
/// Get the progress and status of a job
/// </summary>
/// <param name="jobId"></param>
internal static async Task<JobProgress> GetJobProgressAsync(Guid jobId)
{
using (AyContext ct = ServiceProviderProvider.DBContext)
{
var o = await ct.OpsJob.AsNoTracking().SingleOrDefaultAsync(z => z.GId == jobId);
if (o == null) return new JobProgress() { JobStatus = JobStatus.Absent, Progress = string.Empty };
return new JobProgress() { JobStatus = o.JobStatus, Progress = o.Progress };
}
}
#endregion Job ops
#region PROCESSOR
internal static bool KeepOnWorking()
{
ApiServerState serverState = ServiceProviderProvider.ServerState;
//system lock (no license) is a complete deal breaker for continuation beyond here
if (serverState.IsSystemLocked) return false;
return true;
}
static bool ActivelyProcessing = false;
/// <summary>
/// Process all jobs (stock jobs and those found in operations table)
/// </summary>
/// <returns></returns>
internal static async Task ProcessJobsAsync()
{
if (ActivelyProcessing)
{
//System.Diagnostics.Debug.WriteLine("ProcessJobs called but actively processing other jobs so returning");
//log.LogTrace("ProcessJobs called but actively processing other jobs so returning");
return;
}
//Do not process if there is no db, everything relies on it below here
if (!ServerGlobalOpsSettingsCache.DBAVAILABLE)
{
//This will set dbavailable flag if it becomes available
DbUtil.CheckDatabaseServerAvailable(log);
return;
}
ActivelyProcessing = true;
log.LogTrace("Processing internal jobs");
try
{
log.LogTrace("Processing level 1 internal jobs");
//######################################################################################
//### Critical internal jobs
//METRICS
CoreJobMetricsSnapshot.DoWork();
//######################################################################################
//## JOBS that will not run in a license or import mode or other system lock scenario from here down
if (!KeepOnWorking()) return;
log.LogTrace("Processing level 2 internal jobs");
//BACKUP
await CoreJobBackup.DoWorkAsync();
if (!KeepOnWorking()) return;
//NOTIFICATIONS
await CoreJobNotify.DoWorkAsync();
if (!KeepOnWorking()) return;
await CoreNotificationSweeper.DoWorkAsync();
if (!KeepOnWorking()) return;
//JOB SWEEPER / AND USER COUNT CHECK
await CoreJobSweeper.DoWorkAsync();
if (!KeepOnWorking()) return;
//Cleanup temp folder
CoreJobTempFolderCleanup.DoWork();
if (!KeepOnWorking()) return;
//Check for and kill stuck report rendering engine processes
await CoreJobReportRenderEngineProcessCleanup.DoWork();
if (!KeepOnWorking()) return;
//CUSTOMER NOTIFICATIONS
TaskUtil.Forget(Task.Run(() => CoreJobCustomerNotify.DoWorkAsync()));//must fire and forget as it will call a report render job. In fact probably all of these can be fire and forget
//INTEGRATION LOG SWEEP
await CoreIntegrationLogSweeper.DoWorkAsync();
if (!KeepOnWorking()) return;
log.LogTrace("Processing exclusive dynamic jobs");
//BIZOBJECT DYNAMIC JOBS
//get a list of exclusive jobs that are due to happen
//Call into each item in turn
List<OpsJob> exclusiveJobs = await GetReadyJobsExclusiveOnlyAsync();
foreach (OpsJob j in exclusiveJobs)
{
if (!KeepOnWorking()) return;
try
{
await ProcessJobAsync(j);
}
catch (Exception ex)
{
log.LogError(ex, $"ProcessJobs::Exclusive -> job {j.Name} failed with exception");
await LogJobAsync(j.GId, "LT:JobFailed");
await LogJobAsync(j.GId, ExceptionUtil.ExtractAllExceptionMessages(ex));
await UpdateJobStatusAsync(j.GId, JobStatus.Failed);
}
}
//### Server state dependent jobs
ApiServerState serverState = ServiceProviderProvider.ServerState;
//### API Open only jobs
if (!serverState.IsOpen)
{
log.LogDebug("Server state is NOT open, skipping processing non-exclusive dynamic jobs");
return;
}
///////////////////////////////////////
//NON-EXCLUSIVE JOBS
//
log.LogTrace("Processing non-exclusive dynamic jobs");
if (!KeepOnWorking()) return;
//These fire and forget but use a technique to bubble up exceptions anyway
List<OpsJob> sharedJobs = await GetReadyJobsNotExlusiveOnlyAsync();
foreach (OpsJob j in sharedJobs)
{
if (!KeepOnWorking()) return;
try
{
//System.Diagnostics.Debug.WriteLine($"JobsBiz processing NON-exclusive biz job {j.Name}");
TaskUtil.Forget(Task.Run(() => ProcessJobAsync(j)));
}
catch (Exception ex)
{
log.LogError(ex, $"ProcessJobs::Shared -> job {j.Name} failed with exception");
await LogJobAsync(j.GId, "LT:JobFailed");
await LogJobAsync(j.GId, ExceptionUtil.ExtractAllExceptionMessages(ex));
await UpdateJobStatusAsync(j.GId, JobStatus.Failed);
}
}
}
catch (Exception ex)
{
var msg = "Server::ProcessJobsAsync unexpected error during processing";
log.LogError(ex, msg);
DbUtil.HandleIfDatabaseUnavailableTypeException(ex);
await NotifyEventHelper.AddOpsProblemEvent(msg, ex);
}
finally
{
ActivelyProcessing = false;
//System.Diagnostics.Debug.WriteLine($"JobsBiz in Finally - completed run");
}
}
/// <summary>
/// Process a job by calling into it's biz object
/// </summary>
/// <param name="job"></param>
/// <returns></returns>
internal static async Task ProcessJobAsync(OpsJob job)
{
var JobDescription = $"{job.Name} - {job.JobType.ToString()}";
if (job.SubType != JobSubType.NotSet)
JobDescription += $":{job.SubType}";
await LogJobAsync(job.GId, $"LT:ProcessingJob \"{JobDescription}\"");
log.LogDebug($"ProcessJobAsync -> Processing job {JobDescription}");
IJobObject o = null;
using (AyContext ct = ServiceProviderProvider.DBContext)
{
switch (job.JobType)
{
case JobType.Backup:
//This is called when on demand only, normal backups are processed above with normal system jobs
await CoreJobBackup.DoWorkAsync(true);
await UpdateJobStatusAsync(job.GId, JobStatus.Completed);
break;
case JobType.TestJob:
o = (IJobObject)BizObjectFactory.GetBizObject(SockType.ServerJob, ct, 1, AuthorizationRoles.BizAdmin);
break;
case JobType.AttachmentMaintenance:
o = (IJobObject)BizObjectFactory.GetBizObject(SockType.FileAttachment, ct, 1, AuthorizationRoles.BizAdmin);
break;
case JobType.BatchCoreObjectOperation:
//batch op, hand off to biz object to deal with
//note, convention is that there is an idList in job.jobinfo json if preselected else it's all objects of type
o = (IJobObject)BizObjectFactory.GetBizObject(job.SockType, ct, 1, AuthorizationRoles.BizAdmin);
break;
case JobType.RenderReport:
o = (IJobObject)BizObjectFactory.GetBizObject(SockType.Report, ct, 1, AuthorizationRoles.BizAdmin);
break;
default:
throw new System.NotSupportedException($"ProcessJobAsync type {job.JobType.ToString()} is not supported");
}
if (o != null)
await o.HandleJobAsync(job);
}
log.LogDebug($"ProcessJobAsync -> Job completed {JobDescription}");
}
#endregion process jobs
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

511
server/biz/MemoBiz.cs Normal file
View File

@@ -0,0 +1,511 @@
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.Linq;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Sockeye.Biz
{
internal class MemoBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject, IImportAbleObject, INotifiableObject
{
internal MemoBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = SockType.Memo;
}
internal static MemoBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new MemoBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new MemoBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExistsAsync(long id)
{
return await ct.Memo.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<Memo> CreateAsync(Memo newObject)
{
newObject.Viewed = false;//default, it's new and not viewed yet but could have been set from a prior forward / reply as it's source
newObject.Replied = false;//''
await ValidateAsync(newObject);//a bit different, can't update a memo so only need to worry about new objects
if (HasErrors)
return null;
else
{
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.Memo.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, SockEvent.Created), ct);
await SearchIndexAsync(newObject, true);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await HandlePotentialNotificationEvent(SockEvent.Created, newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//GET
//
internal async Task<Memo> GetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.Memo.AsNoTracking().SingleOrDefaultAsync(m => m.Id == id && m.ToId == UserId);//## SECURITY, if need general purpose then make new method
if (logTheGetEvent && ret != null)
{
//Hijack this method to also flag as read
//batch ops will get but will set logthegetevent as false which is helpful for also setting read or not
if (!ret.Viewed)
{
//FLAG VIEWED
//refetch with tracking
ret = await ct.Memo.SingleOrDefaultAsync(m => m.Id == id && m.ToId == UserId);
ret.Viewed = true;
await ct.SaveChangesAsync();
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, SockEvent.Retrieved), ct);
}
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE - ## NOTE: only internally, not exposed to controller route only here for batch ops
//
//
internal async Task<Memo> PutAsync(Memo putObject)
{
var dbObject = await GetAsync(putObject.Id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
putObject.Tags = TagBiz.NormalizeTags(putObject.Tags);
putObject.CustomFields = JsonUtil.CompactJson(putObject.CustomFields);
ct.Replace(dbObject, putObject);
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, SockEvent.Modified), ct);
await SearchIndexAsync(putObject, false);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
await HandlePotentialNotificationEvent(SockEvent.Modified, putObject, dbObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> DeleteAsync(long id)
{
using (var transaction = await ct.Database.BeginTransactionAsync())
{
var dbObject = await GetAsync(id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND);
return false;
}
ValidateCanDelete(dbObject);
if (HasErrors)
return false;
{
var IDList = await ct.Review.AsNoTracking().Where(x => x.SockType == SockType.Memo && x.ObjectId == id).Select(x => x.Id).ToListAsync();
if (IDList.Count() > 0)
{
ReviewBiz b = new ReviewBiz(ct, UserId, UserTranslationId, CurrentUserRoles);
foreach (long ItemId in IDList)
if (!await b.DeleteAsync(ItemId, transaction))
{
AddError(ApiErrorCode.CHILD_OBJECT_ERROR, null, $"Review [{ItemId}]: {b.GetErrorsAsString()}");
return false;
}
}
}
ct.Memo.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObject.Id, dbObject.Name, ct);
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, BizType, ct);
await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
await transaction.CommitAsync();
await HandlePotentialNotificationEvent(SockEvent.Deleted, dbObject);
return true;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//SEARCH
//
private async Task SearchIndexAsync(Memo obj, bool isNew)
{
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType);
DigestSearchText(obj, SearchParams);
if (isNew)
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
else
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
}
public async Task<Search.SearchIndexProcessObjectParameters> GetSearchResultSummary(long id, SockType specificType)
{
var obj = await GetAsync(id, false);
var SearchParams = new Search.SearchIndexProcessObjectParameters();
DigestSearchText(obj, SearchParams);
return SearchParams;
}
public void DigestSearchText(Memo obj, Search.SearchIndexProcessObjectParameters searchParams)
{
if (obj != null)
searchParams.AddText(obj.Notes)
.AddText(obj.Name)
.AddText(obj.Wiki)
.AddText(obj.Tags)
.AddCustomFields(obj.CustomFields);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task ValidateAsync(Memo proposedObj)
{
//Only can send a memo from your own account
//with bypass for import if superuser
if (proposedObj.FromId != UserId && UserId != 1)
{
AddError(ApiErrorCode.NOT_AUTHORIZED, "FromId", "No impersonation");
return;//no need to bother with any other validation, this is not allowed
}
//valid TO ID?
if (!await ct.User.AnyAsync(m => m.Id == proposedObj.ToId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "ToId");
return;//no need to bother with any other validation
}
//Name ("subject") is still required for a memo, empty subject not valid
//also, subject was required by biz rule in v7 so no need to worry about that on migrate
if (string.IsNullOrWhiteSpace(proposedObj.Name))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name");
//Name does *NOT* need to be unique because for memo it's actually the subject line
//just kept internal naming the same to make coding easier with less workarounds
// //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.Memo.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 == SockType.Memo.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(Memo inObj)
{
//whatever needs to be check to delete this object
}
////////////////////////////////////////////////////////////////////////////////////////////////
//REPORTING
//
public async Task<JArray> GetReportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
{
var idList = dataListSelectedRequest.SelectedRowIds;
JArray ReportData = new JArray();
while (idList.Any())
{
var batch = idList.Take(IReportAbleObject.REPORT_DATA_BATCH_SIZE);
idList = idList.Skip(IReportAbleObject.REPORT_DATA_BATCH_SIZE).ToArray();
//query for this batch, comes back in db natural order unfortunately
var batchResults = await ct.Memo.AsNoTracking().Where(z => batch.Contains(z.Id)).ToArrayAsync();
//order the results back into original
var orderedList = from id in batch join z in batchResults on id equals z.Id select z;
batchResults = null;
foreach (Memo w in orderedList)
{
if (!ReportRenderManager.KeepGoing(jobId)) return null;
await PopulateVizFields(w);
var jo = JObject.FromObject(w);
if (!JsonUtil.JTokenIsNullOrEmpty(jo["CustomFields"]))
jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]);
ReportData.Add(jo);
}
orderedList = null;
}
vc.Clear();
return ReportData;
}
private VizCache vc = new VizCache();
//populate viz fields from provided object
private async Task PopulateVizFields(Memo o)
{
if (o.ToId != null)
{
if (!vc.Has("user", o.ToId))
vc.Add(await ct.User.AsNoTracking().Where(x => x.Id == o.ToId).Select(x => x.Name).FirstOrDefaultAsync(), "user", o.ToId);
o.ToViz = vc.Get("user", o.ToId);
}
if (o.FromId != null)
{
if (!vc.Has("user", o.FromId))
vc.Add(await ct.User.AsNoTracking().Where(x => x.Id == o.FromId).Select(x => x.Name).FirstOrDefaultAsync(), "user", o.FromId);
o.FromViz = vc.Get("user", o.FromId);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// IMPORT EXPORT
//
public async Task<JArray> GetExportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
{
//for now just re-use the report data code
//this may turn out to be the pattern for most biz object types but keeping it seperate allows for custom usage from time to time
return await GetReportData(dataListSelectedRequest, jobId);
}
public async Task<List<string>> ImportData(AyImportData importData)
{
List<string> ImportResult = new List<string>();
string ImportTag = $"imported-{FileUtil.GetSafeDateFileName()}";
var jsset = JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = new Sockeye.Util.JsonUtil.ShouldSerializeContractResolver(new string[] { "Concurrency", "Id", "CustomFields" }) });
foreach (JObject j in importData.Data)
{
var w = j.ToObject<Memo>(jsset);
if (j["CustomFields"] != null)
w.CustomFields = j["CustomFields"].ToString();
w.Tags.Add(ImportTag);//so user can find them all and revert later if necessary
var res = await CreateAsync(w);
if (res == null)
{
ImportResult.Add($"* {w.Name} - {this.GetErrorsAsString()}");
this.ClearErrors();
}
else
{
ImportResult.Add($"{w.Name} - ok");
}
}
return ImportResult;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//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.BatchCoreObjectOperation:
await ProcessBatchJobAsync(job);
break;
default:
throw new System.ArgumentOutOfRangeException($"MemoBiz.HandleJob-> Invalid job type{job.JobType.ToString()}");
}
}
private async Task ProcessBatchJobAsync(OpsJob job)
{
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running);
await JobsBiz.LogJobAsync(job.GId, $"LT:StartJob {job.SubType}");
List<long> idList = new List<long>();
long FailedObjectCount = 0;
JObject jobData = JObject.Parse(job.JobInfo);
if (jobData.ContainsKey("idList"))
idList = ((JArray)jobData["idList"]).ToObject<List<long>>();
else
idList = await ct.Memo.AsNoTracking().Select(z => z.Id).ToListAsync();
bool SaveIt = false;
//---------------------------------
//case 4192
TimeSpan ProgressAndCancelCheckSpan = new TimeSpan(0, 0, ServerBootConfig.JOB_PROGRESS_UPDATE_AND_CANCEL_CHECK_SECONDS);
DateTime LastProgressCheck = DateTime.UtcNow.Subtract(new TimeSpan(1, 1, 1, 1, 1));
var TotalRecords = idList.LongCount();
long CurrentRecord = -1;
//---------------------------------
foreach (long id in idList)
{
try
{
//--------------------------------
//case 4192
//Update progress / cancel requested?
CurrentRecord++;
if (DateUtil.IsAfterDuration(LastProgressCheck, ProgressAndCancelCheckSpan))
{
await JobsBiz.UpdateJobProgressAsync(job.GId, $"{CurrentRecord}/{TotalRecords}");
if (await JobsBiz.GetJobStatusAsync(job.GId) == JobStatus.CancelRequested)
break;
LastProgressCheck = DateTime.UtcNow;
}
//---------------------------------
SaveIt = false;
ClearErrors();
Memo o = null;
//save a fetch if it's a delete
if (job.SubType != JobSubType.Delete)
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.ProcessBatchTagOperation(o.Tags, (string)jobData["tag"], jobData.ContainsKey("toTag") ? (string)jobData["toTag"] : null, job.SubType);
break;
case JobSubType.Delete:
if (!await DeleteAsync(id))
{
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}");
FailedObjectCount++;
}
break;
default:
throw new System.ArgumentOutOfRangeException($"ProcessBatchJobAsync -> Invalid job Subtype{job.SubType}");
}
if (SaveIt)
{
o = await PutAsync(o);
if (o == null)
{
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}");
FailedObjectCount++;
}
}
//delay so we're not tying up all the resources in a tight loop
await Task.Delay(Sockeye.Util.ServerBootConfig.JOB_OBJECT_HANDLE_BATCH_JOB_LOOP_DELAY);
}
catch (Exception ex)
{
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors id({id})");
await JobsBiz.LogJobAsync(job.GId, ExceptionUtil.ExtractAllExceptionMessages(ex));
}
}
//---------------------------------
//case 4192
await JobsBiz.UpdateJobProgressAsync(job.GId, $"{++CurrentRecord}/{TotalRecords}");
//---------------------------------
await JobsBiz.LogJobAsync(job.GId, $"LT:BatchJob {job.SubType} {idList.Count}{(FailedObjectCount > 0 ? " - LT:Failed " + FailedObjectCount : "")}");
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed);
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task HandlePotentialNotificationEvent(SockEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = Sockeye.Util.ApplicationLogging.CreateLogger<MemoBiz>();
log.LogDebug($"HandlePotentialNotificationEvent processing: [SockType:{this.BizType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,20 @@
namespace Sockeye.Biz
{
/// <summary>
/// All Sockeye notification delivery methods
///
/// </summary>
public enum NotifyDeliveryMethod : int
{
App = 1,//deliver in app via notification system
SMTP = 2//deliver to an email address or other entity reachable via smtp such as sms from email etc
//NEW ITEMS REQUIRE translation KEYS
}
}//eons

View File

@@ -0,0 +1,406 @@
using System;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using Sockeye.Util;
using Sockeye.Models;
//using System.Diagnostics;
namespace Sockeye.Biz
{
internal static class NotifyEventHelper
{
private static ILogger log = Sockeye.Util.ApplicationLogging.CreateLogger("NotifyEventProcessor");
///////////////////////////////////////////
// ENSURE USER HAS IN APP NOTIFICATION
//
//
public static async Task EnsureDefaultInAppUserNotificationSubscriptionExists(long userId, AyContext ct)
{
var defaultsub = await ct.NotifySubscription.FirstOrDefaultAsync(z => z.EventType == NotifyEventType.GeneralNotification && z.UserId == userId && z.DeliveryMethod == NotifyDeliveryMethod.App);
if (defaultsub == null)
{
//NOTE: agevalue and advanced notice settings here will ensure that direct in app notifications with a future delivery date ("deadman" switch deliveries) set in their
//notifyevent.eventdate will deliver on that date and not immediately to support all the things that are direct built in notifications for future dates
//such as for an overdue Review which doesn't have or need it's own notifyeventtype and subscription independently
//NEW NOTE: above makes not sense, I'm setting these back to timespan zero
defaultsub = new NotifySubscription()
{
UserId = userId,
EventType = NotifyEventType.GeneralNotification,
DeliveryMethod = NotifyDeliveryMethod.App,
AgeValue = TimeSpan.Zero,//new TimeSpan(0, 0, 1),
AdvanceNotice = TimeSpan.Zero//new TimeSpan(0, 0, 1)
};
await ct.NotifySubscription.AddAsync(defaultsub);
await ct.SaveChangesAsync();
}
return;
}
/////////////////////////////////////////
// PROCESS STANDARD EVENTS
//
//
public static async Task ProcessStandardObjectEvents(SockEvent ayaEvent, ICoreBizObjectModel newObject, AyContext ct)
{
switch (ayaEvent)
{
case SockEvent.Created:
await ProcessStandardObjectCreatedEvents(newObject, ct);
break;
case SockEvent.Deleted:
await ProcessStandardObjectDeletedEvents(newObject, ct);
break;
case SockEvent.Modified:
await ProcessStandardObjectModifiedEvents(newObject, ct);
break;
}
}
/////////////////////////////////////////
// PROCESS STANDARD CREATE NOTIFICATION
//
//
public static async Task ProcessStandardObjectCreatedEvents(ICoreBizObjectModel newObject, AyContext ct)
{
//CREATED SUBSCRIPTIONS
{
var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectCreated && z.SockType == newObject.SType).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
if (ObjectHasAllSubscriptionTags(newObject.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.ObjectCreated,
UserId = sub.UserId,
SockType = newObject.SType,
ObjectId = newObject.Id,
NotifySubscriptionId = sub.Id,
Name = newObject.Name
};
await ct.NotifyEvent.AddAsync(n);
await ct.SaveChangesAsync();
log.LogDebug($"Added NotifyEvent: [{n.ToString()}]");
}
}
}
//AGE SUBSCRIPTIONS
{
var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectAge && z.SockType == newObject.SType).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
if (ObjectHasAllSubscriptionTags(newObject.Tags, sub.Tags))
{
//Note: age is set by advance notice which is consulted by CoreJobNotify in it's run so the deliver date is not required here only the reference EventDate to check for deliver
//ObjectAge is determined by subscription AgeValue in combo with the EventDate NotifyEvent parameter which together determines at what age from notifyevent.EventDate it's considered for the event to have officially occured
//However delivery is determined by sub.advancenotice so all three values play a part
//
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.ObjectAge,
UserId = sub.UserId,
SockType = newObject.SType,
ObjectId = newObject.Id,
NotifySubscriptionId = sub.Id,
Name = newObject.Name
};
await ct.NotifyEvent.AddAsync(n);
await ct.SaveChangesAsync();
log.LogDebug($"Added NotifyEvent: [{n.ToString()}]");
}
}
}
}
///////////////////////////////////////////////
// PROCESS STANDARD MODIFIED NOTIFICATION
//
//
public static async Task ProcessStandardObjectModifiedEvents(ICoreBizObjectModel newObject, AyContext ct)
{
{
var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectModified && z.SockType == newObject.SType).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
if (ObjectHasAllSubscriptionTags(newObject.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.ObjectModified,
UserId = sub.UserId,
SockType = newObject.SType,
ObjectId = newObject.Id,
NotifySubscriptionId = sub.Id,
Name = newObject.Name
};
await ct.NotifyEvent.AddAsync(n);
await ct.SaveChangesAsync();
log.LogDebug($"Added NotifyEvent: [{n.ToString()}]");
}
}
}
}
/////////////////////////////////////////
// PROCESS STANDARD DELETE NOTIFICATION
//
//
public static async Task ProcessStandardObjectDeletedEvents(ICoreBizObjectModel bizObject, AyContext ct)
{
// It's gone and shouldn't have any events left for it
await ClearPriorEventsForObject(ct, bizObject.SType, bizObject.Id);
//------------------------------------------
//ObjectDeleted notification
//
{
var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectDeleted && z.SockType == bizObject.SType).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
if (ObjectHasAllSubscriptionTags(bizObject.Tags, sub.Tags))
{
//TODO: On deliver should point to history event log record or take from there and insert into delivery message?
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.ObjectDeleted,
UserId = sub.UserId,
SockType = bizObject.SType,
ObjectId = bizObject.Id,
NotifySubscriptionId = sub.Id,
Name = bizObject.Name
};
await ct.NotifyEvent.AddAsync(n);
await ct.SaveChangesAsync();
log.LogDebug($"Added NotifyEvent: [{n.ToString()}]");
}
}
}
}
//////////////////////////////////
// CLEAN OUT OLD EVENTS
//
//
//Any specific event created in objects biz code (not simply "Modified")
//should trigger this remove code prior to updates etc where it creates a new one
//particularly for future delivery ones but will catch the case of a quick double edit of an object that
//would alter what gets delivered in the notification and before it's sent out yet
public static async Task ClearPriorEventsForObject(AyContext ct, SockType sockType, long objectId, NotifyEventType eventType)
{
var eventsToDelete = await ct.NotifyEvent.Where(z => z.SockType == sockType && z.ObjectId == objectId && z.EventType == eventType).ToListAsync();
if (eventsToDelete.Count == 0) return;
ct.NotifyEvent.RemoveRange(eventsToDelete);
await ct.SaveChangesAsync();
}
public static async Task ClearPriorCustomerNotifyEventsForObject(AyContext ct, SockType sockType, long objectId, NotifyEventType eventType)
{
var eventsToDelete = await ct.CustomerNotifyEvent.Where(z => z.SockType == sockType && z.ObjectId == objectId && z.EventType == eventType).ToListAsync();
if (eventsToDelete.Count == 0) return;
ct.CustomerNotifyEvent.RemoveRange(eventsToDelete);
await ct.SaveChangesAsync();
}
//scorched earth one for outright delete of objects when you don't want any prior events left for it
//probably only ever used for the delete event, can't think of another one right now new years morning early 2021 a bit hungover but possibly there is :)
public static async Task ClearPriorEventsForObject(AyContext ct, SockType sockType, long objectId)
{
var eventsToDelete = await ct.NotifyEvent.Where(z => z.SockType == sockType && z.ObjectId == objectId).ToListAsync();
if (eventsToDelete.Count == 0) return;
ct.NotifyEvent.RemoveRange(eventsToDelete);
await ct.SaveChangesAsync();
}
//////////////////////////////////
// COMPARE TAGS COLLECTION
//
// A match here means *all* tags in the subscription are present in the object
//
public static bool ObjectHasAllSubscriptionTags(List<string> objectTags, List<string> subTags)
{
//no subscription tags? Then it always will match
if (subTags.Count == 0) return true;
//have sub tags but object has none? Then it's never going to match
if (objectTags.Count == 0) return false;
//not enought tags on object to match sub tags?
if (subTags.Count > objectTags.Count) return false;
//Do ALL the tags in the subscription exist in the object?
return subTags.All(z => objectTags.Any(x => x == z));
}
//////////////////////////////////
// COMPARE TAGS COLLECTION
//
// A match here means *all* tags are the same in both objects (don't have to be in same order)
//
public static bool TwoObjectsHaveSameTags(List<string> firstObjectTags, List<string> secondObjectTags)
{
//no tags on either side?
if (firstObjectTags.Count == 0 && secondObjectTags.Count == 0) return true;
//different counts will always mean not a match
if (firstObjectTags.Count != secondObjectTags.Count) return false;
//Do ALL the tags in the first object exist in the second object?
return firstObjectTags.All(z => secondObjectTags.Any(x => x == z));
}
/////////////////////////////////////////
// CREATE OPS PROBLEM EVENT
//
//
internal static async Task AddOpsProblemEvent(string message, Exception ex = null)
{
if (string.IsNullOrWhiteSpace(message) && ex == null)
return;
//Log as a backup in case there is no one to notify and also for the record and support
if (ex != null)
{
//actually, if there is an exception it's already logged anyway so don't re-log it here, just makes dupes
// log.LogError(ex, $"Ops problem notification: \"{message}\"");
message += $"\nException error: {ExceptionUtil.ExtractAllExceptionMessages(ex)}";
}
else
log.LogWarning($"Ops problem notification: \"{message}\"");
await AddGeneralNotifyEvent(NotifyEventType.ServerOperationsProblem, message, "OPS");
}
/////////////////////////////////////////
// CREATE GENERAL NOTIFY EVENT
//
//
internal static async Task AddGeneralNotifyEvent(NotifyEventType eventType, string message, string name, Exception except = null, long userId = 0)
{
await AddGeneralNotifyEvent(SockType.NoType, 0, eventType, message, name, except, userId);
}
internal static async Task AddGeneralNotifyEvent(SockType sockType, long objectid, NotifyEventType eventType, string message, string name, Exception except = null, long userId = 0)
{
//This handles general notification events not requiring a decision or tied to an object that are basically just a immediate message to the user
//e.g. ops problems, GeneralNotification, NotifyHealthCheck etc
//optional user id to send directly to them
log.LogDebug($"AddGeneralNotifyEvent processing: [type:{eventType}, userId:{userId}, message:{message}]");
#if (DEBUG)
switch (eventType)
{
case NotifyEventType.BackupStatus:
case NotifyEventType.GeneralNotification:
case NotifyEventType.NotifyHealthCheck://created by job processor itself
case NotifyEventType.ServerOperationsProblem:
break;
default://this will likely be a development error, not a production error so no need to log etc
throw (new System.NotSupportedException($"NotifyEventProcessor:AddGeneralNotifyEvent - Type of event {eventType} is unexpected and not supported"));
}
if (eventType != NotifyEventType.GeneralNotification && userId != 0)
{
throw (new System.NotSupportedException($"NotifyEventProcessor:AddGeneralNotifyEvent - event {eventType} was specified with user id {userId} which is unexpected and not supported"));
}
#endif
try
{
using (AyContext ct = Sockeye.Util.ServiceProviderProvider.DBContext)
{
//General notification goes to one specific user only
if (eventType == NotifyEventType.GeneralNotification)
{
if (userId == 0)
{
//this will likely be a development error, not a production error so no need to log etc
throw new System.ArgumentException("NotifyEventProcessor:AddGeneralNotifyEvent: GeneralNotification requires a user id but none was specified");
}
//not for inactive users
if (!await UserBiz.UserIsActive(userId)) return;
var UserName = await ct.User.AsNoTracking().Where(z => z.Id == userId).Select(z => z.Name).FirstOrDefaultAsync();
//if they don't have a regular inapp subscription create one now
await EnsureDefaultInAppUserNotificationSubscriptionExists(userId, ct);
if (string.IsNullOrWhiteSpace(name))
name = UserName;
var gensubs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.GeneralNotification && z.UserId == userId).ToListAsync();
foreach (var sub in gensubs)
{
NotifyEvent n = new NotifyEvent() { EventType = eventType, UserId = userId, Message = message, NotifySubscriptionId = sub.Id, Name = name, SockType = sockType, ObjectId = objectid };
await ct.NotifyEvent.AddAsync(n);
}
if (gensubs.Count > 0)
await ct.SaveChangesAsync();
return;
}
//check subscriptions for event and send accordingly to each user
var subs = await ct.NotifySubscription.Where(z => z.EventType == eventType).ToListAsync();
//append exception message if not null
if (except != null)
message += $"\nException error: {ExceptionUtil.ExtractAllExceptionMessages(except)}";
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
//note flag ~SERVER~ means to client to substitute "Server" translation key text instead
NotifyEvent n = new NotifyEvent() { EventType = eventType, UserId = sub.UserId, Message = message, NotifySubscriptionId = sub.Id, Name = "~SERVER~", SockType = sockType, ObjectId = objectid };
await ct.NotifyEvent.AddAsync(n);
}
if (subs.Count > 0)
await ct.SaveChangesAsync();
}
}
catch (Exception ex)
{
log.LogError(ex, $"Error adding general notify event [type:{eventType}, userId:{userId}, message:{message}]");
DbUtil.HandleIfDatabaseUnavailableTypeException(ex);
}
}//eom
}//eoc
}//eons

View File

@@ -0,0 +1,40 @@
namespace Sockeye.Biz
{
/*
Inspiring quotes used to help complete this huge project by myself
“Accept the things to which fate binds you, and love the people with whom fate brings you together,but do so with all your heart.”
― Marcus Aurelius, Meditations
"Make it happen"
In response to me complaining about how hard this project is and how long it's taking.
- Jim Preiss July 28th 2021
*/
/// <summary>
/// All Sockeye notification event types
///
/// </summary>
public enum NotifyEventType : int
{
//see core-notifications.txt spec doc for a bit more info on each type (they are named a little bit differently)
//#### NOTE: once event is NOTED IN COMMENT (not necessarily coded yet as some can't be yet) in NotifyEventProcessor I'll mark it with a * in the comment so I know if I miss any
//#### NOTE: once event is fully coded I'll mark it with CODED at the start of the comment line
ObjectDeleted = 1,//* Deletion of any object of conditional specific SockType and optionally conditional tags
ObjectCreated = 2,//* creation of any object of conditional specific SockType and optionally conditional tags
ObjectModified = 3,//* Modification / update of any kind of any object of conditional specific SockType and optionally conditional tags
ObjectAge = 10,//* Any object, Age (conditional on AgeValue) after creation event of any object of conditional specific SockType and optionally conditional tags
ReminderImminent = 12,//*Reminder object, Advance notice setting tag conditional
NotifyHealthCheck = 19,//* NO OBJECT, direct subscription to receive recurring daily notify system "ping" sent out between 8am and 10am once every 24 hours minimum every day server local time
BackupStatus = 20,//* NO OBJECT, direct subscription to receive results of last backup operation
GeneralNotification = 27,//* NO OBJECT old quick notification, refers now to any direct text notification internal or user to user used for system notifications (default delivers in app but user can opt to also get email)
ServerOperationsProblem = 28,//* NO OBJECT and serious issue with server operations requiring intervention,
ReviewImminent = 34,//*Review object, Advance notice setting tag conditional
DirectSMTPMessage= 35 //Used internally when sending a message via email directly to a Customer (initially) and possibly other objects in future. Shows in sent log but not user subscribable
//NEW ITEMS REQUIRE translation KEYS
}
}//eons

View File

@@ -0,0 +1,21 @@
namespace Sockeye.Biz
{
/// <summary>
/// All Sockeye smtp notification connection security
///
/// </summary>
public enum NotifyMailSecurity : int
{
None = 0,
StartTls = 1,
SSLTLS=3
//NEW ITEMS REQUIRE translation KEYS
}
}//eons

View File

@@ -0,0 +1,256 @@
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
using System.Linq;
namespace Sockeye.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 = SockType.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.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExistsAsync(long id)
{
return await ct.NotifySubscription.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<NotifySubscription> CreateAsync(NotifySubscription newObject)
{
await ValidateAsync(newObject);
if (HasErrors)
return null;
else
{
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
await ct.NotifySubscription.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, SockEvent.Created), ct);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
return newObject;
}
}
// ////////////////////////////////////////////////////////////////////////////////////////////////
// //DUPLICATE
// //
// internal async Task<NotifySubscription> DuplicateAsync(long id)
// {
// var dbObject = await GetAsync(id, false);
// if (dbObject == null)
// {
// AddError(ApiErrorCode.NOT_FOUND, "id");
// return null;
// }
// NotifySubscription newObject = new NotifySubscription();
// CopyObject.Copy(dbObject, newObject);
// 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<NotifySubscription> GetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.NotifySubscription.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id && z.UserId == UserId);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, SockEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<NotifySubscription> PutAsync(NotifySubscription putObject)
{
//TODO: Must remove all prior events and replace them
var dbObject = await GetAsync(putObject.Id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
putObject.Tags = TagBiz.NormalizeTags(putObject.Tags);
await ValidateAsync(putObject);
if (HasErrors) return null;
ct.Replace(dbObject, putObject);
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, SockEvent.Modified), ct);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> DeleteAsync(long id)
{
using (var transaction = await ct.Database.BeginTransactionAsync())
{
var dbObject = await GetAsync(id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND);
return false;
}
//ValidateCanDelete(dbObject);
if (HasErrors)
return false;
{
var IDList = await ct.Review.AsNoTracking().Where(x => x.SockType == SockType.NotifySubscription && x.ObjectId == id).Select(x => x.Id).ToListAsync();
if (IDList.Count() > 0)
{
ReviewBiz b = new ReviewBiz(ct, UserId, UserTranslationId, CurrentUserRoles);
foreach (long ItemId in IDList)
if (!await b.DeleteAsync(ItemId, transaction))
{
AddError(ApiErrorCode.CHILD_OBJECT_ERROR, null, $"Review [{ItemId}]: {b.GetErrorsAsString()}");
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 TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
//TODO: DELETE RELATED RECORDS HERE
//all good do the commit
await transaction.CommitAsync();
return true;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task ValidateAsync(NotifySubscription proposedObj)
{
//###############################################################################
//todo: validate subscription is valid
//perhaps check if customer type user doesn't have non customer notification etc
//todo: notifysubscriptionbiz Check for duplicate before accepting new / edit in validator
//DISALLOW entirely duplicate notifications (down to email address)
//USE NAME DUPE CHECK PATTERN BELOW
//###############################################################################
//ensure user exists and need it for other shit later
var user = await ct.User.AsNoTracking().FirstOrDefaultAsync(z => z.Id == proposedObj.UserId);
if (user == null)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "UserId");
}
else
{
// //Validate user can see this subscription type
// if (user.UserType == UserType.Customer || user.UserType == UserType.HeadOffice)
// {
// //Outside users can't choose inside user type notification events
// switch (proposedObj.EventType)
// {
// case NotifyEventType.CSRAccepted:
// case NotifyEventType.CSRRejected:
// case NotifyEventType.CustomerServiceImminent:
// case NotifyEventType.WorkorderCompleted:
// case NotifyEventType.WorkorderCreatedForCustomer:
// break;
// default:
// AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "EventType");
// break;
// }
// }
// else
// {
// //Inside users can't use Outside (customer) users notification types
// switch (proposedObj.EventType)
// {
// case NotifyEventType.CSRAccepted:
// case NotifyEventType.CSRRejected:
// case NotifyEventType.CustomerServiceImminent:
// AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "EventType");
// break;
// default:
// break;
// }
// }
}
if (proposedObj.DeliveryMethod == NotifyDeliveryMethod.App && !string.IsNullOrEmpty(proposedObj.DeliveryAddress))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "DeliveryAddress", "In app delivery should not specify a delivery address");
}
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

303
server/biz/PickListBiz.cs Normal file
View File

@@ -0,0 +1,303 @@
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
using Sockeye.PickList;
namespace Sockeye.Biz
{
internal class PickListBiz : BizObject
{
internal PickListBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = SockType.PickListTemplate;
}
internal static PickListBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new PickListBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new PickListBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// GET
//Get one
internal async Task<PickListTemplate> GetAsync(SockType sockType, bool logTheGetEvent = true)
{
long lTypeId = (long)sockType;
//first try to fetch from db
var ret = await ct.PickListTemplate.SingleOrDefaultAsync(z => z.Id == lTypeId);
if (logTheGetEvent && ret != null)
{
//Log
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, lTypeId, BizType, SockEvent.Retrieved), ct);
}
//not in db then get the default
if (ret == null)
{
var PickList = PickListFactory.GetAyaPickList(sockType);
if (PickList != null)
{
ret = new PickListTemplate();
ret.Id = lTypeId;
ret.Template = PickList.DefaultTemplate;
}
}
return ret;
}
//get picklist
internal async Task<List<NameIdActiveItem>> GetPickListAsync(IAyaPickList PickList, string query, bool inactive, long[] preIds, string variant, ILogger log, string template)
{
//Crack and validate the query part set a broken rule if not valid and return null
//else do the query
string TagSpecificQuery = null;
string AutoCompleteQuery = null;
//Here need to handle scenario of badly formed query so user knows they did it wrong and doesn't just assume it's not there
//determine if this is a tag query and extract it
bool HasQuery = !string.IsNullOrWhiteSpace(query);
if (HasQuery)
{
AutoCompleteQuery = query;
//is it a dual template and tag query?
if (AutoCompleteQuery.Contains(" "))
{
// split the query on space
var querySegments = AutoCompleteQuery.Split(' ');
if (querySegments.Length > 2)
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "query", await Translate("ErrorPickListQueryInvalid"));
return null;
}
//check the two query segments, it's valid for the user to put the tag first or the template query first
//we handle either way, but if there are no tas in either then it's not a valid query
if (querySegments[0].Contains(".."))
{
TagSpecificQuery = querySegments[0].Replace("..", "");
AutoCompleteQuery = querySegments[1];
}
else if (querySegments[1].Contains(".."))
{
TagSpecificQuery = querySegments[1].Replace("..", "");
AutoCompleteQuery = querySegments[0];
}
else
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "query", await Translate("ErrorPickListQueryInvalid"));
return null;
}
}
else
{
//is it a tag only query?
if (AutoCompleteQuery.Contains(".."))
{
TagSpecificQuery = AutoCompleteQuery.Replace("..", "");
AutoCompleteQuery = null;
}
}
}
//Final fixup if user specifies tag query but there are not tags on this object then
//rather than error just accept it as a no tag query
//Note: it's not valid to have more than one field with tags in the picklist definition so this works
if (PickList.ColumnDefinitions.FirstOrDefault(z => z.ColumnDataType == UiFieldDataType.Tags) == null)
{
TagSpecificQuery = null;
}
//Autocomplete and tagonly query terms now set for consumption by PickListFetcher, ready to fetch...
List<NameIdActiveItem> items = await PickListFetcher.GetResponseAsync(PickList, AutoCompleteQuery, TagSpecificQuery, inactive, preIds, variant, ct, log, template);
return items;
}
//get picklist display for a single item
//used to populate UI with picklist format display for items
internal async Task<string> GetTemplatedNameAsync(SockType sockType, long id, string variant, ILogger log, string template)
{
//short circuit for empty types
if (id == 0)
{
return string.Empty;
}
long[] preIds = { id };
var PickList = PickListFactory.GetAyaPickList(sockType);
if (log == null)
log = Sockeye.Util.ApplicationLogging.CreateLogger("PickListBiz::GetTemplatedNameAsync");
//Autocomplete and tagonly query terms now set for consumption by PickListFetcher, ready to fetch...
List<NameIdActiveItem> items = await PickListFetcher.GetResponseAsync(PickList, null, null, true, preIds, variant, ct, log, template);
if (items.Count == 0)
{
return string.Empty;
}
return items[0].Name;
}
//get picklist templates, basically all the object types that support picklists
internal List<NameIdItem> GetListOfAllPickListTypes(long translationId)
{
return PickListFactory.GetListOfAllPickListTypes(translationId);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
//put
internal async Task<bool> ReplaceAsync(PickListTemplate template)
{
var o = await ct.PickListTemplate.FirstOrDefaultAsync(z => z.Id == (long)template.Id);
bool bAdd = false;
if (o == null)
{
o = new PickListTemplate();
bAdd = true;
}
o.Id = (long)template.Id;
o.Template = template.Template;
Validate(o);
if (HasErrors)
return false;
if (bAdd)
await ct.PickListTemplate.AddAsync(o);
await ct.SaveChangesAsync();
//Log modification and save context
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, o.Id, BizType, SockEvent.Modified), ct);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE (return to default template)
//
internal async Task<bool> DeleteAsync(SockType sockType)
{
//REMOVE ANY RECORD WITH SAME AYATYPE ID
long lTypeId = (long)sockType;
var o = await ct.PickListTemplate.FirstOrDefaultAsync(z => z.Id == lTypeId);
if (o != null)
{
ct.PickListTemplate.Remove(o);
await ct.SaveChangesAsync();
//Event log process delete
await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, lTypeId, sockType.ToString(), ct);
}
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private void Validate(PickListTemplate inObj)
{
//validate that the template is valid, the type is legit etc
var TemplateType = (SockType)inObj.Id;
if (!TemplateType.HasAttribute(typeof(CoreBizObjectAttribute)))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "sockType", "SockType specified is not a Core object type and doesn't support pick list templates");
return;
}
if (string.IsNullOrWhiteSpace(inObj.Template))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Template");
var PickList = PickListFactory.GetAyaPickList(TemplateType);
//Filter json must parse
//this is all automated normally so not going to do too much parsing here
//just ensure it's basically there
if (!string.IsNullOrWhiteSpace(inObj.Template))
{
try
{
var v = JArray.Parse(inObj.Template);
for (int i = 0; i < v.Count; i++)
{
var filterItem = v[i];
if (filterItem["fld"] == null)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Template", $"Template array item {i}, object is missing required \"fld\" property ");
else
{
var fld = filterItem["fld"].Value<string>();
if (string.IsNullOrWhiteSpace(fld))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Template", $"Template array item {i}, \"fld\" property is empty and required");
//validate the field name if we can
if (PickList != null)
{
var TheField = PickList.ColumnDefinitions.SingleOrDefault(z => z.FieldKey.ToLowerInvariant() == fld.ToLowerInvariant());
if (TheField == null)
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Template", $"Template array item {i}, fld property value \"{fld}\" is not a valid value for SockType specified");
}
}
}
}
}
catch (Newtonsoft.Json.JsonReaderException ex)
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Template", "Template is not valid JSON string: " + ex.Message);
}
}
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

147
server/biz/PrimeData.cs Normal file
View File

@@ -0,0 +1,147 @@
using System.Threading.Tasks;
using System.IO;
using Newtonsoft.Json.Linq;
using Sockeye.Util;
using Sockeye.Models;
namespace Sockeye.Biz
{
//Prime the database with initial, minimum required data to boot and do things (SuperUser account, translations)
public static class PrimeData
{
/// <summary>
/// Prime the database with SuperUser account
/// </summary>
public static async Task PrimeSuperUserAccount(AyContext ct)
{
//get a db and logger
//ILogger log = Sockeye.Util.ApplicationLogging.CreateLogger("PrimeData");
User u = new User();
u.Active = true;
u.AllowLogin=true;
u.Name = "Sockeye SuperUser";
u.Salt = Hasher.GenerateSalt();
u.Login = "ss";
u.Password = Hasher.hash(u.Salt, "ss");
u.Roles = AuthorizationRoles.Accounting | AuthorizationRoles.BizAdmin | AuthorizationRoles.Inventory | AuthorizationRoles.OpsAdmin | AuthorizationRoles.Sales | AuthorizationRoles.Service ;
u.UserType = UserType.NotService;
u.UserOptions = new UserOptions();
u.UserOptions.TranslationId = ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID;//Ensure primeTranslations is called first
await ct.User.AddAsync(u);
await ct.SaveChangesAsync();
}
// //used during development so I don't need to keep installing licenses over and over
// //just to refresh the translations
// public static async Task RePrimeTranslations()
// {
// //delete all the translations
// using (AyContext ct = ServiceProviderProvider.DBContext)
// {
// await ct.Database.ExecuteSqlRawAsync("delete from atranslationitem;");
// await ct.Database.ExecuteSqlRawAsync("delete from atranslation;");
// }
// //replace
// await PrimeTranslations();
// }
/// <summary>
/// Prime the Translations
/// This may be called before there are any users on a fresh db boot
/// </summary>
public static async Task PrimeTranslations()
{//
//Read in each stock translation from a text file and then create them in the DB
var ResourceFolderPath = Path.Combine(ServerBootConfig.SOCKEYE_CONTENT_ROOT_PATH, "resource");
if (!Directory.Exists(ResourceFolderPath))
{
throw new System.Exception($"E1012: \"resource\" folder not found where expected: \"{ResourceFolderPath}\", installation damaged?");
}
await ImportTranslation(ResourceFolderPath, "en");
await ImportTranslation(ResourceFolderPath, "es");
await ImportTranslation(ResourceFolderPath, "fr");
await ImportTranslation(ResourceFolderPath, "de");
//Ensure Translations are present, not missing any keys and that there is a server default translation that exists
using (AyContext ct = ServiceProviderProvider.DBContext)
{
TranslationBiz lb = TranslationBiz.GetBiz(ct);
await lb.ValidateTranslationsAsync();
}
}
private static async Task ImportTranslation(string resourceFolderPath, string translationCode)
{
using (AyContext ct = ServiceProviderProvider.DBContext)
{
var TranslationPath = Path.Combine(resourceFolderPath, $"{translationCode}.json");
if (!File.Exists(TranslationPath))
{
throw new System.Exception($"E1013: stock translation file \"{translationCode}\" not found where expected: \"{TranslationPath}\", installation damaged?");
}
JObject o = JObject.Parse(await File.ReadAllTextAsync(TranslationPath));
Translation l = new Translation();
l.Name = translationCode;
l.BaseLanguage=translationCode;
l.Stock = true;
l.CjkIndex = false;
foreach (JToken t in o.Children())
{
var key = t.Path;
var display = t.First.Value<string>();
l.TranslationItems.Add(new TranslationItem() { Key = key, Display = display });
}
await ct.Translation.AddAsync(l);
await ct.SaveChangesAsync();
}
}
/// <summary>
/// Prime the Report Templates
/// </summary>
public static async Task PrimeReportTemplates()
{
//Read in each stock translation from a text file and then create them in the DB
var ReportFilesPath = Path.Combine(ServerBootConfig.SOCKEYE_CONTENT_ROOT_PATH, "resource", "rpt", "stock-report-templates");
if (!Directory.Exists(ReportFilesPath))
{
throw new System.Exception($"E1012: \"stock-report-templates\" folder not found where expected: \"{ReportFilesPath}\", installation damaged?");
}
//iterate all ayrt files and import each one
using (AyContext ct = ServiceProviderProvider.DBContext)
{
ReportBiz r = ReportBiz.GetBiz(ct);
System.IO.DirectoryInfo di = new DirectoryInfo(ReportFilesPath);
foreach (FileInfo file in di.EnumerateFiles())
{
if (file.Extension.ToLowerInvariant() == ".ayrt")
await r.ImportAsync(JObject.Parse(await File.ReadAllTextAsync(file.FullName)), true);
}
}
}
}//eoc
}//eons

544
server/biz/ReminderBiz.cs Normal file
View File

@@ -0,0 +1,544 @@
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Microsoft.Extensions.Logging;
using Sockeye.Models;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Sockeye.Biz
{
internal class ReminderBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject, IImportAbleObject, INotifiableObject
{
internal ReminderBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = SockType.Reminder;
}
internal static ReminderBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new ReminderBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new ReminderBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExistsAsync(long id)
{
return await ct.Reminder.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<Reminder> CreateAsync(Reminder newObject)
{
await ValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.Reminder.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, SockEvent.Created), ct);
await SearchIndexAsync(newObject, true);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await HandlePotentialNotificationEvent(SockEvent.Created, newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//GET
//
internal async Task<Reminder> GetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.Reminder.AsNoTracking().SingleOrDefaultAsync(m => m.Id == id);
if (ret.UserId != UserId)
{
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror", "A User may only retrieve their own reminders");
return null;
}
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, SockEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<Reminder> PutAsync(Reminder putObject)
{
var dbObject = await GetAsync(putObject.Id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
putObject.Tags = TagBiz.NormalizeTags(putObject.Tags);
putObject.CustomFields = JsonUtil.CompactJson(putObject.CustomFields);
await ValidateAsync(putObject, dbObject);
if (HasErrors) return null;
ct.Replace(dbObject, putObject);
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, SockEvent.Modified), ct);
await SearchIndexAsync(putObject, false);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
await HandlePotentialNotificationEvent(SockEvent.Modified, putObject, dbObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE schedule only
//
internal async Task<bool> PutNewScheduleTimeAsync(ScheduleItemAdjustParams p)
{
Reminder dbObject = await ct.Reminder.SingleOrDefaultAsync(z => z.Id == p.Id);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return false;
}
dbObject.StartDate = p.Start;
dbObject.StopDate = p.End;
await ValidateAsync(dbObject, dbObject);
if (HasErrors) return false;
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await ExistsAsync(dbObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return false;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, dbObject.SType, SockEvent.Modified), ct);
await HandlePotentialNotificationEvent(SockEvent.Modified, dbObject, dbObject);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> DeleteAsync(long id)
{
using (var transaction = await ct.Database.BeginTransactionAsync())
{
Reminder dbObject = await ct.Reminder.SingleOrDefaultAsync(m => m.Id == id);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND);
return false;
}
ValidateCanDelete(dbObject);
if (HasErrors)
return false;
{
var IDList = await ct.Review.AsNoTracking().Where(x => x.SockType == SockType.Reminder && x.ObjectId == id).Select(x => x.Id).ToListAsync();
if (IDList.Count() > 0)
{
ReviewBiz b = new ReviewBiz(ct, UserId, UserTranslationId, CurrentUserRoles);
foreach (long ItemId in IDList)
if (!await b.DeleteAsync(ItemId, transaction))
{
AddError(ApiErrorCode.CHILD_OBJECT_ERROR, null, $"Review [{ItemId}]: {b.GetErrorsAsString()}");
return false;
}
}
}
ct.Reminder.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
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);
await transaction.CommitAsync();
await HandlePotentialNotificationEvent(SockEvent.Deleted, dbObject);
return true;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//SEARCH
//
private async Task SearchIndexAsync(Reminder obj, bool isNew)
{
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType);
DigestSearchText(obj, SearchParams);
if (isNew)
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
else
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
}
public async Task<Search.SearchIndexProcessObjectParameters> GetSearchResultSummary(long id, SockType specificType)
{
var obj = await GetAsync(id, false);
var SearchParams = new Search.SearchIndexProcessObjectParameters();
DigestSearchText(obj, SearchParams);
return SearchParams;
}
public void DigestSearchText(Reminder obj, Search.SearchIndexProcessObjectParameters searchParams)
{
if (obj != null)
searchParams.AddText(obj.Notes)
.AddText(obj.Name)
.AddText(obj.Wiki)
.AddText(obj.Tags)
.AddCustomFields(obj.CustomFields);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task ValidateAsync(Reminder proposedObj, Reminder currentObj)
{
bool isNew = currentObj == null;
//Name required
if (string.IsNullOrWhiteSpace(proposedObj.Name))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name");
//Hexadecimal notation: #RGB[A] R (red), G (green), B (blue), and A (alpha) are hexadecimal characters (09, AF). A is optional. The three-digit notation (#RGB) is a shorter version of the six-digit form (#RRGGBB). For example, #f09 is the same color as #ff0099. Likewise, the four-digit RGB notation (#RGBA) is a shorter version of the eight-digit form (#RRGGBBAA). For example, #0f38 is the same color as #00ff3388.
if (proposedObj.Color.Length > 12 || proposedObj.Color.Length < 4 || proposedObj.Color[0] != '#')
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "UiColor", "UiColor must be valid HEX color value");
}
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(x => x.FormKey == SockType.Reminder.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(Reminder inObj)
{
//whatever needs to be check to delete this object
}
////////////////////////////////////////////////////////////////////////////////////////////////
//REPORTING
//
public async Task<JArray> GetReportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
{
var idList = dataListSelectedRequest.SelectedRowIds;
JArray ReportData = new JArray();
while (idList.Any())
{
var batch = idList.Take(IReportAbleObject.REPORT_DATA_BATCH_SIZE);
idList = idList.Skip(IReportAbleObject.REPORT_DATA_BATCH_SIZE).ToArray();
//query for this batch, comes back in db natural order unfortunately
var batchResults = await ct.Reminder.AsNoTracking().Where(z => batch.Contains(z.Id)).ToArrayAsync();
//order the results back into original
var orderedList = from id in batch join z in batchResults on id equals z.Id select z;
batchResults = null;
foreach (Reminder w in orderedList)
{
if (!ReportRenderManager.KeepGoing(jobId)) return null;
await PopulateVizFields(w);
var jo = JObject.FromObject(w);
if (!JsonUtil.JTokenIsNullOrEmpty(jo["CustomFields"]))
jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]);
ReportData.Add(jo);
}
orderedList = null;
}
vc.Clear();
return ReportData;
}
private VizCache vc = new VizCache();
//populate viz fields from provided object
private async Task PopulateVizFields(Reminder o)
{
if (!vc.Has("user", o.UserId))
vc.Add(await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(), "user", o.UserId);
o.UserViz = vc.Get("user", o.UserId);
}
////////////////////////////////////////////////////////////////////////////////////////////////
// IMPORT EXPORT
//
public async Task<JArray> GetExportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
{
//for now just re-use the report data code
//this may turn out to be the pattern for most biz object types but keeping it seperate allows for custom usage from time to time
return await GetReportData(dataListSelectedRequest, jobId);
}
public async Task<List<string>> ImportData(AyImportData importData)
{
List<string> ImportResult = new List<string>();
string ImportTag = $"imported-{FileUtil.GetSafeDateFileName()}";
var jsset = JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = new Sockeye.Util.JsonUtil.ShouldSerializeContractResolver(new string[] { "Concurrency", "Id", "CustomFields" }) });
foreach (JObject j in importData.Data)
{
var w = j.ToObject<Reminder>(jsset);
if (j["CustomFields"] != null)
w.CustomFields = j["CustomFields"].ToString();
w.Tags.Add(ImportTag);//so user can find them all and revert later if necessary
var res = await CreateAsync(w);
if (res == null)
{
ImportResult.Add($"* {w.Name} - {this.GetErrorsAsString()}");
this.ClearErrors();
}
else
{
ImportResult.Add($"{w.Name} - ok");
}
}
return ImportResult;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//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.BatchCoreObjectOperation:
await ProcessBatchJobAsync(job);
break;
default:
throw new System.ArgumentOutOfRangeException($"ReminderBiz.HandleJob-> Invalid job type{job.JobType.ToString()}");
}
}
private async Task ProcessBatchJobAsync(OpsJob job)
{
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running);
await JobsBiz.LogJobAsync(job.GId, $"LT:StartJob {job.SubType}");
List<long> idList = new List<long>();
long FailedObjectCount = 0;
JObject jobData = JObject.Parse(job.JobInfo);
if (jobData.ContainsKey("idList"))
idList = ((JArray)jobData["idList"]).ToObject<List<long>>();
else
idList = await ct.Reminder.AsNoTracking().Select(z => z.Id).ToListAsync();
bool SaveIt = false;
//---------------------------------
//case 4192
TimeSpan ProgressAndCancelCheckSpan = new TimeSpan(0, 0, ServerBootConfig.JOB_PROGRESS_UPDATE_AND_CANCEL_CHECK_SECONDS);
DateTime LastProgressCheck = DateTime.UtcNow.Subtract(new TimeSpan(1, 1, 1, 1, 1));
var TotalRecords = idList.LongCount();
long CurrentRecord = -1;
//---------------------------------
foreach (long id in idList)
{
try
{
//--------------------------------
//case 4192
//Update progress / cancel requested?
CurrentRecord++;
if (DateUtil.IsAfterDuration(LastProgressCheck, ProgressAndCancelCheckSpan))
{
await JobsBiz.UpdateJobProgressAsync(job.GId, $"{CurrentRecord}/{TotalRecords}");
if (await JobsBiz.GetJobStatusAsync(job.GId) == JobStatus.CancelRequested)
break;
LastProgressCheck = DateTime.UtcNow;
}
//---------------------------------
SaveIt = false;
ClearErrors();
Reminder o = null;
//save a fetch if it's a delete
if (job.SubType != JobSubType.Delete)
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.ProcessBatchTagOperation(o.Tags, (string)jobData["tag"], jobData.ContainsKey("toTag") ? (string)jobData["toTag"] : null, job.SubType);
break;
case JobSubType.Delete:
if (!await DeleteAsync(id))
{
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}");
FailedObjectCount++;
}
break;
default:
throw new System.ArgumentOutOfRangeException($"ProcessBatchJobAsync -> Invalid job Subtype{job.SubType}");
}
if (SaveIt)
{
o = await PutAsync(o);
if (o == null)
{
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}");
FailedObjectCount++;
}
}
//delay so we're not tying up all the resources in a tight loop
await Task.Delay(Sockeye.Util.ServerBootConfig.JOB_OBJECT_HANDLE_BATCH_JOB_LOOP_DELAY);
}
catch (Exception ex)
{
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors id({id})");
await JobsBiz.LogJobAsync(job.GId, ExceptionUtil.ExtractAllExceptionMessages(ex));
}
}
//---------------------------------
//case 4192
await JobsBiz.UpdateJobProgressAsync(job.GId, $"{++CurrentRecord}/{TotalRecords}");
//---------------------------------
await JobsBiz.LogJobAsync(job.GId, $"LT:BatchJob {job.SubType} {idList.Count}{(FailedObjectCount > 0 ? " - LT:Failed " + FailedObjectCount : "")}");
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed);
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task HandlePotentialNotificationEvent(SockEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = Sockeye.Util.ApplicationLogging.CreateLogger<ReminderBiz>();
log.LogDebug($"HandlePotentialNotificationEvent processing: [SockType:{this.BizType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
Reminder o = (Reminder)proposedObj;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
//## DELETED EVENTS
//any event added below needs to be removed, so
//just blanket remove any event for this object of eventtype that would be added below here
//do it regardless any time there's an update and then
//let this code below handle the refreshing addition that could have changes
await NotifyEventHelper.ClearPriorEventsForObject(ct, SockType.Reminder, o.Id, NotifyEventType.ReminderImminent);
//## CREATED / MODIFIED EVENTS
if (ayaEvent == SockEvent.Created || ayaEvent == SockEvent.Modified)
{
//# REMINDER IMMINENT
{
//notify users (time delayed)
var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ReminderImminent).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
//Tag match? (will be true if no sub tags so always safe to call this)
if (NotifyEventHelper.ObjectHasAllSubscriptionTags(o.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.ReminderImminent,
UserId = sub.UserId,
SockType = o.SType,
ObjectId = o.Id,
NotifySubscriptionId = sub.Id,
Name = o.Name,
EventDate = o.StartDate
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//Reminder imminent event
}//end of process notifications
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

900
server/biz/ReportBiz.cs Normal file
View File

@@ -0,0 +1,900 @@
using System.Threading.Tasks;
using System.Linq;
using System.IO;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
using EnumsNET;
using PuppeteerSharp;
using Newtonsoft.Json.Linq;
using System;
namespace Sockeye.Biz
{
internal class ReportBiz : BizObject, IJobObject, ISearchAbleObject
{
internal ReportBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = SockType.Report;
}
internal static ReportBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new ReportBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new ReportBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExistsAsync(long id)
{
return await ct.Report.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<Report> CreateAsync(Report newObject)
{
await ValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
await ct.Report.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, SockEvent.Created), ct);
await SearchIndexAsync(newObject, true);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//IMPORT
//
internal async Task<bool> ImportAsync(JObject o, bool skipIfAlreadyPresent = false)
{
//Report newObject = new Report();
var newObject = o.ToObject<Report>();
var proposedName = (string)o["Name"];
string newUniqueName = proposedName;
bool NotUnique = true;
long l = 1;
do
{
NotUnique = await ct.Report.AnyAsync(z => z.Name == newUniqueName);
if (NotUnique)
{
if (!skipIfAlreadyPresent)
newUniqueName = Util.StringUtil.UniqueNameBuilder(proposedName, l++, 255);
else
{
return true;
}
}
} while (NotUnique);
newObject.Name = newUniqueName;
await ValidateAsync(newObject, null);
if (HasErrors) return false;
await ct.Report.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, SockEvent.Created), ct);
if (skipIfAlreadyPresent)
{
ILogger log = Sockeye.Util.ApplicationLogging.CreateLogger<ReportBiz>();
log.LogInformation($"Stock report '{proposedName}' imported");
}
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//GET
//
internal async Task<Report> GetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.Report.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, SockEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<Report> PutAsync(Report putObject)
{
var dbObject = await GetAsync(putObject.Id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await ValidateAsync(putObject, dbObject);
if (HasErrors) return null;
ct.Replace(dbObject, putObject);
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, SockEvent.Modified), ct);
await SearchIndexAsync(putObject, false);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> DeleteAsync(long id)
{
using (var transaction = await ct.Database.BeginTransactionAsync())
{
var dbObject = await GetAsync(id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND);
return false;
}
await ValidateCanDelete(dbObject);
if (HasErrors)
return false;
{
var IDList = await ct.Review.AsNoTracking().Where(x => x.SockType == SockType.Report && x.ObjectId == id).Select(x => x.Id).ToListAsync();
if (IDList.Count() > 0)
{
ReviewBiz b = new ReviewBiz(ct, UserId, UserTranslationId, CurrentUserRoles);
foreach (long ItemId in IDList)
if (!await b.DeleteAsync(ItemId, transaction))
{
AddError(ApiErrorCode.CHILD_OBJECT_ERROR, null, $"Review [{ItemId}]: {b.GetErrorsAsString()}");
return false;
}
}
}
ct.Report.Remove(dbObject);
await ct.SaveChangesAsync();
await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObject.Id, dbObject.Name, ct);
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, BizType, ct);
await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct);
await transaction.CommitAsync();
return true;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//GET LIST
//
internal async Task<List<NameIdItem>> GetReportListAsync(SockType aType)
{
var rpts = await ct.Report.AsNoTracking().Where(z => z.SockType == aType && z.Active == true).Select(z => new { id = z.Id, name = z.Name, roles = z.Roles }).OrderBy(z => z.name).ToListAsync();
var ret = new List<NameIdItem>();
foreach (var item in rpts)
{
if (CurrentUserRoles.HasAnyFlags(item.roles))
{
ret.Add(new NameIdItem() { Name = item.name, Id = item.id });
}
}
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//SEARCH
//
private async Task SearchIndexAsync(Report obj, bool isNew)
{
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType);
DigestSearchText(obj, SearchParams);
if (isNew)
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
else
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
}
public async Task<Search.SearchIndexProcessObjectParameters> GetSearchResultSummary(long id, SockType specificType)
{
var obj = await GetAsync(id, false);
var SearchParams = new Search.SearchIndexProcessObjectParameters();
DigestSearchText(obj, SearchParams);
return SearchParams;
}
public void DigestSearchText(Report obj, Search.SearchIndexProcessObjectParameters searchParams)
{
if (obj != null)
searchParams.AddText(obj.Notes)
.AddText(obj.Name)
.AddText(obj.Template)
.AddText(obj.Style)
.AddText(obj.JsPrerender)
.AddText(obj.JsHelpers)
.AddText(obj.HeaderTemplate)
.AddText(obj.FooterTemplate);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task ValidateAsync(Report proposedObj, Report currentObj)
{
bool isNew = currentObj == null;
//Name required
if (string.IsNullOrWhiteSpace(proposedObj.Name))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name");
//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
//NOTE: unlike other objects reports can have the same name as long as the type differs
if (await ct.Report.AnyAsync(z => z.Name == proposedObj.Name && z.SockType == proposedObj.SockType && z.Id != proposedObj.Id))
{
AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name");
}
}
}
private async Task ValidateCanDelete(Report inObj)
{
//Referential integrity error
if (await ct.NotifySubscription.AnyAsync(z => z.LinkReportId == inObj.Id) == true)
{
//Note: errorbox will ensure it appears in the general errror box and not field specific
//the translation key is to indicate what the linked object is that is causing the error
AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("NotifySubscription"));
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//REPORT DATA
//Data fetched to return to report render or for designer for Client report design usage
public async Task<Newtonsoft.Json.Linq.JArray> GetReportDataForReportDesigner(DataListSelectedRequest selectedRequest)
{
var log = Sockeye.Util.ApplicationLogging.CreateLogger("ReportBiz::GetReportDataForReportDesigner");
AuthorizationRoles effectiveRoles = CurrentUserRoles;
if (selectedRequest.SockType == SockType.NoType)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, null, $"SockType is required");
return null;
}
//Do we need to rehydrate the ID List from a DataList?
if (selectedRequest.SelectedRowIds.Length == 0)
selectedRequest.SelectedRowIds = await DataListSelectedProcessingOptions.RehydrateIdList(selectedRequest, ct, effectiveRoles, log, UserId, UserTranslationId);
log.LogDebug($"Instantiating biz object handler for {selectedRequest.SockType}");
var biz = BizObjectFactory.GetBizObject(selectedRequest.SockType, ct, UserId, CurrentUserRoles, UserTranslationId);
log.LogDebug($"Fetching data for {selectedRequest.SelectedRowIds.Length} {selectedRequest.SockType} items");
return await ((IReportAbleObject)biz).GetReportData(selectedRequest, Guid.Empty);//Guid.empty signifies it's not a job calling it
}
public async Task<Newtonsoft.Json.Linq.JArray> GetReportData(DataListSelectedRequest selectedRequest, Guid jobId, bool requestIsCustomerWorkOrderReport = false)
{
var log = Sockeye.Util.ApplicationLogging.CreateLogger("ReportBiz::GetReportData");
AuthorizationRoles effectiveRoles = CurrentUserRoles;
if (selectedRequest.SockType == SockType.NoType)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, null, $"SockType is required");
return null;
}
if (!requestIsCustomerWorkOrderReport && !Sockeye.Api.ControllerHelpers.Authorized.HasReadFullRole(effectiveRoles, selectedRequest.SockType))
{
AddError(ApiErrorCode.NOT_AUTHORIZED, null, $"User not authorized for {selectedRequest.SockType} type object");
return null;
}
//Do we need to rehydrate the ID List from a DataList?
if (selectedRequest.SelectedRowIds.Length == 0)
selectedRequest.SelectedRowIds = await DataListSelectedProcessingOptions.RehydrateIdList(selectedRequest, ct, effectiveRoles, log, UserId, UserTranslationId);
if (!ReportRenderManager.KeepGoing(jobId))
return null;
log.LogDebug($"Instantiating biz object handler for {selectedRequest.SockType}");
var biz = BizObjectFactory.GetBizObject(selectedRequest.SockType, ct, UserId, CurrentUserRoles, UserTranslationId);
log.LogDebug($"Fetching data for {selectedRequest.SelectedRowIds.Length} {selectedRequest.SockType} items");
return await ((IReportAbleObject)biz).GetReportData(selectedRequest, jobId);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//RENDER
//
public async Task<Guid?> RequestRenderReport(DataListReportRequest reportRequest, DateTime renderTimeOutExpiry, string apiUrl, string userName)
{
var log = Sockeye.Util.ApplicationLogging.CreateLogger("ReportBiz::RequestRenderReport");
log.LogDebug($"report id {reportRequest.ReportId}, timeout @ {renderTimeOutExpiry.ToString()}");
//get report, vet security, see what we need before init in case of issue
log.LogDebug($"get report from db");
var report = await ct.Report.FirstOrDefaultAsync(z => z.Id == reportRequest.ReportId);
if (report == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
//If we get here via the /viewreport url in the client then there is no object type set so we need to set it here from the report
if (reportRequest.SockType == SockType.NoType)
{
reportRequest.SockType = report.SockType;
}
AuthorizationRoles effectiveRoles = CurrentUserRoles;
if ( !Sockeye.Api.ControllerHelpers.Authorized.HasReadFullRole(effectiveRoles, report.SockType))
{
log.LogDebug($"bail: user unauthorized");
AddError(ApiErrorCode.NOT_AUTHORIZED, null, $"User not authorized for {report.SockType} type object");
return null;
}
//Client meta data is required
if (reportRequest.ClientMeta == null)
{
log.LogDebug($"bail: ClientMeta parameter is missing");
AddError(ApiErrorCode.VALIDATION_MISSING_PROPERTY, null, "ClientMeta parameter is missing and required to render report");
return null;
}
//includeWoItemDescendants?
reportRequest.IncludeWoItemDescendants = report.IncludeWoItemDescendants;
//Do we need to rehydrate the ID List from a DataList?
if (reportRequest.SelectedRowIds.Length == 0)
reportRequest.SelectedRowIds = await DataListSelectedProcessingOptions.RehydrateIdList(reportRequest, ct, effectiveRoles, log, UserId, UserTranslationId);
var JobName = $"LT:Report id: \"{reportRequest.ReportId}\" LT:{reportRequest.SockType} ({reportRequest.SelectedRowIds.LongLength}) LT:User {userName}";
JObject o = JObject.FromObject(new
{
reportRequest = reportRequest,
apiUrl = apiUrl,
userName = userName
});
OpsJob j = new OpsJob();
j.Name = JobName;
j.SockType = SockType.Report;
j.JobType = JobType.RenderReport;
j.SubType = JobSubType.NotSet;
j.Exclusive = false;
j.JobInfo = o.ToString();
await JobsBiz.AddJobAsync(j);
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, 0, SockType.ServerJob, SockEvent.Created, JobName), ct);
return j.GId;
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}
public async Task DoRenderJob(OpsJob job)
{
var log = Sockeye.Util.ApplicationLogging.CreateLogger("ReportBiz::DoRenderJob");
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running);
ReportRenderManager.AddJob(job.GId, log);
// await JobsBiz.LogJobAsync(job.GId, $"LT:StartJob {job.JobType}");
//rehydrate job objects
log.LogDebug($"Start; rehydrate job {job.Name}");
JObject jobData = JObject.Parse(job.JobInfo);
var reportRequest = jobData["reportRequest"].ToObject<DataListReportRequest>();
var apiUrl = jobData["apiUrl"].ToObject<string>();
var userName = jobData["userName"].ToObject<string>();
var report = await ct.Report.FirstOrDefaultAsync(z => z.Id == reportRequest.ReportId);
if (report == null)
{
await JobsBiz.LogJobAsync(job.GId, $"rendererror:error,\"LT:ErrorAPI2010 LT:Report({reportRequest.ReportId})\"");
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Failed);
return;
}
if (!ReportRenderManager.KeepGoing(job.GId))
return;
//Get data
log.LogDebug("Getting report data now");
// var watch = new Stopwatch();
// watch.Start();
var ReportData = await GetReportData(reportRequest, job.GId);
// watch.Stop();
// log.LogInformation($"GetReportData took {watch.ElapsedMilliseconds}ms to execute");
//THIS is here to catch scenario where report data returned null because it expired, not because of an issue
if (!ReportRenderManager.KeepGoing(job.GId))
return;
//if GetReportData errored then will return null so need to return that as well here
if (ReportData == null)
{
log.LogDebug($"bail: ReportData == null");
await JobsBiz.LogJobAsync(job.GId, $"rendererror:error,\"{this.GetErrorsAsString()}\"");//?? circle back on this one
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Failed);
}
if (!ReportRenderManager.KeepGoing(job.GId))
return;
//initialization
log.LogDebug("Initializing report rendering system");
bool AutoDownloadChromium = true;
if (string.IsNullOrWhiteSpace(ServerBootConfig.SOCKEYE_REPORT_RENDER_BROWSER_PATH))
{
log.LogDebug($"Using default Chromium browser (downloaded)");
}
else
{
log.LogDebug($"Using user specified Chromium browser at {ServerBootConfig.SOCKEYE_REPORT_RENDER_BROWSER_PATH}");
AutoDownloadChromium = false;
}
var ReportJSFolderPath = Path.Combine(ServerBootConfig.SOCKEYE_CONTENT_ROOT_PATH, "resource", "rpt");
//Keep for debugging headfully
//var lo = new LaunchOptions { Headless = false };
var lo = new LaunchOptions { Headless = true };
if (!AutoDownloadChromium)
{
lo.ExecutablePath = ServerBootConfig.SOCKEYE_REPORT_RENDER_BROWSER_PATH;
/*
troubleshooting links:
https://developers.google.com/web/tools/puppeteer/troubleshooting
These links might be worth looking at in future if diagnosing other issues:
https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#running-on-alpine
https://github.com/puppeteer/puppeteer/issues/1825
const chromeFlags = [
'--headless',
'--no-sandbox',
"--disable-gpu",
"--single-process",
"--no-zygote"
]
*/
}
else
{
log.LogDebug($"Windows: Calling browserFetcher download async now:");
await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultChromiumRevision);
}
//Set Chromium args
//*** DANGER: --disable-dev-shm-usage will crash linux ayanova when it runs out of memory ****
//that was only suitable for dockerized scenario as it had an alt swap system
//var OriginalDefaultArgs = "--disable-dev-shm-usage --single-process --no-sandbox --disable-gpu --no-zygote ";
//Keep for debugging headfully
// var DefaultArgs = "--no-sandbox";
var DefaultArgs = "--headless --no-sandbox";
if (!string.IsNullOrWhiteSpace(ServerBootConfig.SOCKEYE_REPORT_RENDER_BROWSER_PARAMS))
{
log.LogDebug($"SOCKEYE_REPORT_RENDER_BROWSER_PARAMS will be used: {ServerBootConfig.SOCKEYE_REPORT_RENDER_BROWSER_PARAMS}");
lo.Args = new string[] { $"{ServerBootConfig.SOCKEYE_REPORT_RENDER_BROWSER_PARAMS}" };// SOCKEYE_REPORT_RENDER_BROWSER_PARAMS
}
else
{
log.LogDebug($"SOCKEYE_REPORT_RENDER_BROWSER_PARAMS not set, using defaults '{DefaultArgs}'");
lo.Args = new string[] { DefaultArgs };
}
System.Text.StringBuilder PageLog = new System.Text.StringBuilder();
if (!ReportRenderManager.KeepGoing(job.GId))
return;
//API DOCS http://www.puppeteersharp.com/api/index.html
log.LogDebug($"Launching headless Browser and new page now:");
using (var browser = await Puppeteer.LaunchAsync(lo))
using (var page = (await browser.PagesAsync())[0])
// using (var page = await browser.NewPageAsync())
{
//track this process for timeout purposes
ReportRenderManager.SetProcess(job.GId, browser.Process.Id, log);
page.DefaultTimeout = 0;//infinite timeout as we are controlling how long the process can live for with the reportprocessmanager
try
{
//info and error logging
page.Console += async (sender, args) =>
{
switch (args.Message.Type)
{
case ConsoleType.Error:
try
{
var errorArgs = await Task.WhenAll(args.Message.Args.Select(arg => arg.ExecutionContext.EvaluateFunctionAsync("(arg) => arg instanceof Error ? arg.message : arg", arg)));
PageLog.AppendLine($"ERROR: {args.Message.Text} args: [{string.Join<object>(", ", errorArgs)}]");
}
catch { }
break;
case ConsoleType.Warning:
PageLog.AppendLine($"WARNING: {args.Message.Text}");
break;
default:
PageLog.AppendLine($"INFO: {args.Message.Text}");
break;
}
};
log.LogDebug($"Preparing page: adding base reporting scripts to page");
if (!ReportRenderManager.KeepGoing(job.GId))
return;
//Add Handlebars JS for compiling and presenting
//https://handlebarsjs.com/
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "sock-hb.js") });
//add Marked for markdown processing
//https://github.com/markedjs/marked
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "sock-md.js") });
//add DOM Purify for markdown template sanitization processing
//https://github.com/cure53/DOMPurify
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "sock-pf.js") });
//add Bar code library if our bar code helper is referenced
//https://github.com/metafloor/bwip-js
if (report.Template.Contains("ayBC ") || report.JsHelpers.Contains("ayBC "))
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "sock-bc.js") });
//add stock helpers
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "sock-report.js") });
//execute to add to handlebars
await page.EvaluateExpressionAsync("ayRegisterHelpers();");
if (!ReportRenderManager.KeepGoing(job.GId))
return;
log.LogDebug($"Preparing page: adding this report's scripts, style and templates to page");
//add report pre-render, helpers and style
if (string.IsNullOrWhiteSpace(report.JsPrerender))
{
report.JsPrerender = "async function ayPrepareData(reportData){return reportData;}";
}
await page.AddScriptTagAsync(new AddTagOptions() { Content = report.JsPrerender });
if (!string.IsNullOrWhiteSpace(report.JsHelpers))
await page.AddScriptTagAsync(new AddTagOptions() { Content = report.JsHelpers });
log.LogDebug($"Preparing page: adding Client meta data");
//Client meta data to JSON string
var clientMeta = reportRequest.ClientMeta.ToString();
log.LogDebug($"Preparing page: adding Server meta data");
//Server meta data
var logo = await ct.Logo.AsNoTracking().SingleOrDefaultAsync();
var HasSmallLogo = "false";
var HasMediumLogo = "false";
var HasLargeLogo = "false";
if (logo != null)
{
if (logo.Small != null) HasSmallLogo = "true";
if (logo.Medium != null) HasMediumLogo = "true";
if (logo.Large != null) HasLargeLogo = "true";
}
var HasPostalAddress = !string.IsNullOrWhiteSpace(ServerGlobalBizSettings.Cache.PostAddress) ? "true" : "false";
var HasStreetAddress = !string.IsNullOrWhiteSpace(ServerGlobalBizSettings.Cache.Address) ? "true" : "false";
//case 4209
//latitude and longitude are the only nullable fields in global biz settings and need to be converted to empty strings if null
string sLatitude = "null";
string sLongitude = "null";
if (ServerGlobalBizSettings.Cache.Latitude != null)
sLatitude = ServerGlobalBizSettings.Cache.Latitude.ToString();
if (ServerGlobalBizSettings.Cache.Longitude != null)
sLongitude = ServerGlobalBizSettings.Cache.Longitude.ToString();
var serverMeta = $"{{ayApiUrl:`{apiUrl}`, HasSmallLogo:{HasSmallLogo}, HasMediumLogo:{HasMediumLogo}, HasLargeLogo:{HasLargeLogo},CompanyName: `Ground Zero Tech-Works Inc.`,CompanyWebAddress:`{ServerGlobalBizSettings.Cache.WebAddress}`,CompanyEmailAddress:`{ServerGlobalBizSettings.Cache.EmailAddress}`,CompanyPhone1:`{ServerGlobalBizSettings.Cache.Phone1}`,CompanyPhone2:`{ServerGlobalBizSettings.Cache.Phone2}`,HasPostalAddress:{HasPostalAddress},CompanyPostAddress:`{ServerGlobalBizSettings.Cache.PostAddress}`,CompanyPostCity:`{ServerGlobalBizSettings.Cache.PostCity}`,CompanyPostRegion:`{ServerGlobalBizSettings.Cache.PostRegion}`,CompanyPostCountry:`{ServerGlobalBizSettings.Cache.PostCountry}`,CompanyPostCode:`{ServerGlobalBizSettings.Cache.PostCode}`,HasStreetAddress:{HasStreetAddress},CompanyAddress:`{ServerGlobalBizSettings.Cache.Address}`,CompanyCity:`{ServerGlobalBizSettings.Cache.City}`,CompanyRegion:`{ServerGlobalBizSettings.Cache.Region}`,CompanyCountry:`{ServerGlobalBizSettings.Cache.Country}`,CompanyAddressPostal:`{ServerGlobalBizSettings.Cache.AddressPostal}`,CompanyLatitude:{sLatitude},CompanyLongitude:{sLongitude}}}";
log.LogDebug($"Preparing page: adding Report meta data");
//Custom fields definition for report usage
string CustomFieldsTemplate = "null";
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == report.SockType.ToString());
if (FormCustomization != null)
{
CustomFieldsTemplate = FormCustomization.Template;
}
//Report meta data
var reportMeta = $"{{Id:{report.Id},Name:`{report.Name}`,Notes:`{report.Notes}`,SockType:`{report.SockType}`,CustomFieldsDefinition:{CustomFieldsTemplate},DataListKey:`{reportRequest.DataListKey}`,SelectedRowIds: `{string.Join(",", reportRequest.SelectedRowIds)}`}}";
//duplicate meta data in report page wide variable for use by our internal functions
await page.AddScriptTagAsync(new AddTagOptions() { Content = $"var AYMETA={{ ayReportMetaData:{reportMeta}, ayClientMetaData:{clientMeta}, ayServerMetaData:{serverMeta} }}" });
if (!ReportRenderManager.KeepGoing(job.GId))
return;
//prePareData / preRender
var ReportDataObject = $"{{ ayReportData:{ReportData}, ayReportMetaData:{reportMeta}, ayClientMetaData:{clientMeta}, ayServerMetaData:{serverMeta} }}";
//case 4209
log.LogDebug($"ReportData object about to be pre-rendered is:{ReportDataObject}");
log.LogDebug($"PageLog before render:{PageLog.ToString()}");
log.LogDebug($"Calling ayPreRender...");
await page.WaitForExpressionAsync($"ayPreRender({ReportDataObject})");
log.LogDebug($"ayPreRender completed");
if (!ReportRenderManager.KeepGoing(job.GId))
return;
//compile the template
log.LogDebug($"Calling Handlebars.compile...");
var compileScript = $"Handlebars.compile(`{report.Template}`)(PreParedReportDataObject);";
if (!ReportRenderManager.KeepGoing(job.GId))
return;
var compiledHTML = await page.EvaluateExpressionAsync<string>(compileScript);
if (!ReportRenderManager.KeepGoing(job.GId))
return;
//render report as HTML
log.LogDebug($"Setting render page content style and compiled HTML");
await page.SetContentAsync($"<style>{report.Style}</style>{compiledHTML}");
string outputFileName = StringUtil.ReplaceLastOccurrence(FileUtil.NewRandomFileName, ".", "") + ".pdf";
string outputFullPath = System.IO.Path.Combine(FileUtil.TemporaryFilesFolder, outputFileName);
//Set PDF options
//https://pptr.dev/#?product=Puppeteer&version=v5.3.0&show=api-pagepdfoptions
log.LogDebug($"Resolving PDF Options from report settings");
var PdfOptions = new PdfOptions() { };
PdfOptions.DisplayHeaderFooter = report.DisplayHeaderFooter;
if (report.DisplayHeaderFooter)
{
var ClientPDFDate = reportRequest.ClientMeta["PDFDate"].Value<string>();
var ClientPDFTime = reportRequest.ClientMeta["PDFTime"].Value<string>();
PdfOptions.HeaderTemplate = report.HeaderTemplate.Replace("PDFDate", ClientPDFDate).Replace("PDFTime", ClientPDFTime);
PdfOptions.FooterTemplate = report.FooterTemplate.Replace("PDFDate", ClientPDFDate).Replace("PDFTime", ClientPDFTime);
}
if (report.PaperFormat != ReportPaperFormat.NotSet)
{
switch (report.PaperFormat)
{
case ReportPaperFormat.A0:
PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A0;
break;
case ReportPaperFormat.A1:
PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A1;
break;
case ReportPaperFormat.A2:
PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A2;
break;
case ReportPaperFormat.A3:
PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A3;
break;
case ReportPaperFormat.A4:
PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A4;
break;
case ReportPaperFormat.A5:
PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A5;
break;
case ReportPaperFormat.A6:
PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A6;
break;
case ReportPaperFormat.Ledger:
PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.Ledger;
break;
case ReportPaperFormat.Legal:
PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.Legal;
break;
case ReportPaperFormat.Letter:
PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.Letter;
break;
case ReportPaperFormat.Tabloid:
PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.Tabloid;
break;
}
}
PdfOptions.Landscape = report.Landscape;
if (!string.IsNullOrWhiteSpace(report.MarginOptionsBottom))
PdfOptions.MarginOptions.Bottom = report.MarginOptionsBottom;
if (!string.IsNullOrWhiteSpace(report.MarginOptionsLeft))
PdfOptions.MarginOptions.Left = report.MarginOptionsLeft;
if (!string.IsNullOrWhiteSpace(report.MarginOptionsRight))
PdfOptions.MarginOptions.Right = report.MarginOptionsRight;
if (!string.IsNullOrWhiteSpace(report.MarginOptionsTop))
PdfOptions.MarginOptions.Top = report.MarginOptionsTop;
PdfOptions.PreferCSSPageSize = report.PreferCSSPageSize;
PdfOptions.PrintBackground = report.PrintBackground;
//Defaults to 1. Scale amount must be between 0.1 and 2.
PdfOptions.Scale = report.Scale;
if (!ReportRenderManager.KeepGoing(job.GId))
return;
//render to pdf and return
log.LogDebug($"Calling render page contents to PDF");
await page.PdfAsync(outputFullPath, PdfOptions);
log.LogDebug($"Closing Page");
await page.CloseAsync();
log.LogDebug($"Closing Browser");
await browser.CloseAsync();
log.LogDebug($"Render completed successfully, output filename is: {outputFileName}, logging to job for client");
var json = Newtonsoft.Json.JsonConvert.SerializeObject(new { reportfilename = outputFileName }, Newtonsoft.Json.Formatting.None);
await JobsBiz.LogJobAsync(job.GId, json);
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed);
return;
}
catch (ReportRenderTimeOutException)
{
await HandleTimeOut(job, log, reportRequest, userName);
return;
}
catch (PuppeteerSharp.TargetClosedException)
{
log.LogDebug("Caught PuppeteerSharp.TargetClosedException - report was cancelled by user OR timed out");
//we closed it because the timeout hit and the CoreJobReportRenderEngineProcessCleanup job cleaned it out
//so return the error the client expects in this scenario
await HandleTimeOut(job, log, reportRequest, userName);
return;
}
catch (Exception ex)
{
log.LogDebug(ex, $"Error during report rendering");
//This is the error when a helper is used on the template but doesn't exist:
//Evaluation failed: d
//(it might also mean other things wrong with template)
if (PageLog.Length > 0)
{
log.LogInformation($"Exception caught while rendering report \"{report.Name}\", report Page console log:");
log.LogInformation(PageLog.ToString());
var json = Newtonsoft.Json.JsonConvert.SerializeObject(new { rendererror = new { pagelog = PageLog.ToString(), exception = ExceptionUtil.ExtractAllExceptionMessages(ex) } }, Newtonsoft.Json.Formatting.None);
await JobsBiz.LogJobAsync(job.GId, json);
}
else
{
var json = Newtonsoft.Json.JsonConvert.SerializeObject(new { rendererror = new { exception = ExceptionUtil.ExtractAllExceptionMessages(ex) } }, Newtonsoft.Json.Formatting.None);
await JobsBiz.LogJobAsync(job.GId, json);
}
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Failed);
// var v=await page.GetContentAsync();//for debugging purposes
return;
}
finally
{
log.LogDebug($"reached finally block");
if (!page.IsClosed)
{
log.LogDebug($"Page not closed in finally block, closing now");
await page.CloseAsync();
}
if (!browser.IsClosed)
{
log.LogDebug($"Browser not closed in finally block, closing now");
await browser.CloseAsync();
}
log.LogDebug($"Calling ReportRenderManager.RemoveJob to stop tracking this job/process");
await ReportRenderManager.RemoveJob(job.GId, log, false);
}
}
static async Task HandleTimeOut(OpsJob job, ILogger log, DataListReportRequest reportRequest, string userName)
{
log.LogDebug($"Report render cancelled by user OR exceeded timeout setting of {ServerBootConfig.SOCKEYE_REPORT_RENDERING_TIMEOUT} minutes, report id: {reportRequest.ReportId}, record count:{reportRequest.SelectedRowIds.LongLength}, user:{userName}");
var json = Newtonsoft.Json.JsonConvert.SerializeObject(new { rendererror = new { timeout = true, timeoutsetting = ServerBootConfig.SOCKEYE_REPORT_RENDERING_TIMEOUT } }, Newtonsoft.Json.Formatting.None);
//"{\"rendererror\":{\"timeout\":1}}"
await JobsBiz.LogJobAsync(job.GId, json);
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Failed);
}
}
public async Task CancelJob(Guid jobId)
{
await ReportRenderManager.RemoveJob(jobId, Sockeye.Util.ApplicationLogging.CreateLogger("ReportBiz::CancelJob"), true);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//JOB / OPERATIONS
//
public async Task HandleJobAsync(OpsJob job)
{
switch (job.JobType)
{
case JobType.RenderReport:
await DoRenderJob(job);
break;
default:
throw new System.ArgumentOutOfRangeException($"ReportBiz.HandleJob-> Invalid job type{job.JobType.ToString()}");
}
}
//Other job handlers here...
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,21 @@
namespace Sockeye.Biz
{
/// <summary>
/// All Sockeye report paper format types, passed to pdf generator
/// </summary>
public enum ReportPaperFormat : int
{// http://www.puppeteersharp.com/api/PuppeteerSharp.Media.PaperFormat.html
NotSet=0,
A0 = 1,
A1 = 2,
A2 = 3,
A3 = 4,
A4 = 5,
A5 = 6,
A6 = 7,
Ledger = 8,
Legal = 9,
Letter = 10,
Tabloid = 11
}
}//eons

View File

@@ -0,0 +1,13 @@
namespace Sockeye.Biz
{
/// <summary>
/// All Sockeye report rendering types
/// </summary>
public enum ReportRenderType : int
{// rendertype(type to render, default is pdf, but could be text, csv etc),
PDF = 0,
Text = 1,
CSV = 2,
HTML = 3
}
}//eons

View File

@@ -0,0 +1,15 @@
using System;
namespace Sockeye.Biz
{
/// <summary>
/// Marker attribute indicating that an object is a reportable type
/// Used in <see cref="SockType"/>
/// </summary>
[AttributeUsage(AttributeTargets.All)]
public class ReportableBizObjectAttribute : Attribute
{
//No code required, it's just a marker
//https://docs.microsoft.com/en-us/dotnet/standard/attributes/writing-custom-attributes
}
}//eons

View File

@@ -0,0 +1,141 @@
using Sockeye.Models;
using System.Linq;
using Newtonsoft.Json.Linq;
namespace Sockeye.Biz
{
//VALIDATE **USER DEFINED** (not stock) REQUIRED FIELDS THAT ARE NOT CUSTOM
//(fields that are stock required are validated on their own not here)
internal static class RequiredFieldsValidator
{
internal static void Validate(BizObject biz, FormCustom formCustom, object proposedObject)
{
//No form custom = no template to check against so nothing to do
if (formCustom == null || string.IsNullOrWhiteSpace(formCustom.Template))
return;
var FormTemplate = JArray.Parse(formCustom.Template);
var FormFields = Biz.FormFieldOptionalCustomizableReference.FormFieldReferenceList(formCustom.FormKey);
foreach (JObject jo in FormTemplate)
{
if (jo["required"].Value<bool>() == true)
{
//First get the LT key
var FldLtKey = jo["fld"].Value<string>();
// - e.g.: {template:[{fld:"ltkeyfieldname",hide:"true/false",required:"true/false", type:"bool"},{fld:"ltkeyfieldname",hide:"true/false",required:"true/false", type:"text"]}
//get the FormField object
FormField FF = FormFields.Where(z => z.FieldKey == FldLtKey).Single();
//get the type and because of quote and pm subsections "TKeySection" property being named "WorkOrder" in the formFieldReference due to lack of separate
//translations for quote and pm subitems it's necessary to adjust the name here first before matching
var proposedObjectType = proposedObject.GetType().ToString().Replace("Sockeye.Models.", "").Replace("QuoteItem", "WorkOrderItem").Replace("PMItem", "WorkOrderItem");
//hacky last minute work around but workorder, quote and pm header objects have no tkeysection normally which can cause interference here with duplicate fields i.e. Tags in subsections
//as they will have the header rule applied if we just leave the tkeysection as null so for here we workaround that by creating a temporary tkeysection
if (FF.TKeySection == null)
{
switch (proposedObjectType)
{
case "WorkOrder":
FF.TKeySection = proposedObjectType;
break;
case "Quote":
FF.TKeySection = proposedObjectType;
break;
case "PM":
FF.TKeySection = proposedObjectType;
break;
}
}
//don't validate custom fields, just skip them, make sure if it's sectional it matches the section of the object type (workorder sub sections)
if (!FF.IsCustomField && (FF.TKeySection == null || proposedObjectType == FF.TKeySection))
{
//bugbug: if section is workorderitem and field is tags but there is a tags required in workorder htat has NOT tkeysection it applies that rule
//Now get the actual property name from the available fields using the lt key
string RequiredPropertyName = FF.FieldKey;
//there might be a more specific model property due to being a workorder sub item duplicate such as WorkOrderItemTags vs WorkOrderTags
if (FF.ModelProperty != null)
RequiredPropertyName = FF.ModelProperty;
//Is it an indexed collection field?
if (RequiredPropertyName.Contains("."))
{
/*
flag errors to include parent e.g in PO item validation error field name could be "Items[3].QuantityReceived" to indicate poitems collection 4th row has error in qtyreceived
Note: collections are logically never more than 3 deep so for example the deepest would be workorder granchildren: e.g. Workorder.Items.Parts and most
are two deep however like Po.PoItems.field
for purposes of rule checking they would be flagged by their immediate parent "Items[3].QuantityReceived" for po or for workorder it could be
a required UPC field in Workorder.Items.Parts.UPC would result in a target error return of "Items[2].Parts[3].UPC" would be valid
*/
var FieldKeyParts = RequiredPropertyName.Split('.');
if (FieldKeyParts.Length == 2)
{
//parent collection -> child field
//target name like "Items.FieldName"
var parentCollection = proposedObject.GetType().GetProperty(FieldKeyParts[0]).GetValue(proposedObject, null);
int index = 0;
foreach (object ChildObject in (parentCollection as System.Collections.IEnumerable))
{
var fieldValue = ChildObject.GetType().GetProperty(FieldKeyParts[1]).GetValue(ChildObject, null);
if (fieldValue == null || string.IsNullOrWhiteSpace(fieldValue.ToString()))
biz.AddError(ApiErrorCode.VALIDATION_CUSTOM_REQUIRED_EMPTY, $"{FieldKeyParts[0]}[{index}].{FieldKeyParts[1]}");
index++;
}
}
else if (FieldKeyParts.Length == 3)
{
//grandparent collection -> Parent collection -> Child field
//target name like "WorkOrderItems.WorkOrderItemParts.UPC
var GrandParentCollection = proposedObject.GetType().GetProperty(FieldKeyParts[0]).GetValue(proposedObject, null);
int GrandParentIndex = 0;
foreach (object GrandParentObject in (GrandParentCollection as System.Collections.IEnumerable))
{
var ParentCollection = GrandParentObject.GetType().GetProperty(FieldKeyParts[1]).GetValue(GrandParentObject, null);
int ParentIndex = 0;
foreach (object ChildObject in (ParentCollection as System.Collections.IEnumerable))
{
var fieldValue = ChildObject.GetType().GetProperty(FieldKeyParts[1]).GetValue(ChildObject, null);
if (fieldValue == null || string.IsNullOrWhiteSpace(fieldValue.ToString()))
biz.AddError(ApiErrorCode.VALIDATION_CUSTOM_REQUIRED_EMPTY, $"{FieldKeyParts[0]}[{GrandParentIndex}].{FieldKeyParts[1]}[{ParentIndex}].{FieldKeyParts[2]}");
ParentIndex++;
}
GrandParentIndex++;
}
}
}
else
{
//It's a simple property on the main object
//use reflection to get the underlying value from the proposed object to be saved
//issue the error on the *Models* property name to mirror how all other server error handling and validation works
//so that client end consumes it properly (fieldkey is just for the UI and showing / hiding overall form values)
object propertyValue = proposedObject.GetType().GetProperty(RequiredPropertyName).GetValue(proposedObject, null);
if (propertyValue == null)
biz.AddError(ApiErrorCode.VALIDATION_CUSTOM_REQUIRED_EMPTY, RequiredPropertyName);
else
{
if (RequiredPropertyName == "Tags")
{
if (((System.Collections.Generic.List<string>)propertyValue).Count == 0)
{
biz.AddError(ApiErrorCode.VALIDATION_CUSTOM_REQUIRED_EMPTY, RequiredPropertyName);
}
}
else
if (string.IsNullOrWhiteSpace(propertyValue.ToString()))
biz.AddError(ApiErrorCode.VALIDATION_CUSTOM_REQUIRED_EMPTY, RequiredPropertyName);
}
}
}
}
}
}
}//eoc
}//ens

691
server/biz/ReviewBiz.cs Normal file
View File

@@ -0,0 +1,691 @@
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.Linq;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Sockeye.Biz
{
internal class ReviewBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject, IImportAbleObject, INotifiableObject
{
internal ReviewBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = SockType.Review;
}
internal static ReviewBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new ReviewBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new ReviewBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExistsAsync(long id)
{
return await ct.Review.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<Review> CreateAsync(Review newObject)
{
await ValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.Review.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, SockEvent.Created), ct);
await SearchIndexAsync(newObject, true);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await HandlePotentialNotificationEvent(SockEvent.Created, newObject);
await PopulateVizFields(newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//GET
//
internal async Task<Review> GetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.Review.AsNoTracking().SingleOrDefaultAsync(m => m.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, SockEvent.Retrieved), ct);
await PopulateVizFields(ret);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<Review> PutAsync(Review putObject)
{
var dbObject = await GetAsync(putObject.Id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
putObject.Tags = TagBiz.NormalizeTags(putObject.Tags);
putObject.CustomFields = JsonUtil.CompactJson(putObject.CustomFields);
await ValidateAsync(putObject, dbObject);
if (HasErrors) return null;
ct.Replace(dbObject, putObject);
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, SockEvent.Modified), ct);
await SearchIndexAsync(putObject, false);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
await HandlePotentialNotificationEvent(SockEvent.Modified, putObject, dbObject);
await PopulateVizFields(putObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE schedule only
//
internal async Task<bool> PutNewScheduleTimeAsync(ScheduleItemAdjustParams p)
{
Review dbObject = await ct.Review.SingleOrDefaultAsync(z => z.Id == p.Id);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return false;
}
dbObject.ReviewDate = p.Start;
await ValidateAsync(dbObject, dbObject);
if (HasErrors) return false;
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await ExistsAsync(dbObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return false;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, dbObject.SockType, SockEvent.Modified), ct);
await HandlePotentialNotificationEvent(SockEvent.Modified, dbObject, dbObject);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> DeleteAsync(long id, Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction parentTransaction = null)
{
Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction transaction = null;
if (parentTransaction == null)
transaction = await ct.Database.BeginTransactionAsync();
var dbObject = await GetAsync(id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND);
return false;
}
ValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.Review.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
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 HandlePotentialNotificationEvent(SockEvent.Deleted, dbObject);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//SEARCH
//
private async Task SearchIndexAsync(Review obj, bool isNew)
{
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType);
DigestSearchText(obj, SearchParams);
if (isNew)
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
else
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
}
public async Task<Search.SearchIndexProcessObjectParameters> GetSearchResultSummary(long id, SockType specificType)
{
var obj = await GetAsync(id, false);
var SearchParams = new Search.SearchIndexProcessObjectParameters();
DigestSearchText(obj, SearchParams);
return SearchParams;
}
public void DigestSearchText(Review obj, Search.SearchIndexProcessObjectParameters searchParams)
{
if (obj != null)
searchParams.AddText(obj.Notes)
.AddText(obj.Name)
.AddText(obj.Wiki)
.AddText(obj.Tags)
.AddText(obj.CompletionNotes)
.AddCustomFields(obj.CustomFields);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task ValidateAsync(Review proposedObj, Review currentObj)
{
/*
- RULE Roles: BizAdmin, Service, Inventory, Accounting, Sales can create and assign to anyone else.
- RULE Any other inside role can create for themselves only. (outside roles have no rights to this object so no need to check)
- RULE Restricted roles can only set completed date and enter completion notes not otherwise change or create or delete.
- BIZ RULE users with more than restricted roles can assign other users
*/
bool isNew = currentObj == null;
bool SelfAssigned = proposedObj.AssignedByUserId == UserId && proposedObj.UserId == UserId;
bool HasSupervisorRole =
CurrentUserRoles.HasFlag(AuthorizationRoles.BizAdmin) ||
CurrentUserRoles.HasFlag(AuthorizationRoles.Service) ||
CurrentUserRoles.HasFlag(AuthorizationRoles.Inventory) ||
CurrentUserRoles.HasFlag(AuthorizationRoles.Sales) ||
CurrentUserRoles.HasFlag(AuthorizationRoles.Accounting);
//Checks for non supervisors
if (!HasSupervisorRole)
{
//Non supervisor can't create a Review and assign to other User
if (isNew && !SelfAssigned)
{
AddError(ApiErrorCode.NOT_AUTHORIZED, "UserId");
return;//no need to check any further this is disqualifying completely
}
//Non supervisory roles can only change / set certain fields for non self reviews
if (!isNew && !SelfAssigned)
{
if (
(currentObj.Name != proposedObj.Name) ||
(currentObj.Notes != proposedObj.Notes) ||
(currentObj.ReviewDate != proposedObj.ReviewDate) ||
(currentObj.UserId != proposedObj.UserId) ||
(currentObj.AssignedByUserId != proposedObj.AssignedByUserId))
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror");
return;
}
}
}
//Can't change assigned object id and type after initial save
if (!isNew)
{
if (proposedObj.ObjectId != currentObj.ObjectId)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ObjectId");
return;
}
if (proposedObj.SockType != currentObj.SockType)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "SockType");
return;
}
}
//Can't change Review date after completed
//user must set empty completed before changing start date if they really want to do this
if (!isNew && proposedObj.CompletedDate != null)
{
if (proposedObj.ReviewDate != currentObj.ReviewDate)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ReviewDate");
return;
}
}
//Does the object of this Review actually exist?
if (!await BizObjectExistsInDatabase.ExistsAsync(proposedObj.SockType, proposedObj.ObjectId, ct))
{
AddError(ApiErrorCode.NOT_FOUND, "generalerror", $"LT:ErrorAPI2010 LT:{proposedObj.SockType} id {proposedObj.ObjectId}");
return;
}
//Name required
if (string.IsNullOrWhiteSpace(proposedObj.Name))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name");
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(x => x.FormKey == SockType.Review.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(Review inObj)
{
bool SelfAssigned = inObj.AssignedByUserId == UserId && inObj.UserId == UserId;
bool HasSupervisorRole =
CurrentUserRoles.HasFlag(AuthorizationRoles.BizAdmin) ||
CurrentUserRoles.HasFlag(AuthorizationRoles.Service) ||
CurrentUserRoles.HasFlag(AuthorizationRoles.Inventory) ||
CurrentUserRoles.HasFlag(AuthorizationRoles.Sales) ||
CurrentUserRoles.HasFlag(AuthorizationRoles.Accounting);
if (!SelfAssigned && !HasSupervisorRole)
AddError(ApiErrorCode.NOT_AUTHORIZED);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//REPORTING
//
public async Task<JArray> GetReportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
{
var idList = dataListSelectedRequest.SelectedRowIds;
JArray ReportData = new JArray();
while (idList.Any())
{
var batch = idList.Take(IReportAbleObject.REPORT_DATA_BATCH_SIZE);
idList = idList.Skip(IReportAbleObject.REPORT_DATA_BATCH_SIZE).ToArray();
//query for this batch, comes back in db natural order unfortunately
var batchResults = await ct.Review.AsNoTracking().Where(z => batch.Contains(z.Id)).ToArrayAsync();
//order the results back into original
var orderedList = from id in batch join z in batchResults on id equals z.Id select z;
batchResults = null;
foreach (Review w in orderedList)
{
if (!ReportRenderManager.KeepGoing(jobId)) return null;
await PopulateVizFields(w);
var jo = JObject.FromObject(w);
if (!JsonUtil.JTokenIsNullOrEmpty(jo["CustomFields"]))
jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]);
ReportData.Add(jo);
}
orderedList = null;
}
vc.Clear();
return ReportData;
}
private VizCache vc = new VizCache();
//populate viz fields from provided object
public async Task PopulateVizFields(Review o)
{
if (!vc.Has("user", o.UserId))
vc.Add(await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(), "user", o.UserId);
o.UserViz = vc.Get("user", o.UserId);
if (!vc.Has("user", o.AssignedByUserId))
vc.Add(await ct.User.AsNoTracking().Where(x => x.Id == o.AssignedByUserId).Select(x => x.Name).FirstOrDefaultAsync(), "user", o.AssignedByUserId);
o.AssignedByUserViz = vc.Get("user", o.AssignedByUserId);
if (!vc.Has($"b{o.SockType}{o.ObjectId}"))
vc.Add(BizObjectNameFetcherDirect.Name((SockType)o.SockType, (long)o.ObjectId, UserTranslationId, ct), $"b{o.SockType}{o.ObjectId}");
o.ReviewObjectViz = vc.Get($"b{o.SockType}{o.ObjectId}");
if (o.ReviewObjectViz.StartsWith("LT:"))
{
if (!vc.Has(o.ReviewObjectViz))
vc.Add(await Translate(o.ReviewObjectViz), o.ReviewObjectViz);
o.ReviewObjectViz = vc.Get(o.ReviewObjectViz);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// IMPORT EXPORT
//
public async Task<JArray> GetExportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
{
//for now just re-use the report data code
//this may turn out to be the pattern for most biz object types but keeping it seperate allows for custom usage from time to time
return await GetReportData(dataListSelectedRequest, jobId);
}
public async Task<List<string>> ImportData(AyImportData importData)
{
List<string> ImportResult = new List<string>();
string ImportTag = $"imported-{FileUtil.GetSafeDateFileName()}";
var jsset = JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = new Sockeye.Util.JsonUtil.ShouldSerializeContractResolver(new string[] { "Concurrency", "Id", "CustomFields" }) });
foreach (JObject j in importData.Data)
{
var w = j.ToObject<Review>(jsset);
if (j["CustomFields"] != null)
w.CustomFields = j["CustomFields"].ToString();
w.Tags.Add(ImportTag);//so user can find them all and revert later if necessary
var res = await CreateAsync(w);
if (res == null)
{
ImportResult.Add($"* {w.Name} - {this.GetErrorsAsString()}");
this.ClearErrors();
}
else
{
ImportResult.Add($"{w.Name} - ok");
}
}
return ImportResult;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//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.BatchCoreObjectOperation:
await ProcessBatchJobAsync(job);
break;
default:
throw new System.ArgumentOutOfRangeException($"ReviewBiz.HandleJob-> Invalid job type{job.JobType.ToString()}");
}
}
private async Task ProcessBatchJobAsync(OpsJob job)
{
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running);
await JobsBiz.LogJobAsync(job.GId, $"LT:StartJob {job.SubType}");
List<long> idList = new List<long>();
long FailedObjectCount = 0;
JObject jobData = JObject.Parse(job.JobInfo);
if (jobData.ContainsKey("idList"))
idList = ((JArray)jobData["idList"]).ToObject<List<long>>();
else
idList = await ct.Review.AsNoTracking().Select(z => z.Id).ToListAsync();
bool SaveIt = false;
//---------------------------------
//case 4192
TimeSpan ProgressAndCancelCheckSpan = new TimeSpan(0, 0, ServerBootConfig.JOB_PROGRESS_UPDATE_AND_CANCEL_CHECK_SECONDS);
DateTime LastProgressCheck = DateTime.UtcNow.Subtract(new TimeSpan(1, 1, 1, 1, 1));
var TotalRecords = idList.LongCount();
long CurrentRecord = -1;
//---------------------------------
foreach (long id in idList)
{
try
{
//--------------------------------
//case 4192
//Update progress / cancel requested?
CurrentRecord++;
if (DateUtil.IsAfterDuration(LastProgressCheck, ProgressAndCancelCheckSpan))
{
await JobsBiz.UpdateJobProgressAsync(job.GId, $"{CurrentRecord}/{TotalRecords}");
if (await JobsBiz.GetJobStatusAsync(job.GId) == JobStatus.CancelRequested)
break;
LastProgressCheck = DateTime.UtcNow;
}
//---------------------------------
SaveIt = false;
ClearErrors();
Review o = null;
//save a fetch if it's a delete
if (job.SubType != JobSubType.Delete)
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.ProcessBatchTagOperation(o.Tags, (string)jobData["tag"], jobData.ContainsKey("toTag") ? (string)jobData["toTag"] : null, job.SubType);
break;
case JobSubType.Delete:
if (!await DeleteAsync(id))
{
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}");
FailedObjectCount++;
}
break;
default:
throw new System.ArgumentOutOfRangeException($"ProcessBatchJobAsync -> Invalid job Subtype{job.SubType}");
}
if (SaveIt)
{
o = await PutAsync(o);
if (o == null)
{
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}");
FailedObjectCount++;
}
}
//delay so we're not tying up all the resources in a tight loop
await Task.Delay(Sockeye.Util.ServerBootConfig.JOB_OBJECT_HANDLE_BATCH_JOB_LOOP_DELAY);
}
catch (Exception ex)
{
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors id({id})");
await JobsBiz.LogJobAsync(job.GId, ExceptionUtil.ExtractAllExceptionMessages(ex));
}
}
//---------------------------------
//case 4192
await JobsBiz.UpdateJobProgressAsync(job.GId, $"{++CurrentRecord}/{TotalRecords}");
//---------------------------------
await JobsBiz.LogJobAsync(job.GId, $"LT:BatchJob {job.SubType} {idList.Count}{(FailedObjectCount > 0 ? " - LT:Failed " + FailedObjectCount : "")}");
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed);
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task HandlePotentialNotificationEvent(SockEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = Sockeye.Util.ApplicationLogging.CreateLogger<ReviewBiz>();
log.LogDebug($"HandlePotentialNotificationEvent processing: [SockType:{this.BizType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
Review o = (Review)proposedObj;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
#region OLD
//OLD general notification code removed in favor of specific event for review imminent
// //CREATED / MODIFIED
// if (ayaEvent == AyaEvent.Created || ayaEvent == AyaEvent.Modified)
// {
// //OVERDUE pseudo event
// {
// //Remove prior
// await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.SockType, proposedObj.Id, NotifyEventType.GeneralNotification);//assumes only general event for this object type is overdue here
// //set a deadman automatic internal notification if goes past due
// var r = (Review)proposedObj;
// //it not completed yet and not overdue already (which could indicate an import or something)
// if (r.CompletedDate == null && r.ReviewDate > DateTime.UtcNow)
// {
// //Notify user
// await NotifyEventHelper.EnsureDefaultInAppUserNotificationSubscriptionExists(r.UserId, ct);
// {
// var gensubs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.GeneralNotification && z.UserId == r.UserId).ToListAsync();
// foreach (var sub in gensubs)
// {
// var eventNameTranslated = await TranslationBiz.GetTranslationForUserStaticAsync("ReviewOverDue", r.UserId);
// NotifyEvent n = new NotifyEvent()
// {
// EventType = NotifyEventType.GeneralNotification,
// UserId = r.UserId,
// ObjectId = proposedObj.Id,
// SockType = SockType.Review,
// NotifySubscriptionId = sub.Id,
// Name = $"{eventNameTranslated} - {proposedObj.Name}",
// EventDate = r.ReviewDate
// };
// await ct.NotifyEvent.AddAsync(n);
// log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
// }
// if (gensubs.Count > 0)
// await ct.SaveChangesAsync();
// }
// //Notify supervisor
// if (r.UserId != r.AssignedByUserId)
// {
// await NotifyEventHelper.EnsureDefaultInAppUserNotificationSubscriptionExists(r.AssignedByUserId, ct);
// var gensubs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.GeneralNotification && z.UserId == r.AssignedByUserId).ToListAsync();
// foreach (var sub in gensubs)
// {
// var eventNameTranslated = await TranslationBiz.GetTranslationForUserStaticAsync("ReviewOverDue", r.AssignedByUserId);
// NotifyEvent n = new NotifyEvent()
// {
// EventType = NotifyEventType.GeneralNotification,
// UserId = r.AssignedByUserId,
// ObjectId = proposedObj.Id,
// SockType = SockType.Review,
// NotifySubscriptionId = sub.Id,
// Name = $"{eventNameTranslated} - {proposedObj.Name}",
// EventDate = r.ReviewDate
// };
// await ct.NotifyEvent.AddAsync(n);
// log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
// }
// if (gensubs.Count > 0)
// await ct.SaveChangesAsync();
// }
// }
// }//overdue event
// }//custom events for created / modified
#endregion old
//## DELETED EVENTS
//any event added below needs to be removed, so
//just blanket remove any event for this object of eventtype that would be added below here
//do it regardless any time there's an update and then
//let this code below handle the refreshing addition that could have changes
await NotifyEventHelper.ClearPriorEventsForObject(ct, SockType.Review, o.Id, NotifyEventType.ReviewImminent);
//## CREATED / MODIFIED EVENTS
if (ayaEvent == SockEvent.Created || ayaEvent == SockEvent.Modified)
{
//# REVIEW IMMINENT
if (o.CompletedDate == null && o.ReviewDate > DateTime.UtcNow)
{
//notify users (time delayed)
var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ReviewImminent).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
//Tag match? (will be true if no sub tags so always safe to call this)
if (NotifyEventHelper.ObjectHasAllSubscriptionTags(o.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.ReviewImminent,
UserId = sub.UserId,
SockType = o.SockType,
ObjectId = o.Id,
NotifySubscriptionId = sub.Id,
Name = o.Name,
EventDate = o.ReviewDate
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//review imminent
}//end of process notifications
}//end of process notifications
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

998
server/biz/Search.cs Normal file
View File

@@ -0,0 +1,998 @@
using System.Linq;
using System.Globalization;
using System.Text;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Sockeye.Util;
using Sockeye.Models;
namespace Sockeye.Biz
{
//This class handles word breaking, processing keywords and searching for results
public static class Search
{
#region Search and return results
public class SearchRequestParameters
{
public string Phrase { get; set; }
public SockType TypeOnly { get; set; }
//Note: maxresults of 0 will get all results
public int MaxResults { get; set; }
public SearchRequestParameters()
{
TypeOnly = SockType.NoType;
MaxResults = 500;
}
public bool IsValid
{
get
{
//has a phrase?
if (!string.IsNullOrWhiteSpace(this.Phrase))
return true;
return false;
}
}
}
//Classes to hold search results returned to client
public class SearchResult
{
public string Name { get; set; }
public SockType Type { get; set; }
public long Id { get; set; }
}
public class SearchReturnObject
{
public long TotalResultsFound { get; set; }
public List<SearchResult> SearchResults { get; set; }
public SearchReturnObject()
{
TotalResultsFound = 0;
SearchResults = new List<SearchResult>();
}
}
public static async Task<SearchReturnObject> DoSearchAsync(AyContext ct, long translationId, AuthorizationRoles currentUserRoles, long currentUserId, SearchRequestParameters searchParameters)
{
var ReturnObject = new SearchReturnObject();
//list to hold temporary search/tag hits
List<SockTypeId> MatchingObjects = new List<SockTypeId>();
if (!searchParameters.IsValid)
{
//this is expected, don't throw, just return nothing
//throw new System.ArgumentException("Search::DoSearch - Search request parameters must contain a phrase or tags");
return ReturnObject;
}
//escape literal percentage signs first just in case they are searching for 50% off or something
//https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-LIKE
//need to get around breaking possibly losing the symbol so make it text
searchParameters.Phrase = searchParameters.Phrase.Replace("%", "pctsym");
//Modify Phrase to replace wildcard * with % as breakcore expects sql style wildcards
searchParameters.Phrase = searchParameters.Phrase.Replace("*", "%");
//BREAK SEARCH PHRASE INTO SEPARATE TERMS
var PhraseItems = await BreakSearchPhraseAsync(translationId, searchParameters.Phrase);
//SPLIT OUT WILDCARDS FROM NON WILDCARDS
List<string> PreWildCardedSearchTerms = new List<string>();
List<string> SearchTerms = new List<string>();
foreach (string PhraseItem in PhraseItems)
{
if (PhraseItem.Contains("%"))
PreWildCardedSearchTerms.Add(PhraseItem.Replace("pctsym", @"\%"));//put back literal percentage symbol if necessary
else
SearchTerms.Add(PhraseItem.Replace("pctsym", @"\%"));//put back literal percentage symbol if necessary
}
StringBuilder q = new StringBuilder();
int termCount = 0;
q.Append("WITH qr AS (SELECT asearchkey.sockType, asearchkey.objectid, ");
//EXACT MATCH SEARCH TERMS
foreach (string Term in SearchTerms)
q.Append($"COUNT(*) FILTER (WHERE asearchdictionary.word = '{Term}') AS st{++termCount}, ");
//WILDCARD SEARCH TERMS
foreach (string WildCardSearchTerm in PreWildCardedSearchTerms)
q.Append($"COUNT(*) FILTER (WHERE asearchdictionary.word LIKE '{WildCardSearchTerm}') AS st{++termCount}, ");
q.Length=q.Length-2;//trim the final comma and space
var qTypeOnly=string.Empty;
if(searchParameters.TypeOnly!=SockType.NoType){
//INNER JOIN ASEARCHKEY ON ASEARCHDICTIONARY.ID = ASEARCHKEY.WORDID and asearchkey.sockType=20
qTypeOnly=$"AND ASEARCHKEY.ATYPE={(int)searchParameters.TypeOnly}";
}
q.Append($" FROM asearchdictionary INNER JOIN asearchkey ON asearchdictionary.id = asearchkey.wordid {qTypeOnly} GROUP BY asearchkey.objectid, asearchkey.sockType) SELECT sockType, objectid FROM qr WHERE ");
for (; termCount > 0; termCount--)
q.Append($"st{termCount} > 0 {(termCount > 1 ? "AND " : "")}");
//execute the query and iterate the results
using (var command = ct.Database.GetDbConnection().CreateCommand())
{
await ct.Database.OpenConnectionAsync();
command.CommandText = q.ToString();
using (var dr = await command.ExecuteReaderAsync())
{
while (dr.Read())
{
MatchingObjects.Add(new SockTypeId((SockType)dr.GetInt32(0), dr.GetInt64(1)));
}
}
}
//REMOVE ANY ITEMS THAT USER IS NOT PERMITTED TO READ
//list to hold temporary matches
List<SockTypeId> CanReadMatchingObjects = new List<SockTypeId>();
foreach (SockTypeId t in MatchingObjects)
{
if (t.SockType == SockType.FileAttachment)
{
//have to look up the actual underlying object type and id here
//check if it's readable for user
//then add the PARENT object type and id to the CanREadMatchingObjects list
//this means user will not see it return as an attachment, just as the object
FileAttachment f = await ct.FileAttachment.AsNoTracking().FirstOrDefaultAsync(z => z.Id == t.ObjectId);
if (Sockeye.Api.ControllerHelpers.Authorized.HasReadFullRole(currentUserRoles, f.AttachToAType))
{
CanReadMatchingObjects.Add(new SockTypeId(f.AttachToAType, f.AttachToObjectId));
}
}
else if (t.SockType == SockType.Memo)
{
//Users are only permitted to search their own memo's
if (await ct.Memo.AsNoTracking().AnyAsync(z => z.Id == t.ObjectId && z.ToId == currentUserId))
CanReadMatchingObjects.Add(t);
}
else if (t.SockType == SockType.Reminder)
{
//Users are only permitted to search their own reminder's
if (await ct.Reminder.AsNoTracking().AnyAsync(z => z.Id == t.ObjectId && z.UserId == currentUserId))
CanReadMatchingObjects.Add(t);
}
else
{
if (Sockeye.Api.ControllerHelpers.Authorized.HasReadFullRole(currentUserRoles, t.SockType))
{
CanReadMatchingObjects.Add(t);
}
}
}
//Ok, we're here with the list of allowable objects which is now the master matching objects list so...
MatchingObjects = CanReadMatchingObjects;
//TOTAL RESULTS
//we have the total results here so set accordingly
ReturnObject.TotalResultsFound = MatchingObjects.Count;
//MAXIMUM RESULTS FILTER
//The theory is that it should be filtered BEFORE sorting so that you get the most random collection of results
//As the results are not ranked so...
if (searchParameters.MaxResults > 0)//0 = all results
MatchingObjects = MatchingObjects.Take(searchParameters.MaxResults).ToList();
//Sort and group the matching objects list in return order
var OrderedMatchingObjects = MatchingObjects.OrderBy(z => z.SockType).ThenByDescending(z => z.ObjectId);
//Get names using best performing technique
using (var command = ct.Database.GetDbConnection().CreateCommand())
{
ct.Database.OpenConnection();
//Build the return list from the remaining matching objects list
foreach (SockTypeId i in OrderedMatchingObjects)
{
SearchResult SR = new SearchResult();
SR.Name = BizObjectNameFetcherDirect.Name(i.SockType,
i.ObjectId,translationId,
command);
SR.Id = i.ObjectId;
SR.Type = i.SockType;
ReturnObject.SearchResults.Add(SR);
}
}
return ReturnObject;
}
#endregion dosearch
#region Get info (excerpt)
public static async Task<string> GetInfoAsync(long translationId, AuthorizationRoles currentUserRoles, long userId, string phrase, int max, SockType sockType, long id, AyContext ct)
{
//escape literal percentage signs first just in case they are searching for 50% off or something
//https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-LIKE
//need to get around breaking possibly losing the symbol so make it text
phrase = phrase.Replace("%", "pctsym");
//Modify Phrase to replace wildcard * with % as breakcore expects sql style wildcards
phrase = phrase.Replace("*", "%");
//BREAK SEARCH PHRASE INTO SEPARATE TERMS
var PhraseItems = await BreakSearchPhraseAsync(translationId, phrase);
PhraseItems.ToArray();
//get text
ISearchAbleObject o = (ISearchAbleObject)BizObjectFactory.GetBizObject(sockType, ct, userId, currentUserRoles, translationId);
//get extract
var searchParams = await o.GetSearchResultSummary(id, sockType);
//extract and rank here
ExtractAndRank er = new ExtractAndRank();
er.Process(searchParams, PhraseItems.ToArray(), max);
// sr.Extract = er.Extract;
// sr.Rank = er.Ranking;
return er.Extract;
}
#region Search rank and extract
/// <summary>
/// Rank and extract best excerpt of specified text and search terms
/// </summary>
public sealed class ExtractAndRank
{
#region Fields
private string[] searchTerms;
private string rawtext;
private string extract = "";
private bool flattenExtract = true;
private float ranking;
private int extractionThresholdRank = 10;
private int maximumCharactersToExtract = 40;
#endregion
#region Properties
/// <summary>
/// This is the ranking of the source text as it pertains to the
/// search terms
///
/// A rank of zero means either there was no match or the rank that was calculated
/// was lower than the threshold ranking, either way, no excerpt extraction is done.
///
/// It is a percentage value on a scale of 0 to 100
/// and is weighted:
///
/// 75% of the score is the percentage of all search terms found in the text
/// 25% of the score is the percentage of all characters in the text that are search term characters
///
///
/// </summary>
public float Ranking
{
get
{
return ranking;
}
}
/// <summary>
/// Maximum characters to appear in an extraction
/// default is 80
/// Minimum is 10
/// </summary>
public int MaximumCharactersToExtract
{
get
{
return maximumCharactersToExtract;
}
set
{
if (value > 10)
maximumCharactersToExtract = value;
else
maximumCharactersToExtract = 10;
}
}
/// <summary>
/// ExtractionThresholdRank
/// Extraction will only take place if the rank is
/// this value or higher
///
/// default is 10, maximum is 100 minimum is 0
/// </summary>
public int ExtractionThresholdRank
{
get
{
return extractionThresholdRank;
}
set
{
if (value > 100)
extractionThresholdRank = 100;
else if (value < 0)
extractionThresholdRank = 0;
else
extractionThresholdRank = value;
}
}
/// <summary>
/// If true, carriage returns and line feeds will be removed from extract
/// </summary>
public bool FlattenExtract
{
get
{
return this.flattenExtract;
}
set
{
this.flattenExtract = value;
}
}
/// <summary>
/// Extracted text excerpt that best reflects search terms
/// </summary>
public string Extract
{
get
{
return extract;
}
}
#endregion
#region public methods
/// <summary>
/// Do the extraction and ranking
/// </summary>
public void Process(SearchIndexProcessObjectParameters searchObjectParams, string[] searchTerms, int max)
{
this.maximumCharactersToExtract = max;
ranking = 0;
extract = "";
string rawText = string.Join(" ", searchObjectParams.Words);
//System.Diagnostics.Debug.Assert(rawText!=null && rawText!="","EXTRACT AND RANK","EMPTY RAWTEXT, CHECK OBJECTS GetSearchResult() CODE TO ENSURE IT'S GOT THE correct SP (CHECK THE SP IF NOT)");
if (rawText == null || rawText == "") return;
this.rawtext = rawText;
if (searchTerms == null || searchTerms.Length == 0) return;
this.searchTerms = searchTerms;
ranking = score(0, this.rawtext.Length);
if (ranking > extractionThresholdRank)
DoExtract();
}
#endregion
#region Calculate score
/// <summary>
/// Give a percentage score for a given window of
/// text in the raw text string
/// 75% of the score is the percentage of all search terms found in the window
/// 25% of the score is the percentage of all characters in the search window that are search term characters
///
///
///
/// </summary>
/// <param name="nStartPos"></param>
/// <param name="nEndPos"></param>
/// <returns>Float value of zero to one hundred</returns>
private float score(int nStartPos, int nEndPos)
{
//rewrite this as an integer based calculation
System.Diagnostics.Debug.Assert(nStartPos < nEndPos);
if (nStartPos < 0) nStartPos = 0;
if (nEndPos > this.rawtext.Length) nEndPos = this.rawtext.Length;
int nTermCharsInWindow = 0;//how many of the characters in the window are matching term characters
string SearchString = this.rawtext.Substring(nStartPos, nEndPos - nStartPos).ToLower(System.Globalization.CultureInfo.CurrentCulture);
int nMatches = 0;
foreach (string term in searchTerms)
{
//remove the wild card character if present and set to lower case
string lTerm = term.ToLower(System.Globalization.CultureInfo.CurrentCulture).Replace("%", "");
int nLocation = SearchString.IndexOf(lTerm);
if (nLocation != -1)
{
nMatches++;
while (nLocation != -1)
{
nTermCharsInWindow += lTerm.Length; ;
nLocation = SearchString.IndexOf(lTerm, nLocation + 1);
}
}
}
//If no matches then rank is automatically zero
if (nMatches == 0)
{
return 0;
}
//Rank is calculated on a weighted scale
//75% for matching all search terms
//25% for the quantity of search terms versus other text found
float fTermsFoundPct = 75 * ((float)nMatches / (float)searchTerms.GetLength(0));
float fTermsVsTextPct = 0;
if (nTermCharsInWindow > 0)
fTermsVsTextPct = 25 * ((float)nTermCharsInWindow / (float)SearchString.Length);
return fTermsFoundPct + fTermsVsTextPct;
}
#endregion
#region Extract best excerpt
/// <summary>
/// Extract the best scoring excerpt fragments of
/// raw text
/// </summary>
private void DoExtract()
{
//If the whole thing is less than the max to extract
//just save time and return the whole thing
if (this.rawtext.Length < this.maximumCharactersToExtract)
{
this.extract = this.rawtext;
return;
}
string BestWindow = "";
float BestScore = 0;
float thisscore = 0;
int BestWindowStartPos = 0;
//Get the shortest search term length so
//we can save time iterating over the window in the extract
//function below
int shortestSearchTermLength = int.MaxValue;
foreach (string s in this.searchTerms)
{
if (s.Length < shortestSearchTermLength)
shortestSearchTermLength = s.Length;
}
//slide a window over the text and check it's score, the highest scoring window wins
//move the length of the shortest search term so as to ensure we won't
//miss it, but faster than moving one character at a time
for (int z = 0; z < this.rawtext.Length - maximumCharactersToExtract; z += shortestSearchTermLength)
{
thisscore = score(z, z + (maximumCharactersToExtract));
if (thisscore == 0) continue;
if (thisscore > BestScore)
{
BestScore = thisscore;
BestWindow = this.rawtext.Substring(z, maximumCharactersToExtract);
//Best window to get if the future score is equal
//I.E. put the terms in the center of the window if
//the score is equal
BestWindowStartPos = z + (maximumCharactersToExtract / 2);
}
//If it's equal to the last and we're positioned over
//the best spot (terms in center) then capture that
if (thisscore == BestScore && z == BestWindowStartPos)
{
BestWindow = this.rawtext.Substring(z, maximumCharactersToExtract);
}
}
if (this.flattenExtract)
this.extract = "..." + BestWindow.Trim().Replace("\r", "").Replace("\n", "").Replace("\t", "") + "...";//case 1593 added tab character removal
else
this.extract = "..." + BestWindow.Trim() + "...";
}
//========================================================================
#endregion
}
#endregion Xtract
#endregion
#region ProcessKeywords into Database
//Class to hold process input parameters
//also used for getting summary search results
public class SearchIndexProcessObjectParameters
{
public long TranslationId { get; set; }
public long ObjectId { get; set; }
public SockType SockType { get; set; }
public List<string> Words { get; set; }
public SearchIndexProcessObjectParameters(long translationId, long objectID, SockType aType)
{
Words = new List<string>();
TranslationId = translationId;
ObjectId = objectID;
SockType = aType;
}
//format used for getsummmary by biz objects
public SearchIndexProcessObjectParameters()
{
Words = new List<string>();
TranslationId = 0;
ObjectId = 0;
SockType = 0;
}
public SearchIndexProcessObjectParameters AddText(string s)
{
if (!string.IsNullOrWhiteSpace(s))
{
Words.Add(s);
}
return this;
}
public SearchIndexProcessObjectParameters AddText(long l)
{
Words.Add(l.ToString());
return this;
}
// public SearchIndexProcessObjectParameters AddText(decimal? d)
// {
// if (d != null)
// Words.Add(d.ToString());
// return this;
// }
public SearchIndexProcessObjectParameters AddText(List<string> lWords)
{
if (lWords != null)
{
foreach (string s in lWords)
{
if (!string.IsNullOrWhiteSpace(s))
{
Words.Add(s);
}
}
}
return this;
}
public SearchIndexProcessObjectParameters AddCustomFields(string jsonString)
{
//Extract the text from custom fields json fragment as an array of strings and add it here
AddText(JsonUtil.GetCustomFieldsAsStringArrayForSearchIndexing(jsonString));
return this;
}
}
public static async Task ProcessNewObjectKeywordsAsync(SearchIndexProcessObjectParameters searchIndexObjectParameters)
{
await ProcessKeywordsAsync(searchIndexObjectParameters, true);
}
public static async Task ProcessUpdatedObjectKeywordsAsync(SearchIndexProcessObjectParameters searchIndexObjectParameters)
{
await ProcessKeywordsAsync(searchIndexObjectParameters, false);
}
public static async Task ProcessDeletedObjectKeywordsAsync(long objectID, SockType aType, AyContext ct)
{
//Be careful in future, if you put ToString at the end of each object in the string interpolation
//npgsql driver will assume it's a string and put quotes around it triggering an error that a string can't be compared to an int
await ct.Database.ExecuteSqlInterpolatedAsync($"delete from asearchkey where objectid={objectID} and aType={(int)aType}");
//nothing to save here, it's a direct command already executed
}
/// <summary>
/// Process the keywords into the dictionary
/// </summary>
private static async Task ProcessKeywordsAsync(SearchIndexProcessObjectParameters p, bool newRecord)
{
// #if (DEBUG)
// if (!p.SockType.HasAttribute(typeof(CoreBizObjectAttribute)))
// throw new System.NotSupportedException($"Search::ProcessKeywords - Invalid type presented {p.SockType}");
// #endif
List<string> KeyWordList = await BreakAsync(p.TranslationId, p.Words);
if (KeyWordList.Count == 0) return;
//call stored procedure to do the work right at the server (fastest method by far)
using (AyContext ct = ServiceProviderProvider.DBContext)
await ct.Database.ExecuteSqlInterpolatedAsync($"call aydosearchindex({KeyWordList},{p.ObjectId},{p.SockType},{!newRecord})");
return;
}//eoc
#endregion
#region Breaker
public enum TokenTypes
{ Nothing, Separator, CJK, Latin };
/// <summary>
/// Take an array of strings and
/// return a single string
/// containing unique only, lowercase comma delimited
/// keywords suitable for passing to a
/// stored procedure or other function
///
/// Use Translation setting CJKIndex=true to handle Chinese, Japanese, Korean etc
/// (languages with no easily identifiable word boundaries as in english)
/// </summary>
/// <returns>List of strings</returns>
internal static async Task<List<string>> BreakAsync(long translationId, List<string> textStrings)
{
return await BreakCoreAsync(translationId, false, textStrings);
}
/// <summary>
///
/// </summary>
internal static async Task<List<string>> BreakAsync(long translationId, string textString)
{
List<string> textStrings = new List<string>(1);
textStrings.Add(textString);
return await BreakCoreAsync(translationId, false, textStrings);
}
/// <summary>
/// Used to Process users search phrase and preserve wild
/// cards entered
/// </summary>
internal static async Task<List<string>> BreakSearchPhraseAsync(long translationId, string searchPhrase)
{
List<string> textStrings = new List<string>();
textStrings.Add(searchPhrase);
//note: we want stopwords if this is a search phrase break because they might type "some" wanting awesome but some is a stopword so..
return await BreakCoreAsync(translationId, true, textStrings, true);
}
internal static async Task<List<string>> BreakCoreAsync(long translationId, bool KeepWildCards, List<string> textStrings, bool ignoreStopWords = false)
{
//For stopwords and CJKIndex flag value
var translationWordBreakData = await SearchTranslationWordBreakDataCache.GetWordBreakData(translationId);
int MAXWORDLENGTH = 255;
int MINWORDLENGTH = 2;//A word isn't a word unless it's got at least two characters in it
StringBuilder sbResults = new StringBuilder();
//List to temporarily hold parsed words
//used to easily ensure unique words only
List<string> tempParsedWords = new List<string>();
StringBuilder sb = new StringBuilder();
StringBuilder sbWord = new StringBuilder();
List<string> ReturnList = new List<string>();
//Loop through each of the passed in strings
foreach (string s in textStrings)
{
if (s == null || s == "") continue;
//get all the characters in a unicode compliant manner...
TextElementEnumerator t = StringInfo.GetTextElementEnumerator(s);
//start at the top
t.Reset();
TokenTypes LastToken = TokenTypes.Nothing;
//Used by CJK
bool BasicLatinBlock = true;
//Process each "character" (text element,glyph whatever) in the
//current string
while (t.MoveNext())
{
//get it as a character
char c = t.GetTextElement()[0];
if (!translationWordBreakData.CJKIndex)
{
#region regular tokenizer
//Is it a token we want to include?
//Or a wildcard character
if (char.IsLetterOrDigit(c) || (KeepWildCards && c == '%'))
{
#region Include token
//All latin text is converted to lower case
c = char.ToLower(c, System.Globalization.CultureInfo.CurrentCulture);
//Do we already have a word?
if (sbWord.Length > 0)
{
//Maybe we need to flush this word into the word list
//if we're over the word length limit
if (sbWord.Length >= MAXWORDLENGTH)
{
//flush away...
if (!tempParsedWords.Contains(sbWord.ToString()))
{
tempParsedWords.Add(sbWord.ToString());
}
sbWord.Length = 0;
sbWord.Append(c);
LastToken = TokenTypes.Latin;
continue;
}
}
//append character and go on to next one
sbWord.Append(c);
LastToken = TokenTypes.Latin;
continue;
#endregion
}
else
{
#region Word Boundary token
LastToken = TokenTypes.Separator;
if (sbWord.Length > 0)
{
//flush away...
if (!tempParsedWords.Contains(sbWord.ToString()))
{
tempParsedWords.Add(sbWord.ToString());
}
sbWord.Length = 0;
continue;
}
#endregion
}
#endregion
}
else
{
#region CJK Tokenizer
//Is it a basic latin charater? (ascii basically)
//see: http://www.unicode.org/charts/index.html
//and here for a funky online viewer:
//http://www.fileformat.info/info/unicode/block/index.htm
//we need to know this so that regular english text
//within cjk text gets properly indexed as whole words
BasicLatinBlock = false;
if ((int)c < 256) BasicLatinBlock = true;
if (BasicLatinBlock)
{
//Is it a token we want to include?
if (char.IsLetterOrDigit(c) || (KeepWildCards && c == '%'))
{
#region Latin Include token
//All latin text is converted to lower case
c = char.ToLower(c, System.Globalization.CultureInfo.CurrentCulture);
//Do we already have a word?
if (sbWord.Length > 0)
{
//Maybe we need to flush this word into the word list
//if we're over the word length limit or we are going from
//CJK to latin
if (LastToken == TokenTypes.CJK || sbWord.Length >= MAXWORDLENGTH)
{
//flush away...
if (!tempParsedWords.Contains(sbWord.ToString()))
{
tempParsedWords.Add(sbWord.ToString());
}
sbWord.Length = 0;
sbWord.Append(c);
LastToken = TokenTypes.Latin;
continue;
}
}
//append character and go on to next one
sbWord.Append(c);
LastToken = TokenTypes.Latin;
continue;
#endregion
}
else
{
#region Latin Word Boundary token
LastToken = TokenTypes.Separator;
if (sbWord.Length > 0)
{
//flush away...
if (!tempParsedWords.Contains(sbWord.ToString()))
{
tempParsedWords.Add(sbWord.ToString());
}
sbWord.Length = 0;
continue;
}
#endregion
}
}
else//CJK character
{
if (char.IsLetter(c) || (KeepWildCards && c == '%'))
{
#region CJK Include token
//Do we already have a word?
if (sbWord.Length > 0)
{
//Maybe we need to flush this word into the word list
//if we're over the word length limit or we are going from
//latin TO CJK
if (LastToken == TokenTypes.Latin || sbWord.Length >= MAXWORDLENGTH)
{
//flush away...
if (!tempParsedWords.Contains(sbWord.ToString()))
{
tempParsedWords.Add(sbWord.ToString());
}
sbWord.Length = 0;
sbWord.Append(c);
LastToken = TokenTypes.CJK;
continue;
}
if (LastToken == TokenTypes.CJK)
{
//we're here because there is more than zero characters already stored
//and the last was CJK so we need append current character
//and flush the resultant 2 character n-gram
sbWord.Append(c);
System.Diagnostics.Debug.Assert(sbWord.Length == 2);
//flush away...
if (!tempParsedWords.Contains(sbWord.ToString()))
{
tempParsedWords.Add(sbWord.ToString());
}
sbWord.Length = 0;
sbWord.Append(c);
LastToken = TokenTypes.CJK;
continue;
}
}
//append character and go on to next one
sbWord.Append(c);
LastToken = TokenTypes.CJK;
continue;
#endregion
}
else
{
#region CJK Word Boundary token
LastToken = TokenTypes.Separator;
if (sbWord.Length > 0)
{
//flush away...
if (!tempParsedWords.Contains(sbWord.ToString()))
{
tempParsedWords.Add(sbWord.ToString());
}
sbWord.Length = 0;
continue;
}
#endregion
}
}
#endregion
}
}
//Flush out the last word
if (sbWord.Length > 0)
{
//flush away...
if (!tempParsedWords.Contains(sbWord.ToString()))
{
tempParsedWords.Add(sbWord.ToString());
}
sbWord.Length = 0;
}
}
//bail early if there is nothing indexed
if (tempParsedWords.Count == 0) return ReturnList;
//Make a return string array
//from the word list
foreach (string s in tempParsedWords)
{
//Filter out short words if we are breaking for indexing
//but keep them if they are part of a wildcard search phrase
if (s.Length >= MINWORDLENGTH || (KeepWildCards && s.Contains('%')))
{
if (ignoreStopWords)
{
//breaking of search phrase
ReturnList.Add(s);
}
else
{
//Add only non stopwords - regular breaking of object for dictionary entry
if (!translationWordBreakData.StopWords.Contains(s))
{
ReturnList.Add(s);
}
}
}
}
//sometimes all the results are stop words so you end up here with nothing
return ReturnList;
}
#endregion
}//eoc
}//eons

View File

@@ -0,0 +1,77 @@
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using System.Collections.Generic;
using Sockeye.Util;
using Sockeye.Models;
namespace Sockeye.Biz
{
public class SearchTranslationWordBreakDataCache
{
static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);
private static Dictionary<long, TranslationWordBreakingData> theCache = new Dictionary<long, TranslationWordBreakingData>();
public SearchTranslationWordBreakDataCache() { }
public static async Task<TranslationWordBreakingData> GetWordBreakData(long id)
{
await semaphoreSlim.WaitAsync();
try
{
if (!theCache.ContainsKey(id))
theCache.Add(id, await GetTranslationSearchDataAsync(id));
return theCache.First(z=>z.Key==id).Value;
}
finally
{
semaphoreSlim.Release();
}
}
internal static async Task<TranslationWordBreakingData> GetTranslationSearchDataAsync(long translationId)
{
TranslationWordBreakingData LSD = new TranslationWordBreakingData();
using (AyContext ct = ServiceProviderProvider.DBContext)
{
//Get stopwords
//Validate translation id, if not right then use default instead
var Param = new List<string>();
translationId = await TranslationBiz.ReturnSpecifiedTranslationIdIfExistsOrDefaultTranslationId(translationId, ct);
Param.Add("StopWords1");
Param.Add("StopWords2");
Param.Add("StopWords3");
Param.Add("StopWords4");
Param.Add("StopWords5");
Param.Add("StopWords6");
Param.Add("StopWords7");
var Stops = await TranslationBiz.GetSubsetStaticAsync(Param, translationId);
foreach (KeyValuePair<string, string> kvp in Stops)
{
//Each stopwords translation key is a space delimited list of words and in the case of an empty local string (i.e. StopWords7) it's value is a single question mark
if (kvp.Value != "?")
{
LSD.StopWords.AddRange(kvp.Value.Split(" "));
}
}
LSD.CJKIndex = await TranslationBiz.GetCJKIndexAsync(translationId, ct);
}
return LSD;
}
//Class to hold relevant translation data for breaking text
public class TranslationWordBreakingData
{
public bool CJKIndex { get; set; }
public List<string> StopWords { get; set; }
public TranslationWordBreakingData()
{
CJKIndex = false;
StopWords = new List<string>();
}
}
}//eoc
}//eons

View File

@@ -0,0 +1,25 @@
using System;
namespace Sockeye.Biz
{
/// <summary>
/// Days of week
/// </summary>
[Flags]
public enum SockDaysOfWeek : int
{
//https://stackoverflow.com/questions/8447/what-does-the-flags-enum-attribute-mean-in-c
//MAX 31 (2147483647)!!! or will overflow int and needs to be turned into a long
//Must be a power of two: https://en.wikipedia.org/wiki/Power_of_two
//bitwise selection of days of week
//https://stackoverflow.com/a/24174625/8939
Monday = 1,
Tuesday = 2,
Wednesday = 4,
Thursday = 8,
Friday = 16,
Saturday = 32,
Sunday = 64
}
}

37
server/biz/SockEvent.cs Normal file
View File

@@ -0,0 +1,37 @@
namespace Sockeye.Biz
{
/// <summary>
/// All Sockeye event types
/// Used for central biz logging and notification
/// </summary>
public enum SockEvent : int
{
//common events
Deleted = 0,
Created = 1,
Retrieved = 2,
Modified = 3,
//specific events
AttachmentCreate = 4,
AttachmentDelete = 5,
AttachmentDownload = 6,
LicenseFetch = 7,
LicenseTrialRequest = 8,
ServerStateChange = 9,
SeedDatabase = 10,
AttachmentModified = 11,
EraseAllData = 12,
ResetSerial = 13,
UtilityFileDownload = 14,
DirectSMTP = 15//NotifyEventDirectSMTPMessage key can work for this too
//NEW ITEMS REQUIRE translation KEYS and update CLIENT sock-history.vue code in eventypes list and translation fetcher
}
}//eons

85
server/biz/SockType.cs Normal file
View File

@@ -0,0 +1,85 @@
namespace Sockeye.Biz
{
/// <summary>
/// All Sockeye types and their attributes indicating what features that type will support (tagging, attachments etc)
/// </summary>
public enum SockType : int
{
//COREBIZOBJECT attribute must be set on objects that are:
//Attachable objects can have attachments,
//wikiable objects can have a wiki
//reviewable objects can have a review which is basically the same as a Reminder but with an object attached (was follow up schedmarker in v7)
//PIckList-able (has picklist template)
//Pretty much everything that represents some kind of real world object is wikiable or attachable as long as it has an ID and a type
//exceptions would be utility type objects like dataListFilter, dataListColumn, formcustom etc that are not
//NOTE: NEW CORE OBJECTS - All areas of server AND CLIENT code that require adding any new core objects have been tagged with the following comment:
//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
NoType = 0,
Global = 1,
FormUserOptions = 2,
[CoreBizObject, ReportableBizObject]
User = 3,
ServerState = 4,
LogFile = 6,
PickListTemplate = 7,
[CoreBizObject, ReportableBizObject, ImportableBizObject]
Customer = 8,
ServerJob = 9,
ServerMetrics = 12,
Translation = 13,
UserOptions = 14,
[CoreBizObject, ReportableBizObject, ImportableBizObject]
HeadOffice = 15,
FileAttachment = 17,
DataListSavedFilter = 18,
FormCustom = 19,
GlobalOps = 47,//really only used for rights, not an object type of any kind
BizMetrics = 48,//deprecate? Not used for anything as of nov 2020
Backup = 49,
Notification = 50,
NotifySubscription = 51,
[CoreBizObject, ReportableBizObject]
Reminder = 52,
OpsNotificationSettings = 56,
Report = 57,
DashboardView = 58,
[CoreBizObject, ReportableBizObject]
CustomerNote = 59,
[CoreBizObject, ReportableBizObject]
Memo = 60,
[CoreBizObject, ReportableBizObject]
Review = 61,
DataListColumnView = 68,
CustomerNotifySubscription = 84,//proxy subs for customers
Integration = 92 //3rd party or add-on integration data store
//NOTE: New objects added here need to also be added to the following classes:
//Sockeye.Biz.BizObjectExistsInDatabase
//Sockeye.Biz.BizObjectFactory
//Sockeye.Biz.BizRoles
//Sockeye.Biz.BizObjectNameFetcherDIRECT
//and in the CLIENT in socktype.js
//and their model needs to have the ICoreBizObjectModel interface
//and need TRANSLATION KEYS because any type could show in the event log at the client end
//AND QBI mirrors this too
}
}//eons

113
server/biz/SockTypeId.cs Normal file
View File

@@ -0,0 +1,113 @@
using System;
using Sockeye.Models;
namespace Sockeye.Biz
{
public class SockTypeId : System.Object
{
private long _id;
private SockType _sockType;
public long ObjectId
{
get
{
return _id;
}
}
public SockType SockType
{
get
{
return _sockType;
}
}
public int ATypeAsInt
{
get
{
return (int)_sockType;
}
}
public SockTypeId(SockType aType, long Id)
{
_id = Id;
_sockType = aType;
}
[Newtonsoft.Json.JsonConstructor]
public SockTypeId(string sAType, string sId)
{
_id = long.Parse(sId);
int nType = int.Parse(sAType);
if (!SockTypeExists(nType))
_sockType = SockType.NoType;
else
_sockType = (SockType)Enum.Parse(typeof(SockType), sAType);
}
public bool Equals(SockTypeId x, SockTypeId y)
{
//Check whether the compared objects reference the same data.
if (Object.ReferenceEquals(x, y)) return true;
//Check whether any of the compared objects is null.
if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null))
return false;
//Check whether the products' properties are equal.
return x.ObjectId == y.ObjectId && x.SockType == y.SockType;
}
public bool IsEmpty
{
get
{
return (_sockType == SockType.NoType) || (_id == 0);
}
}
/// <summary>
/// Check if the numeric or name type value is an actual enum value
/// </summary>
/// <param name="enumNumber"></param>
/// <returns></returns>
public bool SockTypeExists(int enumNumber)
{
return Enum.IsDefined(typeof(SockType), enumNumber);
}
//Custom attribute checking
/// <summary>
/// Is object a core biz object
/// </summary>
/// <returns></returns>
public bool IsCoreBizObject
{
get
{
return this.SockType.HasAttribute(typeof(CoreBizObjectAttribute));
}
}
}//eoc
}//eons

347
server/biz/TagBiz.cs Normal file
View File

@@ -0,0 +1,347 @@
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
using System.Collections.Generic;
using System;
using System.Linq;
namespace Sockeye.Biz
{
internal class TagBiz : BizObject//, IJobObject
{
internal TagBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = SockType.NoType;
}
internal static TagBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new TagBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new TagBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
#region Utilities
/////////////////////////////////////
//UTILITIES
//
//clean up tags from client submission
//remove dupes, substitute dashes for spaces, lowercase and shorten if exceed 255 chars
//and sorts before returning to ensure consistent ordering
public static List<string> NormalizeTags(List<string> inTags)
{
if (inTags == null || inTags.Count == 0) return inTags;
List<string> outTags = new List<string>();
foreach (var tag in inTags)
outTags.Add(NormalizeTag(tag));
outTags.Sort();
return outTags.Distinct().ToList();
}
public static string NormalizeTag(string inObj)
{
if (string.IsNullOrWhiteSpace(inObj)) return null;
//Must be lowercase per rules
//This may be naive when we get international cust omers but for now supporting utf-8 and it appears it's safe to do this with unicode
inObj = inObj.ToLower(System.Globalization.CultureInfo.CurrentCulture);
//No spaces in tags, replace with dashes
inObj = inObj.Replace(" ", "-");
//Remove multiple dash sequences
inObj = System.Text.RegularExpressions.Regex.Replace(inObj, "-+", "-");
//Ensure doesn't start or end with a dash
inObj = inObj.Trim('-');
//No longer than 255 characters
inObj = StringUtil.MaxLength(inObj, 255);
return inObj;
}
public static async Task ProcessDeleteTagsInRepositoryAsync(AyContext ct, List<string> deleteTags)
{
if (deleteTags.Count == 0) return;
var existing = await ct.Tag.Where(x => deleteTags.Contains(x.Name)).ToListAsync();
foreach (string s in deleteTags)
{
var t = existing.FirstOrDefault(x => x.Name == s);
if (t != null)
{
if (t.RefCount < 2)//catch any that fell through the cracks and are maybe zero or negative event
ct.Remove(t);
else
t.RefCount -= 1;
}
}
await ct.SaveChangesAsync();
#region OLD SLOW METHOD FOR REFERENCE IN CASE CONCURRENCY EXCEPTIONS
// foreach (string s in deleteTags)
// {
// bool bDone = false;
// //Keep on trying until there is success
// //this allows for concurrency issues
// //I considered a circuit breaker / timeout here, but theoretically it should not be an issue
// //at some point it should not be a concurrency issue anymore
// //And this is not critical functionality requiring a transaction and locking
// do
// {
// //START: Get tag word and concurrency token and count
// var ExistingTag = await ct.Tag.FirstOrDefaultAsync(z => z.Name == s);
// //if not present, then nothing to do
// if (ExistingTag != null)
// {
// //No longer needed?
// if (ExistingTag.RefCount == 1)
// {
// ct.Remove(ExistingTag);
// }
// else
// {
// //Decrement the refcount
// ExistingTag.RefCount -= 1;
// }
// try
// {
// await ct.SaveChangesAsync();
// bDone = true;
// }
// catch (Exception ex) when (ex is DbUpdateConcurrencyException)//allow for possible other types
// {
// //allow others to flow past
// // string sss = ex.ToString();
// }
// }
// else
// {
// bDone = true;
// }
// } while (bDone == false);
// }
#endregion old slow method
}
public static async Task ProcessUpdateTagsInRepositoryAsync(AyContext ct, List<string> newTags, List<string> originalTags = null)
{
//todo: Recode this as a procedure like search indexing or at least a direct sql call
//just in case no new tags are present which could mean a user removed all tags from a record so this
//needs to proceed with the code below even if newTags is null as long as originalTags isn't also null
if (newTags == null) newTags = new List<string>();
if (originalTags == null) originalTags = new List<string>();
if (newTags.Count == 0 && originalTags.Count == 0) return;
List<string> deleteTags = new List<string>();
List<string> addTags = new List<string>();
if (originalTags != null)
{
//Update
//This logic to only come up with CHANGES, if the item is in both lists then it will disappear and not need to be dealt with as it's refcount is unchanged in this operation
//testing will validate it
deleteTags = originalTags.Except(newTags).ToList();
addTags = newTags.Except(originalTags).ToList();
}
else
{
//Add
addTags = newTags;
}
#region OLD SLOW METHOD FOR REFERENCE IN CASE CONCURRENCY EXCEPTIONS
// //ADD / INCREMENT TAGS
// //one by one method
// foreach (string s in addTags)
// {
// bool bDone = false;
// //Keep on trying until there is success
// //this allows for concurrency issues
// //I considered a circuit breaker / timeout here, but theoretically it should not be an issue
// //at some point it should not be a concurrency issue anymore
// do
// {
// //START: Get tag word and concurrency token and count
// var ExistingTag = await ct.Tag.FirstOrDefaultAsync(z => z.Name == s);
// //if not present, then add it with a count of 0
// if (ExistingTag == null)
// {
// await ct.Tag.AddAsync(new Tag() { Name = s, RefCount = 1 });
// }
// else
// {
// //Update the refcount
// ExistingTag.RefCount += 1;
// }
// try
// {
// await ct.SaveChangesAsync();
// bDone = true;
// }
// catch (Exception ex) when (ex is DbUpdateConcurrencyException)//this allows for other types
// {
// Console.WriteLine("TagBiz::Exception udring update tags");
// //allow others to flow past
// //string sss = ex.ToString();
// }
// } while (bDone == false);
// }
#endregion old slow method
//ADD / INCREMENT TAGS
var existing = await ct.Tag.Where(x => addTags.Contains(x.Name)).ToListAsync();
foreach (string s in addTags)
{
var t = existing.FirstOrDefault(x => x.Name == s);
if (t != null)
{
t.RefCount += 1;
}
else
{
ct.Tag.Add(new Tag() { Name = s, RefCount = 1 });
}
}
await ct.SaveChangesAsync();
//DELETE TAGS
await ProcessDeleteTagsInRepositoryAsync(ct, deleteTags);
}
//Pick list for driving pick list route
//going with contains for now as I think it's more useful in the long run and still captures startswith intent by user
public static async Task<List<string>> TagListFilteredAsync(AyContext ct, string q)
{
//This path is intended for internal use and accepts that there may not be a filter specified
//however the client will always require a filter to display a tag list for choosing from
if (string.IsNullOrWhiteSpace(q))
{
return await ct.Tag.OrderBy(z => z.Name)
.Select(z => z.Name)
.AsNoTracking()
.ToListAsync();
}
else
{
q = NormalizeTag(q);
return await ct.Tag
.Where(z => z.Name.Contains(q))
.OrderBy(z => z.Name)
.Select(z => z.Name)
.Take(25)
.AsNoTracking()
.ToListAsync();
}
}
//Cloud list
public static async Task<List<TagCloudItem>> CloudListFilteredAsync(AyContext ct, string q)
{
//This path is intended for internal use and accepts that there may not be a filter specified
//however the client will always require a filter to display a tag list for choosing from
if (string.IsNullOrWhiteSpace(q))
{
return await ct.Tag.OrderByDescending(z => z.RefCount).Select(z => new TagCloudItem() { Name = z.Name, RefCount = z.RefCount }).AsNoTracking().ToListAsync();
}
else
{
q = NormalizeTag(q);
//TODO: Use the EF CORE TAKE method to restrict the results to a maximum limit
//however need to ensure it doesn't balk when the limit is higher than the number of results (probably not but test that)
return await ct.Tag
.Where(z => z.Name.Contains(q))
.OrderByDescending(z => z.RefCount)
.Select(z => new TagCloudItem() { Name = z.Name, RefCount = z.RefCount })
.AsNoTracking()
.ToListAsync();
}
}
public class TagCloudItem
{
public long RefCount { get; set; }
public string Name { get; set; }
}
/// <summary>
/// Process batch tag operation
/// </summary>
/// <returns>true if object needs to be saved or false if no changes were made</returns>
internal static bool ProcessBatchTagOperation(List<string> tagCollection, string tag, string toTag, JobSubType subType)
{
switch (subType)
{
case JobSubType.TagAddAny:
case JobSubType.TagAdd:
if (!tagCollection.Contains(tag))
{
tagCollection.Add(tag);
return true;
}
return false;
case JobSubType.TagRemoveAny:
case JobSubType.TagRemove:
return tagCollection.Remove(tag);
case JobSubType.TagReplaceAny:
case JobSubType.TagReplace:
int index = tagCollection.IndexOf(tag);
if (index != -1)
{
tagCollection[index] = toTag;
return true;
}
return false;
default:
throw new System.ArgumentOutOfRangeException($"ProcessBatchTagOperation -> Invalid job Subtype{subType}");
}
}
#endregion utilities
////////////////////////////////////////////////////////////////////////////////////////////////
//JOB / OPERATIONS
//
//Other job handlers here...
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,690 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using Microsoft.Extensions.Logging;
namespace Sockeye.Biz
{
internal class TranslationBiz : BizObject
{
internal TranslationBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles userRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = userRoles;
BizType = SockType.Translation;
}
internal static TranslationBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new TranslationBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new TranslationBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExistsAsync(long id)
{
return await ct.Translation.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<Translation> PutAsync(Translation putObject)
{
//todo: update to use new PUT methodology?
Translation dbObject = await ct.Translation.Include(z => z.TranslationItems).SingleOrDefaultAsync(z => z.Id == putObject.Id);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
//No tags and no validation of prior state required so no snapshot required
CopyObject.Copy(putObject, dbObject, "Id");//note: won't update the child collection has to be done independently
foreach (TranslationItem ti in putObject.TranslationItems)
{
dbObject.TranslationItems.Where(z => z.Id == ti.Id).First().Display = ti.Display;
}
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, SockEvent.Modified), ct);
return dbObject;
}
// internal async Task<bool> PutTranslationItemDisplayTextAsync(TranslationItem dbObj, NewTextIdConcurrencyTokenItem inObj, Translation dbParent)
// {
// if (dbParent.Stock == true)
// {
// AddError(ApiErrorCode.INVALID_OPERATION, "object", "TranslationItem is from a Stock translation and cannot be modified");
// return false;
// }
// //Replace the db object with the PUT object
// //CopyObject.Copy(inObj, dbObj, "Id");
// dbObj.Display = inObj.NewText;
// //Set "original" value of concurrency token to input token
// //this will allow EF to check it out
// ct.Entry(dbObj).OriginalValues["Concurrency"] = inObj.Concurrency;
// //Only thing to validate is if it has data at all in it
// if (string.IsNullOrWhiteSpace(inObj.NewText))
// AddError(ApiErrorCode.VALIDATION_REQUIRED, "Display (NewText)");
// if (HasErrors)
// return false;
// await ct.SaveChangesAsync();
// //Log
// await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbParent.Id, SockType.Translation, AyaEvent.Modified), ct);
// return true;
// }
internal async Task<bool> PutTranslationItemsDisplayTextAsync(List<NewTextIdConcurrencyTokenItem> inObj, Translation dbParent)
{
if (dbParent.Stock == true)
{
AddError(ApiErrorCode.INVALID_OPERATION, "object", "TranslationItem is from a Stock translation and cannot be modified");
return false;
}
foreach (NewTextIdConcurrencyTokenItem tit in inObj)
{
var titem = await ct.TranslationItem.SingleOrDefaultAsync(z => z.Id == tit.Id);
if (titem == null)
{
AddError(ApiErrorCode.NOT_FOUND, $"Translation item ID {tit.Id}");
return false;
}
//Replace the db object with the PUT object
//CopyObject.Copy(inObj, dbObj, "Id");
titem.Display = tit.NewText;
//Set "original" value of concurrency token to input token
//this will allow EF to check it out
ct.Entry(titem).OriginalValues["Concurrency"] = tit.Concurrency;
//Only thing to validate is if it has data at all in it
if (string.IsNullOrWhiteSpace(tit.NewText))
AddError(ApiErrorCode.VALIDATION_REQUIRED, $"Display (NewText) for Id: {tit.Id}");
}
if (HasErrors)
return false;
await ct.SaveChangesAsync();
//Log
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbParent.Id, SockType.Translation, SockEvent.Modified), ct);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DUPLICATE
//
internal async Task<Translation> DuplicateAsync(long id)
{
Translation dbObject = await ct.Translation.Include(z => z.TranslationItems).SingleOrDefaultAsync(z => z.Id == id);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
Translation newObject = new Translation();
//CopyObject.Copy(dbObject, newObject, "Id, Salt, Login, Password, CurrentAuthToken, DlKey, DlKeyExpire, Wiki, Serial");
string newUniqueName = string.Empty;
bool NotUnique = true;
long l = 1;
do
{
newUniqueName = Util.StringUtil.UniqueNameBuilder(dbObject.Name, l++, 255);
NotUnique = await ct.Translation.AnyAsync(z => z.Name == newUniqueName);
} while (NotUnique);
newObject.Name = newUniqueName;
newObject.BaseLanguage = dbObject.BaseLanguage;
newObject.Stock = false;
newObject.CjkIndex = false;
foreach (TranslationItem i in dbObject.TranslationItems)
{
newObject.TranslationItems.Add(new TranslationItem() { Key = i.Key, Display = i.Display });
}
newObject.Id = 0;
newObject.Concurrency = 0;
await ct.Translation.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, SockEvent.Created), ct);
// await SearchIndexAsync(newObject, true);
return newObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//IMPORT
//
internal async Task<bool> ImportAsync(JObject o)
{
Translation t = new Translation();
var proposedName = (string)o["Name"];
string newUniqueName = proposedName;
bool NotUnique = true;
long l = 1;
do
{
NotUnique = await ct.Translation.AnyAsync(z => z.Name == newUniqueName);
if (NotUnique)
newUniqueName = Util.StringUtil.UniqueNameBuilder(proposedName, l++, 255);
} while (NotUnique);
t.Name = newUniqueName;
t.CjkIndex = (bool)o["CjkIndex"];
t.Stock = false;
Translation sample = await ct.Translation.Include(z => z.TranslationItems).SingleOrDefaultAsync(z => z.Id == 1);
int ExpectedKeyCount = sample.TranslationItems.Count();
JArray tItems = (JArray)o["TranslationItems"];
if (tItems.Count() < ExpectedKeyCount)
{
AddError(ApiErrorCode.VALIDATION_FAILED, null, $"TranslationItems incomplete, expected {ExpectedKeyCount} but found {tItems.Count()}");
return false;
}
foreach (JObject j in tItems)
{
var key = (string)j["Key"];
var display = (string)j["Display"];
if (null == sample.TranslationItems.Where(z => z.Key == key).FirstOrDefault())
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, null, $"TranslationItems key {key} is not valid");
return false;
}
t.TranslationItems.Add(new TranslationItem { Key = key, Display = display });
}
await ct.Translation.AddAsync(t);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, t.Id, BizType, SockEvent.Created), ct);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// GET
//Get entire translation
internal async Task<Translation> GetAsync(long fetchId)
{
//This is simple so nothing more here, but often will be copying to a different output object or some other ops
return await ct.Translation.Include(z => z.TranslationItems).SingleOrDefaultAsync(z => z.Id == fetchId);
}
//get list (simple non-paged)
internal async Task<List<NameIdItem>> GetTranslationListAsync()
{
List<NameIdItem> l = new List<NameIdItem>();
l = await ct.Translation
.AsNoTracking()
.OrderBy(z => z.Name)
.Select(z => new NameIdItem()
{
Id = z.Id,
Name = z.Name
}).ToListAsync();
return l;
}
#if (DEBUG)
internal async Task<Sockeye.Api.Controllers.TranslationController.TranslationCoverageInfo> TranslationKeyCoverageAsync()
{
Sockeye.Api.Controllers.TranslationController.TranslationCoverageInfo L = new Sockeye.Api.Controllers.TranslationController.TranslationCoverageInfo();
L.RequestedKeys = ServerBootConfig.TranslationKeysRequested;
L.RequestedKeys.Sort();
var AllKeys = await GetKeyListAsync();
foreach (string StockKey in AllKeys)
{
if (!L.RequestedKeys.Contains(StockKey))
{
L.NotRequestedKeys.Add(StockKey);
}
}
L.NotRequestedKeys.Sort();
L.RequestedKeyCount = L.RequestedKeys.Count;
L.NotRequestedKeyCount = L.NotRequestedKeys.Count;
return L;
}
//Track requests for keys so we can determine which are being used and which are not
//TODO: Ideally this should be paired with tests that either directly request each key that are def. being used
//or the UI needs to be tested in a way that triggers every key to be used even errors etc
internal static void TrackRequestedKey(string key)
{
if (!ServerBootConfig.TranslationKeysRequested.Contains(key))
ServerBootConfig.TranslationKeysRequested.Add(key);
}
internal static void TrackRequestedKey(List<string> keys)
{
foreach (string Key in keys)
{
if (!ServerBootConfig.TranslationKeysRequested.Contains(Key))
ServerBootConfig.TranslationKeysRequested.Add(Key);
}
}
#endif
/////////////////////////////////////////////////////////////////
// Get subset for currently logged in user's translation id
// Standard used by translationcontroller for logged in user
//
//
internal async Task<List<KeyValuePair<string, string>>> GetSubsetAsync(List<string> param)
{
#if (DEBUG)
TrackRequestedKey(param);
#endif
var ret = await ct.TranslationItem.Where(z => z.TranslationId == UserTranslationId && param.Contains(z.Key)).AsNoTracking().ToDictionaryAsync(z => z.Key, z => z.Display);
if (ret.Count != param.Count)
{
ILogger log = Sockeye.Util.ApplicationLogging.CreateLogger<TranslationBiz>();
var missingItems = param.Where(p => ret.All(p2 => p2.Key != p)).ToList();
var duplicateItems = param.GroupBy(x => x)
.Where(g => g.Count() > 1)
.Select(y => y.Key)
.ToList();
if (missingItems.Count > 0)
log.LogWarning($"Non existant translation keys requested: {string.Join(",", missingItems)}");
if (duplicateItems.Count > 0)
log.LogWarning($"Duplicate translation keys requested: {string.Join(",", duplicateItems)}");
}
return ret.ToList();
}
/////////////////////////////////////////////////////////////////
// Get subset for specified translation ID
// called from controller and Used when user is not logged in
// e.g. when resetting their password
// ## NOTE: NO other use for this other than the reset password at this point
internal static async Task<List<KeyValuePair<string, string>>> GetSpecifiedTranslationSubsetStaticAsync(List<string> param, long translationId)
{
#if (DEBUG)
TrackRequestedKey(param);
#endif
using (AyContext ct = ServiceProviderProvider.DBContext)
{
if (!await ct.Translation.AnyAsync(e => e.Id == translationId))
translationId = ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID;
var ret = await ct.TranslationItem.Where(z => z.TranslationId == translationId && param.Contains(z.Key)).AsNoTracking().ToDictionaryAsync(z => z.Key, z => z.Display);
return ret.ToList();
}
}
/////////////////////////////////////////////////////////////////
// Get subset for specified translation ID statically
// called from internal code differs from
// GetSpecifiedTranslationSubsetStaticAsync above only in return signature
// and used for internal classes to call
//
internal static async Task<Dictionary<string, string>> GetSubsetStaticAsync(List<string> param, long translationId)
{
#if (DEBUG)
TrackRequestedKey(param);
#endif
using (AyContext ct = ServiceProviderProvider.DBContext)
{
if (!await ct.Translation.AnyAsync(e => e.Id == translationId))
translationId = ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID;
var ret = await ct.TranslationItem.Where(z => z.TranslationId == translationId && param.Contains(z.Key)).AsNoTracking().ToDictionaryAsync(z => z.Key, z => z.Display);
return ret;
}
}
/////////////////////////////////////////////////////////////////
// Get translation of one key for specified translation id
// Used for internal error message translation to return
// during object validation etc. Intended to be as efficient as possible
//
internal static async Task<string> GetTranslationStaticAsync(string translationKey, long translationId, AyContext ct)
{
#if (DEBUG)
TrackRequestedKey(translationKey);
#endif
return await ct.TranslationItem.Where(z => z.TranslationId == translationId && z.Key == translationKey).AsNoTracking().Select(z => z.Display).FirstOrDefaultAsync();
}
/////////////////////////////////////////////////////////////////
// Get subset for specified user (looks up translation id) statically
// called from internal code (e.g. notification processing)
//
internal static async Task<Dictionary<string, string>> GetSubsetForUserStaticAsync(List<string> param, long userId)
{
long translationId = ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID;
using (AyContext ct = ServiceProviderProvider.DBContext)
{
translationId = await ct.UserOptions.AsNoTracking().Where(z => z.UserId == userId).Select(z => z.TranslationId).SingleAsync();
}
return await GetSubsetStaticAsync(param, translationId);
}
/////////////////////////////////////////////////////////////////
// Get single item for specified user (looks up translation id) statically
// called from internal code (e.g. notification processing)
//
internal static async Task<string> GetTranslationForUserStaticAsync(string translationKey, long userId)
{
long translationId = ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID;
using (AyContext ct = ServiceProviderProvider.DBContext)
{
translationId = await ct.UserOptions.AsNoTracking().Where(z => z.UserId == userId).Select(z => z.TranslationId).SingleAsync();
}
var param = new List<string>() { translationKey };
var ret = await GetSubsetStaticAsync(param, translationId);
if (ret.Count > 0) return ret[translationKey];
return $"??{translationKey}";
}
//used by internal notification and other processes i.e. "Server" in all languages for server based notifications
internal static async Task<Dictionary<long, string>> GetAllTranslationsForKey(string translationKey)
{
#if (DEBUG)
TrackRequestedKey(translationKey);
#endif
using (AyContext ct = ServiceProviderProvider.DBContext)
{
return await ct.TranslationItem.Where(z => z.Key == translationKey).AsNoTracking().ToDictionaryAsync(z => z.TranslationId, z => z.Display);
}
}
//Get the CJKIndex value for the translation specified
internal static async Task<bool> GetCJKIndexAsync(long translationId, AyContext ct)
{
var ret = await ct.Translation.Where(z => z.Id == translationId).AsNoTracking().Select(z => z.CjkIndex).SingleOrDefaultAsync();
return ret;
}
//DEPRECATED
// /// <summary>
// /// Get the value of the key provided in the default translation chosen
// /// </summary>
// /// <param name="key"></param>
// /// <returns></returns>
// internal static async Task<string> GetDefaultTranslationAsync(string key)
// {
// if (string.IsNullOrWhiteSpace(key))
// return "ERROR: GetDefaultTranslation NO KEY VALUE SPECIFIED";
// #if (DEBUG)
// TrackRequestedKey(key);
// #endif
// using (AyContext ct = ServiceProviderProvider.DBContext)
// return await ct.TranslationItem.Where(z => z.TranslationId == ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID && z.Key == key).Select(z => z.Display).AsNoTracking().FirstOrDefaultAsync();
// }
//Get all stock keys that are valid (used for key coverage reporting)
internal static async Task<List<string>> GetKeyListAsync()
{
using (AyContext ct = ServiceProviderProvider.DBContext)
return await ct.TranslationItem.Where(z => z.TranslationId == 1).OrderBy(z => z.Key).Select(z => z.Key).AsNoTracking().ToListAsync();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> DeleteAsync(Translation dbObject)
{
//Determine if the object can be deleted, do the deletion tentatively
await ValidateCanDeleteAsync(dbObject);
if (HasErrors)
return false;
ct.Translation.Remove(dbObject);
await ct.SaveChangesAsync();
//Log
await EventLogProcessor.DeleteObjectLogAsync(UserId, SockType.Translation, dbObject.Id, dbObject.Name, ct);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private async Task ValidateAsync(Translation proposedObj)
{
//run validation and biz rules
//Name required
if (string.IsNullOrWhiteSpace(proposedObj.Name))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name");
//Name must be unique
if (await ct.Translation.AnyAsync(z => z.Name == proposedObj.Name && z.Id != proposedObj.Id))
AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name");
//Ensure there are no empty keys or too long ones
//fixing them up here rather than at the client as it's a bit of fuckery
//to try to validate or fix an item edited inside a data table with vuetify
//rather than try to deal with that just fix it here
foreach (var item in proposedObj.TranslationItems.Where(z => z.Display.Length < 1))
{
item.Display = item.Key;
}
foreach (var item in proposedObj.TranslationItems.Where(z => z.Display.Length > 255))
{
item.Display = item.Display.Substring(0, 255);
}
return;
}
//Can delete?
private async Task ValidateCanDeleteAsync(Translation inObj)
{
//Decided to short circuit these; if there is one issue then return immediately (fail fast rule)
//Ensure it's not a stock translation
if (inObj.Stock == true)
{
AddError(ApiErrorCode.INVALID_OPERATION, null, "Translation is a Stock translation and cannot be deleted");
return;
}
if (inObj.Id == ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID)
{
AddError(ApiErrorCode.INVALID_OPERATION, null, "Translation is set as the default server translation (SOCKEYE_DEFAULT_LANGUAGE_ID) and can not be deleted");
return;
}
//See if any users exist with this translation selected in which case it's not deleteable
if (await ct.UserOptions.AnyAsync(e => e.TranslationId == inObj.Id))
{
AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, null, "Can't be deleted in use by one or more Users");
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UTILITIES
//
public async Task<long> TranslationNameToIdAsync(string translationName)
{
var v = await ct.Translation.AsNoTracking().FirstOrDefaultAsync(z => z.Name == translationName);
if (v == null) return 0;
return v.Id;
}
public static async Task<long> TranslationNameToIdStaticAsync(string translationName)
{
using (AyContext ct = ServiceProviderProvider.DBContext)
{
var v = await ct.Translation.AsNoTracking().FirstOrDefaultAsync(z => z.Name == translationName);
if (v == null) return ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID;
return v.Id;
}
}
public async Task<bool> TranslationExistsAsync(string translationName)
{
return await ct.Translation.AnyAsync(z => z.Name == translationName);
}
public async Task<bool> TranslationExistsAsync(long id)
{
return await ct.Translation.AnyAsync(z => z.Id == id);
}
//this is only called by Search.cs to cache a local cjk and stopwords, no one else calls it currently
public static async Task<long> ReturnSpecifiedTranslationIdIfExistsOrDefaultTranslationId(long id, AyContext ct)
{
if (!await ct.Translation.AnyAsync(z => z.Id == id))
return ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID;
return id;
}
public async Task<bool> TranslationItemExistsAsync(long id)
{
return await ct.TranslationItem.AnyAsync(z => z.Id == id);
}
/// <summary>
/// Ensure stock Translations and setup defaults
/// Called by boot preflight check code AFTER it has already ensured the translation is a two letter code if stock one was chosen
/// </summary>
public async Task ValidateTranslationsAsync()
{
//Ensure default translations are present and that there is a server default translation that exists
if (!await TranslationExistsAsync("en"))
{
throw new System.Exception($"E1015: stock translation English (en) not found in database!");
}
if (!await TranslationExistsAsync("es"))
{
throw new System.Exception($"E1015: stock translation Spanish (es) not found in database!");
}
if (!await TranslationExistsAsync("de"))
{
throw new System.Exception($"E1015: stock translation German (de) not found in database!");
}
if (!await TranslationExistsAsync("fr"))
{
throw new System.Exception($"E1015: stock translation French (fr) not found in database!");
}
//Ensure chosen default translation exists
switch (ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION)
{
case "en":
case "es":
case "de":
case "fr":
break;
default:
if (!await TranslationExistsAsync(ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION))
{
throw new System.Exception($"E1015: stock translation {ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION} not found in database!");
}
break;
}
//Put the default translation ID number into the ServerBootConfig for later use
ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID = await TranslationNameToIdAsync(ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION);
}
/////////////////////////////////////////////////////////////////////
}//eoc
//
}//eons

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
namespace Sockeye.Biz
{
//DataTypes used to format display properly and for custom fields definition etc
public enum UiFieldDataType : int
{
NoType = 0,
DateTime = 1,
Date = 2,
Time = 3,
Text = 4,
Integer = 5,
Bool = 6,
Decimal = 7,
Currency = 8,
Tags = 9,
Enum = 10,
EmailAddress = 11,
HTTP = 12,
InternalId = 13,
MemorySize = 14,//this is so client can convert what would normally be an Integer type to human readable file / ram size value
TimeSpan=15,
PhoneNumber=16,//this is so client can dial directly,
Roles = 17
// ,//for grid display (users), Percentage = 18 ? YAGNI?
}
}

1017
server/biz/UserBiz.cs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,155 @@
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
namespace Sockeye.Biz
{
internal class UserOptionsBiz : BizObject
{
internal UserOptionsBiz(AyContext dbcontext, long currentUserId, AuthorizationRoles userRoles)
{
ct = dbcontext;
UserId = currentUserId;
CurrentUserRoles = userRoles;
BizType = SockType.UserOptions;
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// GET
//Get one
internal async Task<UserOptions> GetAsync(long fetchId)
{
//NOTE: get by UserId as there is a 1:1 relationship, not by useroptions id
//This is simple so nothing more here, but often will be copying to a different output object or some other ops
return await ct.UserOptions.SingleOrDefaultAsync(z => z.UserId == fetchId);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
//Creating a user creates a user options so no need for create ever
// ////////////////////////////////////////////////////////////////////////////////////////////////
// //CREATE
// //
// internal async Task<UserOptions> CreateAsync(UserOptions newObject)
// {
// User u = await ct.User.AsNoTracking().SingleOrDefaultAsync(z => z.Id == newObject.UserId);
// if (u == null)
// {
// AddError(ApiErrorCode.NOT_FOUND, "id");
// return null;
// }
// //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 (
// (u.IsOutsideUser && !Authorized.HasModifyRole(CurrentUserRoles, SockType.Customer)) ||
// (!u.IsOutsideUser && !Authorized.HasModifyRole(CurrentUserRoles, SockType.User))
// )
// {
// AddError(ApiErrorCode.NOT_AUTHORIZED);
// return null;
// }
// Validate(newObject);
// if (HasErrors)
// return null;
// else
// {
// await ct.UserOptions.AddAsync(newObject);
// await ct.SaveChangesAsync();
// await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct);
// return newObject;
// }
// }
//put
internal async Task<bool> PutAsync(UserOptions dbObject, UserOptions inObj)
{
//if it's not the user's own options then we need to check it just as for User / Contact objects
if (dbObject.Id != UserId)
{
User u = await ct.User.AsNoTracking().SingleOrDefaultAsync(z => z.Id == dbObject.Id);
if (u == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
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 (
(u.IsOutsideCustomerContactTypeUser && !Authorized.HasModifyRole(CurrentUserRoles, SockType.Customer)) ||
(!u.IsOutsideCustomerContactTypeUser && !Authorized.HasModifyRole(CurrentUserRoles, SockType.User))
)
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return false;
}
}
//Replace the db object with the PUT object
CopyObject.Copy(inObj, dbObject, "Id, UserId");
//Set "original" value of concurrency token to input token
//this will allow EF to check it out
//BUT NOT IF IT"S FROM A DUPLICATION OP (CONCURRENCY=0)
if (inObj.Concurrency != 0)
ct.Entry(dbObject).OriginalValues["Concurrency"] = inObj.Concurrency;
Validate(dbObject);
if (HasErrors)
return false;
await ct.SaveChangesAsync();
//Log
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, SockType.User, SockEvent.Modified), ct);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private void Validate(UserOptions inObj)
{
//UserOptions is never new, it's created with the User object so were only here for an edit
//UserId required
if (inObj.UserId == 0)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "UserId");
//Hexadecimal notation: #RGB[A] R (red), G (green), B (blue), and A (alpha) are hexadecimal characters (09, AF). A is optional. The three-digit notation (#RGB) is a shorter version of the six-digit form (#RRGGBB). For example, #f09 is the same color as #ff0099. Likewise, the four-digit RGB notation (#RGBA) is a shorter version of the eight-digit form (#RRGGBBAA). For example, #0f38 is the same color as #00ff3388.
if (inObj.UiColor.Length > 12 || inObj.UiColor.Length < 4 || inObj.UiColor[0] != '#')
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "UiColor", "UiColor must be valid HEX color value");
}
return;
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

14
server/biz/UserType.cs Normal file
View File

@@ -0,0 +1,14 @@
namespace Sockeye.Biz
{
/// <summary>
/// Sockeye User types
/// </summary>
public enum UserType : int
{
Service = 1,
NotService = 2,
Customer = 3,
HeadOffice = 4,
ServiceContractor = 5
}
}//eons

View File

@@ -0,0 +1,19 @@
namespace Sockeye.Biz
{
public class ValidationError
{
//TARGET is the Model name of the property which matches the client UI annotations for ref
//if the target error is a child item collection field the Target must be "items[2].field"
//where "items" is the item collection model name and 2 is the index of the collection with the error and field is the ultimate field model name
//Case doesn't matter as the client will compare in lower case all items anyway
public ApiErrorCode Code { get; set; }
public string Target { get; set; }
public string Message { get; set; }
}//eoc
}//eons