From 1bab3af80ceb7fec44662554c54f360e1d9e2da8 Mon Sep 17 00:00:00 2001 From: abhi juneja Date: Fri, 21 Apr 2017 04:17:35 +0530 Subject: [PATCH 1/5] Added Start Index property --- qcApi.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qcApi.js b/qcApi.js index 34fdb5d..0655b36 100644 --- a/qcApi.js +++ b/qcApi.js @@ -167,6 +167,9 @@ qcApi.prototype.buildUrl = function(url, options){ if(options.pageSize) queryString.push('page-size=' + options.pageSize); + if(options.startIndex) + queryString.push('start-index=' + options.startIndex); + if(options.fields && options.fields.length != undefined) queryString.push('fields=' + options.fields.join(',')); From dd22a059cd45404fe4edf6e97c5cac71a64bbe06 Mon Sep 17 00:00:00 2001 From: Abhinav Juneja Date: Mon, 8 May 2017 17:08:25 +0530 Subject: [PATCH 2/5] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 59e7e4c..3506386 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,12 @@ Takes a url, either relative to the *qcbin/rest/* url or relative to the *qcbin/ #### args ###### pageSize *[integer/string]* How many items to retrieve or 'max' to get as many as allowed by the ALM instance (can be configured in site administration interface). +###### pageSize +*[integer/string]* How many items to retrieve or 'max' to get as many as allowed by the ALM instance (can be configured in site administration interface). ###### page *Not yet implemeted* +###### startIndex +*[integer/String]* Fetch rows starting from a row number or index* ###### fields *[Array]* Which fields to retrieve, using this can greatly improve API speed. Skipping description fields etc lessens the size of the response body. ###### filter @@ -178,4 +182,4 @@ npm test However, if you want to run the integration or learning tests you'll need to provide connection information to your own ALM instance. Do this by copying the [test/conn_info_example.json](https://github.com/ddikman/QC.js/blob/master/test/conn_info_example.json) and saving it in the same folder but named *conn_info.json*. -The tests will then pick up the connection info from there and the tests can be run, of course they might fail if your project looks different. \ No newline at end of file +The tests will then pick up the connection info from there and the tests can be run, of course they might fail if your project looks different. From 7193063bb7bb9257bd1e487376f83f70fb513796 Mon Sep 17 00:00:00 2001 From: Abhinav Juneja Date: Mon, 8 May 2017 17:09:07 +0530 Subject: [PATCH 3/5] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 3506386..8f8e25c 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,6 @@ Takes a url, either relative to the *qcbin/rest/* url or relative to the *qcbin/ #### args ###### pageSize *[integer/string]* How many items to retrieve or 'max' to get as many as allowed by the ALM instance (can be configured in site administration interface). -###### pageSize -*[integer/string]* How many items to retrieve or 'max' to get as many as allowed by the ALM instance (can be configured in site administration interface). ###### page *Not yet implemeted* ###### startIndex From 70c2f02511931cf4dc2da51ff7d99eed8b91e300 Mon Sep 17 00:00:00 2001 From: Abhinav Juneja Date: Thu, 1 Mar 2018 11:31:38 +0530 Subject: [PATCH 4/5] Update qcApi.js --- qcApi.js | 201 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 178 insertions(+), 23 deletions(-) diff --git a/qcApi.js b/qcApi.js index 0655b36..bc086c9 100644 --- a/qcApi.js +++ b/qcApi.js @@ -1,6 +1,9 @@ var Promise = require('promise'); var cookies = require('cookie'); var util = require('util'); +var xml2js = require('xml2js'); +require('String.prototype.repeat'); +require('buffer-concat/polyfill'); var Client = new require('node-rest-client').Client; InvalidAuthenticationException = function(msg){ @@ -50,7 +53,7 @@ qcApi.prototype.startSession = function(){ this.client.post(this.rootUrl + "/rest/site-session", { headers: { cookie : this.authCookie } }, function(data, res){ - if(res.statusCode != 201) + if(res.statusCode != 201 && res.statusCode != 200) { reject("Session start failed, status code " + res.statusCode); return; @@ -76,10 +79,10 @@ qcApi.prototype.login = function(connInfo){ this.client = this.getClient({user: connInfo.user, password: connInfo.password}); this.domain = connInfo.domain; this.project = connInfo.project; - + this.client.get(this.rootUrl + "/authentication-point/authenticate", function handleAuthResponse(data, res){ - if(res.statusCode == 200) + if(res.statusCode == 200 || res.statusCode == 201) { this.isAuthenticated = true; this.authCookie = res.headers['set-cookie'].join(';'); @@ -112,37 +115,50 @@ qcApi.prototype.verifyAuthenticated = function(){ throw new InvalidAuthenticationException("Not yet logged in, please call login to authenticate."); } +qcApi.prototype.convertEntity = function(entity){ + var convertedEntity = { + type: entity['$'].Type + }; + + entity.Fields[0].Field.forEach(function(field){ + var name = field['$'].Name; + name = name.replace(/(-\b[a-z](?!\s))/g, function(x){return x.toUpperCase();}); + name = name.replace(/-/g, ''); + var value = field.Value ? field.Value[0] : null; + convertedEntity[name] = value; + }); + + return convertedEntity; +}; + /** * If the REST call response is an entity, some processing is performed on the resulting javascript object, such as putting each field as a property * on the object instead of an object in the entities property list * @param {obj} Should be a javascript object returned from the node-rest-client, parsed from a REST call xml or json response */ qcApi.prototype.convertResult = function(obj){ - - if(obj.Entities == undefined) - return obj; - + var entities = []; var result = []; - result.totalResults = parseInt(obj.Entities['$'].TotalResults); + + if(obj.Entities == undefined && obj.Entity == undefined) //If unknown result, return the object as is + return obj; + else if(obj.Entities == undefined){ //if only one entity + entities.push(obj.Entity); + result.totalResults = 1; + }else{ + entities = obj.Entities.Entity; + result.totalResults = parseInt(obj.Entities['$'].TotalResults); + } if(result.totalResults == 0) return result; - obj.Entities.Entity.forEach(function(entity){ - - var convertedEntity = { - type: entity['$'].Type - }; - - entity.Fields[0].Field.forEach(function(field){ - var name = field['$'].Name; - var value = field.Value ? field.Value[0] : null; - convertedEntity[name] = value; - }); - - result.push(convertedEntity); - }); + entities.forEach(function(entity){ + result.push( this.convertEntity(entity) ); + }.bind(this)); + if (result.totalResults == 1) + return result[0]; return result; }; @@ -167,9 +183,12 @@ qcApi.prototype.buildUrl = function(url, options){ if(options.pageSize) queryString.push('page-size=' + options.pageSize); + if (options.query) + queryString.push("query={" + options.query.join(';')+"}"); + if(options.startIndex) queryString.push('start-index=' + options.startIndex); - + if(options.fields && options.fields.length != undefined) queryString.push('fields=' + options.fields.join(',')); @@ -206,7 +225,143 @@ qcApi.prototype.get = function(url, options) { return promise; }; +qcApi.prototype.objToXml = function(obj){ + var convertedObj = { "$": {Type: (obj.type? obj.type: obj.Type)}, + Fields:{Field:[]}}; + Object.keys( obj ).forEach( function(prop){ + if (prop == 'Type' || prop == 'type') return; + var value = obj[prop]; + convertedObj.Fields.Field.push( {"$": {Name: prop}, "Value": value} ); + }); + + var builder = new xml2js.Builder({rootName : 'Entity', attrkey : '$'}); + var xml = builder.buildObject(convertedObj); + return xml; +}; + +qcApi.prototype.post = function(url, options) { + var promise = new Promise(function(resolve, reject){ + + var h = { headers: { cookie: this.authCookie, + "Content-Type" : "application/xml", + Accept: "application/xml" + } + }; + + this.verifyAuthenticated(); + + url = this.buildUrl(url, options); + + if (options.data) + h["data"] = this.objToXml(options.data); + else + throw 'Expected object data in args parameter '; + + this.client.post(url, h, function handleGetResponse(data, res){ + + if( res.statusCode != 201 ) + reject(new FailedRequestException("Failed to process url", res.statusCode, data.toString('utf8'), url)); + else + resolve(this.convertResult(data)); + + }.bind(this)); + + }.bind(this)); + + return promise; +}; + +qcApi.prototype.put = function(url, options) { + var promise = new Promise(function(resolve, reject){ + + var h = { headers: { cookie: this.authCookie, + "Content-Type" : "application/xml", + Accept: "application/xml" + } + }; + + this.verifyAuthenticated(); + + url = this.buildUrl(url, options); + + if (options.data) + h["data"] = this.objToXml(options.data); + else + throw 'Expected object data in args parameter '; + + this.client.put(url, h, function handleGetResponse(data, res){ + + if( res.statusCode != 200 ) + reject(new FailedRequestException("Failed to process url", res.statusCode, data.toString('utf8'), url)); + else + resolve(this.convertResult(data)); + + }.bind(this)); + + }.bind(this)); + return promise; +}; + +qcApi.prototype.attach = function(obj, options) { + var promise = new Promise(function(resolve, reject){ + + var h = { headers: { cookie: this.authCookie, + "Content-Type" : "application/octet-stream", + Accept: "application/xml" + } + }; + + this.verifyAuthenticated(); + + if (options.data){ + //console.log( "File length orig:" + options.data.length); + if ( options.data.length <= 7486 ) + if ( options.data instanceof String ) + options.data += " ".repeat(7487 - options.data.length ); + else if ( Buffer.isBuffer(options.data) ){ + //console.log( "Buffer bytes length:" + options.data.byteLength ); + var b = new Buffer(" ".repeat(7487 - options.data.length)); + //console.log( "New Buffer bytes length:" + b.byteLength ); + options.data = Buffer.concat([options.data,b]); + } + h["data"] = options.data; + h.headers["Content-Length"] = options.data.length; + //console.log( "File length:" + options.data.length); + //console.log( "Content-Length:" + h.headers["Content-Length"]); + } + else + throw 'Expected object data in args parameter '; + + if (options.filename) + h.headers["Slug"] = options.filename; + else + throw 'Expected filename in args parameter '; + + url = this.buildUrl("/"+(obj.Type?obj.Type:obj.type)+"s/"+obj.id+"/attachments", options); + this.client.post(url, h, function handleGetResponse(data, res){ + //console.log("status:"+res.statusCode+(data.QCRestException? "/" + data.QCRestException.Title[0]:"")); + if( res.statusCode != 201 + && data.QCRestException.Title[0].indexOf("Failed to set content to attachment") == 0 ){ //Workaround of Api error + return this.get("/"+(obj.Type?obj.Type:obj.type)+"s/"+obj.id+"/attachments?order-by={id}").then( function(attachs){ + resolve( ( attachs instanceof Array? attachs[attachs.length-1]: attachs ) ); + }); + }else{ + if ( res.statusCode != 201 ) + reject(new FailedRequestException("Failed to process url", res.statusCode, data.toString('utf8'), url)); + else + resolve(this.convertResult(data)); + } + + }.bind(this),function(err){ + console.log(err); + reject(new FailedRequestException("Failed to process url", res.statusCode, data.toString('utf8'), url)); + }); + + }.bind(this)); + + return promise; +}; module.exports = { create: function(){ From 46e0ad387b264a023df66138a1fc37c79146ddf8 Mon Sep 17 00:00:00 2001 From: Abhinav Juneja Date: Thu, 1 Mar 2018 11:35:44 +0530 Subject: [PATCH 5/5] Update qcApi.js --- qcApi.js | 123 ------------------------------------------------------- 1 file changed, 123 deletions(-) diff --git a/qcApi.js b/qcApi.js index bc086c9..f2d439d 100644 --- a/qcApi.js +++ b/qcApi.js @@ -239,129 +239,6 @@ qcApi.prototype.objToXml = function(obj){ return xml; }; -qcApi.prototype.post = function(url, options) { - var promise = new Promise(function(resolve, reject){ - - var h = { headers: { cookie: this.authCookie, - "Content-Type" : "application/xml", - Accept: "application/xml" - } - }; - - this.verifyAuthenticated(); - - url = this.buildUrl(url, options); - - if (options.data) - h["data"] = this.objToXml(options.data); - else - throw 'Expected object data in args parameter '; - - this.client.post(url, h, function handleGetResponse(data, res){ - - if( res.statusCode != 201 ) - reject(new FailedRequestException("Failed to process url", res.statusCode, data.toString('utf8'), url)); - else - resolve(this.convertResult(data)); - - }.bind(this)); - - }.bind(this)); - - return promise; -}; - -qcApi.prototype.put = function(url, options) { - var promise = new Promise(function(resolve, reject){ - - var h = { headers: { cookie: this.authCookie, - "Content-Type" : "application/xml", - Accept: "application/xml" - } - }; - - this.verifyAuthenticated(); - - url = this.buildUrl(url, options); - - if (options.data) - h["data"] = this.objToXml(options.data); - else - throw 'Expected object data in args parameter '; - - this.client.put(url, h, function handleGetResponse(data, res){ - - if( res.statusCode != 200 ) - reject(new FailedRequestException("Failed to process url", res.statusCode, data.toString('utf8'), url)); - else - resolve(this.convertResult(data)); - - }.bind(this)); - - }.bind(this)); - - return promise; -}; - -qcApi.prototype.attach = function(obj, options) { - var promise = new Promise(function(resolve, reject){ - - var h = { headers: { cookie: this.authCookie, - "Content-Type" : "application/octet-stream", - Accept: "application/xml" - } - }; - - this.verifyAuthenticated(); - - if (options.data){ - //console.log( "File length orig:" + options.data.length); - if ( options.data.length <= 7486 ) - if ( options.data instanceof String ) - options.data += " ".repeat(7487 - options.data.length ); - else if ( Buffer.isBuffer(options.data) ){ - //console.log( "Buffer bytes length:" + options.data.byteLength ); - var b = new Buffer(" ".repeat(7487 - options.data.length)); - //console.log( "New Buffer bytes length:" + b.byteLength ); - options.data = Buffer.concat([options.data,b]); - } - h["data"] = options.data; - h.headers["Content-Length"] = options.data.length; - //console.log( "File length:" + options.data.length); - //console.log( "Content-Length:" + h.headers["Content-Length"]); - } - else - throw 'Expected object data in args parameter '; - - if (options.filename) - h.headers["Slug"] = options.filename; - else - throw 'Expected filename in args parameter '; - - url = this.buildUrl("/"+(obj.Type?obj.Type:obj.type)+"s/"+obj.id+"/attachments", options); - this.client.post(url, h, function handleGetResponse(data, res){ - //console.log("status:"+res.statusCode+(data.QCRestException? "/" + data.QCRestException.Title[0]:"")); - if( res.statusCode != 201 - && data.QCRestException.Title[0].indexOf("Failed to set content to attachment") == 0 ){ //Workaround of Api error - return this.get("/"+(obj.Type?obj.Type:obj.type)+"s/"+obj.id+"/attachments?order-by={id}").then( function(attachs){ - resolve( ( attachs instanceof Array? attachs[attachs.length-1]: attachs ) ); - }); - }else{ - if ( res.statusCode != 201 ) - reject(new FailedRequestException("Failed to process url", res.statusCode, data.toString('utf8'), url)); - else - resolve(this.convertResult(data)); - } - - }.bind(this),function(err){ - console.log(err); - reject(new FailedRequestException("Failed to process url", res.statusCode, data.toString('utf8'), url)); - }); - - }.bind(this)); - - return promise; -}; module.exports = { create: function(){