UNPKG

25.9 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const axios_1 = require("axios");
4const async_1 = require("async");
5const Promise = require("bluebird");
6const http_1 = require("http");
7const https_1 = require("https");
8const lru_cache_1 = require("lru-cache");
9const moment_1 = require("moment");
10const socket_io_client_1 = require("socket.io-client");
11const stream_1 = require("stream");
12const winston_1 = require("winston");
13const util_1 = require("./util");
14function defer() {
15 let resolve;
16 let reject;
17 const promise = new Promise((promiseResolve, promiseReject) => {
18 resolve = promiseResolve;
19 reject = promiseReject;
20 });
21 return {
22 // @ts-ignore
23 resolve,
24 // @ts-ignore
25 reject,
26 promise
27 };
28}
29class CommunibaseError extends Error {
30 constructor(data, task) {
31 super(data.message);
32 this.name = "CommunibaseError";
33 this.code = data.code || 500;
34 this.message = data.message || "";
35 this.errors = data.errors || {};
36 // Error.captureStackTrace(this, CommunibaseError);
37 }
38}
39/**
40 * Constructor for connector.
41 *
42 * @param key - The communibase api key
43 * @constructor
44 */
45// tslint:disable-next-line:max-classes-per-file
46class Connector {
47 constructor(key) {
48 this.getByIdQueue = {};
49 this.getByIdPrimed = false;
50 this.key = key;
51 this.token = "";
52 this.serviceUrl =
53 process.env.COMMUNIBASE_API_URL || "https://api.communibase.nl/0.1/";
54 this.serviceUrlIsHttps = this.serviceUrl.indexOf("https") === 0;
55 this.queue = async_1.default.queue((task, callback) => {
56 function fail(error) {
57 if (error instanceof Error) {
58 task.deferred.reject(error);
59 }
60 else {
61 task.deferred.reject(new CommunibaseError(error, task));
62 }
63 callback();
64 return;
65 }
66 if (!this.key && !this.token) {
67 fail(new Error("Missing key or token for Communibase Connector: please set COMMUNIBASE_KEY environment" +
68 " variable, or spawn a new instance using require('communibase-connector-js').clone('<" +
69 "your api key>')"));
70 return;
71 }
72 if (!task.options.headers) {
73 task.options.headers = {
74 Accept: "application/json",
75 "Content-Type": "application/json"
76 };
77 }
78 if (process.env.COMMUNIBASE_API_HOST) {
79 task.options.headers.Host = process.env.COMMUNIBASE_API_HOST;
80 }
81 if (this.key) {
82 task.options.headers["x-api-key"] = this.key;
83 }
84 if (this.token) {
85 task.options.headers["x-access-token"] = this.token;
86 }
87 axios_1.default
88 .request(Object.assign({ url: task.url }, task.options))
89 .then(result => {
90 const { deferred } = task;
91 let records = result.data;
92 if (records.metadata && records.records) {
93 deferred.promise.metadata = records.metadata;
94 // eslint-disable-next-line prefer-destructuring
95 records = records.records;
96 }
97 deferred.resolve(records);
98 callback();
99 return null;
100 })
101 .catch((err) => {
102 var _a;
103 fail(((_a = err.response) === null || _a === void 0 ? void 0 : _a.data) || err);
104 });
105 }, 8);
106 }
107 setServiceUrl(newServiceUrl) {
108 if (!newServiceUrl) {
109 throw new Error("Cannot set empty service-url");
110 }
111 this.serviceUrl = newServiceUrl;
112 this.serviceUrlIsHttps = newServiceUrl.indexOf("https") === 0;
113 }
114 /**
115 * Get a single object by its id
116 *
117 * @param {string} objectType - E.g. Person
118 * @param {string}objectId - E.g. 52259f95dafd757b06002221
119 * @param {object} [params={}] - key/value store for extra arguments like fields, limit, page and/or sort
120 * @param {string|null} [versionId=null] - optional versionId to retrieve
121 * @returns {Promise} - for object: a key/value object with object data
122 */
123 getById(objectType, objectId, params, versionId) {
124 if (typeof objectId !== "string" || objectId.length !== 24) {
125 return Promise.reject(new Error("Invalid objectId"));
126 }
127 // not combinable...
128 if (versionId || (params && params.fields)) {
129 const deferred = defer();
130 this.queue.push({
131 deferred,
132 url: `${this.serviceUrl + objectType}.json/${versionId ? `history/${objectId}/${versionId}` : `crud/${objectId}`}`,
133 options: {
134 method: "GET",
135 params
136 }
137 });
138 return deferred.promise;
139 }
140 // cached?
141 if (this.cache && this.cache.isAvailable(objectType, objectId)) {
142 return this.cache.objectCache[objectType][objectId];
143 }
144 // since we are not requesting a specific version or fields, we may combine the request..?
145 if (this.getByIdQueue[objectType] === undefined) {
146 this.getByIdQueue[objectType] = {};
147 }
148 if (this.getByIdQueue[objectType][objectId]) {
149 // requested twice?
150 return this.getByIdQueue[objectType][objectId].promise;
151 }
152 this.getByIdQueue[objectType][objectId] = defer();
153 if (this.cache) {
154 if (this.cache.objectCache[objectType] === undefined) {
155 this.cache.objectCache[objectType] = {};
156 }
157 this.cache.objectCache[objectType][objectId] = this.getByIdQueue[objectType][objectId].promise;
158 }
159 if (!this.getByIdPrimed) {
160 process.nextTick(() => {
161 this.spoolQueue();
162 });
163 this.getByIdPrimed = true;
164 }
165 return this.getByIdQueue[objectType][objectId].promise;
166 }
167 /**
168 * Get an array of objects by their ids
169 * If one or more entries are found, they are returned as an array of values
170 *
171 * @param {string} objectType - E.g. Person
172 * @param {Array} objectIds - objectIds - E.g. ['52259f95dafd757b06002221']
173 * @param {object} [params={}] - key/value store for extra arguments like fields, limit, page and/or sort
174 * @returns {Promise} - for array of key/value objects
175 */
176 getByIds(objectType, objectIds, params) {
177 if (objectIds.length === 0) {
178 return Promise.resolve([]);
179 }
180 // not combinable...
181 if (params && params.fields) {
182 return this.privateGetByIds(objectType, objectIds, params);
183 }
184 return Promise.map(objectIds, (objectId) => this.getById(objectType, objectId, params).reflect()).then(inspections => {
185 const result = [];
186 let error = null;
187 inspections.forEach(inspection => {
188 if (inspection.isRejected()) {
189 error = inspection.reason();
190 return;
191 }
192 result.push(inspection.value());
193 });
194 if (result.length) {
195 return result;
196 }
197 if (error) {
198 throw new Error(error);
199 }
200 // return the empty array, if no results and no error
201 return result;
202 });
203 }
204 /**
205 * Get all objects of a certain type
206 *
207 * @param {string} objectType - E.g. Person
208 * @param {object} [params={}] - key/value store for extra arguments like fields, limit, page and/or sort
209 * @returns {Promise} - for array of key/value objects
210 */
211 getAll(objectType, params) {
212 if (this.cache && !(params && params.fields)) {
213 return this.search(objectType, {}, params);
214 }
215 const deferred = defer();
216 this.queue.push({
217 deferred,
218 url: `${this.serviceUrl + objectType}.json/crud`,
219 options: {
220 method: "GET",
221 params
222 }
223 });
224 return deferred.promise;
225 }
226 /**
227 * Get result objectIds of a certain search
228 *
229 * @param {string} objectType - E.g. Person
230 * @param {object} selector - { firstName: "Henk" }
231 * @param {object} [params={}] - key/value store for extra arguments like fields, limit, page and/or sort
232 * @returns {Promise} - for array of key/value objects
233 */
234 getIds(objectType, selector, params) {
235 let hash;
236 let result;
237 if (this.cache) {
238 hash = JSON.stringify([objectType, selector, params]);
239 if (!this.cache.getIdsCaches[objectType]) {
240 this.cache.getIdsCaches[objectType] = new lru_cache_1.default(1000); // 1000 getIds are this.cached, per entityType
241 }
242 const objectTypeGetIdCache = this.cache.getIdsCaches[objectType];
243 result = objectTypeGetIdCache ? objectTypeGetIdCache.get(hash) : null;
244 if (result) {
245 return Promise.resolve(result);
246 }
247 }
248 const resultPromise = this.search(objectType, selector || {}, Object.assign({ fields: "_id" }, params)).then(results => results.map(obj => obj._id));
249 if (this.cache) {
250 return resultPromise.then(ids => {
251 if (this.cache) {
252 const objectTypeGetIdCache = this.cache.getIdsCaches[objectType];
253 if (objectTypeGetIdCache) {
254 objectTypeGetIdCache.set(hash, ids);
255 }
256 }
257 return ids;
258 });
259 }
260 return resultPromise;
261 }
262 /**
263 * Get the id of an object based on a search
264 *
265 * @param {string} objectType - E.g. Person
266 * @param {object} selector - { firstName: "Henk" }
267 * @returns {Promise} - for a string OR undefined if not found
268 */
269 getId(objectType, selector) {
270 return this.getIds(objectType, selector, { limit: 1 }).then(ids => ids.pop() || null);
271 }
272 /**
273 *
274 * @param objectType
275 * @param selector - mongodb style
276 * @param params
277 * @returns {Promise} for objects
278 */
279 search(objectType, selector, params) {
280 if (this.cache && !(params && params.fields)) {
281 return this.getIds(objectType, selector, params).then(ids => this.getByIds(objectType, ids));
282 }
283 if (selector &&
284 typeof selector === "object" &&
285 Object.keys(selector).length) {
286 return this.queueSearch(objectType, selector, params);
287 }
288 return this.getAll(objectType, params);
289 }
290 /**
291 * This will save a document in Communibase. When a _id-field is found, this document will be updated
292 *
293 * @param objectType
294 * @param object - the to-be-saved object data
295 * @returns promise for object (the created or updated object)
296 */
297 update(objectType, object) {
298 const deferred = defer();
299 const operation = object._id && object._id.length > 0 ? "PUT" : "POST";
300 if (object._id &&
301 this.cache &&
302 this.cache.objectCache &&
303 this.cache.objectCache[objectType] &&
304 this.cache.objectCache[objectType][object._id]) {
305 this.cache.objectCache[objectType][object._id] = null;
306 }
307 this.queue.push({
308 deferred,
309 url: `${this.serviceUrl + objectType}.json/crud${operation === "PUT" ? `/${object._id}` : ""}`,
310 options: {
311 method: operation,
312 data: object
313 }
314 });
315 return deferred.promise;
316 }
317 /**
318 * Delete something from Communibase
319 *
320 * @param objectType
321 * @param objectId
322 * @returns promise (for null)
323 */
324 destroy(objectType, objectId) {
325 const deferred = defer();
326 if (this.cache &&
327 this.cache.objectCache &&
328 this.cache.objectCache[objectType] &&
329 this.cache.objectCache[objectType][objectId]) {
330 this.cache.objectCache[objectType][objectId] = null;
331 }
332 this.queue.push({
333 deferred,
334 url: `${this.serviceUrl + objectType}.json/crud/${objectId}`,
335 options: {
336 method: "DELETE"
337 }
338 });
339 return deferred.promise;
340 }
341 /**
342 * Undelete something from Communibase
343 *
344 * @param objectType
345 * @param objectId
346 * @returns promise (for null)
347 */
348 undelete(objectType, objectId) {
349 const deferred = defer();
350 this.queue.push({
351 deferred,
352 url: `${this.serviceUrl + objectType}.json/history/undelete/${objectId}`,
353 options: {
354 method: "POST"
355 }
356 });
357 return deferred.promise;
358 }
359 /**
360 * Get a Promise for a Read stream for a File stored in Communibase
361 *
362 * @param fileId
363 * @returns {Stream} see http://nodejs.org/api/stream.html#stream_stream
364 */
365 createReadStream(fileId) {
366 const request = this.serviceUrlIsHttps ? https_1.request : http_1.request;
367 const fileStream = new stream_1.PassThrough();
368 const req = request(`${this.serviceUrl}File.json/binary/${fileId}?api_key=${this.key}`, (res) => {
369 if (res.statusCode === 200) {
370 res.pipe(fileStream);
371 return;
372 }
373 fileStream.emit("error", new Error(http_1.STATUS_CODES[res.statusCode || 500]));
374 fileStream.emit("end");
375 });
376 if (process.env.COMMUNIBASE_API_HOST) {
377 req.setHeader("Host", process.env.COMMUNIBASE_API_HOST);
378 }
379 req.end();
380 req.on("error", (err) => {
381 fileStream.emit("error", err);
382 });
383 return fileStream;
384 }
385 /**
386 * Uploads the contents of the resource to Communibase (updates or creates a new File)
387 *
388 * Note `File` is not versioned
389 *
390 * @param {Stream|Buffer|String} resource a stream, buffer or a content-string
391 * @param {String} name The binary name (i.e. a filename)
392 * @param {String} destinationPath The "directory location"
393 * @param {String} id The `File` id to replace the contents of (optional; if not set then creates a new File)
394 *
395 * @returns {Promise}
396 */
397 updateBinary(resource, name, destinationPath, id) {
398 const metaData = {
399 path: destinationPath
400 };
401 return util_1.getResourceBufferPromise(resource).then((buffer) => {
402 if (id) {
403 // TODO check is valid id? entails extra dependency (mongodb.ObjectID)
404 // update File identified by id
405 return this.update("File", {
406 _id: id,
407 filename: name,
408 length: buffer.length,
409 uploadDate: moment_1.default().format(),
410 metadata: metaData,
411 content: buffer
412 });
413 }
414 // create a new File
415 const deferred = defer();
416 const formData = new FormData();
417 let stringOrBlob = buffer;
418 // @see https://developer.mozilla.org/en-US/docs/Web/API/FormData/append
419 // officially, formdata may contain blobs or strings. node doesn't do blobs, but when polymorphing in a browser we
420 // may cast it to one for it to work properly...
421 if (typeof window !== "undefined" && window.Blob) {
422 stringOrBlob = new Blob([buffer]);
423 }
424 formData.append("File", stringOrBlob, name);
425 formData.append("metadata", JSON.stringify(metaData));
426 this.queue.push({
427 deferred,
428 url: `${this.serviceUrl}File.json/binary`,
429 options: {
430 method: "POST",
431 data: formData,
432 headers: {
433 Accept: "application/json"
434 }
435 }
436 });
437 return deferred.promise;
438 });
439 }
440 /**
441 * Get a new Communibase Connector, may be with a different API key
442 *
443 * @param apiKey
444 * @returns {Connector}
445 */
446 clone(apiKey) {
447 return new Connector(apiKey);
448 }
449 /**
450 * Get the history information for a certain type of object
451 *
452 * VersionInformation: {
453 * "_id": "ObjectId",
454 * "updatedAt": "Date",
455 * "updatedBy": "string"
456 * }
457 *
458 * @param {string} objectType
459 * @param {string} objectId
460 * @returns promise for VersionInformation[]
461 */
462 getHistory(objectType, objectId) {
463 const deferred = defer();
464 this.queue.push({
465 deferred,
466 url: `${this.serviceUrl + objectType}.json/history/${objectId}`,
467 options: {
468 method: "GET"
469 }
470 });
471 return deferred.promise;
472 }
473 /**
474 *
475 * @param {string} objectType
476 * @param {Object} selector
477 * @returns promise for VersionInformation[]
478 */
479 historySearch(objectType, selector) {
480 const deferred = defer();
481 this.queue.push({
482 deferred,
483 url: `${this.serviceUrl + objectType}.json/history/search`,
484 options: {
485 method: "POST",
486 data: selector
487 }
488 });
489 return deferred.promise;
490 }
491 /**
492 * Get a single object by a DocumentReference-object. A DocumentReference object looks like
493 * {
494 * rootDocumentId: '524aca8947bd91000600000c',
495 * rootDocumentEntityType: 'Person',
496 * path: [
497 * {
498 * field: 'addresses',
499 * objectId: '53440792463cda7161000003'
500 * }, ...
501 * ]
502 * }
503 *
504 * @param {object} ref - DocumentReference style, see above
505 * @param {object} parentDocument
506 * @return {Promise} for referred object
507 */
508 getByRef(ref, parentDocument) {
509 if (!(ref &&
510 ref.rootDocumentEntityType &&
511 (ref.rootDocumentId || parentDocument))) {
512 return Promise.reject(new Error("Please provide a documentReference object with a type and id"));
513 }
514 const rootDocumentEntityTypeParts = ref.rootDocumentEntityType.split(".");
515 let parentDocumentPromise;
516 if (rootDocumentEntityTypeParts[0] !== "parent") {
517 parentDocumentPromise = this.getById(ref.rootDocumentEntityType, ref.rootDocumentId);
518 }
519 else {
520 parentDocumentPromise = Promise.resolve(parentDocument);
521 }
522 if (!(ref.path && ref.path.length && ref.path.length > 0)) {
523 return parentDocumentPromise;
524 }
525 /* tslint:disable no-parameter-reassignment */
526 return parentDocumentPromise.then((result) => {
527 ref.path.some(pathNibble => {
528 if (result && result[pathNibble.field]) {
529 if (!result[pathNibble.field].some((subDocument) => {
530 if (subDocument._id === pathNibble.objectId) {
531 result = subDocument;
532 return true;
533 }
534 return false;
535 })) {
536 result = null;
537 return true;
538 }
539 return false;
540 }
541 result = null;
542 return true;
543 });
544 if (result) {
545 return result;
546 }
547 throw new Error("The referred object within it's parent could not be found");
548 });
549 /* tslint:enable no-parameter-reassignment */
550 }
551 /**
552 *
553 * @param {string} objectType - E.g. Event
554 * @param {array} aggregationPipeline - E.g. A MongoDB-specific Aggregation Pipeline
555 * @see http://docs.mongodb.org/manual/core/aggregation-pipeline/
556 *
557 * E.g. [
558 * { "$match": { "_id": {"$ObjectId": "52f8fb85fae15e6d0806e7c7"} } },
559 * { "$unwind": "$participants" },
560 * { "$group": { "_id": "$_id", "participantCount": { "$sum": 1 } } }
561 * ]
562 */
563 aggregate(objectType, aggregationPipeline) {
564 if (!aggregationPipeline || !aggregationPipeline.length) {
565 return Promise.reject(new Error("Please provide a valid Aggregation Pipeline."));
566 }
567 let hash;
568 if (this.cache) {
569 hash = JSON.stringify([objectType, aggregationPipeline]);
570 let objectTypeAggregateCache = this.cache.aggregateCaches[objectType];
571 if (!objectTypeAggregateCache) {
572 objectTypeAggregateCache = lru_cache_1.default(1000); // // 1000 getIds are this.cached, per entityType
573 this.cache.aggregateCaches[objectType] = objectTypeAggregateCache;
574 }
575 const result = objectTypeAggregateCache.get(hash);
576 if (result) {
577 return Promise.resolve(result);
578 }
579 }
580 const deferred = defer();
581 this.queue.push({
582 deferred,
583 url: `${this.serviceUrl + objectType}.json/aggregate`,
584 options: {
585 method: "POST",
586 data: aggregationPipeline
587 }
588 });
589 const resultPromise = deferred.promise;
590 if (this.cache) {
591 return resultPromise.then(result => {
592 if (this.cache) {
593 const objectTypeAggregateCache = this.cache.aggregateCaches[objectType];
594 if (objectTypeAggregateCache) {
595 objectTypeAggregateCache.set(hash, result);
596 }
597 }
598 return result;
599 });
600 }
601 return resultPromise;
602 }
603 /**
604 * Finalize an invoice by its ID
605 *
606 * @param invoiceId
607 * @returns {*}
608 */
609 finalizeInvoice(invoiceId) {
610 const deferred = defer();
611 this.queue.push({
612 deferred,
613 url: `${this.serviceUrl}Invoice.json/finalize/${invoiceId}`,
614 options: {
615 method: "POST"
616 }
617 });
618 return deferred.promise;
619 }
620 /**
621 * @param communibaseAdministrationId
622 * @param socketServiceUrl
623 */
624 enableCache(communibaseAdministrationId, socketServiceUrl) {
625 this.cache = {
626 getIdsCaches: {},
627 aggregateCaches: {},
628 dirtySock: socket_io_client_1.default.connect(socketServiceUrl, { port: "443" }),
629 objectCache: {},
630 isAvailable(objectType, objectId) {
631 if (!cache) {
632 return false;
633 }
634 return !!(cache.objectCache[objectType] &&
635 cache.objectCache[objectType][objectId]);
636 }
637 };
638 const { cache } = this;
639 cache.dirtySock.on("connect", () => {
640 cache.dirtySock.emit("join", `${communibaseAdministrationId}_dirty`);
641 });
642 cache.dirtySock.on("message", (dirtyness) => {
643 const dirtyInfo = dirtyness.split("|");
644 if (dirtyInfo.length !== 2) {
645 winston_1.default.warn(`${new Date()}: Got weird dirty sock data? ${dirtyness}`);
646 return;
647 }
648 cache.getIdsCaches[dirtyInfo[0]] = null;
649 cache.aggregateCaches[dirtyInfo[0]] = null;
650 if (dirtyInfo.length === 2 && cache.objectCache[dirtyInfo[0]]) {
651 cache.objectCache[dirtyInfo[0]][dirtyInfo[1]] = null;
652 }
653 });
654 }
655 queueSearch(objectType, selector, params) {
656 const deferred = defer();
657 this.queue.push({
658 deferred,
659 url: `${this.serviceUrl + objectType}.json/search`,
660 options: {
661 method: "POST",
662 data: selector,
663 params
664 }
665 });
666 return deferred.promise;
667 }
668 /**
669 * Bare boned retrieval by objectIds
670 * @returns {Promise}
671 */
672 privateGetByIds(objectType, objectIds, params) {
673 return this.queueSearch(objectType, {
674 _id: { $in: objectIds }
675 }, params);
676 }
677 /**
678 * Default object retrieval: should provide cachable objects
679 */
680 spoolQueue() {
681 Object.keys(this.getByIdQueue).forEach(objectType => {
682 const deferredsById = this.getByIdQueue[objectType];
683 const objectIds = Object.keys(deferredsById);
684 this.getByIdQueue[objectType] = {};
685 this.privateGetByIds(objectType, objectIds).then(objects => {
686 const objectHash = objects.reduce((previousValue, object) => {
687 if (object._id) {
688 previousValue[object._id] = object;
689 }
690 return previousValue;
691 }, {});
692 objectIds.forEach((objectId) => {
693 if (objectHash[objectId]) {
694 deferredsById[objectId].resolve(objectHash[objectId]);
695 return;
696 }
697 deferredsById[objectId].reject(new Error(`${objectId} is not found`));
698 });
699 }, err => {
700 objectIds.forEach(objectId => {
701 deferredsById[objectId].reject(err);
702 });
703 });
704 });
705 this.getByIdPrimed = false;
706 }
707}
708exports.Connector = Connector;
709// Backwards compatibility-ish
710exports.default = new Connector(process.env.COMMUNIBASE_KEY);
711//# sourceMappingURL=index.js.map
\No newline at end of file