From 0ebacddb2c23b3c31f5d68e40e6dcd1810922888 Mon Sep 17 00:00:00 2001 From: GraceSolutions Date: Wed, 3 Jun 2026 17:21:02 -0400 Subject: [PATCH] M2: Projects CRUD - model, DTOs, mapper, client, 5 cmdlets + tests --- build.ps1 | 9 +- .../ProjectMapperTests.cs | 108 ++++++++++++ .../Cmdlets/GetInfisicalProjectCmdlet.cs | 35 ++++ .../Cmdlets/GetInfisicalProjectsCmdlet.cs | 32 ++++ .../Cmdlets/NewInfisicalProjectCmdlet.cs | 47 +++++ .../Cmdlets/RemoveInfisicalProjectCmdlet.cs | 41 +++++ .../Cmdlets/UpdateInfisicalProjectCmdlet.cs | 44 +++++ src/PSInfisicalAPI/Models/InfisicalProject.cs | 23 +++ .../Projects/InfisicalProjectClient.cs | 162 ++++++++++++++++++ .../Projects/InfisicalProjectDtos.cs | 56 ++++++ .../Projects/InfisicalProjectMapper.cs | 89 ++++++++++ 11 files changed, 644 insertions(+), 2 deletions(-) create mode 100644 src/PSInfisicalAPI.Tests/ProjectMapperTests.cs create mode 100644 src/PSInfisicalAPI/Cmdlets/GetInfisicalProjectCmdlet.cs create mode 100644 src/PSInfisicalAPI/Cmdlets/GetInfisicalProjectsCmdlet.cs create mode 100644 src/PSInfisicalAPI/Cmdlets/NewInfisicalProjectCmdlet.cs create mode 100644 src/PSInfisicalAPI/Cmdlets/RemoveInfisicalProjectCmdlet.cs create mode 100644 src/PSInfisicalAPI/Cmdlets/UpdateInfisicalProjectCmdlet.cs create mode 100644 src/PSInfisicalAPI/Models/InfisicalProject.cs create mode 100644 src/PSInfisicalAPI/Projects/InfisicalProjectClient.cs create mode 100644 src/PSInfisicalAPI/Projects/InfisicalProjectDtos.cs create mode 100644 src/PSInfisicalAPI/Projects/InfisicalProjectMapper.cs diff --git a/build.ps1 b/build.ps1 index ef7a856..8171355 100644 --- a/build.ps1 +++ b/build.ps1 @@ -103,7 +103,12 @@ function Write-Manifest { 'Get-InfisicalSecrets', 'Get-InfisicalSecret', 'ConvertTo-InfisicalSecretDictionary', - 'Export-InfisicalSecrets' + 'Export-InfisicalSecrets', + 'Get-InfisicalProjects', + 'Get-InfisicalProject', + 'New-InfisicalProject', + 'Update-InfisicalProject', + 'Remove-InfisicalProject' ) AliasesToExport = @() VariablesToExport = @() @@ -163,7 +168,7 @@ if (`$null -eq `$manifest) { Import-Module -Name '$($ModuleDirectory.FullName)' -Force -`$cmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecrets','Get-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets') +`$cmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecrets','Get-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProjects','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject') foreach (`$c in `$cmds) { if (-not (Get-Command -Name `$c -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) { throw "Cmdlet not found: `$c" diff --git a/src/PSInfisicalAPI.Tests/ProjectMapperTests.cs b/src/PSInfisicalAPI.Tests/ProjectMapperTests.cs new file mode 100644 index 0000000..5c02718 --- /dev/null +++ b/src/PSInfisicalAPI.Tests/ProjectMapperTests.cs @@ -0,0 +1,108 @@ +using System.Collections; +using System.Reflection; +using PSInfisicalAPI.Models; +using Xunit; + +namespace PSInfisicalAPI.Tests +{ + public class ProjectMapperTests + { + private static readonly System.Type MapperType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly + .GetType("PSInfisicalAPI.Projects.InfisicalProjectMapper", true); + + private static readonly System.Type DtoType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly + .GetType("PSInfisicalAPI.Projects.InfisicalProjectResponseDto", true); + + private static readonly System.Type EnvDtoType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly + .GetType("PSInfisicalAPI.Projects.InfisicalProjectEnvironmentDto", true); + + private static InfisicalProject InvokeMap(object dto) + { + MethodInfo map = MapperType.GetMethod("Map", BindingFlags.Public | BindingFlags.Static); + return (InfisicalProject)map.Invoke(null, new[] { dto }); + } + + [Fact] + public void Map_Null_Dto_Returns_Null() + { + Assert.Null(InvokeMap(null)); + } + + [Fact] + public void Map_Populates_Core_Fields() + { + object dto = System.Activator.CreateInstance(DtoType); + DtoType.GetProperty("Id").SetValue(dto, "proj-001"); + DtoType.GetProperty("Name").SetValue(dto, "DevOps"); + DtoType.GetProperty("Slug").SetValue(dto, "devops"); + DtoType.GetProperty("Description").SetValue(dto, "Internal DevOps project"); + DtoType.GetProperty("Organization").SetValue(dto, "org-abc"); + DtoType.GetProperty("Type").SetValue(dto, "secret-manager"); + DtoType.GetProperty("AutoCapitalization").SetValue(dto, true); + DtoType.GetProperty("CreatedAt").SetValue(dto, "2026-01-15T12:34:56Z"); + DtoType.GetProperty("UpdatedAt").SetValue(dto, "2026-02-20T09:00:00Z"); + + InfisicalProject project = InvokeMap(dto); + + Assert.Equal("proj-001", project.Id); + Assert.Equal("DevOps", project.Name); + Assert.Equal("devops", project.Slug); + Assert.Equal("Internal DevOps project", project.Description); + Assert.Equal("org-abc", project.OrganizationId); + Assert.Equal("secret-manager", project.Type); + Assert.True(project.AutoCapitalization); + Assert.NotNull(project.CreatedAtUtc); + Assert.NotNull(project.UpdatedAtUtc); + Assert.Empty(project.EnvironmentSlugs); + } + + [Fact] + public void Map_Falls_Back_To_InternalId_And_OrgId() + { + object dto = System.Activator.CreateInstance(DtoType); + DtoType.GetProperty("InternalId").SetValue(dto, "internal-id-1"); + DtoType.GetProperty("OrgId").SetValue(dto, "org-fallback"); + + InfisicalProject project = InvokeMap(dto); + + Assert.Equal("internal-id-1", project.Id); + Assert.Equal("org-fallback", project.OrganizationId); + } + + [Fact] + public void Map_Extracts_EnvironmentSlugs() + { + object dto = System.Activator.CreateInstance(DtoType); + DtoType.GetProperty("Id").SetValue(dto, "proj-002"); + + System.Type listType = typeof(System.Collections.Generic.List<>).MakeGenericType(EnvDtoType); + IList envs = (IList)System.Activator.CreateInstance(listType); + + object env1 = System.Activator.CreateInstance(EnvDtoType); + EnvDtoType.GetProperty("Slug").SetValue(env1, "dev"); + EnvDtoType.GetProperty("Name").SetValue(env1, "Development"); + envs.Add(env1); + + object env2 = System.Activator.CreateInstance(EnvDtoType); + EnvDtoType.GetProperty("Slug").SetValue(env2, "prod"); + envs.Add(env2); + + DtoType.GetProperty("Environments").SetValue(dto, envs); + + InfisicalProject project = InvokeMap(dto); + + Assert.Equal(2, project.EnvironmentSlugs.Length); + Assert.Contains("dev", project.EnvironmentSlugs); + Assert.Contains("prod", project.EnvironmentSlugs); + } + + [Fact] + public void MapMany_Null_Returns_Empty() + { + MethodInfo mapMany = MapperType.GetMethod("MapMany", BindingFlags.Public | BindingFlags.Static); + InfisicalProject[] result = (InfisicalProject[])mapMany.Invoke(null, new object[] { null }); + Assert.NotNull(result); + Assert.Empty(result); + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalProjectCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalProjectCmdlet.cs new file mode 100644 index 0000000..979928d --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalProjectCmdlet.cs @@ -0,0 +1,35 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Projects; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "InfisicalProject")] + [OutputType(typeof(InfisicalProject))] + public sealed class GetInfisicalProjectCmdlet : InfisicalCmdletBase + { + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)] + [Alias("Id")] + public string ProjectId { get; set; } + + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalProjectClient client = new InfisicalProjectClient(HttpClient, Logger); + InfisicalProject project = client.Retrieve(connection, ProjectId); + if (project != null) + { + WriteObject(project); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("GetInfisicalProjectCmdlet", "RetrieveProject", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalProjectsCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalProjectsCmdlet.cs new file mode 100644 index 0000000..fa6150a --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalProjectsCmdlet.cs @@ -0,0 +1,32 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Projects; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "InfisicalProjects")] + [OutputType(typeof(InfisicalProject))] + public sealed class GetInfisicalProjectsCmdlet : InfisicalCmdletBase + { + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalProjectClient client = new InfisicalProjectClient(HttpClient, Logger); + InfisicalProject[] projects = client.List(connection); + + foreach (InfisicalProject project in projects) + { + WriteObject(project); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("GetInfisicalProjectsCmdlet", "ListProjects", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/NewInfisicalProjectCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/NewInfisicalProjectCmdlet.cs new file mode 100644 index 0000000..73cd932 --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/NewInfisicalProjectCmdlet.cs @@ -0,0 +1,47 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Projects; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.New, "InfisicalProject", SupportsShouldProcess = true)] + [OutputType(typeof(InfisicalProject))] + public sealed class NewInfisicalProjectCmdlet : InfisicalCmdletBase + { + [Parameter(Mandatory = true, Position = 0)] + [Alias("Name")] + public string ProjectName { get; set; } + + [Parameter] public string Slug { get; set; } + [Parameter] public string Description { get; set; } + [Parameter] public string Type { get; set; } + [Parameter] public string OrganizationId { get; set; } + + protected override void ProcessRecord() + { + try + { + if (!ShouldProcess(ProjectName, "Create Infisical project")) + { + return; + } + + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + string resolvedOrgId = !string.IsNullOrEmpty(OrganizationId) ? OrganizationId : connection.OrganizationId; + + InfisicalProjectClient client = new InfisicalProjectClient(HttpClient, Logger); + InfisicalProject project = client.Create(connection, ProjectName, Slug, Description, Type, resolvedOrgId); + if (project != null) + { + WriteObject(project); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("NewInfisicalProjectCmdlet", "CreateProject", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalProjectCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalProjectCmdlet.cs new file mode 100644 index 0000000..6cd89e1 --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalProjectCmdlet.cs @@ -0,0 +1,41 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Projects; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Remove, "InfisicalProject", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + public sealed class RemoveInfisicalProjectCmdlet : InfisicalCmdletBase + { + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)] + [Alias("Id")] + public string ProjectId { get; set; } + + [Parameter] public SwitchParameter PassThru { get; set; } + + protected override void ProcessRecord() + { + try + { + if (!ShouldProcess(ProjectId, "Remove Infisical project")) + { + return; + } + + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalProjectClient client = new InfisicalProjectClient(HttpClient, Logger); + client.Delete(connection, ProjectId); + + if (PassThru.IsPresent) + { + WriteObject(ProjectId); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("RemoveInfisicalProjectCmdlet", "DeleteProject", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalProjectCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalProjectCmdlet.cs new file mode 100644 index 0000000..3a58b3f --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalProjectCmdlet.cs @@ -0,0 +1,44 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Projects; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsData.Update, "InfisicalProject", SupportsShouldProcess = true)] + [OutputType(typeof(InfisicalProject))] + public sealed class UpdateInfisicalProjectCmdlet : InfisicalCmdletBase + { + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)] + [Alias("Id")] + public string ProjectId { get; set; } + + [Parameter] public string Name { get; set; } + [Parameter] public string Description { get; set; } + [Parameter] public bool? AutoCapitalization { get; set; } + + protected override void ProcessRecord() + { + try + { + if (!ShouldProcess(ProjectId, "Update Infisical project")) + { + return; + } + + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalProjectClient client = new InfisicalProjectClient(HttpClient, Logger); + InfisicalProject project = client.Update(connection, ProjectId, Name, Description, AutoCapitalization); + if (project != null) + { + WriteObject(project); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("UpdateInfisicalProjectCmdlet", "UpdateProject", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Models/InfisicalProject.cs b/src/PSInfisicalAPI/Models/InfisicalProject.cs new file mode 100644 index 0000000..c4ddc2d --- /dev/null +++ b/src/PSInfisicalAPI/Models/InfisicalProject.cs @@ -0,0 +1,23 @@ +using System; + +namespace PSInfisicalAPI.Models +{ + public sealed class InfisicalProject + { + public string Id { get; set; } + public string Name { get; set; } + public string Slug { get; set; } + public string Description { get; set; } + public string OrganizationId { get; set; } + public string Type { get; set; } + public bool AutoCapitalization { get; set; } + public string[] EnvironmentSlugs { get; set; } + public DateTimeOffset? CreatedAtUtc { get; set; } + public DateTimeOffset? UpdatedAtUtc { get; set; } + + public override string ToString() + { + return string.IsNullOrEmpty(Slug) ? (Name ?? Id) : Slug; + } + } +} diff --git a/src/PSInfisicalAPI/Projects/InfisicalProjectClient.cs b/src/PSInfisicalAPI/Projects/InfisicalProjectClient.cs new file mode 100644 index 0000000..a0c5e28 --- /dev/null +++ b/src/PSInfisicalAPI/Projects/InfisicalProjectClient.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Endpoints; +using PSInfisicalAPI.Errors; +using PSInfisicalAPI.Http; +using PSInfisicalAPI.Logging; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Serialization; + +namespace PSInfisicalAPI.Projects +{ + public sealed class InfisicalProjectClient + { + private const string Component = "ProjectClient"; + + private readonly IInfisicalLogger _logger; + private readonly JsonInfisicalSerializer _serializer; + private readonly InfisicalApiInvoker _invoker; + + public InfisicalProjectClient(IInfisicalHttpClient httpClient, IInfisicalLogger logger) + { + if (httpClient == null) { throw new ArgumentNullException(nameof(httpClient)); } + _logger = logger ?? NullInfisicalLogger.Instance; + _serializer = new JsonInfisicalSerializer(); + _invoker = new InfisicalApiInvoker(httpClient); + } + + public InfisicalProject[] List(InfisicalConnection connection) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + + try + { + _logger.Information(Component, "Attempting to list Infisical projects. Please Wait..."); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.ListProjects, "ListProjects", null, null, null); + InfisicalProjectListResponseDto dto = _serializer.Deserialize(response.Body); + response.Clear(); + + List source = (dto != null && dto.Workspaces != null) ? dto.Workspaces : (dto != null ? dto.Projects : null); + InfisicalProject[] mapped = InfisicalProjectMapper.MapMany(source); + _logger.Information(Component, "Infisical project list retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical project list retrieval failed."); + throw; + } + } + + public InfisicalProject Retrieve(InfisicalConnection connection, string projectId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + + Dictionary pathParameters = new Dictionary { { "projectId", projectId } }; + + try + { + _logger.Information(Component, string.Concat("Attempting to retrieve Infisical project '", projectId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.RetrieveProject, "RetrieveProject", pathParameters, null, null); + InfisicalProjectSingleResponseDto dto = _serializer.Deserialize(response.Body); + response.Clear(); + + InfisicalProjectResponseDto inner = dto != null ? (dto.Workspace ?? dto.Project) : null; + InfisicalProject mapped = InfisicalProjectMapper.Map(inner); + _logger.Information(Component, "Infisical project retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical project retrieval failed."); + throw; + } + } + + public InfisicalProject Create(InfisicalConnection connection, string projectName, string slug, string description, string type, string organizationId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(projectName)) { throw new InfisicalConfigurationException("ProjectName is required."); } + + InfisicalProjectCreateRequestDto request = new InfisicalProjectCreateRequestDto + { + ProjectName = projectName, + Slug = slug, + ProjectDescription = description, + Type = type, + OrganizationId = organizationId + }; + + string body = _serializer.Serialize(request); + + try + { + _logger.Information(Component, string.Concat("Attempting to create Infisical project '", projectName, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.CreateProject, "CreateProject", null, null, body); + InfisicalProjectSingleResponseDto dto = _serializer.Deserialize(response.Body); + response.Clear(); + + InfisicalProjectResponseDto inner = dto != null ? (dto.Project ?? dto.Workspace) : null; + InfisicalProject mapped = InfisicalProjectMapper.Map(inner); + _logger.Information(Component, "Infisical project creation was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical project creation failed."); + throw; + } + } + + public InfisicalProject Update(InfisicalConnection connection, string projectId, string name, string description, bool? autoCapitalization) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + + Dictionary pathParameters = new Dictionary { { "projectId", projectId } }; + InfisicalProjectUpdateRequestDto request = new InfisicalProjectUpdateRequestDto { Name = name, Description = description, AutoCapitalization = autoCapitalization }; + string body = _serializer.Serialize(request); + + try + { + _logger.Information(Component, string.Concat("Attempting to update Infisical project '", projectId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.UpdateProject, "UpdateProject", pathParameters, null, body); + InfisicalProjectSingleResponseDto dto = _serializer.Deserialize(response.Body); + response.Clear(); + + InfisicalProjectResponseDto inner = dto != null ? (dto.Workspace ?? dto.Project) : null; + InfisicalProject mapped = InfisicalProjectMapper.Map(inner); + _logger.Information(Component, "Infisical project update was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical project update failed."); + throw; + } + } + + public void Delete(InfisicalConnection connection, string projectId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + + Dictionary pathParameters = new Dictionary { { "projectId", projectId } }; + + try + { + _logger.Information(Component, string.Concat("Attempting to delete Infisical project '", projectId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.DeleteProject, "DeleteProject", pathParameters, null, null); + response.Clear(); + _logger.Information(Component, "Infisical project deletion was successful."); + } + catch (Exception) + { + _logger.Error(Component, "Infisical project deletion failed."); + throw; + } + } + } +} diff --git a/src/PSInfisicalAPI/Projects/InfisicalProjectDtos.cs b/src/PSInfisicalAPI/Projects/InfisicalProjectDtos.cs new file mode 100644 index 0000000..f6f6757 --- /dev/null +++ b/src/PSInfisicalAPI/Projects/InfisicalProjectDtos.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace PSInfisicalAPI.Projects +{ + internal sealed class InfisicalProjectResponseDto + { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("_id")] public string InternalId { get; set; } + [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("slug")] public string Slug { get; set; } + [JsonProperty("description")] public string Description { get; set; } + [JsonProperty("organization")] public string Organization { get; set; } + [JsonProperty("orgId")] public string OrgId { get; set; } + [JsonProperty("type")] public string Type { get; set; } + [JsonProperty("autoCapitalization")] public bool AutoCapitalization { get; set; } + [JsonProperty("createdAt")] public string CreatedAt { get; set; } + [JsonProperty("updatedAt")] public string UpdatedAt { get; set; } + [JsonProperty("environments")] public List Environments { get; set; } + } + + internal sealed class InfisicalProjectEnvironmentDto + { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("slug")] public string Slug { get; set; } + } + + internal sealed class InfisicalProjectListResponseDto + { + [JsonProperty("workspaces")] public List Workspaces { get; set; } + [JsonProperty("projects")] public List Projects { get; set; } + } + + internal sealed class InfisicalProjectSingleResponseDto + { + [JsonProperty("workspace")] public InfisicalProjectResponseDto Workspace { get; set; } + [JsonProperty("project")] public InfisicalProjectResponseDto Project { get; set; } + } + + internal sealed class InfisicalProjectCreateRequestDto + { + [JsonProperty("projectName")] public string ProjectName { get; set; } + [JsonProperty("slug", NullValueHandling = NullValueHandling.Ignore)] public string Slug { get; set; } + [JsonProperty("projectDescription", NullValueHandling = NullValueHandling.Ignore)] public string ProjectDescription { get; set; } + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public string Type { get; set; } + [JsonProperty("organizationId", NullValueHandling = NullValueHandling.Ignore)] public string OrganizationId { get; set; } + } + + internal sealed class InfisicalProjectUpdateRequestDto + { + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; } + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public string Description { get; set; } + [JsonProperty("autoCapitalization", NullValueHandling = NullValueHandling.Ignore)] public bool? AutoCapitalization { get; set; } + } +} diff --git a/src/PSInfisicalAPI/Projects/InfisicalProjectMapper.cs b/src/PSInfisicalAPI/Projects/InfisicalProjectMapper.cs new file mode 100644 index 0000000..3821b1b --- /dev/null +++ b/src/PSInfisicalAPI/Projects/InfisicalProjectMapper.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using PSInfisicalAPI.Models; + +namespace PSInfisicalAPI.Projects +{ + internal static class InfisicalProjectMapper + { + public static InfisicalProject Map(InfisicalProjectResponseDto dto) + { + if (dto == null) + { + return null; + } + + InfisicalProject project = new InfisicalProject + { + Id = !string.IsNullOrEmpty(dto.Id) ? dto.Id : dto.InternalId, + Name = dto.Name, + Slug = dto.Slug, + Description = dto.Description, + OrganizationId = !string.IsNullOrEmpty(dto.Organization) ? dto.Organization : dto.OrgId, + Type = dto.Type, + AutoCapitalization = dto.AutoCapitalization, + EnvironmentSlugs = MapEnvironmentSlugs(dto.Environments), + CreatedAtUtc = ParseTimestamp(dto.CreatedAt), + UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt) + }; + + return project; + } + + public static InfisicalProject[] MapMany(IEnumerable items) + { + if (items == null) + { + return Array.Empty(); + } + + List results = new List(); + foreach (InfisicalProjectResponseDto dto in items) + { + InfisicalProject mapped = Map(dto); + if (mapped != null) + { + results.Add(mapped); + } + } + + return results.ToArray(); + } + + private static string[] MapEnvironmentSlugs(List environments) + { + if (environments == null || environments.Count == 0) + { + return Array.Empty(); + } + + List slugs = new List(environments.Count); + foreach (InfisicalProjectEnvironmentDto env in environments) + { + if (env != null && !string.IsNullOrEmpty(env.Slug)) + { + slugs.Add(env.Slug); + } + } + + return slugs.ToArray(); + } + + private static DateTimeOffset? ParseTimestamp(string value) + { + if (string.IsNullOrEmpty(value)) + { + return null; + } + + DateTimeOffset parsed; + if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out parsed)) + { + return parsed; + } + + return null; + } + } +}