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: 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: year value: "2025" type: query - name: wfState value: signed type: query - name: page value: "1" type: query 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) => { const nextUrl = setQueryObject(req.getUrl(), { [PAGE_PARAM]: String(n) }); setTimeout(function () { await bru.sendRequest({ url: nextUrl, method: 'GET' }, async function(err, res) { if (err || !res) { test('Error fetching page ' + n, function () { expect(err).to.eql(null); }); return resolve(false); } const ok = res.status >= 200 && res.status < 300; 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); resolve(true); }); }, RATE_LIMIT_DELAY_MS); }); } async function run() { // 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.setEnvVar(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