diff --git a/XrmPluginCore.Tests/CustomApis/CustomApiConfigBuilderTests.cs b/XrmPluginCore.Tests/CustomApis/CustomApiConfigBuilderTests.cs index c479988..e017d8a 100644 --- a/XrmPluginCore.Tests/CustomApis/CustomApiConfigBuilderTests.cs +++ b/XrmPluginCore.Tests/CustomApis/CustomApiConfigBuilderTests.cs @@ -102,6 +102,53 @@ public void WithExecutePrivilegeName_CalledMultipleTimes_UsesLastValue() config.ExecutePrivilegeName.Should().Be("prvFinal"); } + [Theory] + [InlineData(Privilege.Create, "prvCreateaccount")] + [InlineData(Privilege.Read, "prvReadaccount")] + [InlineData(Privilege.Write, "prvWriteaccount")] + [InlineData(Privilege.Delete, "prvDeleteaccount")] + [InlineData(Privilege.Append, "prvAppendaccount")] + [InlineData(Privilege.AppendTo, "prvAppendToaccount")] + [InlineData(Privilege.Assign, "prvAssignaccount")] + [InlineData(Privilege.Share, "prvShareaccount")] + public void WithExecutePrivilege_Generic_ResolvesPrivilegeNameFromEntity(Privilege privilege, string expected) + { + // Arrange + var builder = new CustomApiConfigBuilder("test_api"); + + // Act + var config = builder.WithExecutePrivilege(privilege).Build(); + + // Assert + config.ExecutePrivilegeName.Should().Be(expected); + } + + [Fact] + public void WithExecutePrivilege_Generic_ReturnsBuilderForChaining() + { + // Arrange + var builder = new CustomApiConfigBuilder("test_api"); + + // Act + var result = builder.WithExecutePrivilege(Privilege.Read); + + // Assert + result.Should().BeSameAs(builder); + } + + [Fact] + public void WithExecutePrivilege_WithLogicalName_ResolvesPrivilegeName() + { + // Arrange + var builder = new CustomApiConfigBuilder("test_api"); + + // Act + var config = builder.WithExecutePrivilege("new_widget", Privilege.Write).Build(); + + // Assert + config.ExecutePrivilegeName.Should().Be("prvWritenew_widget"); + } + [Fact] public void Build_PreservesAllConfigurationIncludingExecutePrivilege() { diff --git a/XrmPluginCore/CHANGELOG.md b/XrmPluginCore/CHANGELOG.md index 9005c51..d87ba3a 100644 --- a/XrmPluginCore/CHANGELOG.md +++ b/XrmPluginCore/CHANGELOG.md @@ -1,4 +1,5 @@ ### v1.4.0 - 30 June 2026 +* Add: `WithExecutePrivilege(Privilege)` on the Custom API builder for type-safe execute-privilege configuration, e.g. `.WithExecutePrivilege(Privilege.Read)` resolves to `prvReadaccount`. A `WithExecutePrivilege(string entityLogicalName, Privilege)` overload is available for late-bound scenarios. The existing `WithExecutePrivilegeName(string)` remains for non-standard privilege names. * Add: Type-safe Custom API request/response wrappers. `RegisterAPI(name, handlerMethodName)` now generates `{ApiName}Request`/`{ApiName}Response` classes (named after the API, in the plugin's namespace) from the `AddRequestParameter`/`AddResponseProperty` declarations. The handler accepts the request and returns the response; a generated `ActionWrapper` marshals `InputParameters` into the request and the returned response into `OutputParameters`. When no request parameters are declared the handler takes no argument, and when no response properties are declared it returns `void`. * Add: Error XPC4004: Custom API handler method not found (with code fix to create the method). * Add: Warning XPC4005 / Error XPC4006: Custom API handler signature does not match the declared request parameters and response properties (with code fix to correct the signature). diff --git a/XrmPluginCore/CustomApis/CustomApiConfigBuilder.cs b/XrmPluginCore/CustomApis/CustomApiConfigBuilder.cs index 27929d7..4bef6ec 100644 --- a/XrmPluginCore/CustomApis/CustomApiConfigBuilder.cs +++ b/XrmPluginCore/CustomApis/CustomApiConfigBuilder.cs @@ -119,6 +119,31 @@ public CustomApiConfigBuilder WithExecutePrivilegeName(string privilegeName) return this; } + /// + /// Sets the execute privilege to a standard table privilege of , + /// e.g. WithExecutePrivilege<Account>(Privilege.Read) resolves to prvReadaccount. + /// + /// The early-bound entity type the privilege applies to. + /// The table privilege required to execute the custom API. + public CustomApiConfigBuilder WithExecutePrivilege(Privilege privilege) where T : Entity, new() + { + return WithExecutePrivilege(EntityLogicalNameCache.GetLogicalName(), privilege); + } + + /// + /// Sets the execute privilege to a standard table privilege of the entity identified by + /// , following the prv{Privilege}{EntityLogicalName} + /// convention. Use the generic overload when an + /// early-bound type is available. + /// + /// The logical name of the table the privilege applies to. + /// The table privilege required to execute the custom API. + public CustomApiConfigBuilder WithExecutePrivilege(string entityLogicalName, Privilege privilege) + { + Config.ExecutePrivilegeName = PrivilegeNameResolver.GetExecutePrivilegeName(privilege, entityLogicalName); + return this; + } + public CustomApiConfigBuilder AddRequestParameter(string uniqueName, CustomApiParameterType type, string displayName = null, string description = null, diff --git a/XrmPluginCore/Enums/Privilege.cs b/XrmPluginCore/Enums/Privilege.cs new file mode 100644 index 0000000..333652c --- /dev/null +++ b/XrmPluginCore/Enums/Privilege.cs @@ -0,0 +1,52 @@ +namespace XrmPluginCore.Enums +{ + /// + /// Standard Dataverse table (entity) privileges.
+ /// Each value maps to a privilege whose name follows the convention + /// prv{Privilege}{EntityLogicalName}, e.g. on the + /// account table resolves to prvReadaccount. + ///
+ public enum Privilege + { + /// + /// Permission to create a record (prvCreate...). + /// + Create = 0, + + /// + /// Permission to read a record (prvRead...). + /// + Read = 1, + + /// + /// Permission to update a record (prvWrite...). This is the "Update" of CRUD; + /// Dataverse names the update privilege "Write". + /// + Write = 2, + + /// + /// Permission to delete a record (prvDelete...). + /// + Delete = 3, + + /// + /// Permission to associate a record with this table from another record (prvAppend...). + /// + Append = 4, + + /// + /// Permission to associate another record with a record of this table (prvAppendTo...). + /// + AppendTo = 5, + + /// + /// Permission to assign a record to another user (prvAssign...). + /// + Assign = 6, + + /// + /// Permission to share a record with another user or team (prvShare...). + /// + Share = 7, + } +} diff --git a/XrmPluginCore/Helpers/PrivilegeNameResolver.cs b/XrmPluginCore/Helpers/PrivilegeNameResolver.cs new file mode 100644 index 0000000..bd3a8d7 --- /dev/null +++ b/XrmPluginCore/Helpers/PrivilegeNameResolver.cs @@ -0,0 +1,38 @@ +using System; +using XrmPluginCore.Enums; + +namespace XrmPluginCore.Helpers; + +internal static class PrivilegeNameResolver +{ + /// + /// Builds the Dataverse execute-privilege name for the given on the + /// supplied entity, following the prv{Privilege}{EntityLogicalName} convention. + /// For example, on account resolves to prvReadaccount. + /// + /// + /// Privilege names predate the schema name concept and are based on the entity logical name + /// (e.g. account), so the logical name is used verbatim. + /// + public static string GetExecutePrivilegeName(Privilege privilege, string entityLogicalName) + { + return $"prv{GetPrivilegeVerb(privilege)}{entityLogicalName}"; + } + + private static string GetPrivilegeVerb(Privilege privilege) + { + switch (privilege) + { + case Privilege.Create: return "Create"; + case Privilege.Read: return "Read"; + case Privilege.Write: return "Write"; + case Privilege.Delete: return "Delete"; + case Privilege.Append: return "Append"; + case Privilege.AppendTo: return "AppendTo"; + case Privilege.Assign: return "Assign"; + case Privilege.Share: return "Share"; + default: + throw new ArgumentOutOfRangeException(nameof(privilege), privilege, "Unknown privilege."); + } + } +}