From 1c01ef1e1c3f17ce8d205325c0d155fa3aa65e12 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Tue, 17 Sep 2024 14:54:45 -0700 Subject: [PATCH] fix!: #445 incorrect constructor resolution for cloning ListResult objects. The `IListResult` and `IApiResult` interfaces has been removed and replace with a non-generic `ListResult` abstract base class. --- CHANGELOG.md | 21 +++-- .../Tests/Models/ListResultTests.cs | 81 +++++++++++++++++++ src/IntelliTect.Coalesce/Models/ApiResult.cs | 12 +-- src/IntelliTect.Coalesce/Models/ListResult.cs | 45 +++++++---- 4 files changed, 125 insertions(+), 34 deletions(-) create mode 100644 src/IntelliTect.Coalesce.Tests/Tests/Models/ListResultTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d4aafad8..b533c3a72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,35 +2,36 @@ ## Breaking Changes -- Removed support for Knockout.js +- Support for Knockout.js, previously deprecated, has been fully removed. - Drop all .NET targets except .NET 8 ### Breaking: Backend/C# -- Removed automatic `DbContext.SaveChanges()` call from model instance method invocations. Model instance methods that intend to save their changes to the database now must inject a `DbContext` and perform this save explicitly. (#405) +- Removed automatic `DbContext.SaveChanges()` call from model instance method API endpoints. Model instance methods that intend to save any changes to the database now must inject a `DbContext` and perform this save explicitly. (#405) - ASP.NET `ModelState` validation is now always performed against all inputs to all endpoints. -- `ClaimsPrincipalExtensions` has been removed. Claim names/types can vary significantly depending on the authentication schemes used within a project, so these are best left defined on a per-project basis. +- `ClaimsPrincipalExtensions` has been removed. Claim names/types can vary significantly depending on the authentication schemes used within a project, so these are best left defined on a per-project basis. Replacements are present in the new Coalesce project template. - `IDataSource.GetItemAsync` and `IDataSource.GetListAsync` no longer return tuples. The IncludeTree return from these methods is now attached to the ItemResult/ListResult. - `StandardBehaviors.AfterSave` is now `AfterSaveAsync` and has a different signature and semantics. Instead of modifying the resulting `item` and `includeTree` with `ref` parameters, these values can be optionally overridden by returning an ItemResult with its `Object` and `IncludeTree` properties populated with non-null values. - Generated DTOs now generate separate Parameter DTOs (for inputs) and Response DTOs (for outputs). This results in much cleaner and more accurate OpenAPI definitions. - `IntelliTect.Coalesce.AuditLogging` no longer utilizes `Z.EntityFramework.Plus` - the internal implementation is now purpose-built for Coalesce's needs. Most of the configuration options that were useful to Coalesce applications have been preserved with the same API. -- Audit logging by default now only merges changes when all changes are to non-discrete properties. For all other changes, new records are inserted every time. A string is an example of a non-discrete property, while a foreign key is an example of a discrete property. +- Audit logging by default now only merges changes when all changes are to non-discrete properties. For all other changes, new records are inserted every time. A plain string is an example of a non-discrete property, while a foreign key is an example of a discrete property. +- `ListResult` constructors have been adjusted to prevent incorrect overload resolution when cloning ListResult objects. The `IListResult` and `IApiResult` interfaces has been removed and replace with a non-generic `ListResult` abstract base class. (#445) ### Breaking: Frontend/Typescript -- Many dependencies bumped to their latest versions, including `vue`, `vue-router`, `vuetify`, `typescript` (5.5+), `date-fns`, and more. +- Many dependency minimum versions increased, including `vue`, `vue-router`, `vuetify`, `typescript` (5.5+), `date-fns`, and more. - `bindToQueryString`: some parameters were converted to an options object, overloads added for easier binding to a `ref` (#396) - API callers (e.g. `ViewModel.$save`) now directly return the inner data from their response ListResult/ItemResult instead of returning the outer axios response. The raw response of the previous request is now available on `.rawResponse`. -- The content disposition of file downloads defaults "inline" instead of "attachment". This allows files to be opened for display in a browser tab. Forced downloads can still be achieved by placing the `download` attribute on the HTML `a` tag that links to the endpoint's URL, or by setting `ForceDownload` on the `File` object instantiated in your C# method. +- The content disposition of file downloads defaults to "inline" instead of "attachment". This allows files to be opened for display in a browser tab. Forced downloads can still be achieved by placing the `download` attribute on the HTML `a` tag that links to the endpoint's URL, or by setting `ForceDownload` on the `File` object instantiated in your C# method. - `FilterParameters.filter` is always initialized to an empty object to make binding easier (no need to manually initialize it and sprinkle your code with null forgiveness operators). ## Deprecations -- Support for Vue 2 is officially deprecated. `coalesce-vue-vuetify2` will receive critical bugfixes, but will no longer receive new features that Vue 3 receives. +- Support for Vue 2 is officially deprecated. `coalesce-vue-vuetify2` will receive critical bugfixes, but will no longer receive new features that Vue 3 receives. If your application is still on Vue 2, you should [migrate ASAP](https://intellitect.github.io/Coalesce/stacks/vue/vue2-to-vue3.html). - `ControllerActionAttribute` has been merged into `ExecuteAttribute`. - `LoadFromDataSourceAttribute` has been merged into `ExecuteAttribute`. - `CreateControllerAttribute` deprecated in favor of using either `[InternalUse]` or `[Create]`/`[Read]`/`[Edit]`/`[Delete]` to preclude the API endpoints of types. -- TypeScript API clients - `withSimultaneousRequestCaching` renamed to `useSimultaneousRequestCaching`. +- TypeScript API clients: `withSimultaneousRequestCaching` renamed to `useSimultaneousRequestCaching`. - `SecurityPermissionLevels.AllowAuthorized` has been renamed to `SecurityPermissionLevels.AllowAuthenticated` (this enum is used by security attributes). - `ControllerAttribute` is deprecated. @@ -86,6 +87,10 @@ - Vite-plugin: handle nested `public` folder structures - Remove opening delay when clicking a c-select. +--- + +Notice: Coalesce versions prior to v5 did not use a typical release cadence. The changes for these versions have been broken down by month, up through April 2023. + # 4.0.0 2024-07 - feat: support collections of primitives as data source parameters diff --git a/src/IntelliTect.Coalesce.Tests/Tests/Models/ListResultTests.cs b/src/IntelliTect.Coalesce.Tests/Tests/Models/ListResultTests.cs new file mode 100644 index 000000000..a185a21b5 --- /dev/null +++ b/src/IntelliTect.Coalesce.Tests/Tests/Models/ListResultTests.cs @@ -0,0 +1,81 @@ +using IntelliTect.Coalesce.Models; +using IntelliTect.Coalesce.Tests.TargetClasses.TestDbContext; +using IntelliTect.Coalesce.Tests.Util; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace IntelliTect.Coalesce.Tests.Tests.Models +{ + public class ListResultTests + { + [Fact] + public void Constructor_FromListResult_CopiesPagination() + { + var model = new ComplexModel(); + var original = new ListResult() + { + Page = 1, + PageSize = 2, + TotalCount = 3, + IncludeTree = new IncludeTree(), + List = [model], + Message = "Test", + WasSuccessful = true, + }; + + var replica = new ListResult>(original); + + // List can't be copied over because the type is different. + Assert.Null(replica.List); + // Everything else can and should be copied. + Assert.Equal(original.IncludeTree, replica.IncludeTree); + Assert.Equal(original.Page, replica.Page); + Assert.Equal(original.PageSize, replica.PageSize); + Assert.Equal(original.PageCount, replica.PageCount); + Assert.Equal(original.TotalCount, replica.TotalCount); + Assert.Equal(original.WasSuccessful, replica.WasSuccessful); + Assert.Equal(original.Message, replica.Message); + } + + [Fact] + public void Constructor_FromApiResult_CopiesProperties() + { + var original = new ApiResult(false, "error") + { + IncludeTree = new IncludeTree(), + }; + + var replica = new ListResult>(original); + + Assert.Equal(original.IncludeTree, replica.IncludeTree); + Assert.Equal(original.WasSuccessful, replica.WasSuccessful); + Assert.Equal(original.Message, replica.Message); + } + + [Fact] + public void Constructor_FromQuery_ComputesPagination() + { + var query = new List(){ + new(), + new(), + new() { Name = "Foo" }, + new(), + new(), + new(), + new(), + new(), + }.AsQueryable(); + + var result = new ListResult(query, 2, 2); + + Assert.True(result.WasSuccessful); + Assert.Null(result.Message); + Assert.Equal(2, result.Page); + Assert.Equal(2, result.PageSize); + Assert.Equal(4, result.PageCount); + Assert.Equal(8, result.TotalCount); + Assert.Equal("Foo", result.List[0].Name); + } + } +} diff --git a/src/IntelliTect.Coalesce/Models/ApiResult.cs b/src/IntelliTect.Coalesce/Models/ApiResult.cs index ee0482673..403732deb 100644 --- a/src/IntelliTect.Coalesce/Models/ApiResult.cs +++ b/src/IntelliTect.Coalesce/Models/ApiResult.cs @@ -1,14 +1,6 @@ -using System; - -namespace IntelliTect.Coalesce.Models +namespace IntelliTect.Coalesce.Models { - public interface IApiResult - { - bool WasSuccessful { get; } - string? Message { get; } - } - - public class ApiResult : IApiResult + public class ApiResult { public bool WasSuccessful { get; set; } = true; diff --git a/src/IntelliTect.Coalesce/Models/ListResult.cs b/src/IntelliTect.Coalesce/Models/ListResult.cs index 1a729f090..69f490dc7 100644 --- a/src/IntelliTect.Coalesce/Models/ListResult.cs +++ b/src/IntelliTect.Coalesce/Models/ListResult.cs @@ -1,32 +1,43 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; namespace IntelliTect.Coalesce.Models { - public interface IListResult : IApiResult - { - int Page { get; } - int PageSize { get; } - int PageCount { get; } - int TotalCount { get; } - } - - public class ListResult : ApiResult, IListResult + public abstract class ListResult : ApiResult { public int Page { get; set; } + public int PageSize { get; set; } - public int PageCount => - TotalCount == -1 ? -1 - : PageSize == 0 ? 0 + + public int PageCount => + TotalCount == -1 ? -1 + : PageSize == 0 ? 0 : (TotalCount + PageSize - 1) / PageSize; public int TotalCount { get; set; } + public ListResult() { } + + public ListResult(bool wasSuccessful, string? message = null) : base(wasSuccessful, message) { } + + public ListResult(string errorMessage) : base(errorMessage) { } + + public ListResult(ApiResult result) : base(result) { } + + public ListResult(ListResult result) : base(result) + { + Page = result.Page; + TotalCount = result.TotalCount; + PageSize = result.PageSize; + } + } + + public class ListResult : ListResult + { public IList? List { get; set; } - public ListResult(): base() { } + public ListResult() { } public ListResult(bool wasSuccessful, string? message = null) : base(wasSuccessful, message) { } @@ -34,8 +45,10 @@ public ListResult(string errorMessage) : base(errorMessage) { } public ListResult(ApiResult result) : base(result) { } - public ListResult(IListResult result, IList? items = null) - : this(items, page: result.Page, totalCount: result.TotalCount, pageSize: result.PageSize, wasSuccessful: result.WasSuccessful, message: result.Message) { } + public ListResult(ListResult result, IList? items = null) : base(result) + { + List = items; + } public ListResult(IList? items, int page, int totalCount, int pageSize, bool wasSuccessful = true, string? message = null) {