diff --git a/devdocs/todo.txt b/devdocs/todo.txt index 8ec6dca4..70536456 100644 --- a/devdocs/todo.txt +++ b/devdocs/todo.txt @@ -8,6 +8,8 @@ Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNTYxNDk4NzQ4IiwiZXhwIjoi UPDATE all the things before commencing work - https://docs.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-3.0&tabs=visual-studio-code - https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes + - https://github.com/domaindrivendev/Swashbuckle.AspNetCore#swashbuckleaspnetcoreannotations + - Need a sprint to get to a fully testable client with entry form, list and as much as possible all features from COMMON-* specs list Do the stuff in the Client todo first then back to the server as required. diff --git a/server/AyaNova/AyaNova.csproj b/server/AyaNova/AyaNova.csproj index 7f4df66e..42178ba8 100644 --- a/server/AyaNova/AyaNova.csproj +++ b/server/AyaNova/AyaNova.csproj @@ -31,13 +31,23 @@ + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/server/AyaNova/Startup.cs b/server/AyaNova/Startup.cs index 87d62559..5397aaf8 100644 --- a/server/AyaNova/Startup.cs +++ b/server/AyaNova/Startup.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authentication.JwtBearer; @@ -11,6 +12,9 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.Extensions.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.OpenApi.Models; + + using AyaNova.Models; using AyaNova.Util; using AyaNova.Generator; @@ -24,7 +28,8 @@ using System.IO; using System.Reflection; using System.Linq; using System; - +using System.Collections.Generic; +using Newtonsoft.Json.Serialization; namespace AyaNova @@ -36,7 +41,7 @@ namespace AyaNova ///////////////////////////////////////////////////////////// // - public Startup(ILogger logger, ILoggerFactory logFactory, Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment) + public Startup(ILogger logger, ILoggerFactory logFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment hostingEnvironment) { _log = logger; _hostingEnvironment = hostingEnvironment; @@ -48,7 +53,7 @@ namespace AyaNova private readonly ILogger _log; private string _connectionString = ""; - private readonly Microsoft.AspNetCore.Hosting.IHostingEnvironment _hostingEnvironment; + private readonly Microsoft.AspNetCore.Hosting.IWebHostEnvironment _hostingEnvironment; //////////////////////////////////////////////////////////// // This method gets called by the runtime. Use this method to add services to the container. @@ -57,14 +62,23 @@ namespace AyaNova { _log.LogDebug("BOOT: initializing services..."); + + //Server state service for shutting people out of api _log.LogDebug("BOOT: init ApiServerState service"); services.AddSingleton(new AyaNova.Api.ControllerHelpers.ApiServerState()); + //Init mvc + _log.LogDebug("BOOT: init MVC Core service"); + var mvc = services.AddMvcCore(); + _log.LogDebug("BOOT: add json service"); + mvc.AddNewtonsoftJson(); + + // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service // note: the specified format code will format the version as "'v'major[.minor][-status]" _log.LogDebug("BOOT: init ApiExplorer service"); - services.AddMvcCore().AddVersionedApiExplorer(o => o.GroupNameFormat = "'v'VVV"); + services.AddVersionedApiExplorer(o => o.GroupNameFormat = "'v'VVV"); _log.LogDebug("BOOT: ensuring user and backup folders exist and are separate locations..."); FileUtil.EnsureUserAndUtilityFoldersExistAndAreNotIdentical(_hostingEnvironment.ContentRootPath); @@ -98,7 +112,7 @@ namespace AyaNova bool LOG_SENSITIVE_DATA = false; #if (DEBUG) - // LOG_SENSITIVE_DATA = true; + // LOG_SENSITIVE_DATA = true; #endif @@ -110,7 +124,8 @@ namespace AyaNova )//http://www.npgsql.org/efcore/misc.html?q=execution%20strategy#execution-strategy .ConfigureWarnings(warnings => //https://livebook.manning.com/#!/book/entity-framework-core-in-action/chapter-12/v-10/85 warnings.Throw( //Throw an exception on client eval, not necessarily an error but a smell - Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.QueryClientEvaluationWarning)) + // Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.QueryClientEvaluationWarning + )) .EnableSensitiveDataLogging(LOG_SENSITIVE_DATA) ); @@ -154,7 +169,9 @@ namespace AyaNova }).AddMetrics().AddJsonOptions(options => { - options.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc; + //2019-10-15 - removed this due to fuckery after update, is it required??? Not sure at this point + //if metrics have wrong dates then it's important I guess + //options.JsonSerializerOptions.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc; }); @@ -179,29 +196,55 @@ namespace AyaNova } // add a custom operation filter which sets default values - c.OperationFilter(); + //Removed because will no longer compile the SwaggerDefaultValues but may be needed, not sure it's all a bit muddled + // c.OperationFilter(); // integrate xml comments c.IncludeXmlComments(XmlCommentsFilePath); + //2019-10-15 - Removed this because apikeyscheme is no longer recognized and it appears it *may* not be necessary... TWT + //If I have any issues with bearer tokens in swagger then this is probably necessary but in a new way //this is required to allow authentication when testing secure routes via swagger UI - c.AddSecurityDefinition("Bearer", new ApiKeyScheme - { - Description = "JWT Authorization header using the Bearer scheme. Get your token by logging in via the Auth route then enter it here with the \"Bearer \" prefix. Example: \"Bearer {token}\"", - Name = "Authorization", - In = "header", - Type = "apiKey" + // c.AddSecurityDefinition("Bearer", new ApiKeyScheme + // { + // Description = "JWT Authorization header using the Bearer scheme. Get your token by logging in via the Auth route then enter it here with the \"Bearer \" prefix. Example: \"Bearer {token}\"", + // Name = "Authorization", + // In = "header", + // Type = "apiKey" - }); + // }); - c.AddSecurityRequirement(new System.Collections.Generic.Dictionary> + + + //Obsolete way + // c.AddSecurityRequirement(new System.Collections.Generic.Dictionary> + // { + // { "Bearer", new string[] { } } + // }); + + //https://stackoverflow.com/questions/56234504/migrating-to-swashbuckle-aspnetcore-version-5 + //First we define the security scheme + c.AddSecurityDefinition("Bearer", //Name the security scheme + new OpenApiSecurityScheme { - { "Bearer", new string[] { } } + Description = "JWT Authorization header using the Bearer scheme.", + Type = SecuritySchemeType.Http, //We set the scheme type to http since we're using bearer authentication + Scheme = "bearer" //The name of the HTTP Authorization scheme to be used in the Authorization header. In this case "bearer". }); + c.AddSecurityRequirement(new OpenApiSecurityRequirement{ + { + new OpenApiSecurityScheme{ + Reference = new OpenApiReference{ + Id = "Bearer", //The name of the previously defined security scheme. + Type = ReferenceType.SecurityScheme + } + },new List() + } + }); }); @@ -275,7 +318,7 @@ namespace AyaNova //////////////////////////////////////////////////////////// // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // - public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHostingEnvironment env, + public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IWebHostEnvironment env, AyContext dbContext, IApiVersionDescriptionProvider provider, AyaNova.Api.ControllerHelpers.ApiServerState apiServerState, IServiceProvider serviceProvider) { _log.LogDebug("BOOT: configuring request pipeline..."); @@ -478,9 +521,9 @@ namespace AyaNova } } - static Info CreateInfoForApiVersion(ApiVersionDescription description) + static Microsoft.OpenApi.Models.OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description) { - var info = new Info() + var info = new Microsoft.OpenApi.Models.OpenApiInfo() { Title = $"AyaNova API {description.ApiVersion}", Version = description.ApiVersion.ToString() diff --git a/server/AyaNova/SwaggerDefaultValues.cs b/server/AyaNova/SwaggerDefaultValues.cs index 14d01e0d..eff78291 100644 --- a/server/AyaNova/SwaggerDefaultValues.cs +++ b/server/AyaNova/SwaggerDefaultValues.cs @@ -1,47 +1,47 @@ -namespace AyaNova -{ - using Swashbuckle.AspNetCore.Swagger; - using Swashbuckle.AspNetCore.SwaggerGen; - using System.Linq; +// namespace AyaNova +// { +// using Swashbuckle.AspNetCore.SwaggerGen; +// using System.Linq; - /// - /// Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter. - /// - /// This is only required due to bugs in the . - /// Once they are fixed and published, this class can be removed. - public class SwaggerDefaultValues : IOperationFilter - { - /// - /// Applies the filter to the specified operation using the given context. - /// - /// The operation to apply the filter to. - /// The current operation filter context. - public void Apply( Operation operation, OperationFilterContext context ) - { - // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/412 - // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/413 - foreach ( var parameter in operation.Parameters.OfType() ) - { - var description = context.ApiDescription.ParameterDescriptions.First( p => p.Name == parameter.Name ); - var routeInfo = description.RouteInfo; - if ( parameter.Description == null ) - { - parameter.Description = description.ModelMetadata?.Description; - } +// /// +// /// Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter. +// /// +// /// This is only required due to bugs in the . +// /// Once they are fixed and published, this class can be removed. +// public class SwaggerDefaultValues : IOperationFilter +// { +// /// +// /// Applies the filter to the specified operation using the given context. +// /// +// /// The operation to apply the filter to. +// /// The current operation filter context. +// public void Apply( Operation operation, OperationFilterContext context ) +// { +// // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/412 +// // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/413 +// foreach ( var parameter in operation.Parameters.OfType() ) +// { +// var description = context.ApiDescription.ParameterDescriptions.First( p => p.Name == parameter.Name ); +// var routeInfo = description.RouteInfo; - if ( routeInfo == null ) - { - continue; - } +// if ( parameter.Description == null ) +// { +// parameter.Description = description.ModelMetadata?.Description; +// } - if ( parameter.Default == null ) - { - parameter.Default = routeInfo.DefaultValue; - } +// if ( routeInfo == null ) +// { +// continue; +// } - parameter.Required |= !routeInfo.IsOptional; - } - } - } -} \ No newline at end of file +// if ( parameter.Default == null ) +// { +// parameter.Default = routeInfo.DefaultValue; +// } + +// parameter.Required |= !routeInfo.IsOptional; +// } +// } +// } +// } \ No newline at end of file diff --git a/server/AyaNova/biz/EventLogProcessor.cs b/server/AyaNova/biz/EventLogProcessor.cs index 07193827..71c9f9c6 100644 --- a/server/AyaNova/biz/EventLogProcessor.cs +++ b/server/AyaNova/biz/EventLogProcessor.cs @@ -40,7 +40,7 @@ namespace AyaNova.Biz /// internal static void DeleteObject(long userId, AyaType ayType, long ayId, string textra, AyContext ct) { - ct.Database.ExecuteSqlCommand($"delete from aevent where aytype = {ayType} and ayid={ayId}"); + ct.Database.ExecuteSqlInterpolated($"delete from aevent where aytype = {ayType} and ayid={ayId}"); ct.Event.Add(new Event(userId, ayId, ayType, AyaEvent.Deleted, textra)); } diff --git a/server/AyaNova/biz/JobsBiz.cs b/server/AyaNova/biz/JobsBiz.cs index d0a8b3c2..69f7bb43 100644 --- a/server/AyaNova/biz/JobsBiz.cs +++ b/server/AyaNova/biz/JobsBiz.cs @@ -217,11 +217,17 @@ namespace AyaNova.Biz /// private static async Task removeJobAndLogsAsync(AyContext ct, Guid jobIdToBeDeleted) { - //delete logs - await ct.Database.ExecuteSqlCommandAsync("delete from aopsjoblog where jobid = {0}", new object[] { jobIdToBeDeleted }); + // //delete logs + // await ct.Database.ExecuteSqlCommandAsync("delete from aopsjoblog where jobid = {0}", new object[] { jobIdToBeDeleted }); + + // //delete the job + // await ct.Database.ExecuteSqlCommandAsync("delete from aopsjob where gid = {0}", new object[] { jobIdToBeDeleted }); + + //delete logs + await ct.Database.ExecuteSqlInterpolatedAsync($"delete from aopsjoblog where jobid = {jobIdToBeDeleted}"); //delete the job - await ct.Database.ExecuteSqlCommandAsync("delete from aopsjob where gid = {0}", new object[] { jobIdToBeDeleted }); + await ct.Database.ExecuteSqlInterpolatedAsync($"delete from aopsjob where gid = {jobIdToBeDeleted}"); } diff --git a/server/AyaNova/biz/Search.cs b/server/AyaNova/biz/Search.cs index bea0e1d3..e5a2f6e2 100644 --- a/server/AyaNova/biz/Search.cs +++ b/server/AyaNova/biz/Search.cs @@ -452,7 +452,7 @@ namespace AyaNova.Biz //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 AyContext ct = ServiceProviderProvider.DBContext; - ct.Database.ExecuteSqlCommand($"delete from asearchkey where objectid={objectID} and objecttype={(int)objectType}"); + ct.Database.ExecuteSqlInterpolated($"delete from asearchkey where objectid={objectID} and objecttype={(int)objectType}"); } diff --git a/server/AyaNova/biz/UserBiz.cs b/server/AyaNova/biz/UserBiz.cs index f816cc8f..99734c33 100644 --- a/server/AyaNova/biz/UserBiz.cs +++ b/server/AyaNova/biz/UserBiz.cs @@ -205,18 +205,29 @@ namespace AyaNova.Biz #pragma warning disable EF1000 + // var items = await ct.User + // .AsNoTracking() + // .FromSql(q) + // .Skip(pagingOptions.Offset.Value) + // .Take(pagingOptions.Limit.Value) + // .ToArrayAsync(); + var items = await ct.User - .AsNoTracking() - .FromSql(q) - .Skip(pagingOptions.Offset.Value) - .Take(pagingOptions.Limit.Value) - .ToArrayAsync(); + .FromSqlRaw(q) + .AsNoTracking() + .Skip(pagingOptions.Offset.Value) + .Take(pagingOptions.Limit.Value) + .ToArrayAsync(); - var totalRecordCount = await ct.User - .AsNoTracking() - .FromSql(q) - .CountAsync(); + // var totalRecordCount = await ct.User + // .AsNoTracking() + // .FromSql(q) + // .CountAsync(); + + var totalRecordCount = await ct.User.FromSqlRaw(q) + .AsNoTracking() + .CountAsync(); #pragma warning restore EF1000 int itemCount = items.Count();//totalRecordCount doesn't skip and take so not usable here @@ -364,7 +375,7 @@ namespace AyaNova.Biz //Delete sibling objects //USEROPTIONS - ct.Database.ExecuteSqlCommand($"delete from auseroptions where userid={dbObj.Id}"); + ct.Database.ExecuteSqlInterpolated($"delete from auseroptions where userid={dbObj.Id}"); EventLogProcessor.DeleteObject(UserId, BizType, dbObj.Id, dbObj.Name, ct); diff --git a/server/AyaNova/biz/WidgetBiz.cs b/server/AyaNova/biz/WidgetBiz.cs index a0eaedd7..fda38658 100644 --- a/server/AyaNova/biz/WidgetBiz.cs +++ b/server/AyaNova/biz/WidgetBiz.cs @@ -90,7 +90,7 @@ namespace AyaNova.Biz outObj.Serial = ServerBootConfig.WIDGET_SERIAL.GetNext(); outObj.Tags = TagUtil.NormalizeTags(outObj.Tags); - outObj.CustomFields=JsonUtil.CompactJson(outObj.CustomFields); + outObj.CustomFields = JsonUtil.CompactJson(outObj.CustomFields); await ct.Widget.AddAsync(outObj); await ct.SaveChangesAsync(); @@ -118,7 +118,7 @@ namespace AyaNova.Biz //Test get serial id visible id number from generator outObj.Serial = ServerBootConfig.WIDGET_SERIAL.GetNext(); outObj.Tags = TagUtil.NormalizeTags(outObj.Tags); - outObj.CustomFields=JsonUtil.CompactJson(outObj.CustomFields); + outObj.CustomFields = JsonUtil.CompactJson(outObj.CustomFields); TempContext.Widget.Add(outObj); TempContext.SaveChanges(); @@ -142,9 +142,9 @@ namespace AyaNova.Biz Widget outObj = new Widget(); CopyObject.Copy(dbObj, outObj); - outObj.Name = Util.StringUtil.NameUniquify(outObj.Name,255); - outObj.Id=0; - outObj.ConcurrencyToken=0; + outObj.Name = Util.StringUtil.NameUniquify(outObj.Name, 255); + outObj.Id = 0; + outObj.ConcurrencyToken = 0; //Test get serial id visible id number from generator outObj.Serial = ServerBootConfig.WIDGET_SERIAL.GetNext(); @@ -176,7 +176,7 @@ namespace AyaNova.Biz CopyObject.Copy(inObj, dbObj, "Id,Serial"); dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags); - dbObj.CustomFields=JsonUtil.CompactJson(dbObj.CustomFields); + dbObj.CustomFields = JsonUtil.CompactJson(dbObj.CustomFields); //Set "original" value of concurrency token to input token //this will allow EF to check it out @@ -210,7 +210,7 @@ namespace AyaNova.Biz objectPatch.ApplyTo(dbObj); dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags); - dbObj.CustomFields=JsonUtil.CompactJson(dbObj.CustomFields); + dbObj.CustomFields = JsonUtil.CompactJson(dbObj.CustomFields); ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = concurrencyToken; Validate(dbObj, SnapshotOfOriginalDBObj); @@ -233,7 +233,7 @@ namespace AyaNova.Biz var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, obj.Id, BizType, obj.Name); SearchParams.AddText(obj.Notes).AddText(obj.Name).AddText(obj.Serial).AddText(obj.Tags).AddCustomFields(obj.CustomFields); - + if (isNew) Search.ProcessNewObjectKeywords(SearchParams); else @@ -322,16 +322,16 @@ namespace AyaNova.Biz #pragma warning disable EF1000 var items = await ct.Widget + .FromSqlRaw(q) .AsNoTracking() - .FromSql(q) .Skip(pagingOptions.Offset.Value) .Take(pagingOptions.Limit.Value) .ToArrayAsync(); var totalRecordCount = await ct.Widget - .AsNoTracking() - .FromSql(q) - .CountAsync(); + .FromSqlRaw(q) + .AsNoTracking() + .CountAsync(); #pragma warning restore EF1000 var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, totalRecordCount).PagingLinksObject(); diff --git a/server/AyaNova/models/AyContext.cs b/server/AyaNova/models/AyContext.cs index 5c50ab1b..3a9532bc 100644 --- a/server/AyaNova/models/AyContext.cs +++ b/server/AyaNova/models/AyContext.cs @@ -39,7 +39,8 @@ namespace AyaNova.Models foreach (var entity in modelBuilder.Model.GetEntityTypes()) { // Replace table names - entity.Relational().TableName = "a" + entity.Relational().TableName.ToLowerInvariant(); + //entity.Relational().TableName = "a" + entity.Relational().TableName.ToLowerInvariant(); + entity.SetTableName( "a" + entity.GetTableName().ToLowerInvariant()); // Replace column names foreach (var property in entity.GetProperties()) @@ -48,28 +49,34 @@ namespace AyaNova.Models //set it up to work properly with PostgreSQL if (property.Name == "ConcurrencyToken") { - property.Relational().ColumnName = "xmin"; - property.Relational().ColumnType = "xid"; - property.ValueGenerated = ValueGenerated.OnAddOrUpdate; + property.SetColumnName("xmin"); + property.SetColumnType("xid"); + // property.Relational().ColumnName = "xmin"; + // property.Relational().ColumnType = "xid"; + property.ValueGenerated = ValueGenerated.OnAddOrUpdate; property.IsConcurrencyToken = true; } else - property.Relational().ColumnName = property.Name.ToLowerInvariant(); + property.SetColumnName(property.Name.ToLowerInvariant()); } foreach (var key in entity.GetKeys()) { - key.Relational().Name = key.Relational().Name.ToLowerInvariant(); + key.SetName(key.GetName().ToLowerInvariant()); + // key.Relational().Name = key.Relational().Name.ToLowerInvariant(); } foreach (var key in entity.GetForeignKeys()) { - key.Relational().Name = key.Relational().Name.ToLowerInvariant(); + //key.Relational().Name = key.Relational().Name.ToLowerInvariant(); + + key.SetConstraintName(key.GetConstraintName().ToLowerInvariant()); } foreach (var index in entity.GetIndexes()) { - index.Relational().Name = index.Relational().Name.ToLowerInvariant(); + index.SetName(index.GetName().ToLowerInvariant()); + //index.Relational().Name = index.Relational().Name.ToLowerInvariant(); } }