From 382a677297f78a293c05dd2a11dcc2f67928dbb5 Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Mon, 23 Feb 2026 10:35:40 +0100 Subject: [PATCH] IRIS GW API - update syntax --- ...th two or more participants (scripted).yml | 633 ++++++------------ .../[Runner] Get All Contracts.yml | 106 +-- 2 files changed, 247 insertions(+), 492 deletions(-) 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 index 27756be..36d48ec 100644 --- 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 @@ -53,427 +53,218 @@ http: runtime: scripts: - type: after-response - code: "// Aggregates contracts with 2+ contributors across all pages and reports summary.\r - - // // Fixes TypeError: pm.request.url.query.toObject(...).find is not a function by avoiding Array.prototype.find on toObject() result.\r - - // Adds robust helpers for working with query params across Postman SDK versions and edge cases.\r - - //\r - - // Requirements addressed:\r - - // 1) Read Page-Count header to know total pages\r - - // 2) Iteratively call same endpoint for pages from current+1 to Page-Count, preserving other query params\r - - // 3) Aggregate items where contributorSet exists and has length >= 2\r - - // 4) Build a map pid -> contributorCount across all pages (including initial)\r - - // 5) Print summary in Test Results and set env var `multiContributorContracts` with [{ pid, contributorCount }]\r - - // 6) Robust error handling, rate limiting, early stop on non-2xx; handle JSON array or paginated object response shapes\r - - // 7) Augment aggregation with department (ownerSet[0].organizationUnit.idAb + \" - \" + description). Keep first non-empty.\r - - \r - - (function () {\r - - \ const RATE_LIMIT_DELAY_MS = 200; // small delay between page fetches\r - - \ const TARGET_ENV_VAR = 'multiContributorContracts';\r - - \ const PAGE_PARAM = 'page';\r - - \r - - \ // ---------------- URL and Query helpers (SDK-safe) -----------------\r - - \ // Returns a plain object of query params. Works with:\r - - // // - pm.request.url.query (SDK v8+ as QueryList) using .toObject()\r - - \ // - URL that has no query or is a raw string\r - - \ function getQueryObject(url) {\r - - \ try {\r - - // // pm.request.url can be a Url object or a string. Normalize to raw string.\r - - \ const raw = (typeof url === 'string') ? url : (url && (url.toString ? url.toString() : url.raw || ''));\r - - \ // Try SDK path first if it's a Url object with .query\r - - \ if (url && url.query && typeof url.query.toObject === 'function') {\r - - \ const obj = url.query.toObject();\r - - \ // toObject may return undefined/null on empty query\r - - \ return obj && typeof obj === 'object' ? { ...obj } : {};\r - - \ }\r - - \ // Fallback: parse the raw string\r - - \ if (typeof raw === 'string') {\r - - \ const qIndex = raw.indexOf('?');\r - - \ if (qIndex === -1) return {};\r - - \ const queryStr = raw.substring(qIndex + 1);\r - - \ if (!queryStr) return {};\r - - \ return queryStr.split('&').reduce((acc, pair) => {\r - - \ if (!pair) return acc;\r - - \ const [k, v] = pair.split('=');\r - - \ if (!k) return acc;\r - - \ acc[decodeURIComponent(k)] = v !== undefined ? decodeURIComponent(v) : '';\r - - \ return acc;\r - - \ }, {});\r - - \ }\r - - \ } catch (e) {\r - - \ // fallthrough\r - - \ }\r - - \ return {};\r - - \ }\r - - \r - - \ function setQueryObject(url, updates) {\r - - \ // Returns a new raw URL string with given query params merged\r - - \ const raw = (typeof url === 'string') ? url : (url && (url.toString ? url.toString() : url.raw || ''));\r - - \ const qIndex = raw.indexOf('?');\r - - \ const base = qIndex === -1 ? raw : raw.substring(0, qIndex);\r - - \ const current = getQueryObject(url);\r - - \ const merged = { ...current, ...updates };\r - - \ // Filter out empty/undefined to avoid adding stray keys\r - - \ const parts = Object.keys(merged)\r - - \ .filter(k => merged[k] !== undefined && merged[k] !== null && merged[k] !== '')\r - - \ .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(String(merged[k])));\r - - \ return parts.length ? base + '?' + parts.join('&') : base;\r - - \ }\r - - \r - - \ // ---------------- Response shape helpers -----------------\r - - \ function isObject(x) { return x && typeof x === 'object' && !Array.isArray(x); }\r - - \r - - \ function getItemsFromResponseBody(rb) {\r - - \ // Supports either an array payload or an object with an array at known keys\r - - \ if (Array.isArray(rb)) return rb;\r - - \ if (isObject(rb)) {\r - - \ // Try common keys: 'items', 'data', 'results'\r - - \ if (Array.isArray(rb.items)) return rb.items;\r - - \ if (Array.isArray(rb.data)) return rb.data;\r - - \ if (Array.isArray(rb.results)) return rb.results;\r - - \ }\r - - \ return [];\r - - \ }\r - - \r - - \ function safeJson(body) {\r - - \ try { return JSON.parse(body); } catch (e) { return null; }\r - - \ }\r - - \r - - \ // ---------------- Aggregation store -----------------\r - - \ // Map: pid -> { contributorCount, department }\r - - \ const aggregate = {};\r - - \r - - \ function extractDepartment(item) {\r - - \ try {\r - - \ const owner0 = Array.isArray(item.ownerSet) && item.ownerSet.length > 0 ? item.ownerSet[0] : null;\r - - \ const ou = owner0 && owner0.organizationUnit ? owner0.organizationUnit : null;\r - - \ const idAb = ou && typeof ou.idAb === 'string' ? ou.idAb : null;\r - - \ const desc = ou && typeof ou.description === 'string' ? ou.description : null;\r - - \ if (idAb && desc) return idAb + ' - ' + desc;\r - - \ return '';\r - - \ } catch (e) {\r - - \ return '';\r - - \ }\r - - \ }\r - - \r - - \ function considerItems(items) {\r - - \ items.forEach(it => {\r - - \ const contributors = Array.isArray(it.contributorSet) ? it.contributorSet : [];\r - - \ if (contributors.length >= 2) {\r - - \ const pid = (it.pid != null) ? String(it.pid) : '';\r - - \ if (!pid) return;\r - - \ const contributorCount = contributors.length;\r - - \ const dept = extractDepartment(it);\r - - \ if (!aggregate[pid]) {\r - - \ aggregate[pid] = { contributorCount, department: dept || '' };\r - - \ } else {\r - - \ // keep max contributor count seen (in case of variations) and first non-empty department\r - - \ aggregate[pid].contributorCount = Math.max(aggregate[pid].contributorCount, contributorCount);\r - - \ if (!aggregate[pid].department && dept) {\r - - \ aggregate[pid].department = dept;\r - - \ }\r - - \ }\r - - \ }\r - - \ });\r - - \ }\r - - \r - - \ // ---------------- Paging orchestration -----------------\r - - \ const initialStatus = res.getStatus();\r - - \ const is2xx = initialStatus >= 200 && initialStatus < 300;\r - - \ if (!is2xx) {\r - - \ test('Request failed - not aggregating on non-2xx', function () {\r - - \ expect(is2xx).to.eql(true);\r - - \ });\r - - \ return;\r - - \ }\r - - \r - - \ const rb = safeJson(JSON.stringify(res.getBody()));\r - - \ const initialItems = rb ? getItemsFromResponseBody(rb) : [];\r - - \ considerItems(initialItems);\r - - \r - - \ // Derive total pages from header 'Page-Count' or 'page-count'\r - - \ const pageCountHeader = res.getHeader('Page-Count') || res.getHeader('page-count') || res.getHeader('X-Total-Pages');\r - - \ const totalPages = pageCountHeader ? parseInt(pageCountHeader, 10) : 1;\r - - \r - - \ // Figure out current page from request URL (query param 'page')\r - - \ const currentQuery = getQueryObject(req.getUrl());\r - - \ const currentPage = parseInt(currentQuery[PAGE_PARAM] || '1', 10) || 1;\r - - \r - - \ // Build a function that fetches page N and aggregates\r - - \ function fetchPage(n) {\r - - \ return new Promise((resolve) => {\r - - \ const nextUrl = setQueryObject(req.getUrl(), { [PAGE_PARAM]: String(n) });\r - - \ setTimeout(function () {\r - - \ await bru.sendRequest({ url: nextUrl, method: 'GET' }, async function(err, res) {\r - - \ if (err || !res) {\r - - \ test('Error fetching page ' + n, function () {\r - - \ expect(err).to.eql(null);\r - - \ });\r - - \ return resolve(false);\r - - \ }\r - - \ const ok = res.status >= 200 && res.status < 300;\r - - \ if (!ok) {\r - - \ test('Non-2xx on page ' + n + ' - stop further paging', function () {\r - - \ expect(ok).to.eql(true);\r - - \ });\r - - \ return resolve(false);\r - - \ }\r - - \ const body = res.data;\r - - \ const json = safeJson(body);\r - - \ const items = json ? getItemsFromResponseBody(json) : [];\r - - \ considerItems(items);\r - - \ resolve(true);\r - - \ });\r - - \ }, RATE_LIMIT_DELAY_MS);\r - - \ });\r - - \ }\r - - \r - - \ async function run() {\r - - \ // If there are more pages, iterate\r - - \ for (let p = currentPage + 1; p <= totalPages; p++) {\r - - \ const cont = await fetchPage(p);\r - - \ if (!cont) break;\r - - \ }\r - - \r - - \ // Prepare output array\r - - \ const output = Object.keys(aggregate).map(pid => ({\r - - \ pid,\r - - \ contributorCount: aggregate[pid].contributorCount,\r - - \ department: aggregate[pid].department || ''\r - - \ }));\r - - \r - - \ // Save to environment\r - - \ bru.setEnvVar(TARGET_ENV_VAR, JSON.stringify(output));\r - - \r - - \ // Basic summary tests\r - - \ test('Aggregated items have required properties', function () {\r - - \ output.forEach(item => {\r - - \ expect(item).to.have.property('pid');\r - - \ expect(item.pid).to.be.a('string');\r - - \ expect(item).to.have.property('contributorCount');\r - - \ expect(item.contributorCount).to.be.a('number');\r - - \ expect(item).to.have.property('department');\r - - \ expect(item.department).to.be.a('string');\r - - \ });\r - - \ });\r - - \r - - \ // Optional: Log summary count\r - - \ test('Total multi-contributor contracts aggregated', function () {\r - - \ expect(output.length).to.be.at.least(0);\r - - \ });\r - - \ }\r - - \r - - \ run();\r - - })();" + 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 diff --git a/collections/IRIS GW (Gateway) REST API (v1)/[Runner] Get All Contracts/[Runner] Get All Contracts.yml b/collections/IRIS GW (Gateway) REST API (v1)/[Runner] Get All Contracts/[Runner] Get All Contracts.yml index 5372862..5c31a30 100644 --- a/collections/IRIS GW (Gateway) REST API (v1)/[Runner] Get All Contracts/[Runner] Get All Contracts.yml +++ b/collections/IRIS GW (Gateway) REST API (v1)/[Runner] Get All Contracts/[Runner] Get All Contracts.yml @@ -33,81 +33,45 @@ http: runtime: scripts: - type: after-response - code: "test(\"Status code is 200\", function () {\r + code: |- + test("Status code is 200", function () { + expect(res.getStatus()).to.equal(200); - \ expect(res.getStatus()).to.equal(200);\r + // Get pagination headers + var currentPage = parseInt(res.getHeader("Page")); + var totalPages = parseInt(res.getHeader("Page-Count")); - \r + // If first loop, reset results array + if (currentPage == 1) + { + bru.setVar("runnerResultsArray", JSON.stringify([])); + } - \ // Get pagination headers\r + // Update result array + let resultsArray = JSON.parse(bru.getVar("runnerResultsArray")); + var results = res.getBody(); + resultsArray = resultsArray.concat(results); + bru.setVar("runnerResultsArray", JSON.stringify(resultsArray)); + + // Loop decision + if (currentPage == totalPages) + { + // Loop has finished, reset variables and stop + bru.setVar("runnerCurrentPage", 1); + bru.deleteVar("runnerTotalPages"); + // //pm.collectionVariables.unset("runnerResultsArray"); // Do not unset, so you can find results in the env var! + } + else + { + // Still having pages, update pagination values + bru.setVar("runnerCurrentPage", ++currentPage); + bru.setVar("runnerTotalPages", totalPages); - \ var currentPage = parseInt(res.getHeader(\"Page\"));\r - - \ var totalPages = parseInt(res.getHeader(\"Page-Count\"));\r - - \r - - \ // If first loop, reset results array\r - - \ if (currentPage == 1)\r - - \ {\r - - \ bru.setVar(\"runnerResultsArray\", JSON.stringify([]));\r - - \ }\r - - \r - - \ // Update result array\r - - \ let resultsArray = JSON.parse(bru.getVar(\"runnerResultsArray\"));\r - - \ var results = res.getBody();\r - - \ resultsArray = resultsArray.concat(results);\r - - \ bru.setVar(\"runnerResultsArray\", JSON.stringify(resultsArray));\r - - \ \r - - \ // Loop decision\r - - \ if (currentPage == totalPages)\r - - \ {\r - - \ // Loop has finished, reset variables and stop\r - - \ bru.setVar(\"runnerCurrentPage\", 1);\r - - \ bru.deleteVar(\"runnerTotalPages\");\r - - // //pm.collectionVariables.unset(\"runnerResultsArray\"); // Do not unset, so you can find results in the env var!\r - - \ }\r - - \ else\r - - \ {\r - - \ // Still having pages, update pagination values\r - - \ bru.setVar(\"runnerCurrentPage\", ++currentPage);\r - - \ bru.setVar(\"runnerTotalPages\", totalPages);\r - - \r - - \ // Set next request and wait for 1 second before sending the next request. This is to avoid hitting the rate limit.\r - - \ bru.runner.setNextRequest(req.getName());\r - - \ setTimeout(function(){}, [1000]);\r - - \ }\r - - });" + // Set next request and wait for 1 second before sending the next request. This is to avoid hitting the rate limit. + bru.runner.setNextRequest(req.getName()); + setTimeout(function(){}, [1000]); + } + }); settings: encodeUrl: true