713 lines
24 KiB
PowerShell
713 lines
24 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Interactively replaces one field across confirmed ElixForms metadata rows.
|
|
|
|
.DESCRIPTION
|
|
Loads a schema, prompts for optional filters, previews every matching row, creates
|
|
a timestamped JSON backup, and submits one multipart save per row. The cookie is
|
|
read through a hidden prompt and is never persisted.
|
|
|
|
.PARAMETER BaseUri
|
|
Base URI of the ElixForms metadata API.
|
|
|
|
.PARAMETER BackupDirectory
|
|
Directory for pre-write search-response backups.
|
|
|
|
.PARAMETER PageSize
|
|
Number of rows requested per search page. All reported pages are retrieved.
|
|
|
|
.PARAMETER RunWhenDotSourced
|
|
Runs the interactive workflow even when the script is dot-sourced. Intended for
|
|
debuggers such as the VS Code PowerShell debugger that dot-source launch scripts.
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter()]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string] $BaseUri = 'https://console-unipr.elixforms.it/AJSRV/metadata',
|
|
|
|
[Parameter()]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string] $BackupDirectory = (Join-Path (Get-Location) 'backups'),
|
|
|
|
[Parameter()]
|
|
[ValidateRange(1, 1000)]
|
|
[int] $PageSize = 80,
|
|
|
|
[Parameter()]
|
|
[switch] $RunWhenDotSourced
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
Add-Type -AssemblyName System.Net.Http
|
|
|
|
function ConvertTo-QueryString {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[System.Collections.IDictionary] $Parameters
|
|
)
|
|
|
|
$pairs = foreach ($entry in $Parameters.GetEnumerator()) {
|
|
$encodedName = [Uri]::EscapeDataString([string] $entry.Key)
|
|
$encodedValue = [Uri]::EscapeDataString([string] $entry.Value)
|
|
'{0}={1}' -f $encodedName, $encodedValue
|
|
}
|
|
|
|
return ($pairs -join '&')
|
|
}
|
|
|
|
function New-ElixFormsUri {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string] $BaseUri,
|
|
|
|
[Parameter(Mandatory)]
|
|
[string] $Route,
|
|
|
|
[Parameter(Mandatory)]
|
|
[System.Collections.IDictionary] $Query
|
|
)
|
|
|
|
$root = $BaseUri.TrimEnd('/')
|
|
return [Uri] ('{0}/{1}?{2}' -f $root, $Route, (ConvertTo-QueryString -Parameters $Query))
|
|
}
|
|
|
|
function ConvertFrom-SecureStringToPlainText {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[Security.SecureString] $SecureValue
|
|
)
|
|
|
|
$pointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureValue)
|
|
try {
|
|
return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($pointer)
|
|
}
|
|
finally {
|
|
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pointer)
|
|
}
|
|
}
|
|
|
|
function Invoke-ElixFormsRequest {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[Net.Http.HttpClient] $Client,
|
|
|
|
[Parameter(Mandatory)]
|
|
[ValidateSet('GET', 'POST')]
|
|
[string] $Method,
|
|
|
|
[Parameter(Mandatory)]
|
|
[Uri] $Uri,
|
|
|
|
[Parameter(Mandatory)]
|
|
[string] $Cookie
|
|
)
|
|
|
|
$httpMethod = if ($Method -eq 'GET') { [Net.Http.HttpMethod]::Get } else { [Net.Http.HttpMethod]::Post }
|
|
$request = [Net.Http.HttpRequestMessage]::new($httpMethod, $Uri)
|
|
$null = $request.Headers.TryAddWithoutValidation('Accept', '*/*')
|
|
$null = $request.Headers.TryAddWithoutValidation('Cookie', $Cookie)
|
|
$null = $request.Headers.TryAddWithoutValidation('X-Requested-With', 'XMLHttpRequest')
|
|
|
|
$origin = '{0}://{1}' -f $Uri.Scheme, $Uri.Authority
|
|
$null = $request.Headers.TryAddWithoutValidation('Origin', $origin)
|
|
$request.Headers.Referrer = [Uri] ($origin + '/ajconsole/')
|
|
|
|
try {
|
|
$response = $Client.SendAsync($request).GetAwaiter().GetResult()
|
|
try {
|
|
$responseBytes = $response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult()
|
|
$responseText = [Text.Encoding]::UTF8.GetString($responseBytes)
|
|
|
|
if (-not $response.IsSuccessStatusCode) {
|
|
$previewLength = [Math]::Min(500, $responseText.Length)
|
|
$preview = $responseText.Substring(0, $previewLength)
|
|
throw 'HTTP {0} ({1}) from {2}: {3}' -f [int] $response.StatusCode, $response.ReasonPhrase, $Uri, $preview
|
|
}
|
|
|
|
try {
|
|
$json = $responseText | ConvertFrom-Json -ErrorAction Stop
|
|
}
|
|
catch {
|
|
throw 'The response from {0} was not valid JSON: {1}' -f $Uri, $_.Exception.Message
|
|
}
|
|
|
|
return [pscustomobject]@{
|
|
Json = $json
|
|
Raw = $responseText
|
|
StatusCode = [int] $response.StatusCode
|
|
}
|
|
}
|
|
catch {
|
|
throw 'Failed to read response from {0}: {1}' -f $Uri, $_.Exception.Message
|
|
}
|
|
finally {
|
|
$response.Dispose()
|
|
}
|
|
}
|
|
catch {
|
|
throw 'Request to {0} failed: {1}' -f $Uri, $_.Exception.Message
|
|
}
|
|
finally {
|
|
$request.Dispose()
|
|
}
|
|
}
|
|
|
|
function New-MultipartFieldBody {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[ValidatePattern('^[A-Za-z0-9_]+$')]
|
|
[string] $Name,
|
|
|
|
[Parameter(Mandatory)]
|
|
[AllowEmptyString()]
|
|
[string] $Value
|
|
)
|
|
|
|
$boundary = '---' + [Guid]::NewGuid().ToString('N').Substring(0, 24)
|
|
$newLine = "`r`n"
|
|
$body = '--{0}{1}Content-Disposition: form-data; name="{2}"{1}{1}{3}{1}--{0}--{1}' -f $boundary, $newLine, $Name, $Value
|
|
|
|
return [pscustomobject]@{
|
|
Boundary = $boundary
|
|
ContentType = 'multipart/form-data; boundary={0}' -f $boundary
|
|
Body = $body
|
|
}
|
|
}
|
|
|
|
function Invoke-ElixFormsSaveRequest {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[Uri] $Uri,
|
|
|
|
[Parameter(Mandatory)]
|
|
[string] $Cookie,
|
|
|
|
[Parameter(Mandatory)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string] $ContentType,
|
|
|
|
[Parameter(Mandatory)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string] $Body
|
|
)
|
|
|
|
$headers = @{}
|
|
$headers.Add('Accept', '*/*')
|
|
$headers.Add('Accept-Encoding', 'gzip, deflate, br, zstd')
|
|
$headers.Add('Cookie', $Cookie)
|
|
$headers.Add('x-requested-with', 'XMLHttpRequest')
|
|
$headers.Add('Content-Type', $ContentType)
|
|
|
|
try {
|
|
$response = Invoke-RestMethod -Uri $Uri -Method POST -Headers $headers -ContentType $ContentType -Body $Body -ErrorAction Stop
|
|
return [pscustomobject]@{
|
|
Json = $response
|
|
}
|
|
}
|
|
catch {
|
|
throw 'Request to {0} failed: {1}' -f $Uri, $_.Exception.Message
|
|
}
|
|
}
|
|
|
|
function Read-PositiveInteger {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string] $Prompt
|
|
)
|
|
|
|
while ($true) {
|
|
$text = Read-Host $Prompt
|
|
$value = 0
|
|
if ([int]::TryParse($text, [ref] $value) -and $value -gt 0) {
|
|
return $value
|
|
}
|
|
|
|
Write-Warning 'Enter a positive integer.'
|
|
}
|
|
}
|
|
|
|
function Read-YesNo {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string] $Prompt
|
|
)
|
|
|
|
$answer = (Read-Host ($Prompt + ' [y/N]')).Trim().ToLowerInvariant()
|
|
return $answer -in @('y', 'yes', 's', 'si', 'sì')
|
|
}
|
|
|
|
function Get-OrderedSchemaFields {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[object] $Schema
|
|
)
|
|
|
|
$fields = [Collections.Generic.List[object]]::new()
|
|
foreach ($section in @($Schema.sections | Sort-Object -Property orderKey)) {
|
|
foreach ($field in @($section.fields | Sort-Object -Property orderKey)) {
|
|
$fields.Add([pscustomobject]@{
|
|
SectionKey = [string] $section.key
|
|
SectionTitle = [string] $section.title
|
|
Key = [string] $field.key
|
|
Title = [string] $field.title
|
|
Type = [string] $field.type
|
|
OrderKey = [string] $field.orderKey
|
|
})
|
|
}
|
|
}
|
|
|
|
return $fields.ToArray()
|
|
}
|
|
|
|
function Show-Schema {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[object] $Schema
|
|
)
|
|
|
|
Write-Host ''
|
|
Write-Host ('Schema ID: {0}' -f $Schema.id)
|
|
Write-Host ('Title: {0}' -f $Schema.title)
|
|
Write-Host 'Sections and fields:'
|
|
|
|
foreach ($section in @($Schema.sections | Sort-Object -Property orderKey)) {
|
|
Write-Host (' {0} - {1}' -f $section.key, $section.title)
|
|
foreach ($field in @($section.fields | Sort-Object -Property orderKey)) {
|
|
Write-Host (' {0,-12} {1} ({2})' -f $field.key, $field.title, $field.type)
|
|
}
|
|
}
|
|
|
|
Write-Host ''
|
|
}
|
|
|
|
function Get-ElixFormsSearchResult {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[Net.Http.HttpClient] $Client,
|
|
|
|
[Parameter(Mandatory)]
|
|
[string] $BaseUri,
|
|
|
|
[Parameter(Mandatory)]
|
|
[string] $Cookie,
|
|
|
|
[Parameter(Mandatory)]
|
|
[int] $SchemaId,
|
|
|
|
[Parameter(Mandatory)]
|
|
[System.Collections.IDictionary] $Filters,
|
|
|
|
[Parameter(Mandatory)]
|
|
[int] $PageSize
|
|
)
|
|
|
|
$pages = [Collections.Generic.List[object]]::new()
|
|
$rows = [Collections.Generic.List[object]]::new()
|
|
$columns = $null
|
|
$expectedPages = 1
|
|
|
|
for ($page = 1; $page -le $expectedPages; $page++) {
|
|
$query = [ordered]@{ 'SEARCH_IN[]' = $SchemaId }
|
|
foreach ($entry in $Filters.GetEnumerator()) {
|
|
$query['S_{0}_NEW_{1}' -f $SchemaId, $entry.Key] = $entry.Value
|
|
}
|
|
$query.PAGE = $page
|
|
$query.AJL = 'it'
|
|
$query.PAGESIZE = $PageSize
|
|
$query.SECURE = 'true'
|
|
|
|
$uri = New-ElixFormsUri -BaseUri $BaseUri -Route 'search' -Query $query
|
|
Write-Host ('Searching page {0}...' -f $page)
|
|
$result = Invoke-ElixFormsRequest -Client $Client -Method GET -Uri $uri -Cookie $Cookie
|
|
$pageObject = $result.Json
|
|
|
|
if ($page -eq 1) {
|
|
$columns = @($pageObject.columns)
|
|
$expectedPages = [Math]::Max(1, [int] $pageObject.numPages)
|
|
}
|
|
else {
|
|
$pageColumnKeys = @($pageObject.columns | ForEach-Object { [string] $_.key })
|
|
$firstColumnKeys = @($columns | ForEach-Object { [string] $_.key })
|
|
if (($pageColumnKeys -join "`n") -ne ($firstColumnKeys -join "`n")) {
|
|
throw 'Search column order changed between pages; refusing to merge ambiguous rows.'
|
|
}
|
|
}
|
|
|
|
$pages.Add($pageObject)
|
|
foreach ($row in @($pageObject.data)) {
|
|
if (@($row).Count -ne $columns.Count) {
|
|
throw 'A search row has {0} values but the response defines {1} columns.' -f @($row).Count, $columns.Count
|
|
}
|
|
$rows.Add(@($row))
|
|
}
|
|
}
|
|
|
|
return [pscustomobject]@{
|
|
Columns = @($columns)
|
|
Rows = $rows.ToArray()
|
|
Pages = $pages.ToArray()
|
|
TotalRecords = if ($pages.Count -gt 0) { [int] $pages[0].totalRecords } else { 0 }
|
|
}
|
|
}
|
|
|
|
function ConvertTo-PreviewText {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter()]
|
|
[AllowNull()]
|
|
[AllowEmptyString()]
|
|
[object] $Value,
|
|
|
|
[Parameter()]
|
|
[int] $MaximumLength = 60
|
|
)
|
|
|
|
if ($null -eq $Value) {
|
|
return ''
|
|
}
|
|
|
|
$text = ([string] $Value) -replace '\s+', ' '
|
|
if ($text.Length -le $MaximumLength) {
|
|
return $text
|
|
}
|
|
|
|
return $text.Substring(0, $MaximumLength - 3) + '...'
|
|
}
|
|
|
|
function Show-SearchResults {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[object[]] $Columns,
|
|
|
|
[Parameter(Mandatory)]
|
|
[AllowEmptyCollection()]
|
|
[object[]] $Rows
|
|
)
|
|
|
|
if ($Rows.Count -eq 0) {
|
|
Write-Host 'No matching rows.'
|
|
return
|
|
}
|
|
|
|
$displayRows = for ($rowIndex = 0; $rowIndex -lt $Rows.Count; $rowIndex++) {
|
|
$properties = [ordered]@{ '#' = $rowIndex + 1 }
|
|
for ($columnIndex = 0; $columnIndex -lt $Columns.Count; $columnIndex++) {
|
|
$column = $Columns[$columnIndex]
|
|
$label = '{0} [{1}]' -f $column.value, $column.key
|
|
$properties[$label] = ConvertTo-PreviewText -Value $Rows[$rowIndex][$columnIndex]
|
|
}
|
|
[pscustomobject] $properties
|
|
}
|
|
|
|
Write-Host ''
|
|
Write-Host ('Matching rows: {0} (long values are truncated only in this preview)' -f $Rows.Count)
|
|
$displayRows | Format-Table -AutoSize | Out-Host
|
|
}
|
|
|
|
function Save-SearchBackup {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string] $BackupDirectory,
|
|
|
|
[Parameter(Mandatory)]
|
|
[string] $BaseUri,
|
|
|
|
[Parameter(Mandatory)]
|
|
[int] $SchemaId,
|
|
|
|
[Parameter(Mandatory)]
|
|
[System.Collections.IDictionary] $Filters,
|
|
|
|
[Parameter(Mandatory)]
|
|
[int] $PageSize,
|
|
|
|
[Parameter(Mandatory)]
|
|
[object[]] $Pages
|
|
)
|
|
|
|
$resolvedDirectory = [IO.Path]::GetFullPath($BackupDirectory)
|
|
$null = New-Item -ItemType Directory -Path $resolvedDirectory -Force
|
|
$timestamp = Get-Date -Format 'yyyyMMddHHmmss'
|
|
$baseName = 'elixforms-schema-{0}-search-{1}' -f $SchemaId, $timestamp
|
|
$path = Join-Path $resolvedDirectory ($baseName + '.json')
|
|
$suffix = 1
|
|
while (Test-Path -LiteralPath $path) {
|
|
$path = Join-Path $resolvedDirectory ('{0}-{1}.json' -f $baseName, $suffix)
|
|
$suffix++
|
|
}
|
|
|
|
$filterCopy = [ordered]@{}
|
|
foreach ($entry in $Filters.GetEnumerator()) {
|
|
$filterCopy[[string] $entry.Key] = [string] $entry.Value
|
|
}
|
|
|
|
$backup = [ordered]@{
|
|
backedUpAt = (Get-Date).ToString('o')
|
|
baseUri = $BaseUri
|
|
schemaId = $SchemaId
|
|
pageSize = $PageSize
|
|
filters = $filterCopy
|
|
pages = $Pages
|
|
}
|
|
|
|
$json = $backup | ConvertTo-Json -Depth 100
|
|
[IO.File]::WriteAllText($path, $json, [Text.UTF8Encoding]::new($false))
|
|
return $path
|
|
}
|
|
|
|
function Get-ColumnIndex {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[object[]] $Columns,
|
|
|
|
[Parameter(Mandatory)]
|
|
[string] $Key
|
|
)
|
|
|
|
for ($index = 0; $index -lt $Columns.Count; $index++) {
|
|
if ([string] $Columns[$index].key -ceq $Key) {
|
|
return $index
|
|
}
|
|
}
|
|
|
|
return -1
|
|
}
|
|
|
|
function Select-UpdateField {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[object[]] $Fields
|
|
)
|
|
|
|
Write-Host 'Fields available for update:'
|
|
for ($index = 0; $index -lt $Fields.Count; $index++) {
|
|
Write-Host (' {0,2}. {1,-12} {2} ({3})' -f ($index + 1), $Fields[$index].Key, $Fields[$index].Title, $Fields[$index].Type)
|
|
}
|
|
|
|
while ($true) {
|
|
$choice = (Read-Host 'Field number or key (empty to exit)').Trim()
|
|
if ($choice.Length -eq 0) {
|
|
return $null
|
|
}
|
|
|
|
$number = 0
|
|
if ([int]::TryParse($choice, [ref] $number) -and $number -ge 1 -and $number -le $Fields.Count) {
|
|
return $Fields[$number - 1]
|
|
}
|
|
|
|
$found = @($Fields | Where-Object { $_.Key -ieq $choice })
|
|
if ($found.Count -eq 1) {
|
|
return $found[0]
|
|
}
|
|
|
|
Write-Warning 'Select one of the listed field numbers or keys.'
|
|
}
|
|
}
|
|
|
|
function Invoke-ElixFormsMetadataBulkUpdate {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string] $BaseUri,
|
|
|
|
[Parameter(Mandatory)]
|
|
[string] $BackupDirectory,
|
|
|
|
[Parameter(Mandatory)]
|
|
[int] $PageSize
|
|
)
|
|
|
|
Write-Host 'Log in to ElixForms in your browser, then copy the complete Cookie request-header value.'
|
|
$secureCookie = Read-Host 'Cookie header value (input hidden)' -AsSecureString
|
|
if ($secureCookie.Length -eq 0) {
|
|
Write-Warning 'No cookie supplied. Exiting.'
|
|
return
|
|
}
|
|
|
|
$cookie = ConvertFrom-SecureStringToPlainText -SecureValue $secureCookie
|
|
$secureCookie.Dispose()
|
|
$client = $null
|
|
|
|
try {
|
|
$handler = [Net.Http.HttpClientHandler]::new()
|
|
$handler.AutomaticDecompression = [Net.DecompressionMethods]::GZip -bor [Net.DecompressionMethods]::Deflate
|
|
$client = [Net.Http.HttpClient]::new($handler, $true)
|
|
$client.Timeout = [TimeSpan]::FromMinutes(2)
|
|
|
|
$schemaId = Read-PositiveInteger -Prompt 'Schema ID'
|
|
$listQuery = [ordered]@{ AJL = 'it'; ID = $schemaId; ACL = 'true' }
|
|
$listUri = New-ElixFormsUri -BaseUri $BaseUri -Route 'list' -Query $listQuery
|
|
Write-Host 'Loading schema...'
|
|
$listResult = Invoke-ElixFormsRequest -Client $client -Method GET -Uri $listUri -Cookie $cookie
|
|
if ($listResult.Json -isnot [PSCustomObject] -or -not $listResult.Json.PSObject.Properties.Name.Contains('objectList')) {
|
|
throw 'Schema {0} was not found in the list response. Message: {1}' -f $schemaId, $listResult.Json.message
|
|
}
|
|
$schemas = @($listResult.Json.objectList)
|
|
$schema = @($schemas | Where-Object { [int] $_.id -eq $schemaId }) | Select-Object -First 1
|
|
if ($null -eq $schema) {
|
|
throw 'Schema {0} was not present in the list response.' -f $schemaId
|
|
}
|
|
|
|
Show-Schema -Schema $schema
|
|
if (-not (Read-YesNo -Prompt 'Is this the intended schema?')) {
|
|
Write-Host 'Cancelled before search.'
|
|
return
|
|
}
|
|
|
|
$fields = @(Get-OrderedSchemaFields -Schema $schema)
|
|
if ($fields.Count -eq 0) {
|
|
throw 'The schema has no fields.'
|
|
}
|
|
|
|
$duplicateKeys = @($fields | Group-Object -Property Key | Where-Object Count -gt 1)
|
|
if ($duplicateKeys.Count -gt 0) {
|
|
throw 'The schema contains duplicate field keys: {0}' -f (($duplicateKeys.Name) -join ', ')
|
|
}
|
|
|
|
Write-Host 'Enter optional filter values. Press Enter to omit a field.'
|
|
$filters = [ordered]@{}
|
|
foreach ($field in $fields) {
|
|
$filterValue = Read-Host (' {0} - {1}' -f $field.Key, $field.Title)
|
|
if (-not [string]::IsNullOrEmpty($filterValue)) {
|
|
$filters[$field.Key] = $filterValue
|
|
}
|
|
}
|
|
|
|
$search = Get-ElixFormsSearchResult -Client $client -BaseUri $BaseUri -Cookie $cookie -SchemaId $schemaId -Filters $filters -PageSize $PageSize
|
|
if ($search.Rows.Count -ne $search.TotalRecords) {
|
|
throw 'Search reported {0} records but {1} rows were fetched; refusing a partial bulk update.' -f $search.TotalRecords, $search.Rows.Count
|
|
}
|
|
|
|
Show-SearchResults -Columns $search.Columns -Rows $search.Rows
|
|
if ($search.Rows.Count -eq 0) {
|
|
Write-Host 'Nothing to update.'
|
|
return
|
|
}
|
|
|
|
if (-not (Read-YesNo -Prompt 'Proceed with exactly these rows?')) {
|
|
Write-Host 'Cancelled after preview.'
|
|
return
|
|
}
|
|
|
|
$field = Select-UpdateField -Fields $fields
|
|
if ($null -eq $field) {
|
|
Write-Host 'Cancelled before choosing an update.'
|
|
return
|
|
}
|
|
|
|
$newValue = Read-Host ('New value for {0} - {1} (Enter means an empty string)' -f $field.Key, $field.Title)
|
|
Write-Host ''
|
|
Write-Warning ('This will irreversibly replace {0} [{1}] in {2} row(s).' -f $field.Title, $field.Key, $search.Rows.Count)
|
|
Write-Host ('New value preview: "{0}"' -f (ConvertTo-PreviewText -Value $newValue -MaximumLength 120))
|
|
$confirmationPhrase = 'UPDATE {0}' -f $search.Rows.Count
|
|
$finalAnswer = Read-Host ('Type "{0}" to continue' -f $confirmationPhrase)
|
|
if ($finalAnswer -cne $confirmationPhrase) {
|
|
Write-Host 'Final confirmation did not match. No data was changed.'
|
|
return
|
|
}
|
|
|
|
$objectIdIndex = Get-ColumnIndex -Columns $search.Columns -Key 'ID_OBJECT'
|
|
$dataIdColumnKey = 'S_{0}_ID_GENERICSCHEMA' -f $schemaId
|
|
$dataIdIndex = Get-ColumnIndex -Columns $search.Columns -Key $dataIdColumnKey
|
|
if ($objectIdIndex -lt 0 -or $dataIdIndex -lt 0) {
|
|
throw 'Required search columns ID_OBJECT and/or {0} are missing.' -f $dataIdColumnKey
|
|
}
|
|
|
|
$backupPath = Save-SearchBackup -BackupDirectory $BackupDirectory -BaseUri $BaseUri -SchemaId $schemaId -Filters $filters -PageSize $PageSize -Pages $search.Pages
|
|
Write-Host ('Backup written before the first save: {0}' -f $backupPath)
|
|
|
|
$completed = [Collections.Generic.List[object]]::new()
|
|
$failed = [Collections.Generic.List[object]]::new()
|
|
for ($rowIndex = 0; $rowIndex -lt $search.Rows.Count; $rowIndex++) {
|
|
$row = $search.Rows[$rowIndex]
|
|
$objectId = [string] $row[$objectIdIndex]
|
|
$dataId = [string] $row[$dataIdIndex]
|
|
if ($dataId -notmatch '^\d+$') {
|
|
throw 'Row {0} has an invalid generic-schema/data ID: {1}' -f ($rowIndex + 1), $dataId
|
|
}
|
|
|
|
$partName = 'S_{0}_{1}_{2}' -f $schemaId, $dataId, $field.Key
|
|
$multipart = New-MultipartFieldBody -Name $partName -Value $newValue
|
|
$saveUri = [Uri] ($BaseUri.TrimEnd('/') + '/save')
|
|
|
|
Write-Host ('[{0}/{1}] Updating object {2} (data ID {3})...' -f ($rowIndex + 1), $search.Rows.Count, $objectId, $dataId)
|
|
try {
|
|
$saveResult = Invoke-ElixFormsSaveRequest -Uri $saveUri -Cookie $cookie -ContentType $multipart.ContentType -Body $multipart.Body
|
|
$errors = @($saveResult.Json.errors.objectList)
|
|
$savedObjects = @($saveResult.Json.result.objectList)
|
|
if ($errors.Count -gt 0 -or $savedObjects.Count -eq 0) {
|
|
throw 'Save response contained {0} error(s) and {1} result object(s).' -f $errors.Count, $savedObjects.Count
|
|
}
|
|
|
|
$savedFields = @($savedObjects[0].sections | ForEach-Object { $_.fields })
|
|
$savedField = @($savedFields | Where-Object { $_.key -ceq $field.Key }) | Select-Object -First 1
|
|
$returnedValue = if ($null -ne $savedField -and @($savedField.valueList).Count -gt 0) {
|
|
[string] $savedField.valueList[0].value
|
|
}
|
|
else {
|
|
$null
|
|
}
|
|
|
|
$completed.Add([pscustomobject]@{
|
|
ObjectId = $objectId
|
|
DataId = $dataId
|
|
ReturnedValue = $returnedValue
|
|
})
|
|
Write-Host (' Saved. Returned value: "{0}"' -f (ConvertTo-PreviewText -Value $returnedValue -MaximumLength 120))
|
|
}
|
|
catch {
|
|
$failed.Add([pscustomobject]@{
|
|
ObjectId = $objectId
|
|
DataId = $dataId
|
|
Error = $_.Exception.Message
|
|
})
|
|
Write-Error ('Save failed for object {0} (data ID {1}). Further saves stopped. {2}' -f $objectId, $dataId, $_.Exception.Message) -ErrorAction Continue
|
|
break
|
|
}
|
|
}
|
|
|
|
Write-Host ''
|
|
Write-Host ('Update summary: {0} succeeded, {1} failed, {2} not attempted.' -f $completed.Count, $failed.Count, ($search.Rows.Count - $completed.Count - $failed.Count))
|
|
if ($failed.Count -gt 0) {
|
|
$failed | Format-Table -AutoSize | Out-Host
|
|
Write-Warning ('A partial update may have occurred. Use the backup at {0} to review original values.' -f $backupPath)
|
|
return
|
|
}
|
|
|
|
if (Read-YesNo -Prompt 'Re-run the same search to verify the current results?') {
|
|
$verification = Get-ElixFormsSearchResult -Client $client -BaseUri $BaseUri -Cookie $cookie -SchemaId $schemaId -Filters $filters -PageSize $PageSize
|
|
Show-SearchResults -Columns $verification.Columns -Rows $verification.Rows
|
|
if ($filters.Contains($field.Key)) {
|
|
Write-Host 'Note: the updated field was a search filter, so changed rows may no longer match.'
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
$cookie = $null
|
|
if ($null -ne $client) {
|
|
$client.Dispose()
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($MyInvocation.InvocationName -ne '.' -or $RunWhenDotSourced) {
|
|
Invoke-ElixFormsMetadataBulkUpdate -BaseUri $BaseUri -BackupDirectory $BackupDirectory -PageSize $PageSize
|
|
}
|