initial commit
This commit is contained in:
@@ -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.
|
||||||
+686
@@ -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
|
||||||
|
}
|
||||||
Vendored
+15
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "PowerShell: Launch Current File",
|
||||||
|
"type": "PowerShell",
|
||||||
|
"request": "launch",
|
||||||
|
"script": "${file}",
|
||||||
|
"args": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Runs the repository-local ElixForms schema metadata bulk-update skill.
|
||||||
|
#>
|
||||||
|
|
||||||
|
& (Join-Path $PSScriptRoot '.agents\skills\elixforms-schema-bulk-updater\scripts\Invoke-ElixFormsSchemaBulkUpdate.ps1') @args
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"backedUpAt": "2026-08-24T09:18:48.3200375+02:00",
|
||||||
|
"baseUri": "https://console-unipr.elixforms.it/AJSRV/metadata",
|
||||||
|
"schemaId": 616,
|
||||||
|
"pageSize": 80,
|
||||||
|
"filters": {
|
||||||
|
"COL0010": "azione_form_AUTOR_firmaAutorizzazione"
|
||||||
|
},
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"totalRecords": 1,
|
||||||
|
"pageSize": 80,
|
||||||
|
"numPages": 1,
|
||||||
|
"currentPage": 1,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"key": "ID_OBJECT",
|
||||||
|
"value": "ID_OBJECT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S_616_ID_GENERICSCHEMA",
|
||||||
|
"value": "S_616_ID_GENERICSCHEMA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S_616_COL0014",
|
||||||
|
"value": "Titolo template"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S_616_COL0010",
|
||||||
|
"value": "Tag azione"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S_616_COL0011",
|
||||||
|
"value": "Tag procedimento"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S_616_COL0009",
|
||||||
|
"value": "Template"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S_616_COL0012",
|
||||||
|
"value": "Tag modulo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data": [
|
||||||
|
[
|
||||||
|
"35274",
|
||||||
|
"168008",
|
||||||
|
"Autorizzazione sottodominio",
|
||||||
|
"azione_form_AUTOR_firmaAutorizzazione",
|
||||||
|
"gestione_siti_web_nuovo_wf",
|
||||||
|
"Template autorizzazione sottodominio",
|
||||||
|
"RequestForm_GESTIONE_SITI_WEB_NUOVO"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"extraData": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Add skills and PowerShell script
|
||||||
|
|
||||||
|
> given the flow described in "User Workflow" secion of the document, and considering the whole document for a more detailed description, I want you to:
|
||||||
|
>
|
||||||
|
> - add appropriate skills
|
||||||
|
> - create a Powershell script that does what is described
|
||||||
|
|
||||||
|
<details><summary>15 previous messages</summary>
|
||||||
|
|
||||||
|
> I’ll inspect the workspace instructions and locate the document’s “User Workflow” section first. Then I’ll translate the full specification into the appropriate Codex skills and a PowerShell implementation, with runnable verification.
|
||||||
|
>
|
||||||
|
> <details><summary>Ran a command</summary>
|
||||||
|
>
|
||||||
|
> - Ran `Get-ChildItem -Force | Select-Object Mode,Length,LastWriteTime,Name; rg --files -g 'AGENTS.md' -g '*.md' -g '*.txt' -g '*.docx' -g '*.pdf'`
|
||||||
|
>
|
||||||
|
> </details>
|
||||||
|
>
|
||||||
|
> The request includes creating reusable skills, so I’m applying the `skill-creator` guidance before making changes. I found the specification in `save-metadata-schema-automatic.md`; I’ll read it in full, including its workflow and validation requirements.
|
||||||
|
>
|
||||||
|
> <details><summary>Ran 3 commands</summary>
|
||||||
|
>
|
||||||
|
> - Ran `Get-Content -Raw 'C:\Users\pierp\.codex\skills\.system\skill-creator\SKILL.md'; Get-Content -Raw '.\save-metadata-schema-automatic.md'`
|
||||||
|
> - Ran `$lines = Get-Content '.\save-metadata-schema-automatic.md'; "LINES=$($lines.Count)"; rg -n '^#{1,4} ' '.\save-metadata-schema-automatic.md'`
|
||||||
|
> - Ran `$lines = Get-Content '.\save-metadata-schema-automatic.md'; $lines[0..493]`
|
||||||
|
>
|
||||||
|
> </details>
|
||||||
|
>
|
||||||
|
> The specification is precise about request construction, but the save payload is the fragile part. I’m checking the supplied HAR structurally—without printing session cookies—to reproduce the real multipart field format, headers, and response behavior instead of guessing from prose.
|
||||||
|
>
|
||||||
|
> <details><summary>Ran a command</summary>
|
||||||
|
>
|
||||||
|
> - Ran `$har = Get-Content -Raw '.\save-metadata-schema.har' | ConvertFrom-Json -Depth 100; $entries = @($har.log.entries | Where-Object { $_.request.url -match '/AJSRV/metadata/(list|search|save)' }); foreach ($entry in $entries) { $request = $entry.request; $safeHeaders = @($request.headers | Where-Object { $_.name -notmatch '^(Cookie|Authorization)$' } | Select-Object name,value); [pscustomobject]@{ Method=$request.method; Url=$request.url; Headers=$safeHeaders; PostMime=$request.postData.mimeType; Params=$request.postData.params; Text=$request.postData.text; Status=$entry.response.status; ResponseMime=$entry.response.content.mimeType } | ConvertTo-Json -Depth 20 }`
|
||||||
|
>
|
||||||
|
> </details>
|
||||||
|
>
|
||||||
|
> The HAR reveals one important correction to the prose: save field names use the row’s generic-schema/data ID (the second search column, e.g. `81825`), not the first `ID_OBJECT` value. I’ll encode that verified invariant in both the script and the reusable skill. The script will also preserve column order, fetch every result page, back up all search responses before any write, require an explicit destructive confirmation, and summarize per-row outcomes.
|
||||||
|
>
|
||||||
|
> <details><summary>Ran 3 commands</summary>
|
||||||
|
>
|
||||||
|
> - Ran `Get-Content -Raw 'C:\Users\pierp\.codex\skills\.system\skill-creator\references\openai_yaml.md'; git status --short`
|
||||||
|
> - Ran `python 'C:\Users\pierp\.codex\skills\.system\skill-creator\scripts\init_skill.py' elixforms-schema-bulk-updater --path '.\.agents\skills' --resources scripts,references --interface 'display_name=ElixForms Metadata Bulk Update' --interface 'short_description=Safely update one metadata field in bulk' --interface 'default_prompt=Use $elixforms-schema-bulk-updater to prepare or run a guarded ElixForms metadata bulk update.'`
|
||||||
|
> - Ran `Get-Content -Raw '.\.agents\skills\elixforms-schema-bulk-updater\SKILL.md'; Get-Content -Raw '.\.agents\skills\elixforms-schema-bulk-updater\agents\openai.yaml'; Get-ChildItem -Recurse '.\.agents\skills\elixforms-schema-bulk-updater' | Select-Object FullName`
|
||||||
|
>
|
||||||
|
> </details>
|
||||||
|
>
|
||||||
|
> I’ve finished the design pass. The skill will be repository-local and self-contained under `.agents/skills`, with the full PowerShell implementation packaged as its deterministic helper plus a short root launcher. No credentials will be persisted; backups contain search data and filters but never the cookie.
|
||||||
|
>
|
||||||
|
> The initial patch hit a patch-tool constraint while replacing the generated scaffold; no implementation files were partially written. I’m splitting the change into smaller atomic patches and continuing.
|
||||||
|
>
|
||||||
|
> <details><summary>Created 3 files, edited a file, ran a command</summary>
|
||||||
|
>
|
||||||
|
> - Wrote `./.agents/skills/elixforms-schema-bulk-updater/SKILL.md` (+20 -6)
|
||||||
|
> - Wrote `./.agents/skills/elixforms-schema-bulk-updater/references/protocol.md` (+37 -0)
|
||||||
|
> - Wrote `./Invoke-ElixFormsMetadataBulkUpdate.ps1` (+1 -0)
|
||||||
|
> - Ran `Get-Content -Raw '.\.agents\skills\elixforms-schema-bulk-updater\SKILL.md'; Test-Path '.\.agents\skills\elixforms-schema-bulk-updater\references\protocol.md'; Test-Path '.\Invoke-ElixFormsMetadataBulkUpdate.ps1'`
|
||||||
|
> - Wrote `./.agents/skills/elixforms-schema-bulk-updater/scripts/Invoke-ElixFormsMetadataBulkUpdate.ps1` (+653 -0)
|
||||||
|
>
|
||||||
|
> </details>
|
||||||
|
>
|
||||||
|
> The implementation is now in place. I’m at the verification stage: parsing it with PowerShell’s AST parser, exercising URI encoding and multipart byte construction against the documented examples, checking the root launcher, and validating the skill package for unfinished scaffold content or metadata errors.
|
||||||
|
>
|
||||||
|
> <details><summary>Ran 4 commands</summary>
|
||||||
|
>
|
||||||
|
> - Ran ``$scriptPath = Resolve-Path '.\.agents\skills\elixforms-schema-bulk-updater\scripts\Invoke-ElixFormsMetadataBulkUpdate.ps1'; $tokens = $null; $parseErrors = $null; $ast = [System.Management.Automation.Language.Parser]::ParseFile($scriptPath, [ref]$tokens, [ref]$parseErrors); if ($parseErrors.Count -gt 0) { $parseErrors | Format-List; exit 1 }; 'PowerShell parse: OK'; . $scriptPath; $query = [ordered]@{'SEARCH_IN[]'=616; 'S_616_NEW_COL0014'='antani tapioca'; PAGE=1; AJL='it'; PAGESIZE=80; SECURE='true'}; $uri = New-ElixFormsUri -BaseUri 'https://console-unipr.elixforms.it/AJSRV/metadata' -Route 'search' -Query $query; "URI=$uri"; if ($uri.AbsoluteUri -notmatch 'SEARCH_IN%5B%5D=616' -or $uri.AbsoluteUri -notmatch 'antani%20tapioca') { throw 'Query encoding test failed.' }; $multipart = New-MultipartFieldBody -Name 'S_616_81825_COL0014' -Value ''; $multipartText = [Text.Encoding]::UTF8.GetString($multipart.Bytes); if ($multipartText -notmatch 'name="S_616_81825_COL0014"' -or -not $multipartText.EndsWith("--`r`n")) { throw 'Multipart test failed.' }; "Multipart bytes=$($multipart.Bytes.Length) content-type=$($multipart.ContentType)"; 'Helper tests: OK'; python 'C:\Users\pierp\.codex\skills\.system\skill-creator\scripts\quick_validate.py' '.\.agents\skills\elixforms-schema-bulk-updater'; rg -n '\[TODO|TODO:' '.\.agents\skills\elixforms-schema-bulk-updater' '.\Invoke-ElixFormsMetadataBulkUpdate.ps1'``
|
||||||
|
> - Ran ``$scriptPath = Resolve-Path '.\.agents\skills\elixforms-schema-bulk-updater\scripts\Invoke-ElixFormsMetadataBulkUpdate.ps1'; $search = Get-Content -Raw '.\search-schema-results.json' | ConvertFrom-Json; "Fixture columns=$(@($search.columns).Count) rows=$(@($search.data).Count) total=$($search.totalRecords) pages=$($search.numPages)"; $genericKey = 'S_616_ID_GENERICSCHEMA'; $genericIndex = [Array]::IndexOf([object[]]@($search.columns.key), $genericKey); $objectIndex = [Array]::IndexOf([object[]]@($search.columns.key), 'ID_OBJECT'); "Fixture ID indexes: object=$objectIndex generic-data=$genericIndex"; if ($genericIndex -lt 0 -or $objectIndex -lt 0) { throw 'Fixture identifier columns missing.' }; $firstRow = @($search.data)[0]; "First save-name example=S_616_$($firstRow[$genericIndex])_COL0014 (object $($firstRow[$objectIndex]))"; if (Get-Command Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue) { Invoke-ScriptAnalyzer -Path $scriptPath -Severity Warning,Error | Format-Table -AutoSize } else { 'PSScriptAnalyzer not installed; skipped.' }; if (Get-Command powershell.exe -ErrorAction SilentlyContinue) { powershell.exe -NoProfile -Command "`$tokens=`$null; `$errors=`$null; [System.Management.Automation.Language.Parser]::ParseFile('$($scriptPath.Path.Replace("'", "''"))',[ref]`$tokens,[ref]`$errors) > `$null; if (`$errors.Count) { `$errors | Out-String; exit 1 }; 'Windows PowerShell parse: OK'" }``
|
||||||
|
> - Ran `$failed = $false; Get-ChildItem -Recurse -Filter '*.ps1' | ForEach-Object { $tokens=$null; $errors=$null; [System.Management.Automation.Language.Parser]::ParseFile($_.FullName,[ref]$tokens,[ref]$errors) > $null; if ($errors.Count) { $failed=$true; $errors | ForEach-Object { "PARSE ERROR $($_.Extent.File):$($_.Extent.StartLineNumber): $($_.Message)" } } else { "PARSE OK $($_.FullName)" } }; if ($failed) { exit 1 }; Get-ChildItem -Recurse '.\.agents\skills\elixforms-schema-bulk-updater' | Select-Object FullName,Length; Get-Content -Raw '.\.agents\skills\elixforms-schema-bulk-updater\agents\openai.yaml'`
|
||||||
|
> - Ran `try { throw 'Value {0}' -f 42 } catch { "THROW_TEST=$($_.Exception.Message)" }; . '.\.agents\skills\elixforms-schema-bulk-updater\scripts\Invoke-ElixFormsMetadataBulkUpdate.ps1'; $sample = Get-Content -Raw '.\search-schema-results.json' | ConvertFrom-Json; $columns = @($sample.columns); $rows = @($sample.data); "RowsRuntimeType=$($rows.GetType().FullName) FirstRowType=$($rows[0].GetType().FullName) FirstRowCount=$(@($rows[0]).Count)"; $objectIndex = Get-ColumnIndex -Columns $columns -Key 'ID_OBJECT'; $genericIndex = Get-ColumnIndex -Columns $columns -Key 'S_616_ID_GENERICSCHEMA'; "FunctionIndexes=$objectIndex,$genericIndex"; $schemaFixture = [pscustomobject]@{ sections = @([pscustomobject]@{key='SEC_B'; title='B'; orderKey='ORD002'; fields=@([pscustomobject]@{key='COL_B';title='B';type='STRING';orderKey='ORD001'})},[pscustomobject]@{key='SEC_A';title='A';orderKey='ORD001';fields=@([pscustomobject]@{key='COL_A2';title='A2';type='STRING';orderKey='ORD002'},[pscustomobject]@{key='COL_A1';title='A1';type='STRING';orderKey='ORD001'})})}; $ordered = @(Get-OrderedSchemaFields -Schema $schemaFixture); "OrderedFields=$($ordered.Key -join ',')"; if (($ordered.Key -join ',') -ne 'COL_A1,COL_A2,COL_B') { throw 'Field ordering test failed.' }`
|
||||||
|
>
|
||||||
|
> </details>
|
||||||
|
>
|
||||||
|
> The verification checks are clean in both PowerShell 7 and Windows PowerShell 5.1, and the supplied 56-row fixture maps identifiers as expected. I’m making one final hardening pass on backup naming and inline help, then I’ll hand over the exact run command and the HAR-derived identifier caveat.
|
||||||
|
>
|
||||||
|
> <details><summary>Edited 2 files, ran a command</summary>
|
||||||
|
>
|
||||||
|
> - Wrote `./.agents/skills/elixforms-schema-bulk-updater/scripts/Invoke-ElixFormsMetadataBulkUpdate.ps1` (+25 -1)
|
||||||
|
> - Wrote `./Invoke-ElixFormsMetadataBulkUpdate.ps1` (+5 -0)
|
||||||
|
> - Ran ``$allGood = $true; Get-ChildItem -Recurse -Filter '*.ps1' | ForEach-Object { $tokens=$null; $errors=$null; [System.Management.Automation.Language.Parser]::ParseFile($_.FullName,[ref]$tokens,[ref]$errors) > $null; if ($errors.Count) { $allGood=$false; $errors | Format-List } }; if (-not $allGood) { exit 1 }; . '.\.agents\skills\elixforms-schema-bulk-updater\scripts\Invoke-ElixFormsMetadataBulkUpdate.ps1'; $query=[ordered]@{'SEARCH_IN[]'=616;'S_616_NEW_COL0014'='antani tapioca';PAGE=1;AJL='it';PAGESIZE=80;SECURE='true'}; $uri=New-ElixFormsUri -BaseUri 'https://console-unipr.elixforms.it/AJSRV/metadata' -Route 'search' -Query $query; if ($uri.AbsoluteUri -notmatch 'SEARCH_IN%5B%5D=616' -or $uri.AbsoluteUri -notmatch 'antani%20tapioca') { throw 'URI test failed' }; $part=New-MultipartFieldBody -Name 'S_616_81825_COL0014' -Value ''; if ($part.Bytes.Length -le 0 -or $part.ContentType -notmatch '^multipart/form-data; boundary=') { throw 'Multipart test failed' }; python 'C:\Users\pierp\.codex\skills\.system\skill-creator\scripts\quick_validate.py' '.\.agents\skills\elixforms-schema-bulk-updater'; 'Final validation: OK'; rg -n '^function (Get-ElixFormsSearchResult|Save-SearchBackup|Invoke-ElixFormsMetadataBulkUpdate)|generic-schema/data ID|^Use `scripts' '.\.agents\skills\elixforms-schema-bulk-updater\SKILL.md' '.\.agents\skills\elixforms-schema-bulk-updater\references\protocol.md' '.\.agents\skills\elixforms-schema-bulk-updater\scripts\Invoke-ElixFormsMetadataBulkUpdate.ps1'; Get-ChildItem -Recurse -File '.\.agents\skills\elixforms-schema-bulk-updater','Invoke-ElixFormsMetadataBulkUpdate.ps1' | Select-Object FullName,Length``
|
||||||
|
>
|
||||||
|
> </details>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
Implemented the complete workflow.
|
||||||
|
|
||||||
|
- Added the repository-local skill: [SKILL.md](<./.agents/skills/elixforms-schema-bulk-updater/SKILL.md:1>)
|
||||||
|
- Created the executable launcher: [Invoke-ElixFormsMetadataBulkUpdate.ps1](<./Invoke-ElixFormsMetadataBulkUpdate.ps1:1>)
|
||||||
|
- Added protocol documentation, including the HAR-confirmed requirement to use `ID_GENERICSCHEMA`/`dataId` rather than `ID_OBJECT` when constructing save field names.
|
||||||
|
|
||||||
|
Run it with:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Invoke-ElixFormsMetadataBulkUpdate.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
The script includes hidden cookie input, schema validation, ordered filters, pagination, result preview, two confirmations, timestamped JSON backups, multipart saves with byte-accurate `Content-Length`, failure-safe stopping, summaries, and optional verification.
|
||||||
|
|
||||||
|
Validated with PowerShell 7 and Windows PowerShell 5.1 parsing, fixture-based identifier checks, URI/multipart tests, and the Codex skill validator. The live API was not called because that requires your authenticated cookie and would modify production data.
|
||||||
@@ -0,0 +1,778 @@
|
|||||||
|
# Automatic bulk update of schema metadata
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
|
||||||
|
### Schema metadata
|
||||||
|
|
||||||
|
ID: 616
|
||||||
|
Name: WFTemplate
|
||||||
|
|
||||||
|
1. Call to `https://console-unipr.elixforms.it/AJSRV/metadata/list?AJL=it&ID=616&ACL=true`
|
||||||
|
2. Response is a JSON:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"objectList": [
|
||||||
|
{
|
||||||
|
"id": 616,
|
||||||
|
"title": "WFTemplate",
|
||||||
|
"dataId": 0,
|
||||||
|
"tag": "",
|
||||||
|
"objectId": 0,
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"title": "Template",
|
||||||
|
"key": "SEC_0006",
|
||||||
|
"orderKey": "ORD001",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"key": "COL0014",
|
||||||
|
"title": "Titolo template",
|
||||||
|
"searchable": false,
|
||||||
|
"valueList": [
|
||||||
|
{
|
||||||
|
"key": "",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"itemList": null,
|
||||||
|
"toManyItems": false,
|
||||||
|
"attributes": [],
|
||||||
|
"orderKey": "ORD001",
|
||||||
|
"defaultValue": null,
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "HTML",
|
||||||
|
"key": "COL0009",
|
||||||
|
"title": "Template",
|
||||||
|
"searchable": false,
|
||||||
|
"valueList": [
|
||||||
|
{
|
||||||
|
"key": "",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"itemList": null,
|
||||||
|
"toManyItems": false,
|
||||||
|
"attributes": [],
|
||||||
|
"orderKey": "ORD002",
|
||||||
|
"defaultValue": null,
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"attributes": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Metadata",
|
||||||
|
"key": "SEC_0007",
|
||||||
|
"orderKey": "ORD002",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"key": "COL0010",
|
||||||
|
"title": "Tag azione",
|
||||||
|
"searchable": false,
|
||||||
|
"valueList": [
|
||||||
|
{
|
||||||
|
"key": "",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"itemList": null,
|
||||||
|
"toManyItems": false,
|
||||||
|
"attributes": [],
|
||||||
|
"orderKey": "ORD001",
|
||||||
|
"defaultValue": null,
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"key": "COL0011",
|
||||||
|
"title": "Tag procedimento",
|
||||||
|
"searchable": false,
|
||||||
|
"valueList": [
|
||||||
|
{
|
||||||
|
"key": "",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"itemList": null,
|
||||||
|
"toManyItems": false,
|
||||||
|
"attributes": [],
|
||||||
|
"orderKey": "ORD002",
|
||||||
|
"defaultValue": null,
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"key": "COL0012",
|
||||||
|
"title": "Tag modulo",
|
||||||
|
"searchable": false,
|
||||||
|
"valueList": [
|
||||||
|
{
|
||||||
|
"key": "",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"itemList": null,
|
||||||
|
"toManyItems": false,
|
||||||
|
"attributes": [],
|
||||||
|
"orderKey": "ORD003",
|
||||||
|
"defaultValue": null,
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"attributes": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"categories": [],
|
||||||
|
"aclTable": [
|
||||||
|
{
|
||||||
|
"objectType": "SCHEMA",
|
||||||
|
"objectId": 616,
|
||||||
|
"permissionType": "VIEW",
|
||||||
|
"groupId": 0,
|
||||||
|
"allow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectType": "SCHEMA",
|
||||||
|
"objectId": 616,
|
||||||
|
"permissionType": "INSERT",
|
||||||
|
"groupId": 0,
|
||||||
|
"allow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectType": "SCHEMA",
|
||||||
|
"objectId": 616,
|
||||||
|
"permissionType": "UPDATE",
|
||||||
|
"groupId": 0,
|
||||||
|
"allow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectType": "SCHEMA",
|
||||||
|
"objectId": 616,
|
||||||
|
"permissionType": "DELETE",
|
||||||
|
"groupId": 0,
|
||||||
|
"allow": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"master": false,
|
||||||
|
"attributes": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. `objectList.sections` is an array containing the sections defined in the schema
|
||||||
|
4. in each element of `objectList.sections` there is the `fields` array property, containing the fields definitions
|
||||||
|
5. Each one of the fields (use `key` and `title` properties) can be used as a filter (the `searchable: false` properties are just plain ignored...)
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
### Schema structure
|
||||||
|
|
||||||
|
(taken from schema attributes)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- sectionId: SEC_0006
|
||||||
|
sectionOrderKey: ORD001
|
||||||
|
sectionColumns:
|
||||||
|
- ID: COL0014
|
||||||
|
Name: Titolo template
|
||||||
|
orderKey: ORD001
|
||||||
|
- ID: COL0009
|
||||||
|
Name: Template
|
||||||
|
orderKey: ORD002
|
||||||
|
- sectionId: SEC_0007
|
||||||
|
sectionOrderKey: ORD002
|
||||||
|
sectionColumns:
|
||||||
|
- ID: COL0010
|
||||||
|
Name: Tag azione
|
||||||
|
orderKey: ORD001
|
||||||
|
- ID: COL0011
|
||||||
|
Name: Tag procedimento
|
||||||
|
orderKey: ORD002
|
||||||
|
- ID: COL0012
|
||||||
|
Name: Tag modulo
|
||||||
|
orderKey: ORD003
|
||||||
|
```
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
### Schema search
|
||||||
|
|
||||||
|
Results in table format!
|
||||||
|
|
||||||
|
1. ID object (17460)
|
||||||
|
2. ID object (73743)
|
||||||
|
3. Titolo template (depDUSIC_Delibera)
|
||||||
|
4. Tag azione (liqc__op_a_generaDelibera__depDUSIC)
|
||||||
|
5. Tag procedimento (procedimento_liqc)
|
||||||
|
6. Template (...omitted...)
|
||||||
|
7. Tag modulo (RequestForm_RIPARTIZIONE_CCT_DOCENTE)
|
||||||
|
|
||||||
|
Column order in results is based ON WHAT!? Mmmh... orderKey ASC, Name ASC? Nope...
|
||||||
|
First metadata ordering is based on column's OrderKey.
|
||||||
|
Second metadata ordering? Not section's orderKey (COL0011 and COL0009 should be inverted), so... column id DESCENDING? If so, WHY!?
|
||||||
|
Anyway, column ordering in results table seems to be given by the `search` response content (see below).
|
||||||
|
|
||||||
|
| Order | Name | Column | OrderKey | sectionOrderKey |
|
||||||
|
| ----- | ---------------- | ------- | -------- | --------------- |
|
||||||
|
| 1 | Titolo template | COL0014 | ORD001 | ORD001 |
|
||||||
|
| 2 | Tag azione | COL0010 | ORD001 | ORD002 |
|
||||||
|
| 3 | Tag procedimento | COL0011 | ORD002 | ORD002 |
|
||||||
|
| 4 | Template | COL0009 | ORD002 | ORD001 |
|
||||||
|
| 5 | Tag modulo | COL0012 | ORD003 | ORD002 |
|
||||||
|
|
||||||
|
### Schema save
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"errors": {
|
||||||
|
"objectList": []
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"objectList": [
|
||||||
|
{
|
||||||
|
"id": 616,
|
||||||
|
"title": "WFTemplate",
|
||||||
|
"dataId": 81825,
|
||||||
|
"tag": "",
|
||||||
|
"objectId": 19519,
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"title": "Template",
|
||||||
|
"key": "SEC_0006",
|
||||||
|
"orderKey": "ORD001",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"key": "COL0014",
|
||||||
|
"title": "Titolo template",
|
||||||
|
"searchable": false,
|
||||||
|
"valueList": [
|
||||||
|
{
|
||||||
|
"key": "",
|
||||||
|
"value": "depCLA_Delibera"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"itemList": null,
|
||||||
|
"toManyItems": false,
|
||||||
|
"attributes": [],
|
||||||
|
"orderKey": "ORD001",
|
||||||
|
"defaultValue": null,
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "HTML",
|
||||||
|
"key": "COL0009",
|
||||||
|
"title": "Template",
|
||||||
|
"searchable": false,
|
||||||
|
"valueList": [
|
||||||
|
{
|
||||||
|
"key": "",
|
||||||
|
"value": "<!DOCTYPE html>\n<html>\n[EFTL]...[/EFTL]\n</html>"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"itemList": null,
|
||||||
|
"toManyItems": false,
|
||||||
|
"attributes": [],
|
||||||
|
"orderKey": "ORD002",
|
||||||
|
"defaultValue": null,
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"attributes": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Metadata",
|
||||||
|
"key": "SEC_0007",
|
||||||
|
"orderKey": "ORD002",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"key": "COL0010",
|
||||||
|
"title": "Tag azione",
|
||||||
|
"searchable": false,
|
||||||
|
"valueList": [
|
||||||
|
{
|
||||||
|
"key": "",
|
||||||
|
"value": "liqc__op_a_generaDelibera__depCLA"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"itemList": null,
|
||||||
|
"toManyItems": false,
|
||||||
|
"attributes": [],
|
||||||
|
"orderKey": "ORD001",
|
||||||
|
"defaultValue": null,
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"key": "COL0011",
|
||||||
|
"title": "Tag procedimento",
|
||||||
|
"searchable": false,
|
||||||
|
"valueList": [
|
||||||
|
{
|
||||||
|
"key": "",
|
||||||
|
"value": "procedimento_liqc"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"itemList": null,
|
||||||
|
"toManyItems": false,
|
||||||
|
"attributes": [],
|
||||||
|
"orderKey": "ORD002",
|
||||||
|
"defaultValue": null,
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"key": "COL0012",
|
||||||
|
"title": "Tag modulo",
|
||||||
|
"searchable": false,
|
||||||
|
"valueList": [
|
||||||
|
{
|
||||||
|
"key": "",
|
||||||
|
"value": "RequestForm_RIPARTIZIONE_CCT_DOCENTE"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"itemList": null,
|
||||||
|
"toManyItems": false,
|
||||||
|
"attributes": [],
|
||||||
|
"orderKey": "ORD003",
|
||||||
|
"defaultValue": null,
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"attributes": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"categories": [],
|
||||||
|
"aclTable": [],
|
||||||
|
"master": false,
|
||||||
|
"attributes": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
## HTTP sequences
|
||||||
|
|
||||||
|
### Search by schema
|
||||||
|
|
||||||
|
1. Select schema from dropdown
|
||||||
|
2. Select tab "Cerca", optionally insert filters
|
||||||
|
3. Click on "Cerca" and obtain results
|
||||||
|
|
||||||
|
See file `search-schema.har`
|
||||||
|
|
||||||
|
Sequence is:
|
||||||
|
|
||||||
|
1. Call `https://console-unipr.elixforms.it/AJSRV/metadata/search?SEARCH_IN%5B%5D=616&PAGE=1&AJL=it&PAGESIZE=80&SECURE=true`
|
||||||
|
1. With filters: `https://console-unipr.elixforms.it/AJSRV/metadata/search?SEARCH_IN%5B%5D=616&S_616_NEW_COL0014=antani&S_616_NEW_COL0010=tapioca&PAGE=1&AJL=it&PAGESIZE=80&SECURE=true`
|
||||||
|
2. `search` route returns a JSON
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"totalRecords": 56,
|
||||||
|
"pageSize": 80,
|
||||||
|
"numPages": 1,
|
||||||
|
"currentPage": 1,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"key": "ID_OBJECT",
|
||||||
|
"value": "ID_OBJECT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S_616_ID_GENERICSCHEMA",
|
||||||
|
"value": "S_616_ID_GENERICSCHEMA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S_616_COL0014",
|
||||||
|
"value": "Titolo template"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S_616_COL0010",
|
||||||
|
"value": "Tag azione"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S_616_COL0011",
|
||||||
|
"value": "Tag procedimento"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S_616_COL0009",
|
||||||
|
"value": "Template"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S_616_COL0012",
|
||||||
|
"value": "Tag modulo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data": [
|
||||||
|
[
|
||||||
|
"17460",
|
||||||
|
"73743",
|
||||||
|
"depDUSIC_Delibera",
|
||||||
|
"liqc__op_a_generaDelibera__depDUSIC",
|
||||||
|
"procedimento_liqc",
|
||||||
|
"<!DOCTYPE html>\n<html>\n[EFTL]\n.........[/EFTL]\n</html>",
|
||||||
|
"RequestForm_RIPARTIZIONE_CCT_DOCENTE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"17463",
|
||||||
|
"73782",
|
||||||
|
"depDUSIC_Provvedimento",
|
||||||
|
"liqc__op_a_inviaPerFirmaRemotaProvvedimento__depDUSIC",
|
||||||
|
"procedimento_liqc",
|
||||||
|
"<!DOCTYPE html>\n<html>\n[EFTL]\n.........[/EFTL]\n</html>",
|
||||||
|
"RequestForm_RIPARTIZIONE_CCT_DOCENTE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"19494",
|
||||||
|
"81800",
|
||||||
|
"depGSPI_Delibera",
|
||||||
|
"liqc__op_a_generaDelibera__depGSPI",
|
||||||
|
"procedimento_liqc",
|
||||||
|
"<!DOCTYPE html>\n<html>\n[EFTL]\n.........[/EFTL]\n</html>",
|
||||||
|
"RequestForm_RIPARTIZIONE_CCT_DOCENTE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"19495",
|
||||||
|
"81801",
|
||||||
|
"depGSPI_Provvedimento",
|
||||||
|
"liqc__op_a_inviaPerFirmaRemotaProvvedimento__depGSPI",
|
||||||
|
"procedimento_liqc",
|
||||||
|
"<!DOCTYPE html>\n<html>\n[EFTL]\n.........[/EFTL]\n</html>",
|
||||||
|
"RequestForm_RIPARTIZIONE_CCT_DOCENTE"
|
||||||
|
],
|
||||||
|
/* other elements */
|
||||||
|
[
|
||||||
|
"35397",
|
||||||
|
"168258",
|
||||||
|
"Titolo template (WFTemplate)",
|
||||||
|
"azione_ACQUISTI_generaPdfDetermina",
|
||||||
|
"edilizia_rda_manutenzione_wf",
|
||||||
|
"Template (WFTemplate)",
|
||||||
|
"RequestForm_EDILIZIA_RDA_MANUTENZIONE"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"extraData": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. FOR EACH of the results, `objectTitle` route is called TWICE! These are NOT really needed in the process:
|
||||||
|
- `https://console-unipr.elixforms.it/AJSRV/metadata/objectTitle?ATTR_NAME=master_field&ATTR_VALUE=true&TYPE=SCHEMADATA&OBJECT_ID=17460`
|
||||||
|
- `https://console-unipr.elixforms.it/AJSRV/metadata/objectTitle?ATTR_NAME=master_field&ATTR_VALUE=true&TYPE=GENERICSCHEMA&OBJECT_ID=73743`
|
||||||
|
4. Each one return a JSON, do not know what it's for:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"objectList":[]}
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTP requests
|
||||||
|
|
||||||
|
**Schema data**:
|
||||||
|
|
||||||
|
https://console-unipr.elixforms.it/AJSRV/metadata/list?AJL=it&ID=616&ACL=true
|
||||||
|
|
||||||
|
**Search**:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET https://console-unipr.elixforms.it/AJSRV/metadata/search?SEARCH_IN%5B%5D=616&PAGE=1&AJL=it&PAGESIZE=80&SECURE=true HTTP/1.1
|
||||||
|
Accept: */*
|
||||||
|
Accept-Encoding: gzip, deflate, br, zstd
|
||||||
|
Cookie: ISIPSESSION=047b770fac5627c03c94030ca0c3b45c; AJSRV=BTN_USER_1787207482458_8771527838545232265; ISIPSESSION_TRACK=99b2488e73af7c61f6e5affceeb32502; JSESSIONID=e3c44b958c4f282660a5fc8ed994
|
||||||
|
X-Requested-With: XMLHttpRequest
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## User Workflow
|
||||||
|
|
||||||
|
Routes:
|
||||||
|
|
||||||
|
- **list**: GET `https://console-unipr.elixforms.it/AJSRV/metadata/list?AJL=it&ID=616&ACL=true` (ID is dynamic)
|
||||||
|
- **search**: GET `https://console-unipr.elixforms.it/AJSRV/metadata/search?SEARCH_IN%5B%5D=616&S_616_NEW_COL0014=antani&S_616_NEW_COL0010=tapioca&PAGE=1&AJL=it&PAGESIZE=80&SECURE=true` (some parameters are dynamic)
|
||||||
|
- **save**: POST `https://console-unipr.elixforms.it/AJSRV/metadata/save`
|
||||||
|
|
||||||
|
Sequence:
|
||||||
|
|
||||||
|
1. Obtain the cookie value by logging manually to elixForms
|
||||||
|
2. Ask the user to input the cookie value and use that as `Cookie` header in the following requests
|
||||||
|
3. Ask the user for the following values:
|
||||||
|
1. Schema ID (e.g. 616)
|
||||||
|
4. Use the `list` route to obtain schema data (columns that can be used as a filter, see [Schema data](#schema-data))
|
||||||
|
5. Present the user with the schema details, such as
|
||||||
|
1. ID: `objectList.id`
|
||||||
|
2. Title: `objectList.title`
|
||||||
|
3. Sections: one for each `objectList.sections`, ordered by orderKey ASC, showing:
|
||||||
|
1. key: `objectList.sections[*].key`
|
||||||
|
2. title: `objectList.sections[*].title`
|
||||||
|
3. Fields: for each element in fields, ordered by orderKey ASC, showing:
|
||||||
|
1. key: `objectList.sections[*].fields[*].key`
|
||||||
|
2. title: `objectList.sections[*].fields[*].title`
|
||||||
|
3. type: `objectList.sections[*].fields[*].type`
|
||||||
|
6. Ask confirmation that the schema is the right one, then, for each one of the fields (consider them globally, independent of section), ask the user for a value:
|
||||||
|
1. If no value, or empty string, is given, the field will not be used as a filter
|
||||||
|
2. If a non-empty value is given, that value will be used as a filter for the subsequent search; the query string values will be:
|
||||||
|
- `SEARCH_IN[]`: the schema ID
|
||||||
|
- for each of the given values, build a query param such as `S_${schemaId}_NEW_${fieldKey}` and assign to it the value given by the user
|
||||||
|
- `PAGE`: 1
|
||||||
|
- `AJL`: it
|
||||||
|
- `PAGESIZE`: 80
|
||||||
|
- `SECURE`: true
|
||||||
|
7. Call the `search` route with:
|
||||||
|
- the query params defined above
|
||||||
|
- the `Cookie` header given by the user
|
||||||
|
- the `x-requested-with` header with value `XMLHttpRequest`
|
||||||
|
8. The search route will give a JSON like the following:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"totalRecords": 56,
|
||||||
|
"pageSize": 80,
|
||||||
|
"numPages": 1,
|
||||||
|
"currentPage": 1,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"key": "ID_OBJECT",
|
||||||
|
"value": "ID_OBJECT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S_616_ID_GENERICSCHEMA",
|
||||||
|
"value": "S_616_ID_GENERICSCHEMA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S_616_COL0014",
|
||||||
|
"value": "Titolo template"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S_616_COL0010",
|
||||||
|
"value": "Tag azione"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S_616_COL0011",
|
||||||
|
"value": "Tag procedimento"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S_616_COL0009",
|
||||||
|
"value": "Template"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S_616_COL0012",
|
||||||
|
"value": "Tag modulo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data": [
|
||||||
|
[
|
||||||
|
"17460",
|
||||||
|
"73743",
|
||||||
|
"depDUSIC_Delibera",
|
||||||
|
"liqc__op_a_generaDelibera__depDUSIC",
|
||||||
|
"procedimento_liqc",
|
||||||
|
"<!DOCTYPE html>\n<html>\n[EFTL]\n.........[/EFTL]\n</html>",
|
||||||
|
"RequestForm_RIPARTIZIONE_CCT_DOCENTE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"17463",
|
||||||
|
"73782",
|
||||||
|
"depDUSIC_Provvedimento",
|
||||||
|
"liqc__op_a_inviaPerFirmaRemotaProvvedimento__depDUSIC",
|
||||||
|
"procedimento_liqc",
|
||||||
|
"<!DOCTYPE html>\n<html>\n[EFTL]\n.........[/EFTL]\n</html>",
|
||||||
|
"RequestForm_RIPARTIZIONE_CCT_DOCENTE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"19494",
|
||||||
|
"81800",
|
||||||
|
"depGSPI_Delibera",
|
||||||
|
"liqc__op_a_generaDelibera__depGSPI",
|
||||||
|
"procedimento_liqc",
|
||||||
|
"<!DOCTYPE html>\n<html>\n[EFTL]\n.........[/EFTL]\n</html>",
|
||||||
|
"RequestForm_RIPARTIZIONE_CCT_DOCENTE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"19495",
|
||||||
|
"81801",
|
||||||
|
"depGSPI_Provvedimento",
|
||||||
|
"liqc__op_a_inviaPerFirmaRemotaProvvedimento__depGSPI",
|
||||||
|
"procedimento_liqc",
|
||||||
|
"<!DOCTYPE html>\n<html>\n[EFTL]\n.........[/EFTL]\n</html>",
|
||||||
|
"RequestForm_RIPARTIZIONE_CCT_DOCENTE"
|
||||||
|
],
|
||||||
|
/* other elements */
|
||||||
|
[
|
||||||
|
"35397",
|
||||||
|
"168258",
|
||||||
|
"Titolo template (WFTemplate)",
|
||||||
|
"azione_ACQUISTI_generaPdfDetermina",
|
||||||
|
"edilizia_rda_manutenzione_wf",
|
||||||
|
"Template (WFTemplate)",
|
||||||
|
"RequestForm_EDILIZIA_RDA_MANUTENZIONE"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"extraData": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
1. The `columns` array property contains key-value pairs that define the order in which the schema columns will be shown, based on the order in the array (use order-maintaining structures)
|
||||||
|
2. Note that:
|
||||||
|
- pair with `key` having `ID_OBJECT` doesn't match any of the schema fields
|
||||||
|
- there is a pair with `key` composed like `S_${schemaId}_ID_GENERICSCHEMA`, this one also doesn't match any of the schema field
|
||||||
|
- other pairs should have their `key` composed like `S_${schemaId}_${columnId}`
|
||||||
|
- the `value` property is a string that will be used as column header
|
||||||
|
3. The `data` array contains one element for each of the results row
|
||||||
|
4. Each one of the `data` elements are string arrays: their content is ordered as the columns described above, and each value is corresponding to the columns
|
||||||
|
9. Show the obtained results (store them in memory, they will be used later) and ask confirmation to proceed (exit if no cofirmation is given)
|
||||||
|
10. Based on the response of the `list` route, ask the user which one field must be updated with a new value; empty value means exit
|
||||||
|
11. Ask for the user for the new value to be used for that field; empty value means empty string, don't exit
|
||||||
|
12. Ask for a final confirmation that the rows shown in step 9 will be irremediably replaced!
|
||||||
|
13. Make a backup of the `search` route response anyway, appending date in yyyyMMddHHmmss format
|
||||||
|
14. Now, for each results obtained in step 9:
|
||||||
|
1. Build a request with multipart/form-data with a random boundary string and add the relative `Content-Type` header
|
||||||
|
2. Add to the form a name-value JSON element, where `name` is composed as `S_${schemaId}_${ID_OBJECT}_${columnId}` and `value` is the value given by the user
|
||||||
|
3. Compute and add a `Content-Length` header
|
||||||
|
4. Add a `Cookie` header given by the user
|
||||||
|
5. Add a `x-requested-with` header with value `XMLHttpRequest`
|
||||||
|
6. Call the `save` route `POST` request `https://console-unipr.elixforms.it/AJSRV/metadata/save`
|
||||||
|
7. The response will be a JSON such as:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"errors": {
|
||||||
|
"objectList": []
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"objectList": [
|
||||||
|
{
|
||||||
|
"id": 616,
|
||||||
|
"title": "WFTemplate",
|
||||||
|
"dataId": 81825,
|
||||||
|
"tag": "",
|
||||||
|
"objectId": 19519,
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"title": "Template",
|
||||||
|
"key": "SEC_0006",
|
||||||
|
"orderKey": "ORD001",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"key": "COL0014",
|
||||||
|
"title": "Titolo template",
|
||||||
|
"searchable": false,
|
||||||
|
"valueList": [
|
||||||
|
{
|
||||||
|
"key": "",
|
||||||
|
"value": "depCLA_Delibera"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"itemList": null,
|
||||||
|
"toManyItems": false,
|
||||||
|
"attributes": [],
|
||||||
|
"orderKey": "ORD001",
|
||||||
|
"defaultValue": null,
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "HTML",
|
||||||
|
"key": "COL0009",
|
||||||
|
"title": "Template",
|
||||||
|
"searchable": false,
|
||||||
|
"valueList": [
|
||||||
|
{
|
||||||
|
"key": "",
|
||||||
|
"value": "<!DOCTYPE html>\n<html>\n[EFTL]...[/EFTL]\n</html>"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"itemList": null,
|
||||||
|
"toManyItems": false,
|
||||||
|
"attributes": [],
|
||||||
|
"orderKey": "ORD002",
|
||||||
|
"defaultValue": null,
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"attributes": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Metadata",
|
||||||
|
"key": "SEC_0007",
|
||||||
|
"orderKey": "ORD002",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"key": "COL0010",
|
||||||
|
"title": "Tag azione",
|
||||||
|
"searchable": false,
|
||||||
|
"valueList": [
|
||||||
|
{
|
||||||
|
"key": "",
|
||||||
|
"value": "liqc__op_a_generaDelibera__depCLA"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"itemList": null,
|
||||||
|
"toManyItems": false,
|
||||||
|
"attributes": [],
|
||||||
|
"orderKey": "ORD001",
|
||||||
|
"defaultValue": null,
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"key": "COL0011",
|
||||||
|
"title": "Tag procedimento",
|
||||||
|
"searchable": false,
|
||||||
|
"valueList": [
|
||||||
|
{
|
||||||
|
"key": "",
|
||||||
|
"value": "procedimento_liqc"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"itemList": null,
|
||||||
|
"toManyItems": false,
|
||||||
|
"attributes": [],
|
||||||
|
"orderKey": "ORD002",
|
||||||
|
"defaultValue": null,
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"key": "COL0012",
|
||||||
|
"title": "Tag modulo",
|
||||||
|
"searchable": false,
|
||||||
|
"valueList": [
|
||||||
|
{
|
||||||
|
"key": "",
|
||||||
|
"value": "RequestForm_RIPARTIZIONE_CCT_DOCENTE"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"itemList": null,
|
||||||
|
"toManyItems": false,
|
||||||
|
"attributes": [],
|
||||||
|
"orderKey": "ORD003",
|
||||||
|
"defaultValue": null,
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"attributes": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"categories": [],
|
||||||
|
"aclTable": [],
|
||||||
|
"master": false,
|
||||||
|
"attributes": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
8. Show the result of the operation and, possibly the new value of the chosen field, taken from this JSON
|
||||||
|
|
||||||
|
15. Optionally, askl the user if they want to re-run the `search` route to show updates
|
||||||
|
16. Exit
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+38342
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user