From bfaa3ee00343be575dbbdd55fce9648f8fa75c90 Mon Sep 17 00:00:00 2001 From: Marc Collins Date: Tue, 19 Nov 2024 13:07:01 +1100 Subject: [PATCH] Restructured module with improved organization and added documentation - Reorganized functions into private/public folders for clarity. - Added help documentation for functions. - Implemented GitHub Actions for build and release workflows. --- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .github/workflows/BuildModule.yml | 65 ++++ .github/workflows/Release.yml | 32 ++ .gitignore | 2 + LICENSE | 2 +- Module_Config.json | 22 ++ QlikNPrinting-CLI.psm1 | Bin 67588 -> 3612 bytes src/Private/AuthenticateNPrinting.ps1 | 25 ++ src/Private/DecodeUnicodeEscapes.ps1 | 12 + src/Private/GetNPFilter.ps1 | 28 ++ src/Private/GetXSRFToken.ps1 | 25 ++ src/Private/SetTrustAllCertificates.ps1 | 27 ++ src/Public/Connect-NPrinting.ps1 | 153 ++++++++ src/Public/Get-NPConnections.ps1 | 31 ++ src/Public/Get-NPFilters.ps1 | 33 ++ src/Public/Get-NPGroups.ps1 | 62 ++++ src/Public/Get-NPRoles.ps1 | 96 +++++ src/Public/Get-NPTasks.ps1 | 93 +++++ src/Public/Invoke-NPRequest.ps1 | 186 ++++++++++ .../BuildFiles/Scripts/DynamicLoading.ps1 | 35 ++ .../BuildFiles/Scripts/ModuleVersion.ps1 | 37 ++ src/Resources/BuildModule.ps1 | 333 ++++++++++++++++++ 22 files changed, 1299 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/BuildModule.yml create mode 100644 .github/workflows/Release.yml create mode 100644 Module_Config.json create mode 100644 src/Private/AuthenticateNPrinting.ps1 create mode 100644 src/Private/DecodeUnicodeEscapes.ps1 create mode 100644 src/Private/GetNPFilter.ps1 create mode 100644 src/Private/GetXSRFToken.ps1 create mode 100644 src/Private/SetTrustAllCertificates.ps1 create mode 100644 src/Public/Connect-NPrinting.ps1 create mode 100644 src/Public/Get-NPConnections.ps1 create mode 100644 src/Public/Get-NPFilters.ps1 create mode 100644 src/Public/Get-NPGroups.ps1 create mode 100644 src/Public/Get-NPRoles.ps1 create mode 100644 src/Public/Get-NPTasks.ps1 create mode 100644 src/Public/Invoke-NPRequest.ps1 create mode 100644 src/Resources/BuildFiles/Scripts/DynamicLoading.ps1 create mode 100644 src/Resources/BuildFiles/Scripts/ModuleVersion.ps1 create mode 100644 src/Resources/BuildModule.ps1 diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 2a9b062..aee3fad 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -7,7 +7,7 @@ about: Suggest an idea for this project **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -**Is there and [API](https://help.qlik.com/en-US/nprinting/November2018/APIs/NP+API/index.html) that needs to be implemented? If so which one.** +**Is there and [API](https://help.qlik.com/en-US/nprinting/csh/Content/NPrinting/Extending/NPrinting-APIs-Reference-Redirect.htm) that needs to be implemented? If so which one.** **Describe the solution you'd like** A clear and concise description of what you want to happen. diff --git a/.github/workflows/BuildModule.yml b/.github/workflows/BuildModule.yml new file mode 100644 index 0000000..391cd86 --- /dev/null +++ b/.github/workflows/BuildModule.yml @@ -0,0 +1,65 @@ +# This is a basic workflow to help you get started with Actions + +name: Module Build + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the "main" branch + push: + branches-ignore: [ 'main', '**-alpha', '**-dev' ] + paths-ignore: + - '.github/workflows/**' + - 'docs/**' + pull_request: + branches: [ "main" ] + paths-ignore: + - '.github/workflows/**' + - 'docs/**' + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + build-module: + name: Build Module Manifest + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + - name: Build the PowerShell Module + id: pwsh-build + shell: pwsh + env: + GH_TOKEN: ${{ secrets.API_TOKEN }} + run: | + Get-ChildItem -Recurse -Include "BuildModule.ps1"|%{. $_.FullName} + - name: Build the Readme + uses: baileyjm02/markdown-to-pdf@v1 + with: + input_dir: ${{ steps.pwsh-build.outputs.ModuleDir }} + output_dir: ${{ steps.pwsh-build.outputs.ModuleDir }} + # Default is true, can set to false to only get PDF files + table_of_contents: value + build_html: true + build_pdf: true + - name: Upload test results + uses: actions/upload-artifact@v3 + id: artifact + with: + name: ${{ steps.pwsh-build.outputs.ModuleName }} + path: ${{ steps.pwsh-build.outputs.ModuleDir }} + - name: Release + uses: softprops/action-gh-release@v1 + if: ${{ github.ref == 'refs/heads/main' }} + with: + token: ${{ secrets.API_TOKEN }} + name: ${{ steps.pwsh-build.outputs.ModuleName }} - ${{ steps.pwsh-build.outputs.Version }} + tag_name: ${{ steps.pwsh-build.outputs.Version }} + generate_release_notes: true + draft: true + files: | + ${{ steps.pwsh-build.outputs.ModuleZip }} diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml new file mode 100644 index 0000000..c192705 --- /dev/null +++ b/.github/workflows/Release.yml @@ -0,0 +1,32 @@ +name: Publish Module + +on: + release: + types: [published] + + workflow_dispatch: + +jobs: + notification: + runs-on: ubuntu-latest + env: + REQUIRED_TOKEN: ${{ secrets.PWSHGALLERY_TOKEN != '' }} + + steps: + - name: Download release asset + if: ${{ env.REQUIRED_TOKEN == 'true' }} + id: download-release-asset + uses: robinraju/release-downloader@v1.5 + with: + latest: true + #zipBall: true + token: ${{ secrets.API_TOKEN }} + - name: Release the PowerShell Module + if: ${{ env.REQUIRED_TOKEN == 'true' }} + id: pwsh-release + shell: pwsh + env: + PWSHGALLERY_TOKEN: ${{ secrets.PWSHGALLERY_TOKEN }} + run: | + Get-ChildItem -File -Filter "*.zip"|Expand-Archive + Get-ChildItem -File -Filter "*.psd1" -Recurse |%{Publish-Module -Path $_.Directory.FullName -NuGetApiKey $ENV:PWSHGALLERY_TOKEN} \ No newline at end of file diff --git a/.gitignore b/.gitignore index e69de29..597dc0d 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,2 @@ +QlikNPrinting-CLI/* +QlikNPrinting-CLI.zip diff --git a/LICENSE b/LICENSE index c49c212..85691db 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Marc Collins +Copyright (c) 2024 Marc Collins Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Module_Config.json b/Module_Config.json new file mode 100644 index 0000000..f7310fc --- /dev/null +++ b/Module_Config.json @@ -0,0 +1,22 @@ +{ + "RepositoryURI": "https://github.com/QlikProfessionalServices/QlikNPrinting-CLI", + "ModuleName": "QlikNPrinting-CLI", + "ModuleManifest": { + "Author": "Marc Collins", + "CompanyName": "Qlik - Customer Success", + "Copyright": null, + "Description": "The Qlik NPrinting CLI is a PowerShell module designed to interact with and manage Qlik NPrinting environments programmatically", + "DotNetFrameworkVersion": null, + "FileList": null, + "FunctionsToExport": null, + "Guid": "eca92804-c4ca-4aa8-9313-44d71005379d", + "HelpInfoUri": null, + "LicenseUri": null, + "ModuleVersion": "1.2", + "PrivateData": null, + "ProjectUri": null, + "ReleaseNotes": null, + "Tags": null + }, + "AllowDynamicContent": true +} \ No newline at end of file diff --git a/QlikNPrinting-CLI.psm1 b/QlikNPrinting-CLI.psm1 index 513d79241dc2287ce5a9b38df316a91cd5996b70..86398977dc378643ab3c7ef44fb8f6c02bc6a727 100644 GIT binary patch literal 3612 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 diff --git a/src/Private/AuthenticateNPrinting.ps1 b/src/Private/AuthenticateNPrinting.ps1 new file mode 100644 index 0000000..e095ca0 --- /dev/null +++ b/src/Private/AuthenticateNPrinting.ps1 @@ -0,0 +1,25 @@ +function AuthenticateNPrinting { + param ( + [string]$AuthScheme, + [pscredential]$Credentials + ) + + $LoginUrl = if ($AuthScheme -eq 'NPrinting') { + "$($script:NPEnv.URLServerBase)/login" + } else { + "$($script:NPEnv.URLServerAPI)/login/$AuthScheme" + } + + Write-Verbose "Authenticating to $LoginUrl" + if ($AuthScheme -eq 'NPrinting' -and $Credentials) { + $Body = @{ + username = $Credentials.UserName + password = $Credentials.GetNetworkCredential().Password + } | ConvertTo-Json -Depth 3 + + return Invoke-NPRequest -Path $LoginUrl -method 'Post' -Data $Body + } + + return Invoke-NPRequest -Path $LoginUrl -method 'Get' +} + \ No newline at end of file diff --git a/src/Private/DecodeUnicodeEscapes.ps1 b/src/Private/DecodeUnicodeEscapes.ps1 new file mode 100644 index 0000000..73bd607 --- /dev/null +++ b/src/Private/DecodeUnicodeEscapes.ps1 @@ -0,0 +1,12 @@ +function DecodeUnicodeEscapes { + param ( + [string]$InputString + ) + + $decoded = [regex]::Replace($InputString, '\\u([0-9a-fA-F]{4})', { + param($match) + [char]::ConvertFromUtf32([int]::Parse($match.Groups[1].Value, [System.Globalization.NumberStyles]::HexNumber)) + }) + + return $decoded +} \ No newline at end of file diff --git a/src/Private/GetNPFilter.ps1 b/src/Private/GetNPFilter.ps1 new file mode 100644 index 0000000..e0141c8 --- /dev/null +++ b/src/Private/GetNPFilter.ps1 @@ -0,0 +1,28 @@ +function GetNPFilter { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, HelpMessage = "The property to filter by.")] + [string]$Property, + + [Parameter(Mandatory = $true, HelpMessage = "The value to filter for.")] + [string]$Value, + + [Parameter(Mandatory = $true, HelpMessage = "The existing filter string.")] + [string]$Filter + ) + + # Process the property and value for filtering + if ($null -ne $Property -and $null -ne $Value) { + # Replace wildcard character `*` with `%` + $Value = $Value -replace '\*', '%' + + # Determine the query separator based on the current filter + $QuerySeparator = if ($Filter.StartsWith('?')) { '&' } else { '?' } + + # Append the new filter clause + $Filter = "$Filter$QuerySeparator$Property=$Value" + } + + # Return the updated filter string + return $Filter +} diff --git a/src/Private/GetXSRFToken.ps1 b/src/Private/GetXSRFToken.ps1 new file mode 100644 index 0000000..668278b --- /dev/null +++ b/src/Private/GetXSRFToken.ps1 @@ -0,0 +1,25 @@ + +function GetXSRFToken { + [CmdletBinding()] + param ( + [Parameter(HelpMessage = 'Return only the raw token value instead of the header dictionary.')] + [switch]$Raw + ) + + try { + # Retrieve the XSRF token from the cookies + $Token = $script:NPEnv.WebRequestSession.Cookies.GetCookies($script:NPEnv.URLServerBase) | Where-Object { $_.Name -eq 'NPWEBCONSOLE_XSRF-TOKEN' } + + # Return the raw token value or header dictionary based on the Raw parameter + if ($Raw) { + return $Token.Value + } else { + # Create a header dictionary with the token + $Header = [System.Collections.Generic.Dictionary[String, String]]::new() + $Header.Add('X-XSRF-TOKEN', $Token.Value) + return $Header + } + } catch { + Write-Error "Failed to retrieve XSRF token: $_" + } +} diff --git a/src/Private/SetTrustAllCertificates.ps1 b/src/Private/SetTrustAllCertificates.ps1 new file mode 100644 index 0000000..2f4b582 --- /dev/null +++ b/src/Private/SetTrustAllCertificates.ps1 @@ -0,0 +1,27 @@ + +function SetTrustAllCertificates { + if (-not ('CTrustAllCerts' -as [type])) { + Add-Type -TypeDefinition @' + using System; + using System.Net; + using System.Net.Security; + using System.Security.Cryptography.X509Certificates; + + public static class CTrustAllCerts { + public static bool ReturnTrue(object sender, + X509Certificate certificate, + X509Chain chain, + SslPolicyErrors sslPolicyErrors) { return true; } + + public static RemoteCertificateValidationCallback GetDelegate() { + return new RemoteCertificateValidationCallback(CTrustAllCerts.ReturnTrue); + } + } +'@ + Write-Verbose -Message 'Added Cert Ignore Type' + } + + [System.Net.ServicePointManager]::ServerCertificateValidationCallback = [CTrustAllCerts]::GetDelegate() + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Write-Verbose -Message 'Server Certificate Validation Bypass' +} \ No newline at end of file diff --git a/src/Public/Connect-NPrinting.ps1 b/src/Public/Connect-NPrinting.ps1 new file mode 100644 index 0000000..bcf5b10 --- /dev/null +++ b/src/Public/Connect-NPrinting.ps1 @@ -0,0 +1,153 @@ +<# +.SYNOPSIS + Creates an authenticated session token. + +.DESCRIPTION + Connect-NPrinting initializes the `$Script:NPEnv` variable, + which is used for authenticated requests to the NPrinting server. + +.PARAMETER Prefix + The protocol prefix (http or https). + +.PARAMETER Computer + The NPrinting server hostname or IP address. + +.PARAMETER Port + The port to connect to (default: 4993). + +.PARAMETER Return + If specified, returns the authenticated user ID. + +.PARAMETER Credentials + The credentials to use for authentication. + +.PARAMETER TrustAllCertificates + If specified, bypasses SSL certificate validation. + +.PARAMETER AuthScheme + Authentication scheme (ntlm or NPrinting). + +.EXAMPLE + Connect-NPrinting -Computer 'nprinting-server' -Credentials (Get-Credential) + + This example connects to the NPrinting server using the specified credentials. + +.EXAMPLE + Connect-NPrinting -Computer 'nprinting-server' -TrustAllCertificates + + This example connects to the NPrinting server and bypasses SSL certificate validation. + +.NOTES + For more information, visit the NPrinting API documentation. + +#> +function Connect-NPrinting { + [CmdletBinding(DefaultParameterSetName = 'Default')] + param ( + [Parameter(ParameterSetName = 'Default')] + [Parameter(ParameterSetName = 'Creds')] + [ValidateSet('http', 'https')] + [string]$Prefix = 'https', + + [Parameter(ParameterSetName = 'Default')] + [Parameter(ParameterSetName = 'Creds')] + [string]$Computer = $env:COMPUTERNAME, + + [Parameter(ParameterSetName = 'Default')] + [Parameter(ParameterSetName = 'Creds')] + [string]$Port = '4993', + + [Parameter(ParameterSetName = 'Default')] + [Parameter(ParameterSetName = 'Creds')] + [switch]$Return, + + [Parameter(ParameterSetName = 'Creds', Mandatory = $true)] + [pscredential]$Credentials, + + [Parameter(ParameterSetName = 'Default')] + [Parameter(ParameterSetName = 'Creds')] + [Alias('TrustAllCerts')] + [switch]$TrustAllCertificates, + + [Parameter(ParameterSetName = 'Default')] + [Parameter(ParameterSetName = 'Creds')] + [ValidateSet('ntlm', 'NPrinting')] + [string]$AuthScheme = 'ntlm' + ) + + # Enforce dynamic validation for NPrinting AuthScheme + if ($AuthScheme -eq 'NPrinting' -and $PSCmdlet.ParameterSetName -ne 'Creds') { + throw "Credentials are mandatory when AuthScheme is set to 'NPrinting'. Please use the 'Creds' parameter set." + } + + # Define API paths + $APIPath = 'api' + $APIVersion = 'v1' + + # Trust all certificates if specified + if ($TrustAllCertificates) { + SetTrustAllCertificates + Write-Verbose 'TrustAllCertificates enabled' + } + + # Validate local service if connecting to localhost + if ($Computer -eq $env:COMPUTERNAME) { + try { + Get-Service -Name 'QlikNPrintingWebEngine' -ErrorAction Stop | Out-Null + } catch { + Write-Error "Local service 'QlikNPrintingWebEngine' is not running." + return + } + } + + # Parse Computer parameter for protocol and port + if ($Computer -match ':') { + if ($Computer -match '^http') { + $Prefix, $Computer = $Computer -split '://' + } + if ($Computer -match ':') { + $Computer, $Port = $Computer -split ':' + } + } + + # Initialize environment + $CookieContainer = New-Object System.Net.CookieContainer + # Construct base URL + $BaseURL = "$($Prefix)://$($Computer):$($Port)" + + # Initialize the NPEnv hash table + $script:NPEnv = @{ + TrustAllCertificates = $TrustAllCertificates + Prefix = $Prefix + Computer = $Computer + Port = $Port + API = $APIPath + APIVersion = $APIVersion + URLServerBase = $BaseURL + URLServerAPI = "$BaseURL/$APIPath/$APIVersion" + URLServerNPE = "$BaseURL/npe" + WebRequestSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession + } + + $script:NPEnv.WebRequestSession.UserAgent = 'Windows' + $script:NPEnv.WebRequestSession.Cookies = $CookieContainer + + # Handle authentication based on parameter set + switch ($PSCmdlet.ParameterSetName) { + 'Default' { + $script:NPEnv.WebRequestSession.UseDefaultCredentials = $true + } + 'Creds' { + $script:NPEnv.WebRequestSession.Credentials = $Credentials + } + } + + # Authenticate and get token + $AuthToken = AuthenticateNPrinting -AuthScheme $AuthScheme -Credentials $Credentials + + # Return token if requested + if ($Return) { + return $AuthToken + } +} + diff --git a/src/Public/Get-NPConnections.ps1 b/src/Public/Get-NPConnections.ps1 new file mode 100644 index 0000000..c8e1e7b --- /dev/null +++ b/src/Public/Get-NPConnections.ps1 @@ -0,0 +1,31 @@ +<# +.SYNOPSIS + Retrieves the list of NPrinting connections. + +.DESCRIPTION + The Get-NPConnections function retrieves the list of connections from the NPrinting server. + It uses the Invoke-NPRequest function to send a GET request to the 'connections' endpoint of the NPrinting API. + +.EXAMPLE + Get-NPConnections + + This example retrieves all NPrinting connections. + +.NOTES + For more information, visit the NPrinting API documentation: + https://help.qlik.com/en-US/nprinting/February2024/APIs/NP+API/index.html?page=19#Connections + +.LINK + https://help.qlik.com/en-US/nprinting/February2024/APIs/NP+API/index.html?page=19#Connections +#> +function Get-NPConnections { + [CmdletBinding()] + param() + try { + # Retrieve the NPrinting Connections + $NPConnections = Invoke-NPRequest -Path 'connections' -Method Get + return $NPConnections + } catch { + Write-Error "Failed to retrieve NPrinting connections: $_" + } +} \ No newline at end of file diff --git a/src/Public/Get-NPFilters.ps1 b/src/Public/Get-NPFilters.ps1 new file mode 100644 index 0000000..08282f8 --- /dev/null +++ b/src/Public/Get-NPFilters.ps1 @@ -0,0 +1,33 @@ +<# +.SYNOPSIS + Retrieves the list of NPrinting filters. + +.DESCRIPTION + The Get-NPFilters function retrieves the list of filters from the NPrinting server. + It uses the Invoke-NPRequest function to send a GET request to the 'filters' endpoint of the NPrinting API. + +.EXAMPLE + Get-NPFilters + + This example retrieves all NPrinting filters. + +.NOTES + For more information, visit the NPrinting API documentation: + https://help.qlik.com/en-US/nprinting/February2024/APIs/NP+API/index.html?page=26 + +.LINK + https://help.qlik.com/en-US/nprinting/February2024/APIs/NP+API/index.html?page=26 +#> +function Get-NPFilters { + [CmdletBinding()] + param ( + ) + + try { + # Retrieve and update the global NPFilters variable + $NPFilters = Invoke-NPRequest -Path 'filters' -Method Get + return $NPFilters + } catch { + Write-Error "Failed to retrieve NPFilters: $_" + } +} \ No newline at end of file diff --git a/src/Public/Get-NPGroups.ps1 b/src/Public/Get-NPGroups.ps1 new file mode 100644 index 0000000..39f3077 --- /dev/null +++ b/src/Public/Get-NPGroups.ps1 @@ -0,0 +1,62 @@ +<# +.SYNOPSIS + Retrieves the list of NPrinting groups. + +.DESCRIPTION + The Get-NPGroups function retrieves the list of groups from the NPrinting server. + It uses the Invoke-NPRequest function to send a GET request to the 'groups' endpoint of the NPrinting API. + Optional parameters can be specified to filter the results. + +.PARAMETER Limit + Specifies the maximum number of groups to retrieve. + +.PARAMETER Offset + Specifies the number of groups to skip before starting to return results. + +.EXAMPLE + Get-NPGroups + + This example retrieves all NPrinting groups. + +.EXAMPLE + Get-NPGroups -Limit 10 + + This example retrieves a maximum of 10 NPrinting groups. + +.EXAMPLE + Get-NPGroups -Limit 10 -Offset 5 + + This example retrieves a maximum of 10 NPrinting groups, skipping the first 5. + +.NOTES + For more information, visit the NPrinting API documentation: + https://help.qlik.com/en-US/nprinting/February2024/APIs/NP+API/index.html?page=32 + +.LINK + https://help.qlik.com/en-US/nprinting/February2024/APIs/NP+API/index.html?page=32 +#> +function Get-NPGroups { + [CmdletBinding()] + param ( + [Parameter(HelpMessage = "Specifies the maximum number of groups to retrieve.")] + [int32]$Limit, + [Parameter(HelpMessage = "Specifies the number of groups to skip before starting to return results.")] + [int32]$Offset + ) + + try { + $Filter = "" + if ($PSBoundParameters.ContainsKey('Limit')) { + $Filter = GetNPFilter -Filter $Filter -Property "limit" -Value $Limit.ToString() + } + if ($PSBoundParameters.ContainsKey('Offset')) { + $Filter = GetNPFilter -Filter $Filter -Property "offset" -Value $Offset.ToString() + } + + # Retrieve the NPrinting Groups + $NPGroups = Invoke-NPRequest -Path "groups$Filter" -Method Get + return $NPGroups + } catch { + Write-Error "Failed to retrieve NPrinting groups: $_" + } +} \ No newline at end of file diff --git a/src/Public/Get-NPRoles.ps1 b/src/Public/Get-NPRoles.ps1 new file mode 100644 index 0000000..e0df816 --- /dev/null +++ b/src/Public/Get-NPRoles.ps1 @@ -0,0 +1,96 @@ +<# +.SYNOPSIS + Retrieves the list of NPrinting roles. + +.DESCRIPTION + The Get-NPRoles function retrieves the list of roles from the NPrinting server. + It uses the Invoke-NPRequest function to send a GET request to the 'roles' endpoint of the NPrinting API. + Optional parameters can be specified to filter the results. + +.PARAMETER appId + Specifies the ID of the app to filter the roles. + +.PARAMETER roleName + Specifies the name of the role to filter the results. + +.PARAMETER enabled + Specifies whether to filter the roles by their enabled status. + +.PARAMETER offset + Specifies the number of roles to skip before starting to return results. + +.PARAMETER limit + Specifies the maximum number of roles to retrieve. + +.EXAMPLE + Get-NPRoles + + This example retrieves all NPrinting roles. + +.EXAMPLE + Get-NPRoles -appId "12345" + + This example retrieves all NPrinting roles for the specified app ID. + +.EXAMPLE + Get-NPRoles -roleName "Admin" + + This example retrieves all NPrinting roles with the name "Admin". + +.EXAMPLE + Get-NPRoles -enabled $true + + This example retrieves all enabled NPrinting roles. + +.EXAMPLE + Get-NPRoles -limit 10 -offset 5 + + This example retrieves a maximum of 10 NPrinting roles, skipping the first 5. + +.NOTES + For more information, visit the NPrinting API documentation: + https://help.qlik.com/en-US/nprinting/February2024/APIs/NP+API/index.html?page=50 + +.LINK + https://help.qlik.com/en-US/nprinting/February2024/APIs/NP+API/index.html?page=50 +#> +function Get-NPRoles { + [CmdletBinding()] + param ( + [Parameter(HelpMessage = 'Specifies the number of roles to skip before starting to return results.')] + [int32]$offset, + [Parameter(HelpMessage = 'Specifies the maximum number of roles to retrieve.')] + [int32]$limit, + [Parameter(HelpMessage = 'Specifies the ID of the app to filter the roles.')] + [string]$appId, + [Parameter(HelpMessage = 'Specifies the name of the role to filter the results.')] + [string]$roleName, + [Parameter(HelpMessage = 'Specifies whether to filter the roles by their enabled status.')] + [bool]$enabled + ) + + try { + $Filter = '' + if ($PSBoundParameters.ContainsKey('appId')) { + $Filter = GetNPFilter -Filter $Filter -Property 'appId' -Value $appId + } + if ($PSBoundParameters.ContainsKey('roleName')) { + $Filter = GetNPFilter -Filter $Filter -Property 'roleName' -Value $roleName + } + if ($PSBoundParameters.ContainsKey('enabled')) { + $Filter = GetNPFilter -Filter $Filter -Property 'enabled' -Value $enabled.ToString() + } + if ($PSBoundParameters.ContainsKey('offset')) { + $Filter = GetNPFilter -Filter $Filter -Property 'offset' -Value $offset.ToString() + } + if ($PSBoundParameters.ContainsKey('limit')) { + $Filter = GetNPFilter -Filter $Filter -Property 'limit' -Value $limit.ToString() + } + + # Retrieve the NPrinting Roles + $NPRoles = Invoke-NPRequest -Path "roles$Filter" -method Get + return $NPRoles + } catch { + Write-Error "Failed to retrieve NPRoles: $_" + } +} \ No newline at end of file diff --git a/src/Public/Get-NPTasks.ps1 b/src/Public/Get-NPTasks.ps1 new file mode 100644 index 0000000..377aee1 --- /dev/null +++ b/src/Public/Get-NPTasks.ps1 @@ -0,0 +1,93 @@ +<# +.SYNOPSIS + Retrieves the list of NPrinting tasks. + +.DESCRIPTION + The Get-NPTasks function retrieves the list of tasks from the NPrinting server. + It uses the Invoke-NPRequest function to send a GET request to the 'tasks' endpoint of the NPrinting API. + Optional parameters can be specified to filter the results. + +.PARAMETER ID + Specifies the ID of the task to retrieve. + +.PARAMETER Name + Specifies the name of the task to filter the results. + +.PARAMETER Executions + Specifies whether to include execution details for each task. + +.EXAMPLE + Get-NPTasks + + This example retrieves all NPrinting tasks. + +.EXAMPLE + Get-NPTasks -ID "12345" + + This example retrieves the NPrinting task with the specified ID. + +.EXAMPLE + Get-NPTasks -Name "Monthly Report" + + This example retrieves all NPrinting tasks with the name "Monthly Report". + +.EXAMPLE + Get-NPTasks -Executions + + This example retrieves all NPrinting tasks and includes execution details for each task. + +.NOTES + For more information, visit the NPrinting API documentation: + https://help.qlik.com/en-US/nprinting/February2024/APIs/NP+API/index.html?page=60 + +.LINK + https://help.qlik.com/en-US/nprinting/February2024/APIs/NP+API/index.html?page=60 +#> +function Get-NPTasks { + [CmdletBinding()] + param ( + [Parameter(HelpMessage = "Specifies the ID of the task to retrieve.")] + [string]$ID, + [Parameter(HelpMessage = "Specifies the name of the task to filter the results.")] + [string]$Name, + [Parameter(HelpMessage = "Specifies whether to include execution details for each task.")] + [switch]$Executions + ) + + try { + # Construct the base path + $BasePath = 'tasks' + $Path = if ($ID) { "$BasePath/$ID" } else { $BasePath } + + # Add filter by name if specified + if ($Name) { + $Path = "$Path?filter=name eq '$Name'" + } + + Write-Verbose "Request Path: $Path" + + # Fetch tasks from the API + $NPTasks = Invoke-NPRequest -Path $Path -method Get + + # Include execution details if requested + if ($Executions) { + $NPTasks | ForEach-Object { + try { + $ExecutionPath = "tasks/$($_.id)/Executions" + Write-Verbose "Fetching executions for task ID: $($_.id)" + $NPTaskExecutions = Invoke-NPRequest -Path $ExecutionPath -method Get + + # Add executions as a NoteProperty to the task object + Add-Member -InputObject $_ -MemberType NoteProperty -Name 'Executions' -Value $NPTaskExecutions -Force + } catch { + Write-Warning "Failed to fetch executions for task ID: $($_.id): $_" + } + } + } + + return $NPTasks + + } catch { + Write-Error "Failed to retrieve NPTasks: $_" + } +} \ No newline at end of file diff --git a/src/Public/Invoke-NPRequest.ps1 b/src/Public/Invoke-NPRequest.ps1 new file mode 100644 index 0000000..dbc04c6 --- /dev/null +++ b/src/Public/Invoke-NPRequest.ps1 @@ -0,0 +1,186 @@ +<# +.SYNOPSIS + Sends a request to the NPrinting API. + +.DESCRIPTION + The Invoke-NPRequest function sends a request to the NPrinting API. + It supports various HTTP methods and can handle different types of requests, including those for NPrinting Private Endpoints (NPE). + +.PARAMETER Path + Specifies the API endpoint path. + +.PARAMETER Method + Specifies the HTTP method to use for the request. Valid values are 'Get', 'Post', 'Patch', 'Delete', and 'Put'. + +.PARAMETER Data + Specifies the data to send with the request, if applicable. + +.PARAMETER NPE + Indicates that the request is for NPrinting Private Endpoints. + +.PARAMETER Count + Specifies the number of items to retrieve for NPE requests. + +.PARAMETER OrderBy + Specifies the field by which to order the results for NPE requests. + +.PARAMETER Page + Specifies the page number to retrieve for NPE requests. + +.PARAMETER OutFile + Specifies the file to which the response should be written for download requests. + +.PARAMETER InFile + Specifies the file to upload for upload requests. + +.EXAMPLE + Invoke-NPRequest -Path 'connections' -Method Get + + This example sends a GET request to the 'connections' endpoint of the NPrinting API. + +.EXAMPLE + Invoke-NPRequest -Path 'reports' -Method Post -Data $reportData + + This example sends a POST request to the 'reports' endpoint of the NPrinting API with the specified data. + +.EXAMPLE + Invoke-NPRequest -Path 'tasks' -Method Get -NPE -Count 10 -OrderBy 'Name' -Page 2 + + This example sends a GET request to the 'tasks' endpoint of the NPrinting API for NPrinting Private Endpoints, retrieving 10 items ordered by 'Name' on page 2. + +.NOTES + For more information, visit the NPrinting API documentation. + +#> +function Invoke-NPRequest { + param ( + [Parameter(Mandatory = $true, Position = 0)] + [string]$Path, + + [ValidateSet('Get', 'Post', 'Patch', 'Delete', 'Put')] + [string]$Method = 'Get', + + $Data, + + [Parameter(ParameterSetName = 'NPE')] + [Parameter(ParameterSetName = 'NPEDownload')] + [Parameter(ParameterSetName = 'NPEUpload')] + [switch]$NPE, + + [Parameter(ParameterSetName = 'NPE')] + [Parameter(ParameterSetName = 'NPEDownload')] + [Parameter(ParameterSetName = 'NPEUpload')] + [int]$Count = -1, + + [Parameter(ParameterSetName = 'NPE')] + [Parameter(ParameterSetName = 'NPEDownload')] + [Parameter(ParameterSetName = 'NPEUpload')] + [string]$OrderBy = 'Name', + + [Parameter(ParameterSetName = 'NPE')] + [Parameter(ParameterSetName = 'NPEDownload')] + [Parameter(ParameterSetName = 'NPEUpload')] + [int]$Page = 1, + + [Parameter(ParameterSetName = 'NPEDownload', Mandatory = $true)] + [System.IO.FileInfo]$OutFile, + + [Parameter(ParameterSetName = 'NPEUpload', Mandatory = $true)] + [System.IO.FileInfo]$InFile + ) + + $NPEnv = $script:NPEnv + if (-not $NPEnv) { + Write-Warning 'Attempting to establish Default connection' + Connect-NPrinting + } + + # Build query parameters + $QueryParameters = @{} + if ($NPE -and (-not $PSBoundParameters.ContainsKey('Infile') -and $PSBoundParameters.ContainsKey('Outfile'))) { + if ($Count -ne -1) { $QueryParameters['count'] = $Count } + if ($OrderBy) { $QueryParameters['orderBy'] = $OrderBy } + $QueryParameters['page'] = $Page + } + + # Build URI + $URI = BuildNPURI -Path $Path -NPE:$NPE -URLServerAPI $NPEnv.URLServerAPI -URLServerNPE $NPEnv.URLServerNPE -QueryParameters $QueryParameters + + # Splat for Invoke-RestMethod + $SplatRest = @{ + URI = $URI + WebSession = $NPEnv.WebRequestSession + Method = $Method + ContentType = 'application/json;charset=UTF-8' + Headers = GetXSRFToken + } + + # Add credentials if session is invalid + if ([string]::IsNullOrEmpty($NPEnv.WebRequestSession.Cookies.GetCookies($NPEnv.URLServerAPI)) -and $NPEnv.Credentials) { + $SplatRest['Credential'] = $NPEnv.Credentials + } + + # Add JSON body if data exists + if ($Data) { + $JsonData = if ($Data -is [string]) { $Data } else { ConvertTo-Json -InputObject $Data -Depth 5 } + $SplatRest['Body'] = $JsonData + } + + # Handle file upload/download + if ($OutFile) { $SplatRest['OutFile'] = $OutFile } + if ($InFile) { + # + if ($InFile.Extension -eq '.zip') { + # Define the boundary + $boundary = '-----------------------------' + [System.Guid]::NewGuid().ToString('N') + + # Create the Content-Type header with the boundary + $SplatRest.contentType = "multipart/form-data; boundary=$boundary" + + # Read the file content as bytes + $fileBytes = [System.IO.File]::ReadAllBytes($InFile.FullName) + + # Convert the bytes to a Base64 string + $fileContent = [System.Text.Encoding]::GetEncoding('ISO-8859-1').GetString($fileBytes) + + # Now construct your body + $body = @" +--$boundary +Content-Disposition: form-data; name="file"; filename="$($InFile.Name)" +Content-Type: application/x-zip-compressed + +$fileContent +--$boundary-- +"@ + $SplatRest['Body'] = $body + } else { + $SplatRest['InFile'] = $InFile + } + } + + # Debug output + if ($PSBoundParameters.Debug.IsPresent) { + Write-Warning "Debug: $($SplatRest | Out-String)" + } + + # Invoke the request + try { + $Result = Invoke-RestMethod @SplatRest + } catch { + Write-Warning "Error during REST call: $($_.Exception.Message)" + Write-Warning "From: $($_.Exception.Response.ResponseUri.AbsoluteUri) `nResponse: $($_.Exception.Response.StatusDescription)" + return $_ + } + + # Handle the result + if ($OutFile) { return } + elseif ($Result) { + if ($NPE -and $Result.result) { return $Result.result } + if ($Result.data) { + if ($Result.data.items) { return $Result.data.items } else { return $Result.data } + } + return $Result + } else { + Write-Error 'No results received' + } +} \ No newline at end of file diff --git a/src/Resources/BuildFiles/Scripts/DynamicLoading.ps1 b/src/Resources/BuildFiles/Scripts/DynamicLoading.ps1 new file mode 100644 index 0000000..ce656aa --- /dev/null +++ b/src/Resources/BuildFiles/Scripts/DynamicLoading.ps1 @@ -0,0 +1,35 @@ +$DynamicLoading = @' +[System.IO.DirectoryInfo]$modulePath = $PSScriptRoot +[System.IO.DirectoryInfo]$publicFunctionsPath = Join-Path $modulePath -ChildPath 'Public' +[System.IO.DirectoryInfo]$privateFunctionsPath = Join-Path $modulePath -ChildPath 'Private' +[System.IO.DirectoryInfo]$classesPath = Join-Path $modulePath -ChildPath 'Classes' +Write-Warning "$PSScriptRoot" +$aliases = @() +[regex]$FunctionName = [regex]::new('(?<=function )([\w]+-[\w]+)(?>[\s]+\{)', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) + +if ($publicFunctionsPath.Exists) { + $publicFunctions = Get-ChildItem -Path $publicFunctionsPath.FullName | Where-Object { $_.Extension -eq '.ps1' } + $publicFunctions | ForEach-Object { . $_.FullName } + $publicFunctions | ForEach-Object { # Export all of the public functions from this module + $content = Get-Content $_.FullName + $functions = $FunctionName.Matches($($content)).ForEach({ $_.Groups[1].value }) | Sort-Object -Unique + foreach ($function in $functions) { + # The command has already been sourced in above. Query any defined aliases. + $alias = Get-Alias -Definition $function -ErrorAction SilentlyContinue + if ($alias) { + $aliases += $alias + Export-ModuleMember -Function $function -Alias $alias -Verbose + } else { + Export-ModuleMember -Function "$($function)" -Verbose + } + } + } +} +if ($privateFunctionsPath.Exists) { + Get-ChildItem -Path $privateFunctionsPath.FullName | Where-Object { $_.Extension -eq '.ps1' } | ForEach-Object { . $_.FullName } +} +if ($classesPath.Exists) { + Get-ChildItem -Path $classesPath.FullName | Where-Object { $_.Extension -eq '.ps1' } | ForEach-Object { . $_.FullName } +} +'@ +return $DynamicLoading \ No newline at end of file diff --git a/src/Resources/BuildFiles/Scripts/ModuleVersion.ps1 b/src/Resources/BuildFiles/Scripts/ModuleVersion.ps1 new file mode 100644 index 0000000..0edab4b --- /dev/null +++ b/src/Resources/BuildFiles/Scripts/ModuleVersion.ps1 @@ -0,0 +1,37 @@ +function Get-ModuleVersion { + param ( + [Parameter(Mandatory = $false)] + [PSCustomObject]$Config, + [int]$Major, + [int]$Minor, + [int]$Build, + [int]$Revision + ) + + # Set the Version + if ($null -ne $Config.ModuleManifest.ModuleVersion) { + $ModuleVersion = $Config.ModuleManifest.ModuleVersion + } else { + try { + $Tag = $(git tag -l --sort=refname v* | Select-Object -Last 1) + } catch { + $Tag = 'v0.0.0.0' + } + if ($null -eq $Tag) { + $Tag = 'v0.0.0.0' + } + try { + $Version = [version]::new(($Tag).substring(1)) + } catch { + $Version = [version]::new('0.0.0.0') + } + $Major = if ($PSBoundParameters.ContainsKey('Major')) { $Major } else { $Version.Major } + $Minor = if ($PSBoundParameters.ContainsKey('Minor')) { $Minor } else { $Version.Minor } + $Year = [datetime]::UtcNow.Year + $DayOfYear = [datetime]::UtcNow.DayOfYear + $Build = if ($PSBoundParameters.ContainsKey('Build')) { $Build } else { '{0:D2}{1:D3}' -f ($Year % 100), $DayOfYear } + $Revision = if ($PSBoundParameters.ContainsKey('Revision')) { $Revision } else { [datetime]::UtcNow.TimeOfDay.TotalSeconds.ToString('#')/2 } + $ModuleVersion = [version]::new("$Major.$Minor.$Build.$Revision") + } + return $ModuleVersion +} \ No newline at end of file diff --git a/src/Resources/BuildModule.ps1 b/src/Resources/BuildModule.ps1 new file mode 100644 index 0000000..8dcb2a2 --- /dev/null +++ b/src/Resources/BuildModule.ps1 @@ -0,0 +1,333 @@ +<# + .NOTES + =========================================================================== + Created on: 2022-11-30 10:44 PM + Created by: Marc Collins + Filename: BuildModule.ps1 + Version: 1.0.2022.1130 + =========================================================================== + .DESCRIPTION + This uses a Environment variable `GH_TOKEN` to connect to GitHub to pull information about the Reporitory, + The Module Manifest informaiton will be automatically generated by this script. + All setting can be hardcoded in the Module_Config.json if necessary. + Files placed in the SRC subfolder will be compiled into the Module +#> + +[CmdletBinding()] +param +( + [system.IO.DirectoryInfo]$BuildRoot = $(Get-Item $PWD), + [switch]$ExportAll +) + +$DirSeparator = $([system.io.path]::DirectorySeparatorChar) +$BinDIR = 'bin' +[system.IO.FileInfo]$ConfigFile = "$($BuildRoot.FullName)$($DirSeparator)Module_Config.json" +if ($ConfigFile.Exists) { + $Config = Get-Content "$($BuildRoot.FullName)$($DirSeparator)Module_Config.json" | ConvertFrom-Json +} else { + $Config = [ordered]@{ RepositoryURI = 'https://localhost'; ModuleName = "$($BuildRoot.Name)"; ModuleManifest = @{ Author = $null; CompanyName = $null; Copyright = $null; Description = $null; DotNetFrameworkVersion = $null; FileList = $null; FunctionsToExport = $null; Guid = $null; HelpInfoUri = $null; LicenseUri = $null; ModuleVersion = $null; PrivateData = $null; ProjectUri = $null; ReleaseNotes = $null; Tags = $null; } } + ConvertTo-Json $Config | Out-File -Encoding utf8 -FilePath $ConfigFile.FullName +} + +$paramNewModuleManifest = @{ + Guid = [guid]::NewGuid() +} + +if ($Null -eq $Env:GH_TOKEN) { + $RequiredValues = $true + Write-Warning "This build is primariy designed to run as a GitHub Action: Ensure `$Env:GH_Token is set" + if ($null -eq $Config.RepositoryURI) { + Write-Warning 'You must specify the RepositoryURI in the Config.json' + $RequiredValues = $false + } else { + $RepositoryURI = $Config.RepositoryURI + } + + if ($null -eq $Config.ModuleName) { + Write-Warning 'You must specify the ModuleName in the Config.json' + $RequiredValues = $false + } else { + $ModuleName = $Config.ModuleName + } + + if ($RequiredValues -eq $false) { + return + } +} else { + $GH_TOKEN = $Env:GH_TOKEN + $RepositoryURI = "$($Env:GITHUB_API_URL)/repos/$($Env:GITHUB_REPOSITORY)" + $RepositoryInfo = Invoke-RestMethod $RepositoryURI -ContentType 'application/json' -Headers @{ + Authorization = "Bearer $GH_TOKEN" + } + $RepositoryOwnerInfo = Invoke-RestMethod $RepositoryInfo.owner.url -ContentType 'application/json' -Headers @{ + Authorization = "Bearer $GH_TOKEN" + } + + $Author = $RepositoryOwnerInfo.name + if ($Null -ne $RepositoryOwnerInfo.company) { + $Company = $RepositoryOwnerInfo.company + } else { + $Company = $RepositoryOwnerInfo.login + } + $ModuleName = $RepositoryInfo.name +} + +if ($Null -ne $Config.ModuleManifest.Author) { + $paramNewModuleManifest.Author = $Config.ModuleManifest.Author +} elseif ($Null -ne $RepositoryInfo) { + $paramNewModuleManifest.Author = $Author +} + +if ($Null -ne $Config.ModuleManifest.CompanyName) { + $paramNewModuleManifest.CompanyName = $Config.ModuleManifest.CompanyName +} elseif ($Null -ne $RepositoryInfo) { + $paramNewModuleManifest.CompanyName = $Company +} + +if ($Null -ne $Config.ModuleManifest.Copyright) { + $paramNewModuleManifest.Copyright = $Config.ModuleManifest.Copyright +} + +if ($Null -ne $Config.ModuleManifest.Description) { + $paramNewModuleManifest.Description = $Config.ModuleManifest.Description +} elseif ($Null -ne $RepositoryInfo) { + $paramNewModuleManifest.Description = $RepositoryInfo.Description +} + +if ($Null -ne $Config.ModuleManifest.DotNetFrameworkVersion) { + $paramNewModuleManifest.DotNetFrameworkVersion = $Config.ModuleManifest.DotNetFrameworkVersion +} + +if ($Null -ne $Config.ModuleManifest.FileList) { + $paramNewModuleManifest.FileList = $Config.ModuleManifest.FileList +} + +if ($Null -ne $Config.ModuleManifest.Guid) { + $paramNewModuleManifest.Guid = $Config.ModuleManifest.Guid +} elseif ($Null -ne $Env:GITHUB_SHA) { + $paramNewModuleManifest.Guid = [guid]::Parse($Env:GITHUB_SHA.Substring(0, 32)) +} + +if ($Null -ne $Config.ModuleManifest.HelpInfoUri) { + $paramNewModuleManifest.HelpInfoUri = $Config.ModuleManifest.HelpInfoUri +} elseif ($Null -ne $RepositoryInfo) { + if ($Null -ne $RepositoryInfo.homepage) { + $paramNewModuleManifest.HelpInfoUri = $RepositoryInfo.homepage + } elseif ($RepositoryInfo.has_issues) { + $paramNewModuleManifest.HelpInfoUri = "$($RepositoryInfo.html_url)/issues" + } else { + $paramNewModuleManifest.HelpInfoUri = $RepositoryInfo.html_url + } +} + +if ($Null -ne $Config.ModuleManifest.LicenseUri) { + $paramNewModuleManifest.LicenseUri = $Config.ModuleManifest.LicenseUri +} elseif ($Null -ne $RepositoryInfo) { + $paramNewModuleManifest.LicenseUri = "$($RepositoryInfo.html_url)/blob/$($env:GITHUB_REF_NAME)/LICENSE" +} + +if ($Null -ne $Config.ModuleManifest.PrivateData) { + $paramNewModuleManifest.PrivateData = $Config.ModuleManifest.PrivateData +} + +if ($Null -ne $Config.ModuleManifest.ProjectUri) { + $paramNewModuleManifest.ProjectUri = $Config.ModuleManifest.ProjectUri +} elseif ($Null -ne $RepositoryInfo) { + $paramNewModuleManifest.ProjectUri = $RepositoryInfo.html_url +} + +if ($Null -ne $Config.ModuleManifest.Tags) { + $paramNewModuleManifest.Tags = $Config.ModuleManifest.Tags +} elseif ($Null -ne $RepositoryInfo) { + # $paramNewModuleManifest.Tags = $RepositoryInfo.Tags ### CheckTHIS +} + +##################################################################### + +#Set the Version +if ($Null -ne $Config.ModuleManifest.ModuleVersion) { + $Version = [version]::new($Config.ModuleManifest.ModuleVersion) +} else { + try { + $Tag = $(git tag -l --sort=refname v* | Select-Object -Last 1) + } catch { + } + if ($Null -eq $Tag) { + $Tag = 'v0.0.0.0' + } + try { + $Version = [version]::new(($Tag).substring(1)); + } catch { + $Version = [version]::new('0.0.0.0'); + } +} +$ModuleVersion = [version]::new("$($Version.Major).$($Version.Minor).$("$([datetime]::utcnow.ToString('yy'))$([datetime]::UtcNow.DayOfYear.ToString('000'))").$([datetime]::UtcNow.TimeOfDay.TotalSeconds.ToString('#'))") + +if ($Null -ne $Config.ModuleManifest.ReleaseNotes) { + $paramNewModuleManifest.ReleaseNotes = $Config.ModuleManifest.ReleaseNotes +} elseif ($Null -ne $RepositoryInfo) { + $paramNewModuleManifest.ReleaseNotes = "$($RepositoryInfo.html_url)/releases/tag/v$($ModuleVersion)" +} + +$paramNewModuleManifest.ModuleVersion = $ModuleVersion +$RootModule = "$($ModuleName).psm1" +$ModuleManifest = "$($ModuleName).psd1" +$paramNewModuleManifest.path = $ModuleManifest +$paramNewModuleManifest.RootModule = $RootModule +$SRCDir = Join-Path $BuildRoot src +$ScriptFiles = Get-ChildItem $SRCDir -Recurse -Include '*.ps1' | Where-Object { + -not ($_.FullName -match '\\bin\\' -or $_.FullName -match '\\Resources\\' -or $_.Name -like 'Build-*') +} | Sort-Object -Property DirectoryName,BaseName + +$paramNewModuleManifest.FileList = $ScriptFiles| ForEach-Object { +join-path $_.Directory.name $_.Name +} + +$ModuleDIR = $Null +[System.io.DirectoryInfo]$ModuleDir = "$($BuildRoot.FullName)$($DirSeparator)$ModuleName" +$ModuleDIR.Refresh() +if ($ModuleDIR.Exists) { + Write-Host "Clearing Existing DIR: $($ModuleDIR.FullName)" + try { + $ModuleDir.Delete($true) + } catch { + Write-Error 'Module DIR exists and files are locked' + return $Null + } + + Start-Sleep -Seconds 1 + $ModuleDIR.Refresh() +} +$ModuleDir.Create() +$ModuleDIR.Refresh() +$ModuleZip = [System.IO.FileInfo]::new("$($BuildRoot.FullName)$($DirSeparator)$($ModuleName).zip").FullName + +$ScriptContent = foreach ($ScriptFile in $ScriptFiles) { + Get-Content $ScriptFile.FullName +} + +if ($Null -ne $Config.AllowDynamicContent -and $Config.AllowDynamicContent -eq $true) { + $moduleContent = . (Join-Path -Path $SRCDir -ChildPath "Resources/BuildFiles/Scripts/DynamicLoading.ps1") + Get-ChildItem $SRCDir -Directory -Exclude Resources | Where-Object { $_.GetFiles().Count -gt 0 } | Copy-Item -Destination $ModuleDIR -Recurse -Force +} else { + $moduleContent = $ScriptContent +} + +[system.io.fileInfo]$RMMD = Join-Path $SRCDir -ChildPath 'ReadMe.md' +if (-not $RMMD.Exists){ + [system.io.fileInfo]$RRMMD = Join-Path $BuildRoot -ChildPath 'ReadMe.md' + if($RRMMD.Exists){ + Copy-Item $RRMMD.FullName -Destination $(Join-Path $ModuleDIR -ChildPath 'ReadMe.md') -Force + } +} +$MDs = Get-ChildItem $SRCDir -Filter '*.md' +foreach ($MD in $MDs) { + if ($MD.Exists) { + Copy-Item -Path $MD.FullName -Destination "$($ModuleDIR.FullName)$($DirSeparator)" + } +} + +$BinPath = "$($BuildRoot.FullName)$($DirSeparator)src$($DirSeparator)$($BinDIR)" + +if (-not [string]::IsNullOrEmpty($BinPath)) { + $Directories = Get-ChildItem $BinPath -Recurse -Directory + $Modules = $Directories | Where-Object { + $_.GetFiles('*.psd1').Count -gt 0 + } + $Assemblies = $Directories | Where-Object { + $_.GetFiles('*.psd1').Count -eq 0 + } + + if ($Modules.count -gt 0) { + $NestedModules = $Modules.GetFiles('*.psd1').FullName.substring($BinPath.length - $BinDIR.length) + $Modules | ForEach-Object { + #"$($ModuleDir.FullName)$($_.FullName.Substring($BinPath.length - $BinDIR.length - 1))" + Copy-Item -Recurse -Path $_.FullName -Destination "$($ModuleDir.FullName)$($_.FullName.Substring($BinPath.length - $BinDIR.length - 1))" + } + $paramNewModuleManifest.NestedModules = $NestedModules + $paramNewModuleManifest.ModuleList = $NestedModules + } + + if ($Assemblies.count -gt 0) { + $RequiredDLLs = $Assemblies.GetFiles('*.dll').FullName.substring($BinPath.length - $BinDIR.length) + $Assemblies.GetFiles('*.dll') | ForEach-Object { + [system.io.fileInfo]$destfile = "$($ModuleDir.FullName)$($_.FullName.Substring($BinPath.length - $BinDIR.length - 1))" + $null = $destfile.Directory.Create() + Copy-Item -Recurse -Path $_.FullName -Destination "$($ModuleDir.FullName)$($_.FullName.Substring($BinPath.length - $BinDIR.length - 1))" + } + $paramNewModuleManifest.RequiredAssemblies = $RequiredDLLs + } +} + +Push-Location $ModuleDir.FullName +$moduleContent| Out-File $RootModule -Encoding utf8 + +if ($Null -ne $Config.ModuleManifest.FunctionsToExport) { + $paramNewModuleManifest.FunctionsToExport = $Config.ModuleManifest.FunctionsToExport +} else { + [regex]$FunctionName = [regex]::new('(?<=function )([\w]+-[\w]+)(?>[\s]+\{)', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) + if ($ExportAll) { + [regex]$FunctionName = [regex]::new('(?<=[\s]?function )([\w]+-?[\w]+)(?>[\s]+\{)', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) + } + $paramNewModuleManifest.FunctionsToExport = $FunctionName.Matches($ScriptContent).ForEach({ $_.Groups[1].value }) | Sort-Object -Unique +} + +if ($Null -ne $Config.ModuleManifest.CmdletsToExport) { + $paramNewModuleManifest.CmdletsToExport = $Config.ModuleManifest.CmdletsToExport +} else { + $paramNewModuleManifest.CmdletsToExport = @() +} + +if ($Null -ne $Config.ModuleManifest.VariablesToExport) { + $paramNewModuleManifest.VariablesToExport = $Config.ModuleManifest.VariablesToExport +} else { + $paramNewModuleManifest.VariablesToExport = @() +} + +if ($Null -ne $Config.ModuleManifest.AliasesToExport) { + $paramNewModuleManifest.AliasesToExport = $Config.ModuleManifest.AliasesToExport +} else { + $paramNewModuleManifest.AliasesToExport = @() +} + +#Write-Warning $($paramNewModuleManifest | ConvertTo-Json) +New-ModuleManifest @paramNewModuleManifest +Pop-Location +Compress-Archive -Path $ModuleDir -DestinationPath $ModuleZip -CompressionLevel Optimal -Force + +"Version=v$($ModuleVersion)" >> $ENV:GITHUB_OUTPUT +"ModuleName=$($ModuleName)" >> $ENV:GITHUB_OUTPUT +"ModuleDir=$($ModuleDir.FullName)" >> $ENV:GITHUB_OUTPUT +"ModuleDirRelative=$(Resolve-Path -Relative $ModuleDir.FullName)" >> $ENV:GITHUB_OUTPUT +"ModuleZip=$($ModuleZip)" >> $ENV:GITHUB_OUTPUT + +'### Finished! :rocket:' >> $env:GITHUB_STEP_SUMMARY +"#### Built ``$($ModuleName)``" >> $env:GITHUB_STEP_SUMMARY +"#### Version ``$($ModuleVersion)``" >> $env:GITHUB_STEP_SUMMARY +if ($($paramNewModuleManifest.FunctionsToExport | Measure-Object).count -gt 0) { + '##### Functions Included' >> $env:GITHUB_STEP_SUMMARY + ($paramNewModuleManifest.FunctionsToExport | ForEach-Object { + "- ``$($_)``" + }) >> $env:GITHUB_STEP_SUMMARY +} + +if ($($paramNewModuleManifest.VariablesToExport | Measure-Object).count -gt 0) { + '##### Variables Included' >> $env:GITHUB_STEP_SUMMARY + ($paramNewModuleManifest.VariablesToExport | ForEach-Object { + "- ``$($_)``" + }) >> $env:GITHUB_STEP_SUMMARY +} + +if ($($paramNewModuleManifest.CmdletsToExport | Measure-Object).count -gt 0) { + '##### Cmdlets Included' >> $env:GITHUB_STEP_SUMMARY + ($paramNewModuleManifest.CmdletsToExport | ForEach-Object { + "- ``$($_)``" + }) >> $env:GITHUB_STEP_SUMMARY +} +if ($($paramNewModuleManifest.AliasesToExport | Measure-Object).count -gt 0) { + '##### Aliases Included' >> $env:GITHUB_STEP_SUMMARY + ($paramNewModuleManifest.AliasesToExport | ForEach-Object { + "- ``$($_)``" + }) >> $env:GITHUB_STEP_SUMMARY +}