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