UNPKG

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