added all checks

This commit is contained in:
Philip Ellis
2024-01-19 15:53:15 -06:00
parent c5f0b48d98
commit 0ea5beadee
11 changed files with 148 additions and 1104 deletions

View File

@@ -1,2 +0,0 @@
.env
node_modules

View File

@@ -1,507 +0,0 @@
// Deploy collection inncrementally
// ---------------------------------------------------------------
// Cycle through the collection objects and deploy them one by one
// ---------------------------------------------------------------
// Folders -> Requests -> Responses
// ---------------------------------------------------------------
// An updated object has a different id.
// If the id matches, then the object is unchanged.
// If the id does not match, then the object is new.
// In the end delete all objects that are not in the updated collection.
// Sort the objects and update the parent order property.
// ---------------------------------------------------------------
// Except for folders which would force the update of the entire collection.
// ---------------------------------------------------------------
const pmConvert = require('./PostmanCovertions')
const pmAPI = require('./postmanAPI')
var crypto = require('crypto');
const { GenID } = require('./Utils')
const deployIncremental = async (privateRemoteCollectionId, localCollection, publicRemoteCollectionId) => {
let remoteCollection = await refreshRemoteCollection(privateRemoteCollectionId)
console.log('Incremental deployment of collection ', localCollection.info.name)
// const collectioHeadHasChanged =
await upadteCollectionHead(remoteCollection, localCollection)
remoteCollection = await refreshRemoteCollection(privateRemoteCollectionId)
// const foldersHaveChanged =
await mergeFolders(remoteCollection, localCollection)
remoteCollection = await refreshRemoteCollection(privateRemoteCollectionId)
// const requestsHaveChanged =
await mergeRequests(remoteCollection, localCollection)
remoteCollection = await refreshRemoteCollection(privateRemoteCollectionId)
// const responsesHaveChanged =
await mergeResponses(remoteCollection, localCollection)
// should we always merge into the public collection?
// There is teh case that if an error happens in the merge phase
// the private collection is fully updated
// and in the next run the public collection will NOT be updated
// because there are no changes in the private collection
// if (!(collectioHeadHasChanged || foldersHaveChanged || requestsHaveChanged || responsesHaveChanged)) {
// console.log('Incremental deployment of collection ', localCollection.info.name, ' completed\n\n')
// return
// }
const msg = 'Merging to public collection'
console.log('\n' + msg + '...')
await new pmAPI.Collection(privateRemoteCollectionId).merge(publicRemoteCollectionId)
.then(() => { console.log(msg, '-> OK\n') })
.catch((error) => {
console.log(msg, '-> FAIL')
handlePostmanAPIError(error)
})
console.log('Incremental deployment of collection ', localCollection.info.name, ' completed\n\n')
}
async function upadteCollectionHead (remoteCollection, localCollection) {
// the colelction head shoul dbe updated if there are changes in
// Authorization
// Pre-request Script
// Tests
// Variables
const localEmptyCollection = { ...localCollection }
localEmptyCollection.item = []
// Check changes in info
const hasChangesInfo = checkInfoChanges(remoteCollection, localCollection)
// Check if there are changes in the Authorization
const hasChangesAuth = checkObjectChanges(remoteCollection.collection.auth, localEmptyCollection.auth)
// Check if there are changes in the Scripts (pre-request and tests)
const hasChangesPreRequestScript = checkScriptChanges('prerequest', remoteCollection, localEmptyCollection)
const hasChangesTestScript = checkScriptChanges('test', remoteCollection, localEmptyCollection)
// Check if there are changes in the Variables
const hasChangesVariables = checkVariableChanges(remoteCollection, localEmptyCollection)
const hasFolderSortChanges = checkFolderSortChanges(remoteCollection, localCollection)
const hasChanges = (
hasFolderSortChanges ||
hasChangesInfo ||
hasChangesAuth ||
hasChangesPreRequestScript ||
hasChangesTestScript ||
hasChangesVariables
)
if (hasChanges) {
const msg = 'Updating collection head'
console.log('\n' + msg + '...')
await new pmAPI.Collection(remoteCollection.collection.info.uid)
.update({ collection: localEmptyCollection })
.then(() => { console.log(msg, '-> OK\n') })
.catch((error) => {
console.log(msg, '-> FAIL')
handlePostmanAPIError(error)
})
}
return hasChanges
}
const checkFolderSortChanges = (remoteCollection, localCollection) => {
const remoteFolders = remoteCollection.collection.item
.map(folder => ({ id: folder.id }))
const localFolders = localCollection.item
.filter(folder => !folder.folder)
.map(folder => ({ id: folder.id }))
const remoteFoldersHash = GenID(JSON.stringify(remoteFolders))
const localFoldersHash = GenID(JSON.stringify(localFolders))
return remoteFoldersHash !== localFoldersHash
}
const checkInfoChanges = (remoteCollection, localEmptyCollection) => {
// collection info does not have a specific id
// so we need to generate a hash and compare them
// The hash is only beig generated for name, description and schema
const { name, description, schema } = remoteCollection.collection.info
const remoteInfo = { name, description, schema }
const { name: localName, description: localDescription, schema: localSchema } = localEmptyCollection.info
const localInfo = { name: localName, description: localDescription, schema: localSchema }
const remoteInfoHash = calculateHash(JSON.stringify(remoteInfo))
const localInfoHash = calculateHash(JSON.stringify(localInfo))
return remoteInfoHash !== localInfoHash
}
const checkObjectChanges = (remoteCollectionObject, localCollectionObject) => {
if (!remoteCollectionObject && !localCollectionObject) {
return false
}
if (!remoteCollectionObject || !localCollectionObject) {
return true
}
// certain object like auth do not have an id,
// so we need to generate on and compare them
const remoteCollectionAuthID = GenID(JSON.stringify(remoteCollectionObject))
const localCollectionAuthID = GenID(JSON.stringify(localCollectionObject))
return remoteCollectionAuthID !== localCollectionAuthID
}
const checkScriptChanges = (scriptType, remoteCollection, localCollection) => {
// RB 2020-10-20: The collection may be empty or have no events at all
let remoteScript = null
let localScript = null
if (remoteCollection.collection.event) {
remoteScript = remoteCollection.collection.event.find(event => event.listen === scriptType)
}
if (localCollection.event) {
localScript = localCollection.event.find(event => event.listen === scriptType)
}
// const remoteScript = remoteCollection.collection.event.find(event => event.listen === scriptType)
// const localScript = localCollection.event.find(event => event.listen === scriptType)
if (!remoteScript && !localScript) {
return false
}
if (!remoteScript || !localScript) {
return true
}
// files can be big, so we hash them
const remoteHash = calculateHash(remoteScript.script.exec[0])
const localHash = calculateHash(localScript.script.exec[0])
return remoteHash !== localHash
}
const checkVariableChanges = (remoteCollection, localCollection) => {
const remoteVariables = remoteCollection.collection.variable
const localVariables = localCollection.variable.map(variable => ({ key: variable.key, value: variable.value }))
// check if null
if (!remoteVariables && !localVariables) {
return false
}
if (!remoteVariables || !localVariables) {
return true
}
// although the local collection does have a deterministic id
// the remote variable looses that value when it is updated
// so we need to generate an id for the remote variable
const remoteVariablesHash = GenID(remoteVariables)
const localVariablesHash = GenID(localVariables)
return remoteVariablesHash !== localVariablesHash
}
async function mergeFolders (remoteCollection, localCollection) {
console.log(' Deploying Folders:')
const remoteFolders = getAllFoldersFromCollectionItem(remoteCollection.collection.item)
const localFolders = localCollection.item // all folders
const newFolders = localFolders.filter(localFolder => !remoteFolders.find(remoteFolder => remoteFolder.id === localFolder.id))
const oldFolders = remoteFolders.filter(remoteFolder => !localFolders.find(localFolder => localFolder.id === remoteFolder.id))
let hasChanges = newFolders.length > 0 || oldFolders.length > 0
if (!hasChanges) {
console.log(' -> No changes')
return hasChanges
}
// create new folders
for (const folder of newFolders) {
const msg = ` Creating new folder [${folder.name}]`
await new pmAPI.Folder(remoteCollection.collection.info.uid)
.create(folder)
.then(() => {
hasChanges = true
console.log(msg, '-> OK')
})
.catch((error) => {
console.log(msg, '-> FAIL')
handlePostmanAPIError(error)
})
}
// delete old folders
for (const folder of oldFolders) {
const msg = ` Deleting old folder [${folder.name}]`
await new pmAPI.Folder(remoteCollection.collection.info.uid)
.delete(folder.id)
.then(() => {
hasChanges = true
console.log(msg, '-> OK')
})
.catch((error) => {
console.log(msg, '-> FAIL')
handlePostmanAPIError(error)
})
}
// sort folders is not supported for now
// const order = localFolders.map(folder => folder.id)
// const msg = ' Sorting folders'
// // create a temporsary root folder
// const rootFolder = await new pmAPI.Folder(remoteCollection.collection.info.uid)
// .create({ id: GenID(), name: 'root', folders: order })
// .catch((error) => {
// console.log(msg, '-> FAIL')
// handlePostmanAPIError(error)
// })
// console.log('root folder', rootFolder)
// // move all remote folders into root folder
return hasChanges
}
async function mergeRequests (remoteCollection, localCollection) {
const remoteFolders = getAllFoldersFromCollectionItem(remoteCollection.collection.item)
const localFolders = localCollection.item // all folders
console.log('\n Deploying Requests:')
let anyRequestHasChanged = false
// loop folders
for (const localFolder of localFolders) {
const remoteRemoteFolder = remoteFolders.find(remoteFolder => ((remoteFolder.id === localFolder.id)))
// TODO: RB: get requests by folder
// handle undefined items
remoteRemoteFolder.item = remoteRemoteFolder.item || []
// filter out anything that is not a request
remoteRemoteFolder.item = remoteRemoteFolder.item.filter(request => request.request)
const remoteRequests = remoteRemoteFolder.item
const localRequests = localFolder.item
// Identify old and new requests
const newRequests = localRequests.filter(localRequest => !remoteRequests.find(remoteRequest => remoteRequest.id === localRequest.id))
const oldRequests = remoteRequests.filter(remoteRequest => !localRequests.find(localRequest => localRequest.id === remoteRequest.id))
const requestsInFolderHaveChanges = newRequests.length > 0 || oldRequests.length > 0
if (!requestsInFolderHaveChanges) {
console.log(' In Folder: ', localFolder.name, '-> No changes')
continue
}
console.log(' In Folder: ', localFolder.name)
// create new requests
for (const request of newRequests) {
// check request format and convert if necessary
let pmRequest = null
if (!request.request) { // => Postman Format
pmRequest = request
} else { // => OpenAPI Format
pmRequest = pmConvert.requestFromLocal(request)
}
const msg = ` Creating new request [${request.name}]`
await new pmAPI.Request(remoteCollection.collection.info.uid)
.create(pmRequest, localFolder.id)
.then((req) => {
console.log(msg, '-> OK')
return req
})
.catch((error) => {
console.log(msg, '-> FAIL')
handlePostmanAPIError(error)
})
// console.log('\nequest', request)
// console.log('\npmRequest', pmRequest)
// console.log('\nremoteRequest', remoteRequest)
}
// delete old requests
for (const request of oldRequests) {
const msg = ` Deleting old request [${request.name}]`
await new pmAPI.Request(remoteCollection.collection.info.uid)
.delete(request.id)
.then(() => {
console.log(msg, '-> OK')
})
.catch((error) => {
console.log(msg, '-> FAIL')
handlePostmanAPIError(error)
})
}
if (requestsInFolderHaveChanges) {
// sort requests in folder
const order = localRequests.map(request => request.id)
const msg = ` Sorting requests in folder [${localFolder.name}]`
await new pmAPI.Folder(remoteCollection.collection.info.uid)
.update(localFolder.id, { order })
.then(() => { console.log(msg, '-> OK') })
.catch((error) => {
console.log(msg, '-> FAIL')
handlePostmanAPIError(error)
})
}
anyRequestHasChanged = anyRequestHasChanged || requestsInFolderHaveChanges
}
return anyRequestHasChanged
}
async function mergeResponses (remoteCollection, localCollection) {
console.log('\n Deploying Response:')
const remoteFolders = getAllFoldersFromCollectionItem(remoteCollection.collection.item)
const localFolders = localCollection.item
let anyResponseHasChanged = false
// loop folders
for (const localFolder of localFolders) {
const remoteRemoteFolder = remoteFolders.find(remoteFolder => ((remoteFolder.id === localFolder.id)))
// handle undefined items
remoteRemoteFolder.item = remoteRemoteFolder.item || []
// filter out anything that is not a request
remoteRemoteFolder.item = remoteRemoteFolder.item.filter(request => request.request)
const remoteRequests = remoteRemoteFolder.item
const localRequests = localFolder.item
console.log(' In Folder: ', localFolder.name)
// loop requests
for (const localRequest of localRequests) {
// Postman Request format does not have a response property
const remoteResponses = remoteRequests.find(remoteRequest => remoteRequest.id === localRequest.id).response
const localResponses = localRequest.response
// the request may not have responses
if (!localResponses) {
continue
}
const newResponses = localResponses.filter(localResponse => !remoteResponses.find(remoteResponse => remoteResponse.id === localResponse.id))
const oldResponses = remoteResponses.filter(remoteResponse => !localResponses.find(localResponse => localResponse.id === remoteResponse.id))
const ResponsesInReqquestHaveChanges = newResponses.length > 0 || oldResponses.length > 0
if (!ResponsesInReqquestHaveChanges) {
console.log(' In Request: ', localRequest.name, '-> No changes')
continue
}
console.log(' In Request: ', localRequest.name)
// create new responses
for (const response of newResponses) {
const pmResponse = pmConvert.responseFromLocal(response)
const msg = ` Creating new response [${response.code} ${response.status}]`
await new pmAPI.Response(remoteCollection.collection.info.uid)
.create(pmResponse, localRequest.id)
.then(() => {
console.log(msg, '-> OK')
})
.catch((error) => {
console.log(msg, '-> FAIL')
handlePostmanAPIError(error)
})
}
// delete old responses
for (const response of oldResponses) {
const msg = ` Deleting old response [${response.code} ${response.status}]`
await new pmAPI.Response(remoteCollection.collection.info.uid)
.delete(response.id)
.then(() => {
console.log(msg, '-> OK')
})
.catch((error) => {
console.log(msg, '-> FAIL')
handlePostmanAPIError(error)
})
}
// updating the requests with the order of the responses, doesn't seem to be necessary
if (ResponsesInReqquestHaveChanges) {
// sort responses in requests
const responsesOrder = localResponses.map(response => response.id)
const msg = ` Sorting responses in request [${localRequest.name}]`
await new pmAPI.Request(remoteCollection.collection.info._postman_id)
.update(localRequest.id,
{
responses_order: responsesOrder
})
.then(() => { console.log(msg, '-> OK') })
.catch((error) => {
console.log(msg, '-> FAIL')
handlePostmanAPIError(error)
})
}
anyResponseHasChanged = anyResponseHasChanged || ResponsesInReqquestHaveChanges
}
}
return anyResponseHasChanged
}
async function refreshRemoteCollection (remoteCollectionID) {
const msg = 'Refreshing remote collection'
console.log('\n' + msg + '...\n')
const remoteCollection = await new pmAPI.Collection(remoteCollectionID).get()
.catch((error) => {
console.log(msg, '-> FAIL')
handlePostmanAPIError(error)
})
return remoteCollection
}
// return all folders in the collection
// independently of where they are
const getAllFoldersFromCollectionItem = (collectionItem) => {
const folders = []
const processItem = (item) => {
if (!item.request && !item.responses) {
folders.push({ id: item.id, name: item.name, item: item.item })
}
if (item.item) {
item.item.forEach(processItem)
}
}
collectionItem.forEach(processItem)
return folders
}
// Handle axios error
const handlePostmanAPIError = (error) => {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.log('API ERROR:', error.response.data)
} else {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
console.log('NO RESPONSE:', error.message)
if (error.cause) {
console.log('CAUSE:', error.cause)
}
}
const { method, url, data } = error.config
const smallData = data.substring(0, 1000)
console.log('REQUEST DETAILS', { method, url, smallData })
process.exit(1)
}
const calculateHash = (stringToHash) => {
return crypto.createHash('sha256').update(stringToHash).digest('hex')
}
module.exports = {
deployIncremental
}

View File

@@ -1,143 +0,0 @@
// Postman Convertions
// ------------------------------------------------------------
// Handles the conversions from the local generated object
// to the postman api specific objects.
// The converted objects are necessary for the updates to work.
// ------------------------------------------------------------
// The interesting part is that the postman api uses a different
// object structure for folders, requests, adn responses,
// when compared to what you get from the collection json file.
// ------------------------------------------------------------
const requestFromLocal = (localRequest) => {
// console.log('localRequest', localRequest)
let url = localRequest.request.url.protocol + '://' + localRequest.request.url.host + localRequest.request.url.path
let data = []
let dataMode = null
let rawModeData = null
if (localRequest.request.body && localRequest.request.body.urlencoded) {
data = dataFromLocalURLEncode(localRequest.request.body.urlencoded)
dataMode = localRequest.request.body.mode
rawModeData = localRequest.request.body.raw
}
let queryParams = []
if (localRequest.request.url.query) {
queryParams = dataFromLocalURLEncode(localRequest.request.url.query)
url += '?'
for (const param of queryParams) {
if (param.enabled === false) continue
url += param.key + '=' + param.value + '&'
}
url = url.slice(0, -1)
}
let pathVariableData = []
if (localRequest.request.url.variable) {
pathVariableData = dataFromLocalURLEncode(localRequest.request.url.variable)
}
let headerData = []
if (localRequest.request.header) {
headerData = dataFromLocalURLEncode(localRequest.request.header)
.map((header) => ({ key: header.key, value: header.value, enabled: header.enabled, description: header.description }))
}
const request = {
// owner: '8119550',
// lastUpdatedBy: '8119550',
// lastRevision: 32526900683,
// folder: 'dfa47710-b3d3-4a2c-bbc8-fbd25ad12244',
// collection: 'fa89c950-c947-4061-9d13-fb18d7c6bc51',
id: localRequest.id, //
name: localRequest.name, //
dataMode, //
data, //
auth: localRequest.request.auth, //
events: localRequest.events, //
//
rawModeData, // body os request
//
descriptionFormat: localRequest.descriptionFormat, // it can be in either ``html`` or ``markdown`` formats.
description: localRequest.request.description, //
// Headers
headers: null,
headerData,
//
variables: localRequest.variables,
method: localRequest.request.method, //
// Path Variables
pathVariables: pathVariableData, //
pathVariableData, //
//
url, //
preRequestScript: localRequest.preRequestScript,
tests: localRequest.tests,
currentHelper: localRequest.currentHelper,
helperAttributes: localRequest.helperAttributes,
queryParams, //
protocolProfileBehavior: localRequest.protocolProfileBehavior,
dataDisabled: localRequest.dataDisabled,
responses_order: localRequest.responses_order
// createdAt: '2023-09-12T16:25:20.000Z',
// updatedAt: '2023-09-12T16:25:23.000Z',
// dataOptions: {{"raw":{}}},
}
return request
}
const responseFromLocal = (localResponse) => {
const headers = localResponse.header.map((item) => ({ key: item.key, value: item.value }))
const response = {
// owner: '8119550',
// lastUpdatedBy: '8119550',
// lastRevision: 32546597265,
// request: '331bbc94-5425-46f3-8c02-31c353d2ced8',
id: localResponse.id, //
name: localResponse.name, //
status: localResponse.status, //
responseCode: {
code: localResponse.code, //
name: localResponse.status, //
detail: ''
},
// time: null,
headers, //
cookies: [],
mime: null,
text: localResponse.body, //
language: 'Text', //
rawDataType: 'text'//
// requestObject: null,
// createdAt: '2023-09-13T14:53:05.000Z',
// updatedAt: '2023-09-13T14:53:05.000Z'
}
return response
}
const dataFromLocalURLEncode = (localFormData) => {
const data = []
for (const param of localFormData) {
const item = {
key: param.key,
description: param.description,
value: param.value,
enabled: !param.disabled
}
data.push(item)
}
return data
}
module.exports = {
requestFromLocal,
responseFromLocal
}

View File

@@ -1,37 +0,0 @@
const uuid = require('uuid')
const NAMESPACE = '33c4e6fc-44cb-4190-b19f-4a02821bc8c3'
const genID = (objectJSON = null) => {
if (objectJSON) {
return uuid.v5(objectJSON, NAMESPACE)
} else {
return uuid.v4()
}
}
// Sort 2 objects by name
const byName = (a, b) => {
if (a.name < b.name) {
return -1
} else if (a.name > b.name) {
return 1
}
return 0
}
// Sort two object by priority
const byPriority = (a, b) => {
if (a['x-box-priority']) {
return -1
} else if (b['x-box-priority']) {
return 1
}
return 0
}
module.exports = {
GenID: genID,
ByName: byName,
ByPriority: byPriority
// logAxiosError
}

View File

@@ -1,11 +0,0 @@
const fs = require('fs')
const { deployIncremental } = require('./DeployIncremental')
const release = async () => {
let privateRemoteCollectionId = '23836355-c5640083-7523-4ad7-9f92-b23e079cbb7b'
let publicRemoteCollectionId = '23836355-6224d51a-d924-4c39-a58f-6970735aac8e'
let localCollection = JSON.parse(fs.readFileSync(`C:\\git\\api-specs\\postman\\collections\\sailpoint-api-v3.json`).toString())
await deployIncremental(privateRemoteCollectionId, localCollection, publicRemoteCollectionId)
}
release()

View File

@@ -1,131 +0,0 @@
{
"name": "partialupdate",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "partialupdate",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"axios": "^1.6.5",
"dotenv": "^16.3.1",
"uuid": "^9.0.1"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz",
"integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==",
"dependencies": {
"follow-redirects": "^1.15.4",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dotenv": {
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/motdotla/dotenv?sponsor=1"
}
},
"node_modules/follow-redirects": {
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/bin/uuid"
}
}
}
}

View File

@@ -1,16 +0,0 @@
{
"name": "partialupdate",
"version": "1.0.0",
"description": "partial updates to postman files",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^1.6.5",
"dotenv": "^16.3.1",
"uuid": "^9.0.1"
}
}

View File

@@ -1,236 +0,0 @@
require('dotenv').config()
const axios = require('axios')
class Collection {
constructor (collectionId) {
this.collectionId = collectionId
this.apiKey = process.env.POSTMAN_API_KEY
this.axios = axios.create({
timeout: 1000 * 60, // 60 seconds
headers: { 'Content-Type': 'application/json', 'X-Api-Key': this.apiKey }
})
}
async get () {
return await this.axios.get(
`https://api.getpostman.com/collections/${this.collectionId}`
, { timeout: 1000 * 60 * 5 } // 5 minutes
).then(function (response) {
if (response.status !== 200) {
throw new Error(`Error getting collection ${this.collectionId}: ${response.status} ${response.statusText}`)
} else {
return response.data
}
})
}
async merge (destinationCollectionId, strategy = 'updateSourceWithDestination') {
return await this.axios.post(
'https://api.getpostman.com/collections/merge',
{ source: this.collectionId, destination: destinationCollectionId, strategy },
{ timeout: 1000 * 60 * 5 } // 5 minutes
).then(function (response) {
if (response.status !== 200) {
throw new Error(`Error merging collection from ${this.collectionId} to ${destinationCollectionId}: ${response.status} ${response.statusText}`)
} else {
return response.data
}
})
}
async update (collection) {
return await this.axios.put(
`https://api.getpostman.com/collections/${this.collectionId}`,
collection,
{ timeout: 1000 * 60 * 5 } // 5 minutes
).then(function (response) {
if (response.status !== 200) {
throw new Error(`Error updating collection ${collection.id}: ${response.status} ${response.statusText}`)
} else {
return response.data
}
})
}
}
class Folder {
constructor (collectionId) {
this.collectionId = collectionId
this.apiKey = process.env.POSTMAN_API_KEY
this.axios = axios.create({
timeout: 1000 * 60, // 60 seconds
headers: { 'Content-Type': 'application/json', 'X-Api-Key': this.apiKey }
})
}
async get (folderId) {
return await this.axios.get(
`https://api.getpostman.com/collections/${this.collectionId}/folders/${folderId}`
).then(function (response) {
if (response.status !== 200) {
throw new Error(`Error getting folder ${folderId}: ${response.status} ${response.statusText}`)
} else {
return response.data
}
})
}
async create (folder) {
return await this.axios.post(
`https://api.getpostman.com/collections/${this.collectionId}/folders`,
folder
).then(function (response) {
if (response.status !== 200) {
throw new Error(`Error creating folder ${folder.Id}: ${response.status} ${response.statusText}`)
} else {
return response.data
}
})
}
async update (folderId, folder) {
return await this.axios.put(
`https://api.getpostman.com/collections/${this.collectionId}/folders/${folderId}`,
folder
).then(function (response) {
if (response.status !== 200) {
throw new Error(`Error updating folder ${folder.Id}: ${response.status} ${response.statusText}`)
} else {
return response.data
}
})
}
async delete (folderId) {
return await this.axios.delete(
`https://api.getpostman.com/collections/${this.collectionId}/folders/${folderId}`
).then(function (response) {
if (response.status !== 200) {
throw new Error(`Error deleting folder ${folderId}: ${response.status} ${response.statusText}`)
} else {
return response.data
}
})
}
} // class Folder
class Request {
constructor (collectionId) {
this.collectionId = collectionId
this.apiKey = process.env.POSTMAN_API_KEY
this.axios = axios.create({
timeout: 1000 * 60, // 60 seconds
headers: { 'Content-Type': 'application/json', 'X-Api-Key': this.apiKey }
})
}
async get (requestId) {
return await this.axios.get(
`https://api.getpostman.com/collections/${this.collectionId}/requests/${requestId}`
).then(function (response) {
if (response.status !== 200) {
throw new Error(`Error getting request ${requestId}: ${response.status} ${response.statusText}`)
} else {
return response.data
}
})
}
async create (request, folderId) {
return await this.axios.post(
`https://api.getpostman.com/collections/${this.collectionId}/requests`,
request,
{ params: { folder: folderId } }
).then(function (response) {
if (response.status !== 200) {
throw new Error(`Error creating request ${request.id}: ${response.status} ${response.statusText}`)
} else {
return response.data
}
})
}
async update (requestId, request) {
return await this.axios.put(
`https://api.getpostman.com/collections/${this.collectionId}/requests/${requestId}`,
request
).then(function (response) {
if (response.status !== 200) {
throw new Error(`Error updating request ${request.id}: ${response.status} ${response.statusText}`)
} else {
return response.data
}
})
}
async delete (requestId) {
return await this.axios.delete(
`https://api.getpostman.com/collections/${this.collectionId}/requests/${requestId}`
).then(function (response) {
if (response.status !== 200) {
throw new Error(`Error deleting request ${requestId}: ${response.status} ${response.statusText}`)
} else {
return response.data
}
})
}
} // class Request
class Response {
constructor (collectionId) {
this.collectionId = collectionId
this.apiKey = process.env.POSTMAN_API_KEY
this.axios = axios.create({
timeout: 1000 * 60, // 60 seconds
headers: { 'Content-Type': 'application/json', 'X-Api-Key': this.apiKey }
})
}
async get (responseId) {
return await this.axios.get(
`https://api.getpostman.com/collections/${this.collectionId}/responses/${responseId}`
).then(function (axiosResp) {
if (axiosResp.status !== 200) {
throw new Error(`Error getting response ${responseId}: ${axiosResp.status} ${axiosResp.statusText}`)
} else {
return axiosResp.data
}
})
}
async create (response, requestId) {
return await this.axios.post(
`https://api.getpostman.com/collections/${this.collectionId}/responses`,
response,
{ params: { request: requestId } }
).then(function (axiosResp) {
if (axiosResp.status !== 200) {
throw new Error(`Error creating response ${response.id}: ${axiosResp.status} ${axiosResp.statusText}`)
} else {
return axiosResp.data
}
})
}
async delete (responseId) {
return await this.axios.delete(
`https://api.getpostman.com/collections/${this.collectionId}/responses/${responseId}`
).then(function (axiosResp) {
if (axiosResp.status !== 200) {
throw new Error(`Error deleting response ${responseId}: ${axiosResp.status} ${axiosResp.statusText}`)
} else {
return axiosResp.data
}
})
}
} // class Response
module.exports = {
Collection,
Folder,
Request,
Response
}

View File

@@ -12,7 +12,7 @@
const requestFromLocal = (localRequest, responses) => {
// console.log('localRequest', localRequest)
let url = localRequest.request.url.host + '/' + localRequest.request.url.path
let url = localRequest.request.url.host + '/' + localRequest.request.url.path.join('/')
let data = []
let dataMode = null
@@ -48,6 +48,12 @@ const requestFromLocal = (localRequest, responses) => {
.map((header) => ({ key: header.key, value: header.value, enabled: header.enabled, description: header.description }))
}
let headers = []
if (localRequest.request.header) {
headers = dataFromLocalURLEncode(localRequest.request.header)
.map((header) => ({ key: header.key, value: header.value, description: header.description }))
}
if (JSON.stringify(localRequest.request.description) === '{}') {
localRequest.request.description = ''
}
@@ -70,9 +76,9 @@ const requestFromLocal = (localRequest, responses) => {
rawModeData, // body os request
//
descriptionFormat: localRequest.descriptionFormat, // it can be in either ``html`` or ``markdown`` formats.
description: localRequest.request.description.content ? localRequest.request.description.content : localRequest.request.description, //
description: localRequest.request.description && localRequest.request.description.content ? localRequest.request.description.content : localRequest.request.description, //
// Headers
headers: localRequest.request.header,
headers: headers,
headerData,
//
variables: localRequest.variables,
@@ -102,7 +108,17 @@ const requestFromLocal = (localRequest, responses) => {
}
const responseFromLocal = (localResponse, requestObject) => {
const headers = localResponse.header.map((item) => ({ key: item.key, value: item.value }))
const headers = localResponse.header
.map((item) => ({ key: item.key, value: item.value }))
.sort((a, b) => {
if (a.key < b.key) {
return -1;
}
if (a.key > b.key) {
return 1;
}
return 0;
});
const response = {
// owner: '8119550',
// lastUpdatedBy: '8119550',
@@ -121,7 +137,7 @@ const requestFromLocal = (localRequest, responses) => {
headers, //
cookies: [],
mime: null,
text: localResponse.body, //
text: localResponse.body ? localResponse.body : '', //
language: 'json', //
rawDataType: 'text',//
requestObject: requestObject,
@@ -135,11 +151,18 @@ const requestFromLocal = (localRequest, responses) => {
const dataFromLocalURLEncode = (localFormData) => {
const data = []
for (const param of localFormData) {
// check if param.key is a number
if (!param.key || !isNaN(param.key)) {
continue
}
if (JSON.stringify(param.description) === '{}') {
param.description = ''
}
const item = {
key: param.key,
description: param.description,
description: param.description && param.description.content ? param.description.content : param.description,
value: param.value,
enabled: !param.disabled
enabled: false
}
data.push(item)
}

View File

@@ -14,7 +14,7 @@ const release = async () => {
// This function just cleans up the variables so they match what is returned in postman
// This step just cleans up the variables so they match what is returned in postman
for (let variable of localCollection.variable) {
if (variable.type) {
delete variable.type
@@ -41,11 +41,35 @@ const release = async () => {
// add any missing folders
for (let item of localCollection.item) {
let folder = getMatchingFolder(item, remoteCollection.collection.item)
if (checkIfDifferent(folder.description, item.description)) {
console.log(`updating folder ${folder.name}`)
await new pmAPI.Folder(publicRemoteCollectionId).update(folder.id, { description: item.description })
console.log(`updated folder ${folder.name}`)
}
if (folder == null) {
await updateEntireFolder(item)
}
}
// delete any folders that are no longer in the collection
for (let folder of remoteCollection.collection.item) {
let localFolder = getMatchingFolder(folder, localCollection.item)
if (localFolder == null) {
await new pmAPI.Folder(publicRemoteCollectionId).delete(folder.id)
}
}
// delete any requests that are no longer in the collection
for (let folder of remoteCollection.collection.item) {
let localFolder = getMatchingFolder(folder, localCollection.item)
for (let items of folder.item) {
let remoteRequest = getMatchingRequest(items, localFolder.item)
if (remoteRequest == null) {
await new pmAPI.Request(publicRemoteCollectionId).delete(items.id)
}
}
}
// update any requests that have changed
for (let item of localCollection.item) {
let folder = getMatchingFolder(item, remoteCollection.collection.item)
@@ -54,7 +78,6 @@ const release = async () => {
}
}
console.log(remoteCollection)
}
@@ -69,7 +92,7 @@ function getMatchingFolder(localFolder, remoteFolders) {
function getMatchingRequest(localRequest, remoteRequests) {
for (let request of remoteRequests) {
if (request.name === localRequest.name) {
if (request.name === localRequest.name && request.request.method === localRequest.request.method && localRequest.request.url.host + '/' + localRequest.request.url.path.join('/') === request.request.url.host + '/' + request.request.url.path.join('/')) {
return request
}
}
@@ -77,6 +100,9 @@ function getMatchingRequest(localRequest, remoteRequests) {
}
function buildRequestBody(items) {
if (items === null) {
return null
}
let responses = []
for (let response of items.response) {
@@ -87,12 +113,87 @@ function buildRequestBody(items) {
}
function checkIfDifferent(source, dest) {
if (JSON.stringify(source) === JSON.stringify(dest)) {
removeIdFields(source)
removeIdFields(dest)
if (isDeepEqual(source, dest)) {
return false
}
return true
}
function removeIdFields(obj) {
if (typeof obj !== 'object') {
return
}
if (obj === null) {
return
}
// Check if the current object has the 'id' property
if (obj.hasOwnProperty('id')) {
delete obj.id;
}
// Recursively call removeIdFields on each property if it's an object
for (let key in obj) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
removeIdFields(obj[key]);
}
}
}
function isNullorEmpty(obj) {
if (obj === null || obj === '' || obj === undefined) {
return true
}
return false
}
function isDeepEqual(obj1, obj2) {
if (areValuesEqual(obj1, obj2)) {
return true
}
if (typeof obj1 !== 'object' || typeof obj2 !== 'object' || obj1 == null || obj2 == null) {
return false;
}
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) {
return false;
}
for (let key of keys1) {
const val1 = obj1[key];
const val2 = obj2[key];
const areObjects = isObject(val1) && isObject(val2);
if (
areObjects && !isDeepEqual(val1, val2) ||
(!areObjects && !areValuesEqual(val1, val2))
) {
return false;
}
}
return true;
}
function areValuesEqual(val1, val2) {
if (isNullorEmpty(val1) && isNullorEmpty(val2)) {
return true;
}
if (val1 === val2) {
return true
}
return false
}
function isObject(object) {
return object != null && typeof object === 'object';
}
async function updateEntireFolder(item, folderId) {
if (item.item && !folderId) {
@@ -118,8 +219,12 @@ async function updateRequestsInFolder(item, folderId, remoteItem) {
let postmanRequestBody = buildRequestBody(items)
let remotePostmanBody = buildRequestBody(remoteRequest)
if (checkIfDifferent(postmanRequestBody, remotePostmanBody)) {
let newRequestDelete = await new pmAPI.Request(publicRemoteCollectionId).delete(remoteRequest.id)
console.log(`deleting request ${newRequestDelete.data.id}`)
if (remoteRequest) {
console.log(`deleting request ${remoteRequest.name}`)
let newRequestDelete = await new pmAPI.Request(publicRemoteCollectionId).delete(remoteRequest.id)
console.log(`deleted request ${newRequestDelete.data.id}`)
}
postmanRequestBody = buildRequestBody(items)
let newRequest = await new pmAPI.Request(publicRemoteCollectionId).create(postmanRequestBody, folderId)
console.log(`creating request ${newRequest.data.name}`)
} else {

View File

@@ -31,14 +31,13 @@ class Collection {
})
axiosRetry(this.axios, {
retries: 10,
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
console.log('error, retrying')
return error.code === 'ECONNRESET' || error.code === 'ECONNABORTED' || axiosRetry.isNetworkOrIdempotentRequestError(error) || error.response.status === 429
}
})
this.axios.interceptors.response.use(response => response, handleError);
//this.axios.interceptors.response.use(response => response, handleError);
}
@@ -94,13 +93,13 @@ class Folder {
})
axiosRetry(this.axios, {
retries: 10,
retryDelay: axiosRetry.exponentialDelay,
//retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
console.log('error, retrying')
return error.code === 'ECONNRESET' || error.code === 'ECONNABORTED' || axiosRetry.isNetworkOrIdempotentRequestError(error) || error.response.status === 429
}
})
this.axios.interceptors.response.use(response => response, handleError);
//this.axios.interceptors.response.use(response => response, handleError);
}
async get (folderId) {
@@ -165,13 +164,13 @@ class Request {
})
axiosRetry(this.axios, {
retries: 10,
retryDelay: axiosRetry.exponentialDelay,
//retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
console.log('error, retrying')
return error.code === 'ECONNRESET' || error.code === 'ECONNABORTED' || axiosRetry.isNetworkOrIdempotentRequestError(error) || error.response.status === 429
}
})
this.axios.interceptors.response.use(response => response, handleError);
//this.axios.interceptors.response.use(response => response, handleError);
}
async get (requestId) {
@@ -237,13 +236,13 @@ class Response {
})
axiosRetry(this.axios, {
retries: 10,
retryDelay: axiosRetry.exponentialDelay,
//retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
console.log('error, retrying')
return error.code === 'ECONNRESET' || error.code === 'ECONNABORTED' || axiosRetry.isNetworkOrIdempotentRequestError(error) || error.response.status === 429
}
})
this.axios.interceptors.response.use(response => response, handleError);
//this.axios.interceptors.response.use(response => response, handleError);
}
async get (responseId) {