UNPKG

10.5 kBJavaScriptView Raw
1/* jshint
2browser: true, jquery: true, node: true,
3bitwise: true, camelcase: false, curly: true, eqeqeq: true, esversion: 6, evil: true, expr: true, forin: true, immed: true, indent: 4, latedef: true, newcap: true, noarg: true, noempty: true, nonew: true, quotmark: single, regexdash: true, strict: true, sub: true, trailing: true, undef: true, unused: vars, white: true
4*/
5
6// New 2.0 endpoint: /v4_6_release/apis/2.0/integration_io.asmx
7
8'use strict';
9
10const _ = require('lodash');
11const concat = require('concat-stream');
12const events = require('events');
13const hq = require('hyperquest');
14const js2xmlparser = require('js2xmlparser');
15const parseString = require('xml2js').Parser({ explicitArray: false }).parseString;
16const qs = require('querystring');
17const request = require('request');
18const sax = require('./sax-1.2.1.patched');
19const saxpath = require('saxpath');
20
21const api = {
22 makeActionXml (actionName, actionData) {
23 actionData = _.merge(actionData, {$: { 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', 'xmlns:xsd': 'http://www.w3.org/2001/XMLSchema' }});
24 return js2xmlparser.parse(actionName, actionData, {declaration: {encoding: 'utf-16'}, attributeString: '$'});
25 },
26
27 // Send action request to server and stream back XML response.
28 actionXmlStream (psaServerHostName, actionXml) {
29 var uri = `https://${psaServerHostName}/v4_6_release/services/system_io/integration_io/processclientaction.rails`;
30
31 var req = hq.post(uri);
32 req.setHeader('content-type', 'application/x-www-form-urlencoded');
33 req.end(qs.stringify({ actionString : actionXml }));
34
35 return req;
36 },
37
38 // Filter action results with xPath and stream results using event emitter
39 // api.
40 actionStream (psaServerHostName, actionXml, actionName, xPath) {
41 xPath = xPath || '//' + actionName;
42
43 var strict = true;
44 var saxParser = sax.createStream(strict, {entities: false});
45 var streamer = new saxpath.SaXPath(saxParser, xPath);
46 var objectEmitter = new events.EventEmitter();
47
48 streamer.on('match', xml => {
49 parseString(xml, (error, match) => {
50 error && objectEmitter.emit('error', error);
51 match && objectEmitter.emit('match', match);
52 });
53 });
54
55 var xmlStream = api.actionXmlStream(psaServerHostName, actionXml)
56 .on('response', response => {
57 if (response.statusCode !== 200) {
58 xmlStream.pipe(concat({encoding: 'string'}, body => {
59 objectEmitter.emit('error', `http response status code ${response.statusCode}\n\n${body}`);
60 }));
61 return;
62 }
63 xmlStream.pipe(saxParser);
64 })
65 .on('error', error => {
66 objectEmitter.emit('error', error);
67 })
68 .on('end', function () {
69 objectEmitter.emit('end');
70 });
71
72 return objectEmitter;
73 },
74
75 // Buffer action results and return with callback api.
76 action (psaServerHostName, actionXml, actionName, returnErrorAndResult) {
77 var xmlStream = api.actionXmlStream(psaServerHostName, actionXml)
78 .on('response', function (response) {
79 xmlStream.pipe(concat({encoding: 'string'}, body => {
80 if (response.statusCode !== 200) {
81 returnErrorAndResult(`http response status code ${response.statusCode}\n\n${body}`);
82 return;
83 }
84 parseString(body, returnErrorAndResult);
85 }));
86 })
87 .on('error', error => returnErrorAndResult(error));
88 },
89
90 uploadDocumentToTicket (psaServerHostName, psaCompanyName, integrationLoginId, integrationPassword, sRServiceRecID, fileName, fileBufferOrString, returnErrorAndResult) {
91 var mime = require('mime');
92
93 var uri = `https://${psaServerHostName}/v4_6_release/services/system_io/integration_io/uploaddocumenttoticket.aspx`;
94
95 var requestOptions = {
96 method: 'POST',
97 uri: uri,
98 strictSSL: false,
99 headers: {
100 'content-type' : 'multipart/form-data'
101 },
102 multipart: [
103 {'Content-Disposition' : 'form-data; name="PsaCompanyName"', body: psaCompanyName },
104 {'Content-Disposition' : 'form-data; name="IntegrationLoginId"', body: integrationLoginId },
105 {'Content-Disposition' : 'form-data; name="IntegrationPassword"', body: integrationPassword },
106 {'Content-Disposition' : 'form-data; name="SRServiceRecID"', body: sRServiceRecID },
107 {'Content-Disposition' : 'form-data; name="file"; filename="' + fileName + '"', 'Content-Type' : mime.getType(fileName), body: fileBufferOrString}
108 ]
109 };
110
111 request(requestOptions, (error, response, body) => {
112 if (error) {
113 returnErrorAndResult(error);
114 return;
115 }
116
117 if (response.statusCode !== 200) {
118 returnErrorAndResult(`response status code ${response.statusCode}\r\n${body}`);
119 return;
120 }
121
122 returnErrorAndResult(null, body);
123 });
124 },
125
126
127 // Collections can be parsed as various different data types depending on
128 // if they containe 0, 1 or many results. This function normalizes a
129 // collection into an array.
130 normalizeCollection (collection) {
131 if (_.isString(collection)) {
132 return [];
133 }
134
135 var values = _.values(collection);
136 if (values.length === 0) {
137 return [];
138 }
139
140 var firstValue = values[0];
141 if (!_.isArray(firstValue)) {
142 return [firstValue];
143 }
144
145 return firstValue;
146 },
147
148 // Necessary because FindPartnerTicketsAction does not return Id key.
149 // Apparently this is fixed in API's 2.0
150 normalizeTicketsFromFindPartnerTicketsAction (tickets) {
151 return _.map(tickets, ticket => {
152 ticket.Id = ticket.Id || ticket.SRServiceRecID;
153 return ticket;
154 });
155 },
156
157 normalizeRunReportQueryActionResult (result) {
158 return result.reduce((previous, metaValue) => {
159 previous[metaValue.$.Name] = (value => { switch (metaValue.$.Type) {
160 case 'DateTime': return api.parsePsaDate(value);
161 case 'Numeric': return Number(value);
162 case 'Boolean': return value && value.toLowerCase() === 'true';
163 default: return value;
164 }})(metaValue._);
165 return previous;
166 }, {});
167 },
168
169 normalizeRunReportQueryAction (metaData) {
170 return api.normalizeCollection(metaData.RunReportQueryAction.Results).map(metaData => {
171 return api.normalizeRunReportQueryActionResult(metaData.Value);
172 });
173 },
174
175 normalizeGetAvailableReportsActionResponse (response) {
176 var normalizedReports = api.normalizeCollection(response.GetAvailableReportsAction.AvailableReports);
177 var reduced = normalizedReports.reduce((reports, report) => {
178 reports[report.$.Name] = report.Field && report.Field.reduce((fields, field) => {
179 fields[field.$.Name] = _.omit(field.$, 'Name');
180 return fields;
181 }, {});
182 return reports;
183 }, {});
184
185 return reduced;
186 },
187
188 // dateTime can be
189 // 'MM/DD/YYYY HH:MM:SS AM'
190 // 'YYYY-MM-DDTHH:MM:SS'
191 //
192 // optionalTimeZone examples
193 // '-0400'
194 // 'GMT'
195 // 'EST'
196 parsePsaDate (dateTime, optionalTimeZone) {
197 if (typeof dateTime === 'undefined') {
198 return dateTime;
199 }
200
201 var parts = dateTime.split('T');
202 optionalTimeZone && parts.push(optionalTimeZone);
203 return new Date(parts.join(' '));
204 },
205
206 // Make a service ticket URL.
207 connectWiseUrl (psaServerHostName) {
208 return 'https://' + psaServerHostName;
209 },
210
211 // Make a service ticket URL.
212 ticketUrl (psaServerHostName, psaCompanyName, ticketId) {
213 return `https://${psaServerHostName}/v4_6_release/services/system_io/Service/fv_sr100_request.rails?companyName=${psaCompanyName}&service_recid=${ticketId}`;
214 },
215
216 // Hacky but at least gets you a page with some data.
217 activityUrl (psaServerHostName, activityId) {
218 return `https://${psaServerHostName}/v4_6_release/contact/activity/default.rails?action=viewSOActivity&screenid=tm100&ac_flag=false&recordid=${activityId}`;
219 },
220
221 configure (psaServerHostName, psaCompanyName, integrationLoginId, integrationPassword) {
222 var configured = _.clone(api);
223
224 configured.action = (actionName, actionData, returnErrorAndResult) => {
225 var defaultActionData = {
226 CompanyName: psaCompanyName,
227 IntegrationLoginId: integrationLoginId,
228 IntegrationPassword: integrationPassword
229 };
230 var actionXml = api.makeActionXml(actionName, _.merge(actionData, defaultActionData));
231 api.action(psaServerHostName, actionXml, actionName, returnErrorAndResult);
232 };
233
234 configured.actionStream = (actionName, xPath, actionData) => {
235 var defaultActionData = {
236 CompanyName: psaCompanyName,
237 IntegrationLoginId: integrationLoginId,
238 IntegrationPassword: integrationPassword
239 };
240 var actionXml = api.makeActionXml(actionName, _.merge(actionData, defaultActionData));
241 return api.actionStream(psaServerHostName, actionXml, actionName, xPath);
242 };
243
244 configured.uploadDocumentToTicket = _.partial(api.uploadDocumentToTicket, psaServerHostName, psaCompanyName, integrationLoginId, integrationPassword);
245 configured.connectWiseUrl = _.partial(api.connectWiseUrl, psaServerHostName);
246 configured.ticketUrl = _.partial(api.ticketUrl, psaServerHostName, psaCompanyName);
247 configured.activityUrl = _.partial(api.activityUrl, psaServerHostName);
248
249 return configured;
250 }
251};
252
253module.exports = api;
\No newline at end of file