initial commit

This commit is contained in:
2026-08-24 09:45:51 +02:00
commit f18cd2c19d
12 changed files with 42013 additions and 0 deletions
@@ -0,0 +1,20 @@
---
name: elixforms-schema-bulk-updater
description: Safely inspect an ElixForms schema, search its records, and replace one field across the confirmed result set through the schema HTTP API. Use for guarded bulk schema updates driven by an authenticated browser cookie; do not use for unrelated ElixForms APIs or unattended production writes.
---
# ElixForms Schema Metadata Bulk Update
Use `scripts/Invoke-ElixFormsSchemaBulkUpdate.ps1` for the operation. It implements schema discovery, ordered field/filter prompts, paginated search, result preview, a timestamped pre-write backup, explicit destructive confirmation, one-field multipart saves, result reporting, and optional verification.
Before changing the helper or constructing requests another way, read [references/protocol.md](references/protocol.md). In particular, use the search result's generic-schema/data ID in save field names; the supplied HAR proves that this differs from `ID_OBJECT`.
Run interactively with PowerShell 7 or Windows PowerShell 5.1:
```powershell
.\Invoke-ElixFormsSchemaBulkUpdate.ps1
```
The operator must authenticate in ElixForms first and paste the complete `Cookie` header value into the hidden prompt. Never put the cookie in arguments, source files, logs, backups, or user-visible output.
Treat the preview and final confirmation as authorization only for the displayed rows, chosen field, and entered value. Do not remove either confirmation or broaden the result set after confirmation. If a save fails, stop further saves and report the partial outcome.
@@ -0,0 +1,4 @@
interface:
display_name: "ElixForms Schema Metadata Bulk Update"
short_description: "Safely update one schema field in bulk"
default_prompt: "Use $elixforms-schema-bulk-updater to prepare or run a guarded ElixForms schema metadata bulk update."
@@ -0,0 +1,37 @@
# ElixForms metadata protocol
## Endpoints
The default base URI is `https://console-unipr.elixforms.it/AJSRV/metadata`.
- Schema: `GET list?AJL=it&ID={schemaId}&ACL=true`
- Search: `GET search?SEARCH_IN[]={schemaId}&S_{schemaId}_NEW_{fieldKey}={value}&PAGE={page}&AJL=it&PAGESIZE=80&SECURE=true`
- Save: `POST save`
Send the manually obtained cookie and `X-Requested-With: XMLHttpRequest` on every request. For saves, send one UTF-8 `multipart/form-data` part and an explicit byte-based content length.
## Ordering and identifiers
Display schema sections by `orderKey`, then their fields by `orderKey`. Search result ordering comes only from the response's `columns` array; each row's values have matching indexes.
Two leading search columns are special:
- `ID_OBJECT` identifies the displayed metadata object.
- `S_{schemaId}_ID_GENERICSCHEMA` contains the generic-schema/data ID used by the save form.
Despite the wording in step 14 of the source workflow, do not use `ID_OBJECT` in a save field name. The supplied save HAR contains `S_616_81825_COL0014`, while its response identifies `dataId: 81825` and `objectId: 19519`. Therefore the save part name is:
```text
S_{schemaId}_{genericSchemaDataId}_{fieldKey}
```
The part value is the replacement text itself, including an intentionally empty string. Only the chosen field is sent, minimizing the chance of overwriting concurrent changes to other fields.
## Safety invariants
- Fetch and preview all pages, not only page 1.
- Keep the full unmodified search page objects in memory.
- After final confirmation but before the first save, write all page objects, filters, and timestamp to a UTF-8 JSON backup. Never include the cookie.
- Do not write if there are no results, the schema or field cannot be resolved, or confirmation is withheld.
- Stop after the first failed save because the operation is not transactional; report completed and failed rows so the operator knows whether a partial update occurred.
- A post-update search is verification only. If the updated field was also a filter, changed rows may correctly disappear from the refreshed results.
@@ -0,0 +1,686 @@
<#
.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.
#>
[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
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
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,
[Parameter()]
[byte[]] $Body,
[Parameter()]
[string] $ContentType
)
$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/')
if ($PSBoundParameters.ContainsKey('Body')) {
if ([string]::IsNullOrWhiteSpace($ContentType)) {
throw 'ContentType is required when Body is supplied.'
}
$request.Content = [Net.Http.ByteArrayContent]::new($Body)
$null = $request.Content.Headers.TryAddWithoutValidation('Content-Type', $ContentType)
$request.Content.Headers.ContentLength = $Body.Length
}
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 = '----ElixFormsBoundary' + [Guid]::NewGuid().ToString('N')
$newLine = "`r`n"
$bodyText = '--{0}{1}Content-Disposition: form-data; name="{2}"{1}{1}{3}{1}--{0}--{1}' -f $boundary, $newLine, $Name, $Value
$encoding = [Text.UTF8Encoding]::new($false)
$bytes = $encoding.GetBytes($bodyText)
return [pscustomobject]@{
Boundary = $boundary
ContentType = 'multipart/form-data; boundary={0}' -f $boundary
Bytes = $bytes
}
}
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-ElixFormsRequest -Client $client -Method POST -Uri $saveUri -Cookie $cookie -Body $multipart.Bytes -ContentType $multipart.ContentType
$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 '.') {
Invoke-ElixFormsMetadataBulkUpdate -BaseUri $BaseUri -BackupDirectory $BackupDirectory -PageSize $PageSize
}