UNPKG

25.2 kBJavaScriptView Raw
1const util = require('util');
2const validate = require('jsonschema').validate;
3const commonUtils = require('./common-utils');
4const Cache = require('./cache.js');
5const Batch = require('./batch');
6const SriClientError = require('./sri-client-error');
7
8const mergeObjRecursive = (obj, patch) => {
9 const ret = obj ?{...obj} : {};
10 if(patch) {
11 Object.keys(patch).forEach(key => {
12 if (typeof patch[key] === 'object') {
13 Object.assign(ret, {[key]: mergeObjRecursive(obj[key], patch[key])});
14 } else {
15 Object.assign(ret, {[key]: patch[key]});
16 }
17 });
18 }
19
20 return ret;
21};
22
23module.exports = class SriClient {
24 constructor(config = {}) {
25 this.configuration = config;
26 this.groupBy = config.groupBy || 100;
27 this.cache = new Cache(config.caching, this);
28 }
29
30 createBatch() {
31 return new Batch(this);
32 }
33
34 /*get configuration() {
35 return this._configuration;
36 }*/
37
38 setConfiguration(configuration) {
39 this.configuration = configuration;
40 }
41
42 patchConfiguration(patch) {
43 this.setConfiguration(mergeObjRecursive(this.configuration, patch));
44 }
45
46 getBaseUrl(options = {}) {
47 const baseUrl = options.baseUrl || this.configuration.baseUrl;
48 if (!baseUrl) {
49 throw Error({message: "There is no baseUrl configured. The specification for the node-sri-client module can be found at https://bitbucket.org/vskovzw/kathondvla-utils"});
50 }
51 return baseUrl;
52 }
53
54 //should be defined by sub classes.
55 getRaw() {}
56
57 get(href, params, options = {}) {
58 return this.wrapGet(href, params, options, true);
59 }
60
61 async wrapGet(href, params, options, isSingleResource) {
62 try {
63 let result;
64 if(options.inBatch && !this.cache.has(href, params, options.caching)) {
65 const batch = [{
66 href: commonUtils.parametersToString(href, params),
67 verb: 'GET'
68 }];
69 const batchResp = await this.wrapSendPayload(options.inBatch, batch, options, options.batchMethod && options.batchMethod === 'POST' ? 'POST' : 'PUT');
70 // const batchResp = await this.put(options.inBatch, batch, options);
71 if(batchResp[0].status < 300) {
72 result = batchResp[0].body;
73 } else {
74 throw batchResp[0].body;
75 }
76 } else {
77 result = await this.cache.get(href, params, options, !isSingleResource);
78 if(isSingleResource && result.results) {
79 throw Error('Do not use the get method to ask for lists. Use getList or getAll instead. You can also use getRaw but this method does not use caching, client side expansion and inclusion.');
80 }
81 }
82 if(options.expand) {
83 await this.expandJson(result, options.expand, options.caching, options.logging ? options.logging.replace('get', '').replace('expand', 'expand,get') : undefined);
84 }
85 if(options.include) {
86 await this.includeJson(result, options.include, options.caching, options.logging ? options.logging.replace('get', '').replace('expand', 'expand,get') : undefined);
87 }
88 return result;
89 } catch(error) {
90 if (error instanceof SriClientError && options && options.retry && options.retry.retries !== 0 && !(error.status && error.status < 500)) {
91 let wait = options.retry.wait;
92 if (!wait) {
93 wait = options.retry.initialWait ? options.retry.initialWait : 500;
94 } else {
95 wait = wait * (options.retry.factor ? options.retry.factor : 2);
96 }
97 console.log(`[sri-client->RETRY:${wait}] GET to ${commonUtils.parametersToString(href, params)} failed! We will try again in ${wait} miliseconds...`);
98 const newOptions = { ...options, retry: {
99 ...options.retry,
100 retries: options.retry.retries - 1,
101 wait
102 } };
103 await commonUtils.sleep(wait);
104 return this.wrapGet(href, params, newOptions, isSingleResource);
105 } else {
106 throw error;
107 }
108 }
109 }
110
111 async getAllFromResult(data, options) {
112 var results = data.results;
113 if (data.$$meta.next) {
114 const nextResult = await this.wrapGet(data.$$meta.next, undefined, options);
115 const nextResults = await this.getAllFromResult(nextResult, options);
116 results = results.concat(nextResults);
117 }
118 return results;
119 }
120
121 async getAll(href, params = {}, options = {}) {
122 // We want to do expansions for all results at the end of this method.
123 // We set it to undifined that the underlying get method does not take care of this.
124 const expand = options.expand;
125 options.expand = undefined;
126 if(!params.limit && params.limit !== null) {
127 params.limit = 500;
128 }
129 const result = await this.wrapGet(href, params, options);
130 if(!result || !result.$$meta) {
131 console.log('no results for ' + href);
132 }
133 var allResults = await this.getAllFromResult(result, options);
134 if (!options.raw && !(params && params.expand && params.expand === 'NONE')) {
135 allResults = allResults.map(function (item) {
136 return item.$$expanded;
137 });
138 }
139 if(result.$$meta) {
140 allResults.count = result.$$meta.count;
141 }
142 if(expand) {
143 await this.expandJson(allResults, expand, options.caching, options.logging ? options.logging.replace('get', '').replace('expand', 'expand,get') : undefined);
144 }
145 return allResults;
146 }
147
148 async getList(href, params, options = {}) {
149 const result = await this.wrapGet(href, params, options);
150 let results = result.results;
151 if (!options.raw && !(params && params.expand && params.expand === 'NONE')) {
152 results = results.map(function (item) {
153 return item.$$expanded;
154 });
155 }
156 if(result.$$meta) {
157 results.count = result.$$meta.count;
158 results.next = result.$$meta.next;
159 }
160 return results;
161 }
162
163 async getAllHrefsWithoutBatch(baseHref, parameterName, hrefs, params, options) {
164 if (hrefs.length === 0) {
165 return [];
166 }
167 //const promises = []; TODO make use of pQueue to do this in concurrency
168 const map = {};
169 let allResults = [];
170 if (options.inBatch) {
171 const thisParams = Object.assign({}, params);
172 const thisOptions = Object.assign({}, options);
173 thisParams[parameterName] = hrefs.join(',');
174 allResults = await this.getAll(baseHref, thisParams, thisOptions);
175 } else {
176 const groupBy = options.groupBy || Math.floor((6900 - commonUtils.parametersToString(baseHref, params).length - parameterName.length - 1) / (encodeURIComponent(hrefs[0]).length + 3));
177 var total = 0;
178 while(total < hrefs.length) {
179 //var query = commonUtils.parametersToString(baseHref, params) + '&'+parameterName+'=';
180 let parameterValue = '';
181 for(var i = 0; i < groupBy && total < hrefs.length; i++) {
182 map[hrefs[i]] = null;
183 parameterValue += (i === 0 ? '' : ',')+hrefs[total];
184 total++;
185 }
186 const thisParams = Object.assign({}, params);
187 const thisOptions = Object.assign({}, options);
188 thisParams[parameterName] = parameterValue;
189 const results = await this.getAll(baseHref, thisParams, thisOptions);
190 allResults = allResults.concat(results);
191 }
192 }
193
194 if(options.raw) {
195 throw new Error('You can not get a raw result for getAllHrefs or getAllReferencesTo');
196 } else if(options.asMap) {
197 allResults.forEach(function (item) {
198 map[item.$$meta.permalink] = item;
199 });
200 return map;
201 } else {
202 return allResults;
203 }
204 }
205
206 getAllReferencesTo(baseHref, params = {}, parameterName, values, options = {}) {
207 if(!params.limit && params.limit !== null) {
208 params.limit = 500;
209 }
210 if (options.inBatch) {
211 // TODO
212 }
213 return this.getAllHrefsWithoutBatch(baseHref, parameterName, values, params, options);
214 }
215
216 async getAllHrefs(hrefs, batchHref, params = {}, options = {}) {
217 if(hrefs.length === 0) {
218 return [];
219 }
220 if(batchHref && typeof batchHref !== 'string' && !(batchHref instanceof String)) {
221 options = params;
222 params = batchHref;
223 batchHref = null;
224 }
225 params.limit = 500;
226 const baseHref = commonUtils.getPathFromPermalink(hrefs[0]);
227 if(!batchHref) {
228 return this.getAllHrefsWithoutBatch(baseHref, 'hrefs', hrefs, params, options);
229 }
230 const batch = [];
231 const map = {};
232 let remainingHrefs = [].concat(hrefs);
233
234 while(remainingHrefs.length) {
235 var query = commonUtils.parametersToString(baseHref, params) + '&hrefs=';
236
237 const thisBatchHrefs = remainingHrefs.slice(0, params.limit);
238 remainingHrefs = remainingHrefs.slice(params.limit, remainingHrefs.length);
239
240 for (let href in thisBatchHrefs) {
241 map[href] = null;
242 }
243 query += thisBatchHrefs.join(',');
244
245 var part = {
246 verb: "GET",
247 href: query
248 };
249 batch.push(part);
250 }
251 const batchResp = await this.sendPayload(batchHref, batch, options, batchHref === '/persons/batch' ? 'PUT' : 'POST');
252 if(options.expand) {
253 await this.expandJson(batchResp, options.expand, options.caching, options.logging ? options.logging.replace('get', '').replace('expand', 'expand,get') : undefined);
254 }
255 if(options.include) {
256 await this.includeJson(batchResp, options.include, options.logging ? options.logging.replace('get', '').replace('expand', 'expand,get') : undefined);
257 }
258 return new Promise(function(resolve, reject) {
259 var ret = [];
260 for(var i = 0; i < batchResp.length; i++) {
261 if(batchResp[i].status === 200) {
262 var results = batchResp[i].body.results;
263 if(options.asMap) {
264 results.forEach(function (item) {
265 map[item.href] = item.$$expanded;
266 });
267 } else {
268 ret = ret.concat(results);
269 }
270 } else {
271 reject(batchResp);
272 }
273 }
274 if(options.asMap) {
275 resolve(map);
276 } else {
277 resolve(ret);
278 }
279 });
280 }
281
282 //should be defined by subClasses
283 sendPayload() {}
284
285 async wrapSendPayload(href, payload, options = {}, method) {
286 try {
287 const originallyFullResponse = options.fullResponse;
288 if (options.keepBatchAlive) {
289 if (!href.match(/batch$/)) {
290 throw new Error({ message: 'You can only add the streaming option for batch requests' });
291 }
292 const batchResp = await this.sendPayload(href + '_streaming', payload, { ...options, fullResponse: true }, method);
293 if (batchResp.status) {
294 // in ng-client there is no fullResponse option, so no option to retrieve headers
295 if (batchResp.status >= 300) {
296 throw new SriClientError({
297 status: batchResp.status,
298 body: batchResp.results
299 });
300 } else {
301 return batchResp.results;
302 }
303 }
304 if (batchResp.body.status >= 300) {
305 throw new SriClientError({
306 status: batchResp.body.status,
307 body: batchResp.body.results,
308 headers: batchResp.headers
309 });
310 } else {
311 return originallyFullResponse ? batchResp.body : batchResp.body.results;
312 }
313 }
314 const resp = await this.sendPayload(href, payload, options, method);
315 this.cache.onDataAltered(href, payload, method);
316 return resp;
317 } catch(error) {
318 if (error instanceof SriClientError && options && options.retry && options.retry.retries !== 0 && !(error.status && error.status < 500)) {
319 let wait = options.retry.wait;
320 if (!wait) {
321 wait = options.retry.initialWait ? options.retry.initialWait : 500;
322 } else {
323 wait = wait * (options.retry.factor ? options.retry.factor : 2);
324 }
325 console.log(`[sri-client->RETRY:${wait}] ${method} to ${href} failed! We will try again in ${wait} miliseconds...`);
326 const newOptions = { ...options, retry: {
327 ...options.retry,
328 retries: options.retry.retries - 1,
329 wait
330 } };
331 await commonUtils.sleep(wait);
332 return this.wrapSendPayload(href, payload, newOptions, method);
333 } else {
334 throw error;
335 }
336 }
337 }
338
339 put(href, payload, options) {
340 return this.wrapSendPayload(href, payload, options, 'PUT');
341 }
342 patch(href, payload, options) {
343 return this.wrapSendPayload(href, payload, options, 'PATCH');
344 }
345 updateResource(resource, options) {
346 return this.put(resource.$$meta.permalink, resource, options);
347 }
348 post(href, payload, options) {
349 return this.wrapSendPayload(href, payload, options, 'POST');
350 }
351
352 //should be defined by subClasses
353 delete() {}
354
355 async wrapDelete(href, options) {
356 const resp = await this.delete(href, options);
357 this.cache.onDataAltered(href, null, 'DELETE');
358 return resp;
359 }
360
361 //group all hrefs with the same path together so we can also retreive them togheter in one API call
362/*const getPathsMap = function(hrefs) {
363
364};*/
365
366 async add$$expanded(hrefs, json, properties, includeOptions, expandOptions, cachingOptions, loggingOptions) {
367 //const pathsMap = getPathsMap(hrefs);
368 const cachedResources = [];
369 const uncachedHrefs = {};
370 const hrefsMap = {};
371 for(let href of hrefs) {
372 if(this.cache.has(href, undefined, cachingOptions)) {
373 hrefsMap[href] = await this.cache.get(href, undefined, {caching: cachingOptions, logging: loggingOptions}, false);
374 cachedResources.push(hrefsMap[href]);
375 } else {
376 const path = commonUtils.getPathFromPermalink(href);
377 if(!uncachedHrefs[path]) {
378 uncachedHrefs[path] = [];
379 }
380 uncachedHrefs[path].push(href);
381 }
382 }
383 if(expandOptions && cachedResources.length > 0) {
384 await this.expandJson(cachedResources, expandOptions, cachingOptions, loggingOptions);
385 }
386 const promises = [];
387 for(let path of Object.keys(uncachedHrefs)) {
388 // TODO: make configurable to know on which batch the hrefs can be retrieved
389 // TODO: use p-fun here because this could be a problem if there are too many hrefs
390 promises.push(this.getAllHrefs(uncachedHrefs[path], null, {}, {asMap: true, include: includeOptions, expand: expandOptions, caching: cachingOptions, logging: loggingOptions}).then(function(newMap) {
391 Object.assign(hrefsMap, newMap);
392 }));
393 }
394 await Promise.all(promises);
395 let newHrefs = new Set();
396 for(let property of properties) {
397 let propertyName = property;
398 let required = true;
399 if (!(typeof property === 'string' || property instanceof String)) {
400 propertyName = property.property;
401 required = property.required;
402 }
403 const localHrefs = travelHrefsOfJson(json, propertyName.split('.'), {
404 required: required,
405 handlerFunction: function(object, propertyArray, resource, isDirectReference) {
406 let expandedObject = hrefsMap[object.href];
407 if(!expandedObject) {
408 return [object.href];
409 }
410 if(isDirectReference) {
411 object['$$'+propertyArray[0]] = hrefsMap[object[propertyArray[0]]];
412 return travelHrefsOfJson(object['$$'+propertyArray[0]], propertyArray);
413 }
414 object.$$expanded = expandedObject;
415 return travelHrefsOfJson(object.$$expanded, propertyArray);
416 }
417 });
418 newHrefs = new Set([...newHrefs, ...localHrefs]);
419 };
420 newHrefs = [...newHrefs];
421 hrefs = [...hrefs];
422 let converged = hrefs.size === newHrefs.size;
423 if(converged) {
424 for(let i = 0; i < hrefs.length; i++) {
425 if(hrefs[i] !== newHrefs[i]) {
426 converged = false;
427 break;
428 }
429 }
430 }
431 if(converged) {
432 console.warn('[WARNING] The data is inconsistent. There are hrefs that can not be retrieved because they do not exist or because they are deleted. hrefs: ' + JSON.stringify([...newHrefs]));
433 }
434 if(newHrefs.length > 0 && !converged) {
435 await this.add$$expanded(newHrefs, json, properties, null, null, cachingOptions, loggingOptions);
436 }
437 }
438
439 async expandJson(json, properties, cachingOptions, loggingOptions) {
440 if(!Array.isArray(properties)) {
441 properties = [properties];
442 }
443 let allHrefs = new Set();
444 for(let property of properties) {
445 let propertyName = property;
446 let includeOptions = null;
447 let expandOptions = [];
448 let required = true;
449 let localCachingOptions = null;
450 if (!(typeof property === 'string' || property instanceof String)) {
451 propertyName = property.property;
452 includeOptions = property.include;
453 required = property.required;
454 localCachingOptions = property.caching;
455 expandOptions = property.expand;
456 }
457 if(includeOptions || localCachingOptions || expandOptions) {
458 let localHrefs = travelHrefsOfJson(json, propertyName.split('.'), {required: required});
459 if(localHrefs.length > 0) {
460 await this.add$$expanded(localHrefs, json, [property], includeOptions, expandOptions, localCachingOptions || cachingOptions, loggingOptions);
461 }
462 } else {
463 allHrefs = new Set([...allHrefs, ...travelHrefsOfJson(json, propertyName.split('.'), {required: required})]);
464 }
465 };
466 if(allHrefs.size > 0) {
467 await this.add$$expanded(allHrefs, json, properties, undefined, undefined, cachingOptions, loggingOptions);
468 }
469 }
470
471 async includeJson(json, inclusions, cachingOptions = {}, loggingOptions) {
472 if(!Array.isArray(inclusions)) {
473 inclusions = [inclusions];
474 }
475 for(let options of inclusions) {
476 validate(options, includeOptionsSchema);
477 options.expanded = options.expanded ? options.expanded : true; // default is true
478 //options.required = options.required ? options.required : true; // default is true
479 //options.reference can just be a string when the parameter name is the same as the reference property itself or it can be an object which specifies both.
480 let referenceProperty = options.reference;
481 let referenceParameterName = options.reference;
482 const localCachingOptions = options.caching;
483 if (!(typeof options.reference === 'string' || options.reference instanceof String)) {
484 referenceProperty = options.reference.property;
485 referenceParameterName = options.reference.parameterName ? options.reference.parameterName : referenceProperty;
486 }
487 if(!options.expanded) {
488 // with collapsed you can not get all references and map them again because the resource information will not be there
489 const promises = [];
490 travelHrefsOfJson(json, ('$$meta.permalink').split('.'), {
491 required: true,
492 handlerFunction: function(object, propertyArray, resource) {
493 options.filters = options.filters || {};
494 if(options.collapsed) {
495 options.filters.expand = 'NONE';
496 }
497 options.filters[referenceParameterName] = object[propertyArray[0]];
498 promises.push(this.getAll(options.href, options.filters, {include: options.include, caching: localCachingOptions || cachingOptions, logging: loggingOptions}).then(function(results) {
499 resource[options.alias] = options.singleton ? (results.length === 0 ? null : results[0]) : results;
500 }));
501 return [];
502 }
503 });
504 await Promise.all(promises);
505 } else {
506 const hrefs = travelHrefsOfJson(json, ('$$meta.permalink').split('.'));
507 const results = await this.getAllReferencesTo(options.href, options.filters, referenceParameterName, hrefs, {expand: options.expand, include: options.include, caching: localCachingOptions || cachingOptions, logging: loggingOptions});
508 // this is not super optimal. Everything splits out in groups of 100. Expansion and inclusion is done for each batch of 100 urls. But the bit bellow is not working.
509 /*if(options.expand) {
510 expandJson(results, options.expand, core);
511 }*/
512 const map = {};
513 for(let result of results) {
514 const permalinks = travelHrefsOfJson(result, referenceProperty.split('.'), {required: true});
515 if(permalinks.length > 1) {
516 console.warn('SRI_CLIENT_INCLUDE: we do not support yet the possibility that options.reference references an array property. Contact us to request that we add this feature.');
517 }
518 const permalink = permalinks[0];
519 if(!map[permalink]) {
520 map[permalink] = [];
521 }
522 map[permalink].push(result);
523 }
524 // travel resources and add $$ included property
525 const resources = travelResourcesOfJson(json);
526 for(let resource of resources) {
527 let inclusions = map[resource.$$meta.permalink];
528 if(!inclusions) {
529 inclusions = [];
530 }
531 resource[options.alias] = options.singleton ? (inclusions.length === 0 ? null : inclusions[0]) : inclusions;
532 }
533 }
534 }
535 }
536
537
538};
539
540const travelHrefsOfObject = function(object, propertyArray, options) {// required, handlerFunction, resource) {
541 if(propertyArray.length === 1 && typeof object[propertyArray[0]] === 'string' && object[propertyArray[0]].match(/^(\/[-a-zA-Z0-9@:%_\+.~#?&=]+)+$/g)) {
542 if(options.handlerFunction) {
543 return options.handlerFunction(object, propertyArray, options.resource, true);
544 } else {
545 return [object[propertyArray[0]]];
546 }
547 }
548 if(object.href) {
549 if(object.$$expanded) {
550 options.resource = options.resource ? options.resource : object.$$expanded;
551 /*console.log(propertyArray)
552 if(propertyArray[0] === '$$contactDetails')
553 console.log(object)*/
554 return travelHrefsOfJson(object.$$expanded, propertyArray, options);
555 } else if (!options.resource && object.body) {
556 return travelHrefsOfJson(object.body, propertyArray, options);
557 }
558 if(options.handlerFunction) {
559 return options.handlerFunction(object, propertyArray, options.resource);
560 } else {
561 return [object.href];
562 }
563 } else {
564 return travelHrefsOfJson(object, propertyArray, options);
565 }
566};
567
568const travelHrefsOfJson = function(json, propertyArray, options = {}) {//, required = true, handlerFunction, resource) {
569 options.required = options.required === false ? options.required : true;
570 if(propertyArray.length === 0) {
571 return [];
572 }
573 let hrefs = [];
574 if(json.$$meta && json.results) {
575 json = json.results;
576 }
577 if(!options.resource && Array.isArray(json)) {
578 for(let item of json) {
579 hrefs = [...hrefs, ...travelHrefsOfObject(item, [...propertyArray], Object.assign({}, options))];
580 }
581 } else {
582 if(!options.resource) {
583 options.resource = json;
584 }
585 const nextPropertyName = propertyArray.shift();
586 const subResource = json[nextPropertyName];
587 if(!subResource) {
588 // When the config says the property is not required
589 if(!options.required) {
590 return [];
591 }
592 throw new Error('There is no property ' + nextPropertyName + ' in the object: \n' + util.inspect(json, {depth: 5}) + '\n Set required = false if the property path contains non required resources.');
593 }
594 if(Array.isArray(subResource)) {
595 for(let item of subResource) {
596 hrefs = [...hrefs, ...travelHrefsOfObject(item, [...propertyArray], Object.assign({}, options))];
597 }
598 } else {
599 hrefs = travelHrefsOfObject(subResource, propertyArray, options);
600 }
601 }
602 return hrefs.filter(href => href.match(/^\//)); // in content api there can be relations to external absolute urls.
603};
604
605const travelResoure = function(resource, handlerFunction) {
606 if(handlerFunction) {
607 return handlerFunction(resource);
608 } else {
609 return resource;
610 }
611};
612
613const travelResourcesOfJson = function(json, handlerFunction) {
614 let resources = [];
615 if(json.$$meta && json.results) {
616 json = json.results;
617 }
618 if(Array.isArray(json)) {
619 for(let item of json) {
620 if(item.href) {
621 if(item.$$expanded) {
622 resources = [...resources, travelResoure(item.$$expanded, handlerFunction)];
623 } else if (item.body) {
624 resources = [...resources, ...travelResourcesOfJson(item.body, handlerFunction)];
625 }
626 } else {
627 resources = [...resources, travelResoure(item, handlerFunction)];
628 }
629 }
630 } else {
631 resources = [travelResoure(json, handlerFunction)];
632 }
633 return resources;
634};
635
636const includeOptionsSchema = {
637 type: "object",
638 properties: {
639 alias: {
640 type: "string"
641 },
642 href: {
643 type: "string",
644 pattern: "^\/.*$"
645 },
646 reference: {
647 oneOf: [{
648 type: "string"
649 }, {
650 type: "object",
651 properties: {
652 property: {
653 type: "string"
654 },
655 parameterName: {
656 type: "string"
657 }
658 },
659 required: ["property"]
660 }]
661 },
662 params: {
663 type:"object"
664 },
665 collapsed: {
666 type: "boolean"
667 },
668 singleton: {
669 type: "boolean"
670 },
671 expand: {
672 type: "array",
673 items: {
674 type: "string"
675 }
676 }
677 },
678 required: ['alias', 'url', 'reference']
679};