UNPKG

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