diff --git a/build.ps1 b/build.ps1 index 7ba58a5..78bdbdd 100644 --- a/build.ps1 +++ b/build.ps1 @@ -113,7 +113,12 @@ function Write-Manifest { 'Get-InfisicalEnvironment', 'New-InfisicalEnvironment', 'Update-InfisicalEnvironment', - 'Remove-InfisicalEnvironment' + 'Remove-InfisicalEnvironment', + 'Get-InfisicalFolders', + 'Get-InfisicalFolder', + 'New-InfisicalFolder', + 'Update-InfisicalFolder', + 'Remove-InfisicalFolder' ) AliasesToExport = @() VariablesToExport = @() @@ -173,7 +178,7 @@ if (`$null -eq `$manifest) { Import-Module -Name '$($ModuleDirectory.FullName)' -Force -`$cmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecrets','Get-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProjects','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironments','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment') +`$cmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecrets','Get-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProjects','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironments','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolders','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder') 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/FolderMapperTests.cs b/src/PSInfisicalAPI.Tests/FolderMapperTests.cs new file mode 100644 index 0000000..f4427a7 --- /dev/null +++ b/src/PSInfisicalAPI.Tests/FolderMapperTests.cs @@ -0,0 +1,80 @@ +using System.Reflection; +using PSInfisicalAPI.Models; +using Xunit; + +namespace PSInfisicalAPI.Tests +{ + public class FolderMapperTests + { + private static readonly System.Type MapperType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly + .GetType("PSInfisicalAPI.Folders.InfisicalFolderMapper", true); + + private static readonly System.Type DtoType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly + .GetType("PSInfisicalAPI.Folders.InfisicalFolderResponseDto", true); + + private static InfisicalFolder InvokeMap(object dto, string fallbackProjectId, string fallbackEnvironment) + { + MethodInfo map = MapperType.GetMethod("Map", BindingFlags.Public | BindingFlags.Static); + return (InfisicalFolder)map.Invoke(null, new object[] { dto, fallbackProjectId, fallbackEnvironment }); + } + + [Fact] + public void Map_Null_Returns_Null() + { + Assert.Null(InvokeMap(null, "proj-x", "dev")); + } + + [Fact] + public void Map_Populates_Fields_With_Explicit_ProjectId() + { + object dto = System.Activator.CreateInstance(DtoType); + DtoType.GetProperty("Id").SetValue(dto, "fld-001"); + DtoType.GetProperty("Name").SetValue(dto, "config"); + DtoType.GetProperty("Path").SetValue(dto, "/app/config"); + DtoType.GetProperty("ParentId").SetValue(dto, "fld-root"); + DtoType.GetProperty("Environment").SetValue(dto, "prod"); + DtoType.GetProperty("ProjectId").SetValue(dto, "proj-001"); + + InfisicalFolder folder = InvokeMap(dto, "fallback-proj", "fallback-env"); + + Assert.Equal("fld-001", folder.Id); + Assert.Equal("config", folder.Name); + Assert.Equal("/app/config", folder.Path); + Assert.Equal("fld-root", folder.ParentId); + Assert.Equal("prod", folder.Environment); + Assert.Equal("proj-001", folder.ProjectId); + } + + [Fact] + public void Map_Uses_WorkspaceId_When_ProjectId_Empty() + { + object dto = System.Activator.CreateInstance(DtoType); + DtoType.GetProperty("Id").SetValue(dto, "fld-002"); + DtoType.GetProperty("WorkspaceId").SetValue(dto, "wks-002"); + + InfisicalFolder folder = InvokeMap(dto, "fallback-proj", "fallback-env"); + Assert.Equal("wks-002", folder.ProjectId); + } + + [Fact] + public void Map_Uses_Fallback_When_No_ProjectId_Or_Environment() + { + object dto = System.Activator.CreateInstance(DtoType); + DtoType.GetProperty("Id").SetValue(dto, "fld-003"); + + InfisicalFolder folder = InvokeMap(dto, "fallback-proj", "fallback-env"); + Assert.Equal("fallback-proj", folder.ProjectId); + Assert.Equal("fallback-env", folder.Environment); + } + + [Fact] + public void Map_Falls_Back_To_InternalId_For_Id() + { + object dto = System.Activator.CreateInstance(DtoType); + DtoType.GetProperty("InternalId").SetValue(dto, "internal-fld"); + + InfisicalFolder folder = InvokeMap(dto, "p", "e"); + Assert.Equal("internal-fld", folder.Id); + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalFolderCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalFolderCmdlet.cs new file mode 100644 index 0000000..30da2af --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalFolderCmdlet.cs @@ -0,0 +1,39 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Folders; +using PSInfisicalAPI.Models; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "InfisicalFolder")] + [OutputType(typeof(InfisicalFolder))] + public sealed class GetInfisicalFolderCmdlet : InfisicalCmdletBase + { + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)] + [Alias("Name", "Id")] + public string FolderNameOrId { get; set; } + + [Parameter] public string ProjectId { get; set; } + [Parameter] public string Environment { get; set; } + [Parameter] public string Path { get; set; } + + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalFolderClient client = new InfisicalFolderClient(HttpClient, Logger); + InfisicalFolder folder = client.Retrieve(connection, ProjectId, Environment, Path, FolderNameOrId); + if (folder != null) + { + WriteObject(folder); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("GetInfisicalFolderCmdlet", "RetrieveFolder", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalFoldersCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalFoldersCmdlet.cs new file mode 100644 index 0000000..7c64a2b --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalFoldersCmdlet.cs @@ -0,0 +1,35 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Folders; +using PSInfisicalAPI.Models; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "InfisicalFolders")] + [OutputType(typeof(InfisicalFolder))] + public sealed class GetInfisicalFoldersCmdlet : InfisicalCmdletBase + { + [Parameter] public string ProjectId { get; set; } + [Parameter] public string Environment { get; set; } + [Parameter] public string Path { get; set; } + + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalFolderClient client = new InfisicalFolderClient(HttpClient, Logger); + InfisicalFolder[] folders = client.List(connection, ProjectId, Environment, Path); + foreach (InfisicalFolder folder in folders) + { + WriteObject(folder); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("GetInfisicalFoldersCmdlet", "ListFolders", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/NewInfisicalFolderCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/NewInfisicalFolderCmdlet.cs new file mode 100644 index 0000000..ac1d044 --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/NewInfisicalFolderCmdlet.cs @@ -0,0 +1,41 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Folders; +using PSInfisicalAPI.Models; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.New, "InfisicalFolder", SupportsShouldProcess = true)] + [OutputType(typeof(InfisicalFolder))] + public sealed class NewInfisicalFolderCmdlet : InfisicalCmdletBase + { + [Parameter(Mandatory = true, Position = 0)] public string Name { get; set; } + [Parameter] public string ProjectId { get; set; } + [Parameter] public string Environment { get; set; } + [Parameter] public string Path { get; set; } + + protected override void ProcessRecord() + { + try + { + if (!ShouldProcess(Name, "Create Infisical folder")) + { + return; + } + + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalFolderClient client = new InfisicalFolderClient(HttpClient, Logger); + InfisicalFolder folder = client.Create(connection, ProjectId, Environment, Name, Path); + if (folder != null) + { + WriteObject(folder); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("NewInfisicalFolderCmdlet", "CreateFolder", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalFolderCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalFolderCmdlet.cs new file mode 100644 index 0000000..825bdf3 --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalFolderCmdlet.cs @@ -0,0 +1,44 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Folders; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Remove, "InfisicalFolder", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + public sealed class RemoveInfisicalFolderCmdlet : InfisicalCmdletBase + { + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)] + [Alias("Id")] + public string FolderId { get; set; } + + [Parameter] public string ProjectId { get; set; } + [Parameter] public string Environment { get; set; } + [Parameter] public string Path { get; set; } + [Parameter] public SwitchParameter PassThru { get; set; } + + protected override void ProcessRecord() + { + try + { + if (!ShouldProcess(FolderId, "Remove Infisical folder")) + { + return; + } + + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalFolderClient client = new InfisicalFolderClient(HttpClient, Logger); + client.Delete(connection, ProjectId, Environment, FolderId, Path); + + if (PassThru.IsPresent) + { + WriteObject(FolderId); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("RemoveInfisicalFolderCmdlet", "DeleteFolder", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalFolderCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalFolderCmdlet.cs new file mode 100644 index 0000000..891b398 --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalFolderCmdlet.cs @@ -0,0 +1,45 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Folders; +using PSInfisicalAPI.Models; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsData.Update, "InfisicalFolder", SupportsShouldProcess = true)] + [OutputType(typeof(InfisicalFolder))] + public sealed class UpdateInfisicalFolderCmdlet : InfisicalCmdletBase + { + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)] + [Alias("Id")] + public string FolderId { get; set; } + + [Parameter(Mandatory = true, Position = 1)] public string Name { get; set; } + [Parameter] public string ProjectId { get; set; } + [Parameter] public string Environment { get; set; } + [Parameter] public string Path { get; set; } + + protected override void ProcessRecord() + { + try + { + if (!ShouldProcess(FolderId, "Update Infisical folder")) + { + return; + } + + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalFolderClient client = new InfisicalFolderClient(HttpClient, Logger); + InfisicalFolder folder = client.Update(connection, ProjectId, Environment, FolderId, Name, Path); + if (folder != null) + { + WriteObject(folder); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("UpdateInfisicalFolderCmdlet", "UpdateFolder", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Folders/InfisicalFolderClient.cs b/src/PSInfisicalAPI/Folders/InfisicalFolderClient.cs new file mode 100644 index 0000000..4722add --- /dev/null +++ b/src/PSInfisicalAPI/Folders/InfisicalFolderClient.cs @@ -0,0 +1,195 @@ +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.Folders +{ + public sealed class InfisicalFolderClient + { + private const string Component = "FolderClient"; + + private readonly IInfisicalLogger _logger; + private readonly JsonInfisicalSerializer _serializer; + private readonly InfisicalApiInvoker _invoker; + + public InfisicalFolderClient(IInfisicalHttpClient httpClient, IInfisicalLogger logger) + { + if (httpClient == null) { throw new ArgumentNullException(nameof(httpClient)); } + _logger = logger ?? NullInfisicalLogger.Instance; + _serializer = new JsonInfisicalSerializer(); + _invoker = new InfisicalApiInvoker(httpClient); + } + + public InfisicalFolder[] List(InfisicalConnection connection, string projectId, string environment, string path) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); + string resolvedEnvironment = FirstNonEmpty(environment, connection.Environment); + string resolvedPath = FirstNonEmpty(path, connection.DefaultSecretPath, "/"); + if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(resolvedEnvironment)) { throw new InfisicalConfigurationException("Environment is required."); } + + List> queryParameters = new List> + { + new KeyValuePair("workspaceId", resolvedProjectId), + new KeyValuePair("environment", resolvedEnvironment), + new KeyValuePair("path", resolvedPath) + }; + + try + { + _logger.Information(Component, "Attempting to list Infisical folders. Please Wait..."); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.ListFolders, "ListFolders", null, queryParameters, null); + InfisicalFolderListResponseDto dto = _serializer.Deserialize(response.Body); + response.Clear(); + + InfisicalFolder[] mapped = InfisicalFolderMapper.MapMany(dto != null ? dto.Folders : null, resolvedProjectId, resolvedEnvironment); + _logger.Information(Component, "Infisical folder list retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical folder list retrieval failed."); + throw; + } + } + + public InfisicalFolder Retrieve(InfisicalConnection connection, string projectId, string environment, string path, string folderNameOrId) + { + if (string.IsNullOrEmpty(folderNameOrId)) { throw new InfisicalConfigurationException("Folder name or id is required."); } + + InfisicalFolder[] all = List(connection, projectId, environment, path); + foreach (InfisicalFolder folder in all) + { + if (string.Equals(folder.Id, folderNameOrId, StringComparison.OrdinalIgnoreCase) || + string.Equals(folder.Name, folderNameOrId, StringComparison.OrdinalIgnoreCase)) + { + return folder; + } + } + + return null; + } + + public InfisicalFolder Create(InfisicalConnection connection, string projectId, string environment, string name, string path) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); + string resolvedEnvironment = FirstNonEmpty(environment, connection.Environment); + string resolvedPath = FirstNonEmpty(path, connection.DefaultSecretPath, "/"); + if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(resolvedEnvironment)) { throw new InfisicalConfigurationException("Environment is required."); } + if (string.IsNullOrEmpty(name)) { throw new InfisicalConfigurationException("Name is required."); } + + InfisicalFolderCreateRequestDto request = new InfisicalFolderCreateRequestDto + { + WorkspaceId = resolvedProjectId, + Environment = resolvedEnvironment, + Name = name, + Path = resolvedPath + }; + string body = _serializer.Serialize(request); + + try + { + _logger.Information(Component, string.Concat("Attempting to create Infisical folder '", name, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.CreateFolder, "CreateFolder", null, null, body); + InfisicalFolderSingleResponseDto dto = _serializer.Deserialize(response.Body); + response.Clear(); + + InfisicalFolder mapped = InfisicalFolderMapper.Map(dto != null ? dto.Folder : null, resolvedProjectId, resolvedEnvironment); + _logger.Information(Component, "Infisical folder creation was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical folder creation failed."); + throw; + } + } + + public InfisicalFolder Update(InfisicalConnection connection, string projectId, string environment, string folderId, string name, string path) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); + string resolvedEnvironment = FirstNonEmpty(environment, connection.Environment); + string resolvedPath = FirstNonEmpty(path, connection.DefaultSecretPath, "/"); + if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(resolvedEnvironment)) { throw new InfisicalConfigurationException("Environment is required."); } + if (string.IsNullOrEmpty(folderId)) { throw new InfisicalConfigurationException("FolderId is required."); } + if (string.IsNullOrEmpty(name)) { throw new InfisicalConfigurationException("Name is required."); } + + Dictionary pathParameters = new Dictionary { { "folderId", folderId } }; + InfisicalFolderUpdateRequestDto request = new InfisicalFolderUpdateRequestDto + { + WorkspaceId = resolvedProjectId, + Environment = resolvedEnvironment, + Name = name, + Path = resolvedPath + }; + string body = _serializer.Serialize(request); + + try + { + _logger.Information(Component, string.Concat("Attempting to update Infisical folder '", folderId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.UpdateFolder, "UpdateFolder", pathParameters, null, body); + InfisicalFolderSingleResponseDto dto = _serializer.Deserialize(response.Body); + response.Clear(); + + InfisicalFolder mapped = InfisicalFolderMapper.Map(dto != null ? dto.Folder : null, resolvedProjectId, resolvedEnvironment); + _logger.Information(Component, "Infisical folder update was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical folder update failed."); + throw; + } + } + + public void Delete(InfisicalConnection connection, string projectId, string environment, string folderId, string path) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); + string resolvedEnvironment = FirstNonEmpty(environment, connection.Environment); + string resolvedPath = FirstNonEmpty(path, connection.DefaultSecretPath, "/"); + if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(resolvedEnvironment)) { throw new InfisicalConfigurationException("Environment is required."); } + if (string.IsNullOrEmpty(folderId)) { throw new InfisicalConfigurationException("FolderId is required."); } + + Dictionary pathParameters = new Dictionary { { "folderId", folderId } }; + List> queryParameters = new List> + { + new KeyValuePair("workspaceId", resolvedProjectId), + new KeyValuePair("environment", resolvedEnvironment), + new KeyValuePair("path", resolvedPath) + }; + + try + { + _logger.Information(Component, string.Concat("Attempting to delete Infisical folder '", folderId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.DeleteFolder, "DeleteFolder", pathParameters, queryParameters, null); + response.Clear(); + _logger.Information(Component, "Infisical folder deletion was successful."); + } + catch (Exception) + { + _logger.Error(Component, "Infisical folder deletion failed."); + throw; + } + } + + private static string FirstNonEmpty(params string[] values) + { + if (values == null) { return null; } + foreach (string value in values) { if (!string.IsNullOrEmpty(value)) { return value; } } + return null; + } + } +} diff --git a/src/PSInfisicalAPI/Folders/InfisicalFolderDtos.cs b/src/PSInfisicalAPI/Folders/InfisicalFolderDtos.cs new file mode 100644 index 0000000..5c29e2a --- /dev/null +++ b/src/PSInfisicalAPI/Folders/InfisicalFolderDtos.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace PSInfisicalAPI.Folders +{ + internal sealed class InfisicalFolderResponseDto + { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("_id")] public string InternalId { get; set; } + [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("path")] public string Path { get; set; } + [JsonProperty("parentId")] public string ParentId { get; set; } + [JsonProperty("envId")] public string EnvId { get; set; } + [JsonProperty("environment")] public string Environment { get; set; } + [JsonProperty("projectId")] public string ProjectId { get; set; } + [JsonProperty("workspaceId")] public string WorkspaceId { get; set; } + [JsonProperty("createdAt")] public string CreatedAt { get; set; } + [JsonProperty("updatedAt")] public string UpdatedAt { get; set; } + } + + internal sealed class InfisicalFolderListResponseDto + { + [JsonProperty("folders")] public List Folders { get; set; } + } + + internal sealed class InfisicalFolderSingleResponseDto + { + [JsonProperty("folder")] public InfisicalFolderResponseDto Folder { get; set; } + } + + internal sealed class InfisicalFolderCreateRequestDto + { + [JsonProperty("workspaceId")] public string WorkspaceId { get; set; } + [JsonProperty("environment")] public string Environment { get; set; } + [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("path", NullValueHandling = NullValueHandling.Ignore)] public string Path { get; set; } + [JsonProperty("directory", NullValueHandling = NullValueHandling.Ignore)] public string Directory { get; set; } + } + + internal sealed class InfisicalFolderUpdateRequestDto + { + [JsonProperty("workspaceId")] public string WorkspaceId { get; set; } + [JsonProperty("environment")] public string Environment { get; set; } + [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("path", NullValueHandling = NullValueHandling.Ignore)] public string Path { get; set; } + } +} diff --git a/src/PSInfisicalAPI/Folders/InfisicalFolderMapper.cs b/src/PSInfisicalAPI/Folders/InfisicalFolderMapper.cs new file mode 100644 index 0000000..9fa8b79 --- /dev/null +++ b/src/PSInfisicalAPI/Folders/InfisicalFolderMapper.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using PSInfisicalAPI.Models; + +namespace PSInfisicalAPI.Folders +{ + internal static class InfisicalFolderMapper + { + public static InfisicalFolder Map(InfisicalFolderResponseDto dto, string fallbackProjectId, string fallbackEnvironment) + { + if (dto == null) + { + return null; + } + + string projectId = !string.IsNullOrEmpty(dto.ProjectId) + ? dto.ProjectId + : (!string.IsNullOrEmpty(dto.WorkspaceId) ? dto.WorkspaceId : fallbackProjectId); + + string environment = !string.IsNullOrEmpty(dto.Environment) ? dto.Environment : fallbackEnvironment; + + return new InfisicalFolder + { + Id = !string.IsNullOrEmpty(dto.Id) ? dto.Id : dto.InternalId, + Name = dto.Name, + Path = dto.Path, + ParentId = dto.ParentId, + Environment = environment, + ProjectId = projectId, + CreatedAtUtc = ParseTimestamp(dto.CreatedAt), + UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt) + }; + } + + public static InfisicalFolder[] MapMany(IEnumerable items, string fallbackProjectId, string fallbackEnvironment) + { + if (items == null) + { + return Array.Empty(); + } + + List results = new List(); + foreach (InfisicalFolderResponseDto dto in items) + { + InfisicalFolder mapped = Map(dto, fallbackProjectId, fallbackEnvironment); + if (mapped != null) + { + results.Add(mapped); + } + } + + return results.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; + } + } +} diff --git a/src/PSInfisicalAPI/Models/InfisicalFolder.cs b/src/PSInfisicalAPI/Models/InfisicalFolder.cs new file mode 100644 index 0000000..17e4ca3 --- /dev/null +++ b/src/PSInfisicalAPI/Models/InfisicalFolder.cs @@ -0,0 +1,21 @@ +using System; + +namespace PSInfisicalAPI.Models +{ + public sealed class InfisicalFolder + { + public string Id { get; set; } + public string Name { get; set; } + public string Path { get; set; } + public string ParentId { get; set; } + public string Environment { get; set; } + public string ProjectId { get; set; } + public DateTimeOffset? CreatedAtUtc { get; set; } + public DateTimeOffset? UpdatedAtUtc { get; set; } + + public override string ToString() + { + return string.IsNullOrEmpty(Path) ? (Name ?? Id) : Path; + } + } +}