From 975ae6c118671d60c338fcd5285b8929253ecfec Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Fri, 10 Jul 2026 15:55:12 +0200 Subject: [PATCH 1/3] add scripted request for contracts with multiple owners or contributors --- ...ontracts with two or more contributors.yml | 264 +++++++++++++++++ ... Get Contracts with two or more owners.yml | 278 ++++++++++++++++++ 2 files changed, 542 insertions(+) create mode 100644 collections/IRIS GW (Gateway) REST API (v1)/Contracts/SCRIPT - Get Contracts with two or more contributors.yml create mode 100644 collections/IRIS GW (Gateway) REST API (v1)/Contracts/SCRIPT - Get Contracts with two or more owners.yml diff --git a/collections/IRIS GW (Gateway) REST API (v1)/Contracts/SCRIPT - Get Contracts with two or more contributors.yml b/collections/IRIS GW (Gateway) REST API (v1)/Contracts/SCRIPT - Get Contracts with two or more contributors.yml new file mode 100644 index 0000000..520453e --- /dev/null +++ b/collections/IRIS GW (Gateway) REST API (v1)/Contracts/SCRIPT - Get Contracts with two or more contributors.yml @@ -0,0 +1,264 @@ +info: + name: SCRIPT - Get Contracts with two or more contributors + type: http + seq: 5 + +http: + method: GET + url: "{{IrisApiUrl}}/contracts;full?year=2026" + params: + - name: year + value: "2026" + type: query + - name: wfState + value: signed + type: query + disabled: true + - name: page + value: "1" + type: query + disabled: true + auth: inherit + +runtime: + scripts: + - type: after-response + code: |- + // Aggregates contracts with 2+ contributors across all pages and reports summary. + // // Fixes TypeError: pm.request.url.query.toObject(...).find is not a function by avoiding Array.prototype.find on toObject() result. + // Adds robust helpers for working with query params across Postman SDK versions and edge cases. + // + // Requirements addressed: + // 1) Read Page-Count header to know total pages + // 2) Iteratively call same endpoint for pages from current+1 to Page-Count, preserving other query params + // 3) Aggregate items where contributorSet exists and has length >= 2 + // 4) Build a map pid -> contributorCount across all pages (including initial) + // 5) Print summary in Test Results and set env var `multiContributorContracts` with [{ pid, contributorCount }] + // 6) Robust error handling, rate limiting, early stop on non-2xx; handle JSON array or paginated object response shapes + // 7) Augment aggregation with department (ownerSet[0].organizationUnit.idAb + " - " + description). Keep first non-empty. + + (function () { + const RATE_LIMIT_DELAY_MS = 200; // small delay between page fetches + const TARGET_ENV_VAR = 'multiContributorContracts'; + const PAGE_PARAM = 'page'; + + // ---------------- URL and Query helpers (SDK-safe) ----------------- + // Returns a plain object of query params. Works with: + // - pm.request.url.query (SDK v8+ as QueryList) using .toObject() + // - URL that has no query or is a raw string + function getQueryObject(url) { + try { + // // pm.request.url can be a Url object or a string. Normalize to raw string. + const raw = (typeof url === 'string') ? url : (url && (url.toString ? url.toString() : url.raw || '')); + // Try SDK path first if it's a Url object with .query + if (url && url.query && typeof url.query.toObject === 'function') { + const obj = url.query.toObject(); + // toObject may return undefined/null on empty query + return obj && typeof obj === 'object' ? { ...obj } : {}; + } + // Fallback: parse the raw string + if (typeof raw === 'string') { + const qIndex = raw.indexOf('?'); + if (qIndex === -1) return {}; + const queryStr = raw.substring(qIndex + 1); + if (!queryStr) return {}; + return queryStr.split('&').reduce((acc, pair) => { + if (!pair) return acc; + const [k, v] = pair.split('='); + if (!k) return acc; + acc[decodeURIComponent(k)] = v !== undefined ? decodeURIComponent(v) : ''; + return acc; + }, {}); + } + } catch (e) { + // fallthrough + } + return {}; + } + + function setQueryObject(url, updates) { + // Returns a new raw URL string with given query params merged + const raw = (typeof url === 'string') ? url : (url && (url.toString ? url.toString() : url.raw || '')); + const qIndex = raw.indexOf('?'); + const base = qIndex === -1 ? raw : raw.substring(0, qIndex); + const current = getQueryObject(url); + const merged = { ...current, ...updates }; + // Filter out empty/undefined to avoid adding stray keys + const parts = Object.keys(merged) + .filter(k => merged[k] !== undefined && merged[k] !== null && merged[k] !== '') + .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(String(merged[k]))); + return parts.length ? base + '?' + parts.join('&') : base; + } + + // ---------------- Response shape helpers ----------------- + function isObject(x) { return x && typeof x === 'object' && !Array.isArray(x); } + + function getItemsFromResponseBody(rb) { + // Supports either an array payload or an object with an array at known keys + if (Array.isArray(rb)) return rb; + if (isObject(rb)) { + // Try common keys: 'items', 'data', 'results' + if (Array.isArray(rb.items)) return rb.items; + if (Array.isArray(rb.data)) return rb.data; + if (Array.isArray(rb.results)) return rb.results; + } + return []; + } + + function safeJson(body) { + try { return JSON.parse(body); } catch (e) { return null; } + } + + // ---------------- Aggregation store ----------------- + // Map: pid -> { contributorCount, department } + const aggregate = {}; + + function extractDepartment(item) { + try { + const owner0 = Array.isArray(item.ownerSet) && item.ownerSet.length > 0 ? item.ownerSet[0] : null; + const ou = owner0 && owner0.organizationUnit ? owner0.organizationUnit : null; + const idAb = ou && typeof ou.idAb === 'string' ? ou.idAb : null; + const desc = ou && typeof ou.description === 'string' ? ou.description : null; + if (idAb && desc) return idAb + ' - ' + desc; + return ''; + } catch (e) { + return ''; + } + } + + function considerItems(items) { + items.forEach(it => { + const contributors = Array.isArray(it.contributorSet) ? it.contributorSet : []; + if (contributors.length >= 2) { + const pid = (it.pid != null) ? String(it.pid) : ''; + if (!pid) return; + const contributorCount = contributors.length; + const dept = extractDepartment(it); + if (!aggregate[pid]) { + aggregate[pid] = { contributorCount, department: dept || '' }; + } else { + // keep max contributor count seen (in case of variations) and first non-empty department + aggregate[pid].contributorCount = Math.max(aggregate[pid].contributorCount, contributorCount); + if (!aggregate[pid].department && dept) { + aggregate[pid].department = dept; + } + } + } + }); + } + + // ---------------- Paging orchestration ----------------- + const initialStatus = res.getStatus(); + const is2xx = initialStatus >= 200 && initialStatus < 300; + if (!is2xx) { + test('Request failed - not aggregating on non-2xx', function () { + expect(is2xx).to.eql(true); + }); + return; + } + + const rb = safeJson(JSON.stringify(res.getBody())); + const initialItems = rb ? getItemsFromResponseBody(rb) : []; + considerItems(initialItems); + + // Derive total pages from header 'Page-Count' or 'page-count' + const pageCountHeader = res.getHeader('Page-Count') || res.getHeader('page-count') || res.getHeader('X-Total-Pages'); + const totalPages = pageCountHeader ? parseInt(pageCountHeader, 10) : 1; + + // Figure out current page from request URL (query param 'page') + const currentQuery = getQueryObject(req.getUrl()); + const currentPage = parseInt(currentQuery[PAGE_PARAM] || '1', 10) || 1; + + // Build a function that fetches page N and aggregates + function fetchPage(n) { + return new Promise((resolve) => { + //console.log("Setting current page to: " + n); + const nextUrl = setQueryObject(req.getUrl(), { [PAGE_PARAM]: String(n) }); + const reqHeaders = req.getHeaders(); + + console.log("Setting headers: " + JSON.stringify(reqHeaders)); + setTimeout(async function () { + console.log("Calling next URL... (" + nextUrl + ")"); + await bru.sendRequest({ url: nextUrl, method: 'GET', headers: reqHeaders }, async function(err, res) { + if (err || !res) { + console.log("Error fetching page " + n + " (err: " + JSON.stringify(err) + ")"); + test('Error fetching page ' + n, function () { + expect(err).to.eql(null); + }); + return resolve(false); + } + const ok = res.status >= 200 && res.status < 300; + console.log("Next URL response: " + ok); + if (!ok) { + test('Non-2xx on page ' + n + ' - stop further paging', function () { + expect(ok).to.eql(true); + }); + return resolve(false); + } + const body = res.data; + const json = safeJson(body); + const items = json ? getItemsFromResponseBody(json) : []; + considerItems(items); + return resolve(true); + }); + + }, RATE_LIMIT_DELAY_MS); + }); + } + + async function run() { + console.log("Running..."); + console.log("Total pages found: " + totalPages); + // If there are more pages, iterate + for (let p = currentPage + 1; p <= totalPages; p++) { + const cont = await fetchPage(p); + if (!cont) break; + } + + // Prepare output array + const output = Object.keys(aggregate).map(pid => ({ + pid, + contributorCount: aggregate[pid].contributorCount, + department: aggregate[pid].department || '' + })); + + // Save to environment + bru.setVar(TARGET_ENV_VAR, JSON.stringify(output)); + + console.log(JSON.stringify(output)); + + // Basic summary tests + test('Aggregated items have required properties', function () { + output.forEach(item => { + expect(item).to.have.property('pid'); + expect(item.pid).to.be.a('string'); + expect(item).to.have.property('contributorCount'); + expect(item.contributorCount).to.be.a('number'); + expect(item).to.have.property('department'); + expect(item.department).to.be.a('string'); + }); + }); + + // Optional: Log summary count + test('Total multi-contributor contracts aggregated', function () { + expect(output.length).to.be.at.least(0); + }); + } + + run(); + })(); + actions: + - type: set-variable + phase: after-response + selector: + expression: "" + method: jsonq + variable: + name: multiContributorContracts + scope: runtime + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/collections/IRIS GW (Gateway) REST API (v1)/Contracts/SCRIPT - Get Contracts with two or more owners.yml b/collections/IRIS GW (Gateway) REST API (v1)/Contracts/SCRIPT - Get Contracts with two or more owners.yml new file mode 100644 index 0000000..f912251 --- /dev/null +++ b/collections/IRIS GW (Gateway) REST API (v1)/Contracts/SCRIPT - Get Contracts with two or more owners.yml @@ -0,0 +1,278 @@ +info: + name: SCRIPT - Get Contracts with two or more owners + type: http + seq: 4 + +http: + method: GET + url: "{{IrisApiUrl}}/contracts?year=2026" + params: + - name: year + value: "2026" + type: query + - name: wfState + value: signed + type: query + disabled: true + - name: page + value: "1" + type: query + disabled: true + auth: inherit + +runtime: + scripts: + - type: after-response + code: |- + // Aggregates contracts with 2+ contributors across all pages and reports summary. + // // Fixes TypeError: pm.request.url.query.toObject(...).find is not a function by avoiding Array.prototype.find on toObject() result. + // Adds robust helpers for working with query params across Postman SDK versions and edge cases. + // + // Requirements addressed: + // 1) Read Page-Count header to know total pages + // 2) Iteratively call same endpoint for pages from current+1 to Page-Count, preserving other query params + // 3) Aggregate items where contributorSet exists and has length >= 2 + // 4) Build a map pid -> contributorCount across all pages (including initial) + // 5) Print summary in Test Results and set env var `multiContributorContracts` with [{ pid, contributorCount }] + // 6) Robust error handling, rate limiting, early stop on non-2xx; handle JSON array or paginated object response shapes + // 7) Augment aggregation with department (ownerSet[0].organizationUnit.idAb + " - " + description). Keep first non-empty. + + (function () { + const RATE_LIMIT_DELAY_MS = 200; // small delay between page fetches + const TARGET_ENV_VAR = 'multiOwnerContracts'; + const PAGE_PARAM = 'page'; + + // ---------------- URL and Query helpers (SDK-safe) ----------------- + // Returns a plain object of query params. Works with: + // - pm.request.url.query (SDK v8+ as QueryList) using .toObject() + // - URL that has no query or is a raw string + function getQueryObject(url) { + try { + // // pm.request.url can be a Url object or a string. Normalize to raw string. + const raw = (typeof url === 'string') ? url : (url && (url.toString ? url.toString() : url.raw || '')); + // Try SDK path first if it's a Url object with .query + if (url && url.query && typeof url.query.toObject === 'function') { + const obj = url.query.toObject(); + // toObject may return undefined/null on empty query + return obj && typeof obj === 'object' ? { ...obj } : {}; + } + // Fallback: parse the raw string + if (typeof raw === 'string') { + const qIndex = raw.indexOf('?'); + if (qIndex === -1) return {}; + const queryStr = raw.substring(qIndex + 1); + if (!queryStr) return {}; + return queryStr.split('&').reduce((acc, pair) => { + if (!pair) return acc; + const [k, v] = pair.split('='); + if (!k) return acc; + acc[decodeURIComponent(k)] = v !== undefined ? decodeURIComponent(v) : ''; + return acc; + }, {}); + } + } catch (e) { + // fallthrough + } + return {}; + } + + function setQueryObject(url, updates) { + // Returns a new raw URL string with given query params merged + const raw = (typeof url === 'string') ? url : (url && (url.toString ? url.toString() : url.raw || '')); + const qIndex = raw.indexOf('?'); + const base = qIndex === -1 ? raw : raw.substring(0, qIndex); + const current = getQueryObject(url); + const merged = { ...current, ...updates }; + // Filter out empty/undefined to avoid adding stray keys + const parts = Object.keys(merged) + .filter(k => merged[k] !== undefined && merged[k] !== null && merged[k] !== '') + .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(String(merged[k]))); + return parts.length ? base + '?' + parts.join('&') : base; + } + + // ---------------- Response shape helpers ----------------- + function isObject(x) { return x && typeof x === 'object' && !Array.isArray(x); } + + function getItemsFromResponseBody(rb) { + // Supports either an array payload or an object with an array at known keys + if (Array.isArray(rb)) return rb; + if (isObject(rb)) { + // Try common keys: 'items', 'data', 'results' + if (Array.isArray(rb.items)) return rb.items; + if (Array.isArray(rb.data)) return rb.data; + if (Array.isArray(rb.results)) return rb.results; + } + return []; + } + + function safeJson(body) { + try { return JSON.parse(body); } catch (e) { return null; } + } + + // ---------------- Aggregation store ----------------- + // Map: pid -> { contributorCount, department } + const aggregate = {}; + + function extractDepartment(item) { + try { + const owner0 = Array.isArray(item.ownerSet) && item.ownerSet.length > 0 ? item.ownerSet[0] : null; + const ou = owner0 && owner0.organizationUnit ? owner0.organizationUnit : null; + const idAb = ou && typeof ou.idAb === 'string' ? ou.idAb : null; + const desc = ou && typeof ou.description === 'string' ? ou.description : null; + if (idAb && desc) return idAb + ' - ' + desc; + return ''; + } catch (e) { + return ''; + } + } + + function considerItems(items) { + items.forEach(it => { + const owners = Array.isArray(it.ownerSet) ? it.ownerSet : []; + if (owners.length >= 2) { + const pid = (it.pid != null) ? String(it.pid) : ''; + if (!pid) return; + const ownersCount = owners.length; + if (!aggregate[pid]) { + aggregate[pid] = { ownersCount: ownersCount }; + } else { + // keep max owner count seen (in case of variations) and first non-empty department + aggregate[pid].ownersCount = Math.max(aggregate[pid].ownersCount, ownersCount); + } + /* + const contributorCount = contributors.length; + const dept = extractDepartment(it); + if (!aggregate[pid]) { + aggregate[pid] = { contributorCount, department: dept || '' }; + } else { + // keep max contributor count seen (in case of variations) and first non-empty department + aggregate[pid].contributorCount = Math.max(aggregate[pid].contributorCount, contributorCount); + if (!aggregate[pid].department && dept) { + aggregate[pid].department = dept; + } + } + */ + } + }); + } + + // ---------------- Paging orchestration ----------------- + const initialStatus = res.getStatus(); + const is2xx = initialStatus >= 200 && initialStatus < 300; + if (!is2xx) { + test('Request failed - not aggregating on non-2xx', function () { + expect(is2xx).to.eql(true); + }); + return; + } + + const rb = safeJson(JSON.stringify(res.getBody())); + const initialItems = rb ? getItemsFromResponseBody(rb) : []; + considerItems(initialItems); + + // Derive total pages from header 'Page-Count' or 'page-count' + const pageCountHeader = res.getHeader('Page-Count') || res.getHeader('page-count') || res.getHeader('X-Total-Pages'); + const totalPages = pageCountHeader ? parseInt(pageCountHeader, 10) : 1; + + // Figure out current page from request URL (query param 'page') + const currentQuery = getQueryObject(req.getUrl()); + const currentPage = parseInt(currentQuery[PAGE_PARAM] || '1', 10) || 1; + + // Build a function that fetches page N and aggregates + function fetchPage(n) { + return new Promise((resolve) => { + //console.log("Setting current page to: " + n); + const nextUrl = setQueryObject(req.getUrl(), { [PAGE_PARAM]: String(n) }); + const reqHeaders = req.getHeaders(); + + console.log("Setting headers: " + JSON.stringify(reqHeaders)); + setTimeout(async function () { + console.log("Calling next URL... (" + nextUrl + ")"); + await bru.sendRequest({ url: nextUrl, method: 'GET', headers: reqHeaders }, async function(err, res) { + if (err || !res) { + console.log("Error fetching page " + n + " (err: " + JSON.stringify(err) + ")"); + test('Error fetching page ' + n, function () { + expect(err).to.eql(null); + }); + return resolve(false); + } + const ok = res.status >= 200 && res.status < 300; + console.log("Next URL response: " + ok); + if (!ok) { + test('Non-2xx on page ' + n + ' - stop further paging', function () { + expect(ok).to.eql(true); + }); + return resolve(false); + } + const body = res.data; + const json = safeJson(body); + const items = json ? getItemsFromResponseBody(json) : []; + considerItems(items); + return resolve(true); + }); + + }, RATE_LIMIT_DELAY_MS); + }); + } + + async function run() { + console.log("Running..."); + console.log("Total pages found: " + totalPages); + // If there are more pages, iterate + for (let p = currentPage + 1; p <= totalPages; p++) { + const cont = await fetchPage(p); + if (!cont) break; + } + + // Prepare output array + const output = Object.keys(aggregate).map(pid => ({ + pid, + ownersCount: aggregate[pid].ownersCount + /* + contributorCount: aggregate[pid].contributorCount, + department: aggregate[pid].department || '' + */ + })); + + // Save to environment + bru.setVar(TARGET_ENV_VAR, JSON.stringify(output)); + + console.log(JSON.stringify(output)); + + // Basic summary tests + test('Aggregated items have required properties', function () { + output.forEach(item => { + expect(item).to.have.property('pid'); + expect(item.pid).to.be.a('string'); + expect(item).to.have.property('ownersCount'); + expect(item.contributorCount).to.be.a('number'); + /* + expect(item).to.have.property('department'); + expect(item.department).to.be.a('string'); + */ + }); + }); + + // Optional: Log summary count + test('Total multi-contributor contracts aggregated', function () { + expect(output.length).to.be.at.least(0); + }); + } + + run(); + })(); + actions: + - type: set-variable + phase: after-response + selector: + expression: "" + method: jsonq + variable: + name: multiOwnerContracts + scope: runtime + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 From 21341ec52d79980eace1cccaa0488d4cbc1645dc Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Fri, 10 Jul 2026 15:55:46 +0200 Subject: [PATCH 2/3] cleanup contracts requests --- .../Contracts/Get Contracts FULL.yml | 96 ++---- ...th two or more participants (scripted).yml | 283 ------------------ .../Contracts/Get Contracts.yml | 13 +- .../Contracts/folder.yml | 11 + 4 files changed, 47 insertions(+), 356 deletions(-) delete mode 100644 collections/IRIS GW (Gateway) REST API (v1)/Contracts/Get Contracts with two or more participants (scripted).yml diff --git a/collections/IRIS GW (Gateway) REST API (v1)/Contracts/Get Contracts FULL.yml b/collections/IRIS GW (Gateway) REST API (v1)/Contracts/Get Contracts FULL.yml index 629fab8..b1e816c 100644 --- a/collections/IRIS GW (Gateway) REST API (v1)/Contracts/Get Contracts FULL.yml +++ b/collections/IRIS GW (Gateway) REST API (v1)/Contracts/Get Contracts FULL.yml @@ -5,78 +5,10 @@ info: http: method: GET - url: "{{IrisApiUrl}}/contracts;full?pid=VERO_L_26_CRCT_RA_AOUPR_01" + url: "{{IrisApiUrl}}/contracts;full?pid=SEGA_A_25_FORM_COM_01" params: - name: pid - value: BELL_B_25_CCS_IST_RA_01 - type: query - disabled: true - - name: pid - value: VIGN_G_25_CCS_COM_RA_FMT_01 - type: query - disabled: true - - name: pid - value: BETT_S_25_CRCT_RA_CHIESI_01 - type: query - disabled: true - - name: pid - value: BUSC_A_25_CRCT_RA_IRENAMBIENTE_01 - type: query - disabled: true - - name: pid - value: RICC_A_25_CRCT_RA_KEMIN_01 - type: query - disabled: true - - name: pid - value: GOBB_G_25_CONV_QUA_01 - type: query - disabled: true - - name: pid - value: DELM_N_25_CRCT_RA_BAUMER_01 - type: query - disabled: true - - name: pid - value: AMER_F_25_CCS_IST_RA_CODICI_01 - type: query - disabled: true - - name: wfState - value: validated - type: query - disabled: true - - name: page - value: "1" - type: query - disabled: true - - name: year - value: "2025" - type: query - disabled: true - - name: pid - value: MAMB_C_25_ACC_ACC_PROROGACONTRATTOCATAGLOGAZIO_01 - type: query - disabled: true - - name: pid - value: ZERB_A_25_SERV_CON_ASSUNTA_01 - type: query - disabled: true - - name: pid - value: COVA_P_24_SERV_CON_01 - type: query - disabled: true - - name: pid - value: GIUL_F_25_CRCT_RA_MOVYON_01 - type: query - disabled: true - - name: pid - value: LONG_S_25_SERV_CON_POLIMI_01 - type: query - disabled: true - - name: pid - value: DONO_G_25_ACC_ACC_ZOETIS_01 - type: query - disabled: true - - name: pid - value: VERO_L_26_CRCT_RA_AOUPR_01 + value: SEGA_A_25_FORM_COM_01 type: query auth: inherit @@ -2192,3 +2124,27 @@ examples: body: type: json data: "[]" + +docs: | + ## Examples + + | name | value | note | + | ---- | ------------------------------------------------- | ----------------------- | + | pid | BELL_B_25_CCS_IST_RA_01 | | + | pid | VIGN_G_25_CCS_COM_RA_FMT_01 | | + | pid | BETT_S_25_CRCT_RA_CHIESI_01 | | + | pid | BUSC_A_25_CRCT_RA_IRENAMBIENTE_01 | | + | pid | RICC_A_25_CRCT_RA_KEMIN_01 | | + | pid | GOBB_G_25_CONV_QUA_01 | | + | pid | DELM_N_25_CRCT_RA_BAUMER_01 | | + | pid | AMER_F_25_CCS_IST_RA_CODICI_01 | | + | pid | MAMB_C_25_ACC_ACC_PROROGACONTRATTOCATAGLOGAZIO_01 | | + | pid | ZERB_A_25_SERV_CON_ASSUNTA_01 | | + | pid | COVA_P_24_SERV_CON_01 | | + | pid | GIUL_F_25_CRCT_RA_MOVYON_01 | | + | pid | LONG_S_25_SERV_CON_POLIMI_01 | | + | pid | DONO_G_25_ACC_ACC_ZOETIS_01 | | + | pid | VERO_L_26_CRCT_RA_AOUPR_01 | | + | pid | SEGA_A_25_FORM_COM_01 | | + | pid | BELL_B_26_CCS_IST_RA_AGENZIAREGIONALEPERLASICU_01 | 4 owner + 3 contributor | + | pid | GIUL_F_25_CRCT_RA_MOVYON_01 | 2 owner + 7 contributor | diff --git a/collections/IRIS GW (Gateway) REST API (v1)/Contracts/Get Contracts with two or more participants (scripted).yml b/collections/IRIS GW (Gateway) REST API (v1)/Contracts/Get Contracts with two or more participants (scripted).yml deleted file mode 100644 index c0a4432..0000000 --- a/collections/IRIS GW (Gateway) REST API (v1)/Contracts/Get Contracts with two or more participants (scripted).yml +++ /dev/null @@ -1,283 +0,0 @@ -info: - name: Get Contracts with two or more participants (scripted) - type: http - seq: 4 - -http: - method: GET - url: "{{IrisApiUrl}}/contracts;full?year=2025&wfState=signed&page=1" - params: - - name: year - value: "2025" - type: query - - name: wfState - value: signed - type: query - - name: page - value: "1" - type: query - - name: pid - value: BELL_B_25_CCS_IST_RA_01 - type: query - disabled: true - - name: pid - value: VIGN_G_25_CCS_COM_RA_FMT_01 - type: query - disabled: true - - name: pid - value: BETT_S_25_CRCT_RA_CHIESI_01 - type: query - disabled: true - - name: pid - value: BUSC_A_25_CRCT_RA_IRENAMBIENTE_01 - type: query - disabled: true - - name: pid - value: RICC_A_25_CRCT_RA_KEMIN_01 - type: query - disabled: true - - name: pid - value: GOBB_G_25_CONV_QUA_01 - type: query - disabled: true - - name: pid - value: DELM_N_25_CRCT_RA_BAUMER_01 - type: query - disabled: true - - name: pid - value: AMER_F_25_CCS_IST_RA_CODICI_01 - type: query - disabled: true - auth: inherit - -runtime: - scripts: - - type: after-response - code: |- - // Aggregates contracts with 2+ contributors across all pages and reports summary. - // // Fixes TypeError: pm.request.url.query.toObject(...).find is not a function by avoiding Array.prototype.find on toObject() result. - // Adds robust helpers for working with query params across Postman SDK versions and edge cases. - // - // Requirements addressed: - // 1) Read Page-Count header to know total pages - // 2) Iteratively call same endpoint for pages from current+1 to Page-Count, preserving other query params - // 3) Aggregate items where contributorSet exists and has length >= 2 - // 4) Build a map pid -> contributorCount across all pages (including initial) - // 5) Print summary in Test Results and set env var `multiContributorContracts` with [{ pid, contributorCount }] - // 6) Robust error handling, rate limiting, early stop on non-2xx; handle JSON array or paginated object response shapes - // 7) Augment aggregation with department (ownerSet[0].organizationUnit.idAb + " - " + description). Keep first non-empty. - - (function () { - const RATE_LIMIT_DELAY_MS = 200; // small delay between page fetches - const TARGET_ENV_VAR = 'multiContributorContracts'; - const PAGE_PARAM = 'page'; - - // ---------------- URL and Query helpers (SDK-safe) ----------------- - // Returns a plain object of query params. Works with: - // - pm.request.url.query (SDK v8+ as QueryList) using .toObject() - // - URL that has no query or is a raw string - function getQueryObject(url) { - try { - // // pm.request.url can be a Url object or a string. Normalize to raw string. - const raw = (typeof url === 'string') ? url : (url && (url.toString ? url.toString() : url.raw || '')); - // Try SDK path first if it's a Url object with .query - if (url && url.query && typeof url.query.toObject === 'function') { - const obj = url.query.toObject(); - // toObject may return undefined/null on empty query - return obj && typeof obj === 'object' ? { ...obj } : {}; - } - // Fallback: parse the raw string - if (typeof raw === 'string') { - const qIndex = raw.indexOf('?'); - if (qIndex === -1) return {}; - const queryStr = raw.substring(qIndex + 1); - if (!queryStr) return {}; - return queryStr.split('&').reduce((acc, pair) => { - if (!pair) return acc; - const [k, v] = pair.split('='); - if (!k) return acc; - acc[decodeURIComponent(k)] = v !== undefined ? decodeURIComponent(v) : ''; - return acc; - }, {}); - } - } catch (e) { - // fallthrough - } - return {}; - } - - function setQueryObject(url, updates) { - // Returns a new raw URL string with given query params merged - const raw = (typeof url === 'string') ? url : (url && (url.toString ? url.toString() : url.raw || '')); - const qIndex = raw.indexOf('?'); - const base = qIndex === -1 ? raw : raw.substring(0, qIndex); - const current = getQueryObject(url); - const merged = { ...current, ...updates }; - // Filter out empty/undefined to avoid adding stray keys - const parts = Object.keys(merged) - .filter(k => merged[k] !== undefined && merged[k] !== null && merged[k] !== '') - .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(String(merged[k]))); - return parts.length ? base + '?' + parts.join('&') : base; - } - - // ---------------- Response shape helpers ----------------- - function isObject(x) { return x && typeof x === 'object' && !Array.isArray(x); } - - function getItemsFromResponseBody(rb) { - // Supports either an array payload or an object with an array at known keys - if (Array.isArray(rb)) return rb; - if (isObject(rb)) { - // Try common keys: 'items', 'data', 'results' - if (Array.isArray(rb.items)) return rb.items; - if (Array.isArray(rb.data)) return rb.data; - if (Array.isArray(rb.results)) return rb.results; - } - return []; - } - - function safeJson(body) { - try { return JSON.parse(body); } catch (e) { return null; } - } - - // ---------------- Aggregation store ----------------- - // Map: pid -> { contributorCount, department } - const aggregate = {}; - - function extractDepartment(item) { - try { - const owner0 = Array.isArray(item.ownerSet) && item.ownerSet.length > 0 ? item.ownerSet[0] : null; - const ou = owner0 && owner0.organizationUnit ? owner0.organizationUnit : null; - const idAb = ou && typeof ou.idAb === 'string' ? ou.idAb : null; - const desc = ou && typeof ou.description === 'string' ? ou.description : null; - if (idAb && desc) return idAb + ' - ' + desc; - return ''; - } catch (e) { - return ''; - } - } - - function considerItems(items) { - items.forEach(it => { - const contributors = Array.isArray(it.contributorSet) ? it.contributorSet : []; - if (contributors.length >= 2) { - const pid = (it.pid != null) ? String(it.pid) : ''; - if (!pid) return; - const contributorCount = contributors.length; - const dept = extractDepartment(it); - if (!aggregate[pid]) { - aggregate[pid] = { contributorCount, department: dept || '' }; - } else { - // keep max contributor count seen (in case of variations) and first non-empty department - aggregate[pid].contributorCount = Math.max(aggregate[pid].contributorCount, contributorCount); - if (!aggregate[pid].department && dept) { - aggregate[pid].department = dept; - } - } - } - }); - } - - // ---------------- Paging orchestration ----------------- - const initialStatus = res.getStatus(); - const is2xx = initialStatus >= 200 && initialStatus < 300; - if (!is2xx) { - test('Request failed - not aggregating on non-2xx', function () { - expect(is2xx).to.eql(true); - }); - return; - } - - const rb = safeJson(JSON.stringify(res.getBody())); - const initialItems = rb ? getItemsFromResponseBody(rb) : []; - considerItems(initialItems); - - // Derive total pages from header 'Page-Count' or 'page-count' - const pageCountHeader = res.getHeader('Page-Count') || res.getHeader('page-count') || res.getHeader('X-Total-Pages'); - const totalPages = pageCountHeader ? parseInt(pageCountHeader, 10) : 1; - - // Figure out current page from request URL (query param 'page') - const currentQuery = getQueryObject(req.getUrl()); - const currentPage = parseInt(currentQuery[PAGE_PARAM] || '1', 10) || 1; - - // Build a function that fetches page N and aggregates - function fetchPage(n) { - return new Promise((resolve) => { - //console.log("Setting current page to: " + n); - const nextUrl = setQueryObject(req.getUrl(), { [PAGE_PARAM]: String(n) }); - const reqHeaders = req.getHeaders - - console.log("Setting headers: " + JSON.stringify(reqHeaders)); - setTimeout(async function () { - console.log("Calling next URL... (" + nextUrl + ")"); - await bru.sendRequest({ url: nextUrl, method: 'GET', headers: reqHeaders }, async function(err, res) { - if (err || !res) { - console.log("Error fetching page " + n + " (err: " + JSON.stringify(err) + ")"); - test('Error fetching page ' + n, function () { - expect(err).to.eql(null); - }); - return resolve(false); - } - const ok = res.status >= 200 && res.status < 300; - console.log("Next URL response: " + ok); - if (!ok) { - test('Non-2xx on page ' + n + ' - stop further paging', function () { - expect(ok).to.eql(true); - }); - return resolve(false); - } - const body = res.data; - const json = safeJson(body); - const items = json ? getItemsFromResponseBody(json) : []; - considerItems(items); - return resolve(true); - }); - - }, RATE_LIMIT_DELAY_MS); - }); - } - - async function run() { - console.log("Running..."); - console.log("Total pages found: " + totalPages); - // If there are more pages, iterate - for (let p = currentPage + 1; p <= totalPages; p++) { - const cont = await fetchPage(p); - if (!cont) break; - } - - // Prepare output array - const output = Object.keys(aggregate).map(pid => ({ - pid, - contributorCount: aggregate[pid].contributorCount, - department: aggregate[pid].department || '' - })); - - // Save to environment - bru.setVar(TARGET_ENV_VAR, JSON.stringify(output)); - - // Basic summary tests - test('Aggregated items have required properties', function () { - output.forEach(item => { - expect(item).to.have.property('pid'); - expect(item.pid).to.be.a('string'); - expect(item).to.have.property('contributorCount'); - expect(item.contributorCount).to.be.a('number'); - expect(item).to.have.property('department'); - expect(item.department).to.be.a('string'); - }); - }); - - // Optional: Log summary count - test('Total multi-contributor contracts aggregated', function () { - expect(output.length).to.be.at.least(0); - }); - } - - run(); - })(); - -settings: - encodeUrl: true - timeout: 0 - followRedirects: true - maxRedirects: 5 diff --git a/collections/IRIS GW (Gateway) REST API (v1)/Contracts/Get Contracts.yml b/collections/IRIS GW (Gateway) REST API (v1)/Contracts/Get Contracts.yml index 2674be5..e0c5c00 100644 --- a/collections/IRIS GW (Gateway) REST API (v1)/Contracts/Get Contracts.yml +++ b/collections/IRIS GW (Gateway) REST API (v1)/Contracts/Get Contracts.yml @@ -5,7 +5,7 @@ info: http: method: GET - url: "{{IrisApiUrl}}/contracts?page={{currentPage}}&sort=pid&dir=asc&wfState=closed&wfState=signed" + url: "{{IrisApiUrl}}/contracts?person.cf=GLNFLC70S16I158E&person.relation=contributor&page={{currentPage}}&pageSize=20&sort=pid&dir=asc" params: - name: id value: "123" @@ -48,6 +48,9 @@ http: value: "" type: query disabled: true + - name: person.cf + value: GLNFLC70S16I158E + type: query - name: person.id value: "" type: query @@ -69,10 +72,9 @@ http: type: query disabled: true - name: person.relation - value: test + value: contributor type: query description: owner|contributor - disabled: true - name: department.id value: "" type: query @@ -108,6 +110,9 @@ http: - name: page value: "{{currentPage}}" type: query + - name: pageSize + value: "20" + type: query - name: sort value: pid type: query @@ -117,9 +122,11 @@ http: - name: wfState value: closed type: query + disabled: true - name: wfState value: signed type: query + disabled: true auth: inherit settings: diff --git a/collections/IRIS GW (Gateway) REST API (v1)/Contracts/folder.yml b/collections/IRIS GW (Gateway) REST API (v1)/Contracts/folder.yml index d6c21a7..7af8a06 100644 --- a/collections/IRIS GW (Gateway) REST API (v1)/Contracts/folder.yml +++ b/collections/IRIS GW (Gateway) REST API (v1)/Contracts/folder.yml @@ -5,3 +5,14 @@ info: request: auth: inherit + +docs: + content: |- + # IRIS GW (Gateway) REST API documentation (v1) + + Link: + + ## Contratti + + Link: + type: text/markdown From c1c2f4d75009a80798b854f927434482298b5239 Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Fri, 10 Jul 2026 15:56:21 +0200 Subject: [PATCH 3/3] add test request to try calling RemoteServiceIntegration --- .../elixForms API v2/Test Remote WS.yml | 41 +++++++++++++++++++ .../elixForms API v2/_HACKS/folder.yml | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 collections/elixForms API v2/Test Remote WS.yml diff --git a/collections/elixForms API v2/Test Remote WS.yml b/collections/elixForms API v2/Test Remote WS.yml new file mode 100644 index 0000000..103d696 --- /dev/null +++ b/collections/elixForms API v2/Test Remote WS.yml @@ -0,0 +1,41 @@ +info: + name: Test Remote WS + type: http + seq: 4 + +http: + method: GET + url: https://console-unipr.elixforms.it//eF/api/remote/www-idem-unipr-it/ws-elix-progetti__pj.php/v1 + headers: + - name: x-requested-with + value: XMLHttpRequest + body: + type: form-urlencoded + data: + - name: username + value: "{{elixFormsApiUsername}}" + - name: requestStatus + value: "" + description: "nuovo stato della request, possibili valori: SUBMITTED, PROCESSED" + disabled: true + - name: swfOptionKey + value: "" + description: |- + indice del valore associato al SWF che si desidera impostare + + più chiaramente, si suppone che il modulo associato alla richiesta abbia un SWF impostato, cioè avente degli stati personalizzati, e che esista un campo SELECT contenente tali stati: il valore indicato da questo parametro è l'indice (base 1) del nuovo stato personalizzato che si vuole impostare + disabled: true + - name: properties + value: '[{"key":"COL0004","value":"cambio stato manuale"},{"key":"COL0002","value":"31-03-2025"}]' + description: |- + mappa chiave,valore/i aggiuntivo/i in formato json (url-encoding richiesto in UTF-8) + + il formato è del tipo: `[{"key":"COL0004","value":"cambio stato manuale"},{"key":"COL0002","value":"31-03-2025"}]` + disabled: true + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/collections/elixForms API v2/_HACKS/folder.yml b/collections/elixForms API v2/_HACKS/folder.yml index ce35725..e6b3212 100644 --- a/collections/elixForms API v2/_HACKS/folder.yml +++ b/collections/elixForms API v2/_HACKS/folder.yml @@ -1,7 +1,7 @@ info: name: _HACKS type: folder - seq: 4 + seq: 3 request: auth: inherit