UNPKG

27.5 kBPlain TextView Raw
1import axios, { AxiosError, AxiosRequestConfig } from "axios";
2import async, { AsyncQueue } from "async";
3import * as Promise from "bluebird";
4import http, { request as httpRequest, STATUS_CODES } from "http";
5import { request as httpsRequest } from "https";
6import lruCache, { Cache } from "lru-cache";
7import moment from "moment";
8import socketIoClient from "socket.io-client";
9import { PassThrough } from "stream";
10import winston from "winston";
11
12import { getResourceBufferPromise } from "./util";
13
14import ReadableStream = NodeJS.ReadableStream;
15
16export interface IDeferred {
17 resolve: (result: any) => void;
18 reject: (error: Error) => void;
19 promise: Promise<any> & { metadata?: any };
20}
21
22export type CommunibaseEntityType =
23 | "Person"
24 | "Membership"
25 | "Event"
26 | "Invoice"
27 | "Contact"
28 | "Debtor"
29 | "File"
30 | string;
31
32export interface ICommunibaseDocument {
33 _id?: string;
34 [prop: string]: any;
35}
36
37export interface ICommunibaseDocumentReference {
38 rootDocumentEntityType: CommunibaseEntityType;
39 rootDocumentId: string;
40 path: Array<{
41 field: string;
42 objectId: string;
43 }>;
44}
45
46export interface ICommunibaseVersionInformation {
47 _id: string;
48 updatedAt: string;
49 updatedBy: string;
50}
51
52interface ICommunibaseTask {
53 options: AxiosRequestConfig & {
54 headers?: {
55 Host?: string;
56 "x-api-key"?: string;
57 "x-access-token"?: string;
58 Accept?: string;
59 "Content-Type"?: string;
60 };
61 };
62 url: string;
63 deferred: IDeferred;
64}
65
66export interface ICommunibaseParams {
67 fields?: string;
68 limit?: number;
69 page?: number;
70 sort?: string;
71 includeMetadata?: boolean;
72 dir?: "ASC" | "DESC";
73 deleted?: boolean;
74}
75
76function defer(): IDeferred {
77 let resolve;
78 let reject;
79 const promise = new Promise((promiseResolve, promiseReject) => {
80 resolve = promiseResolve;
81 reject = promiseReject;
82 });
83 return {
84 // @ts-ignore
85 resolve,
86 // @ts-ignore
87 reject,
88 promise
89 };
90}
91
92class CommunibaseError extends Error {
93 public name: string;
94 public code: number;
95 public message: string;
96 public errors: {};
97
98 constructor(
99 data: { name: string; code: number; message: string; errors: {} },
100 task: ICommunibaseTask
101 ) {
102 super(data.message);
103 this.name = "CommunibaseError";
104 this.code = data.code || 500;
105 this.message = data.message || "";
106 this.errors = data.errors || {};
107 // Error.captureStackTrace(this, CommunibaseError);
108 }
109}
110
111/**
112 * Constructor for connector.
113 *
114 * @param key - The communibase api key
115 * @constructor
116 */
117// tslint:disable-next-line:max-classes-per-file
118export class Connector {
119 private getByIdQueue: {
120 [objectType: string]: {
121 [objectId: string]: IDeferred;
122 };
123 };
124 private getByIdPrimed: boolean;
125 private key?: string;
126 private token: string;
127 private serviceUrl: string;
128 private serviceUrlIsHttps: boolean;
129 private queue: AsyncQueue<ICommunibaseTask>;
130 private cache?: {
131 objectCache: {
132 [objectType: string]: {
133 [objectId: string]: null | Promise<ICommunibaseDocument>;
134 };
135 };
136 aggregateCaches: {
137 [objectType: string]: null | Cache<string, Array<{}>>;
138 };
139 getIdsCaches: {
140 [objectType: string]: null | Cache<string, string[]>;
141 };
142 dirtySock: SocketIOClient.Socket;
143 isAvailable(objectType: CommunibaseEntityType, objectId: string): boolean;
144 };
145
146 constructor(key?: string) {
147 this.getByIdQueue = {};
148 this.getByIdPrimed = false;
149 this.key = key;
150 this.token = "";
151 this.serviceUrl =
152 process.env.COMMUNIBASE_API_URL || "https://api.communibase.nl/0.1/";
153 this.serviceUrlIsHttps = this.serviceUrl.indexOf("https") === 0;
154 this.queue = async.queue((task: ICommunibaseTask, callback) => {
155 function fail(error: Error): void {
156 if (error instanceof Error) {
157 task.deferred.reject(error);
158 } else {
159 task.deferred.reject(new CommunibaseError(error, task));
160 }
161
162 callback();
163 return;
164 }
165
166 if (!this.key && !this.token) {
167 fail(
168 new Error(
169 "Missing key or token for Communibase Connector: please set COMMUNIBASE_KEY environment" +
170 " variable, or spawn a new instance using require('communibase-connector-js').clone('<" +
171 "your api key>')"
172 )
173 );
174 return;
175 }
176
177 if (!task.options.headers) {
178 task.options.headers = {
179 Accept: "application/json",
180 "Content-Type": "application/json"
181 };
182 }
183 if (process.env.COMMUNIBASE_API_HOST) {
184 task.options.headers.Host = process.env.COMMUNIBASE_API_HOST;
185 }
186
187 if (this.key) {
188 task.options.headers["x-api-key"] = this.key;
189 }
190 if (this.token) {
191 task.options.headers["x-access-token"] = this.token;
192 }
193
194 axios
195 .request({
196 url: task.url,
197 ...task.options
198 })
199 .then(result => {
200 const { deferred } = task;
201 let records = result.data;
202 if (records.metadata && records.records) {
203 deferred.promise.metadata = records.metadata;
204 // eslint-disable-next-line prefer-destructuring
205 records = records.records;
206 }
207 deferred.resolve(records);
208 callback();
209 return null;
210 })
211 .catch((err: AxiosError) => {
212 fail(err.response?.data || err);
213 });
214 }, 8);
215 }
216
217 public setServiceUrl(newServiceUrl: string): void {
218 if (!newServiceUrl) {
219 throw new Error("Cannot set empty service-url");
220 }
221 this.serviceUrl = newServiceUrl;
222 this.serviceUrlIsHttps = newServiceUrl.indexOf("https") === 0;
223 }
224
225 /**
226 * Get a single object by its id
227 *
228 * @param {string} objectType - E.g. Person
229 * @param {string}objectId - E.g. 52259f95dafd757b06002221
230 * @param {object} [params={}] - key/value store for extra arguments like fields, limit, page and/or sort
231 * @param {string|null} [versionId=null] - optional versionId to retrieve
232 * @returns {Promise} - for object: a key/value object with object data
233 */
234 public getById<T extends ICommunibaseDocument = ICommunibaseDocument>(
235 objectType: CommunibaseEntityType,
236 objectId: string,
237 params?: ICommunibaseParams,
238 versionId?: string
239 ): Promise<T> {
240 if (typeof objectId !== "string" || objectId.length !== 24) {
241 return Promise.reject(new Error("Invalid objectId"));
242 }
243
244 // not combinable...
245 if (versionId || (params && params.fields)) {
246 const deferred = defer();
247 this.queue.push({
248 deferred,
249 url: `${this.serviceUrl + objectType}.json/${
250 versionId ? `history/${objectId}/${versionId}` : `crud/${objectId}`
251 }`,
252 options: {
253 method: "GET",
254 params
255 }
256 });
257 return deferred.promise;
258 }
259
260 // cached?
261 if (this.cache && this.cache.isAvailable(objectType, objectId)) {
262 return this.cache.objectCache[objectType][objectId] as Promise<T>;
263 }
264
265 // since we are not requesting a specific version or fields, we may combine the request..?
266 if (this.getByIdQueue[objectType] === undefined) {
267 this.getByIdQueue[objectType] = {};
268 }
269
270 if (this.getByIdQueue[objectType][objectId]) {
271 // requested twice?
272 return this.getByIdQueue[objectType][objectId].promise;
273 }
274
275 this.getByIdQueue[objectType][objectId] = defer();
276
277 if (this.cache) {
278 if (this.cache.objectCache[objectType] === undefined) {
279 this.cache.objectCache[objectType] = {};
280 }
281 this.cache.objectCache[objectType][objectId] = this.getByIdQueue[
282 objectType
283 ][objectId].promise;
284 }
285
286 if (!this.getByIdPrimed) {
287 process.nextTick(() => {
288 this.spoolQueue();
289 });
290 this.getByIdPrimed = true;
291 }
292 return this.getByIdQueue[objectType][objectId].promise;
293 }
294
295 /**
296 * Get an array of objects by their ids
297 * If one or more entries are found, they are returned as an array of values
298 *
299 * @param {string} objectType - E.g. Person
300 * @param {Array} objectIds - objectIds - E.g. ['52259f95dafd757b06002221']
301 * @param {object} [params={}] - key/value store for extra arguments like fields, limit, page and/or sort
302 * @returns {Promise} - for array of key/value objects
303 */
304 public getByIds<T extends ICommunibaseDocument = ICommunibaseDocument>(
305 objectType: CommunibaseEntityType,
306 objectIds: string[],
307 params?: ICommunibaseParams
308 ): Promise<T[]> {
309 if (objectIds.length === 0) {
310 return Promise.resolve([]);
311 }
312
313 // not combinable...
314 if (params && params.fields) {
315 return this.privateGetByIds<T>(objectType, objectIds, params);
316 }
317
318 return Promise.map(objectIds, (objectId: string) =>
319 this.getById<T>(objectType, objectId, params).reflect()
320 ).then(inspections => {
321 const result: T[] = [];
322 let error = null;
323 inspections.forEach(inspection => {
324 if (inspection.isRejected()) {
325 error = inspection.reason();
326 return;
327 }
328 result.push(inspection.value());
329 });
330 if (result.length) {
331 return result;
332 }
333
334 if (error) {
335 throw new Error(error);
336 }
337
338 // return the empty array, if no results and no error
339 return result;
340 });
341 }
342
343 /**
344 * Get all objects of a certain type
345 *
346 * @param {string} objectType - E.g. Person
347 * @param {object} [params={}] - key/value store for extra arguments like fields, limit, page and/or sort
348 * @returns {Promise} - for array of key/value objects
349 */
350 public getAll<T extends ICommunibaseDocument = ICommunibaseDocument>(
351 objectType: CommunibaseEntityType,
352 params?: ICommunibaseParams
353 ): Promise<T[]> {
354 if (this.cache && !(params && params.fields)) {
355 return this.search<T>(objectType, {}, params);
356 }
357
358 const deferred = defer();
359 this.queue.push({
360 deferred,
361 url: `${this.serviceUrl + objectType}.json/crud`,
362 options: {
363 method: "GET",
364 params
365 }
366 });
367 return deferred.promise;
368 }
369
370 /**
371 * Get result objectIds of a certain search
372 *
373 * @param {string} objectType - E.g. Person
374 * @param {object} selector - { firstName: "Henk" }
375 * @param {object} [params={}] - key/value store for extra arguments like fields, limit, page and/or sort
376 * @returns {Promise} - for array of key/value objects
377 */
378 public getIds(
379 objectType: CommunibaseEntityType,
380 selector?: {},
381 params?: ICommunibaseParams
382 ): Promise<string[]> {
383 let hash: string;
384 let result;
385
386 if (this.cache) {
387 hash = JSON.stringify([objectType, selector, params]);
388 if (!this.cache.getIdsCaches[objectType]) {
389 this.cache.getIdsCaches[objectType] = new lruCache(1000); // 1000 getIds are this.cached, per entityType
390 }
391 const objectTypeGetIdCache = this.cache.getIdsCaches[objectType];
392 result = objectTypeGetIdCache ? objectTypeGetIdCache.get(hash) : null;
393 if (result) {
394 return Promise.resolve(result);
395 }
396 }
397
398 const resultPromise = this.search(objectType, selector || {}, {
399 fields: "_id",
400 ...params
401 }).then(results => results.map(obj => obj._id as string));
402
403 if (this.cache) {
404 return resultPromise.then(ids => {
405 if (this.cache) {
406 const objectTypeGetIdCache = this.cache.getIdsCaches[objectType];
407 if (objectTypeGetIdCache) {
408 objectTypeGetIdCache.set(hash, ids);
409 }
410 }
411 return ids;
412 });
413 }
414
415 return resultPromise;
416 }
417
418 /**
419 * Get the id of an object based on a search
420 *
421 * @param {string} objectType - E.g. Person
422 * @param {object} selector - { firstName: "Henk" }
423 * @returns {Promise} - for a string OR undefined if not found
424 */
425 public getId(
426 objectType: CommunibaseEntityType,
427 selector?: {}
428 ): Promise<string | null> {
429 return this.getIds(objectType, selector, { limit: 1 }).then(
430 ids => ids.pop() || null
431 );
432 }
433
434 /**
435 *
436 * @param objectType
437 * @param selector - mongodb style
438 * @param params
439 * @returns {Promise} for objects
440 */
441 public search<T extends ICommunibaseDocument = ICommunibaseDocument>(
442 objectType: CommunibaseEntityType,
443 selector: {},
444 params?: ICommunibaseParams
445 ): Promise<T[]> {
446 if (this.cache && !(params && params.fields)) {
447 return this.getIds(objectType, selector, params).then(ids =>
448 this.getByIds<T>(objectType, ids)
449 );
450 }
451
452 if (
453 selector &&
454 typeof selector === "object" &&
455 Object.keys(selector).length
456 ) {
457 return this.queueSearch<T>(objectType, selector, params);
458 }
459
460 return this.getAll<T>(objectType, params);
461 }
462
463 /**
464 * This will save a document in Communibase. When a _id-field is found, this document will be updated
465 *
466 * @param objectType
467 * @param object - the to-be-saved object data
468 * @returns promise for object (the created or updated object)
469 */
470 public update<T extends ICommunibaseDocument = ICommunibaseDocument>(
471 objectType: CommunibaseEntityType,
472 object: T
473 ): Promise<T> {
474 const deferred = defer();
475 const operation = object._id && object._id.length > 0 ? "PUT" : "POST";
476
477 if (
478 object._id &&
479 this.cache &&
480 this.cache.objectCache &&
481 this.cache.objectCache[objectType] &&
482 this.cache.objectCache[objectType][object._id]
483 ) {
484 this.cache.objectCache[objectType][object._id] = null;
485 }
486
487 this.queue.push({
488 deferred,
489 url: `${this.serviceUrl + objectType}.json/crud${
490 operation === "PUT" ? `/${object._id}` : ""
491 }`,
492 options: {
493 method: operation,
494 data: object
495 }
496 });
497
498 return deferred.promise;
499 }
500
501 /**
502 * Delete something from Communibase
503 *
504 * @param objectType
505 * @param objectId
506 * @returns promise (for null)
507 */
508 public destroy(
509 objectType: CommunibaseEntityType,
510 objectId: string
511 ): Promise<null> {
512 const deferred = defer();
513
514 if (
515 this.cache &&
516 this.cache.objectCache &&
517 this.cache.objectCache[objectType] &&
518 this.cache.objectCache[objectType][objectId]
519 ) {
520 this.cache.objectCache[objectType][objectId] = null;
521 }
522
523 this.queue.push({
524 deferred,
525 url: `${this.serviceUrl + objectType}.json/crud/${objectId}`,
526 options: {
527 method: "DELETE"
528 }
529 });
530
531 return deferred.promise;
532 }
533
534 /**
535 * Undelete something from Communibase
536 *
537 * @param objectType
538 * @param objectId
539 * @returns promise (for null)
540 */
541 public undelete(
542 objectType: CommunibaseEntityType,
543 objectId: string
544 ): Promise<ICommunibaseDocument> {
545 const deferred = defer();
546
547 this.queue.push({
548 deferred,
549 url: `${this.serviceUrl + objectType}.json/history/undelete/${objectId}`,
550 options: {
551 method: "POST"
552 }
553 });
554
555 return deferred.promise;
556 }
557
558 /**
559 * Get a Promise for a Read stream for a File stored in Communibase
560 *
561 * @param fileId
562 * @returns {Stream} see http://nodejs.org/api/stream.html#stream_stream
563 */
564 public createReadStream(fileId: string): ReadableStream {
565 const request = this.serviceUrlIsHttps ? httpsRequest : httpRequest;
566 const fileStream = new PassThrough();
567 const req = request(
568 `${this.serviceUrl}File.json/binary/${fileId}?api_key=${this.key}`,
569 (res: http.IncomingMessage) => {
570 if (res.statusCode === 200) {
571 res.pipe(fileStream);
572 return;
573 }
574 fileStream.emit(
575 "error",
576 new Error(STATUS_CODES[res.statusCode || 500])
577 );
578 fileStream.emit("end");
579 }
580 );
581 if (process.env.COMMUNIBASE_API_HOST) {
582 req.setHeader("Host", process.env.COMMUNIBASE_API_HOST);
583 }
584 req.end();
585 req.on("error", (err: Error) => {
586 fileStream.emit("error", err);
587 });
588 return fileStream;
589 }
590
591 /**
592 * Uploads the contents of the resource to Communibase (updates or creates a new File)
593 *
594 * Note `File` is not versioned
595 *
596 * @param {Stream|Buffer|String} resource a stream, buffer or a content-string
597 * @param {String} name The binary name (i.e. a filename)
598 * @param {String} destinationPath The "directory location"
599 * @param {String} id The `File` id to replace the contents of (optional; if not set then creates a new File)
600 *
601 * @returns {Promise}
602 */
603 public updateBinary(
604 resource: ReadableStream | Buffer | string,
605 name: string,
606 destinationPath: string,
607 id?: string
608 ): Promise<ICommunibaseDocument> {
609 const metaData = {
610 path: destinationPath
611 };
612
613 return getResourceBufferPromise(resource).then((buffer: any) => {
614 if (id) {
615 // TODO check is valid id? entails extra dependency (mongodb.ObjectID)
616 // update File identified by id
617 return this.update("File", {
618 _id: id,
619 filename: name,
620 length: buffer.length,
621 uploadDate: moment().format(),
622 metadata: metaData,
623 content: buffer
624 });
625 }
626
627 // create a new File
628 const deferred = defer();
629 const formData: FormData = new FormData();
630 let stringOrBlob: string | Blob = buffer;
631
632 // @see https://developer.mozilla.org/en-US/docs/Web/API/FormData/append
633 // officially, formdata may contain blobs or strings. node doesn't do blobs, but when polymorphing in a browser we
634 // may cast it to one for it to work properly...
635 if (typeof window !== "undefined" && window.Blob) {
636 stringOrBlob = new Blob([buffer]);
637 }
638
639 formData.append("File", stringOrBlob, name);
640 formData.append("metadata", JSON.stringify(metaData));
641
642 this.queue.push({
643 deferred,
644 url: `${this.serviceUrl}File.json/binary`,
645 options: {
646 method: "POST",
647 data: formData,
648 headers: {
649 Accept: "application/json"
650 }
651 }
652 });
653 return deferred.promise;
654 });
655 }
656
657 /**
658 * Get a new Communibase Connector, may be with a different API key
659 *
660 * @param apiKey
661 * @returns {Connector}
662 */
663 public clone(apiKey: string): Connector {
664 return new Connector(apiKey);
665 }
666
667 /**
668 * Get the history information for a certain type of object
669 *
670 * VersionInformation: {
671 * "_id": "ObjectId",
672 * "updatedAt": "Date",
673 * "updatedBy": "string"
674 * }
675 *
676 * @param {string} objectType
677 * @param {string} objectId
678 * @returns promise for VersionInformation[]
679 */
680 public getHistory(
681 objectType: CommunibaseEntityType,
682 objectId: string
683 ): Promise<ICommunibaseVersionInformation[]> {
684 const deferred = defer();
685 this.queue.push({
686 deferred,
687 url: `${this.serviceUrl + objectType}.json/history/${objectId}`,
688 options: {
689 method: "GET"
690 }
691 });
692 return deferred.promise;
693 }
694
695 /**
696 *
697 * @param {string} objectType
698 * @param {Object} selector
699 * @returns promise for VersionInformation[]
700 */
701 public historySearch(
702 objectType: CommunibaseEntityType,
703 selector: {}
704 ): Promise<ICommunibaseVersionInformation[]> {
705 const deferred = defer();
706 this.queue.push({
707 deferred,
708 url: `${this.serviceUrl + objectType}.json/history/search`,
709 options: {
710 method: "POST",
711 data: selector
712 }
713 });
714 return deferred.promise;
715 }
716
717 /**
718 * Get a single object by a DocumentReference-object. A DocumentReference object looks like
719 * {
720 * rootDocumentId: '524aca8947bd91000600000c',
721 * rootDocumentEntityType: 'Person',
722 * path: [
723 * {
724 * field: 'addresses',
725 * objectId: '53440792463cda7161000003'
726 * }, ...
727 * ]
728 * }
729 *
730 * @param {object} ref - DocumentReference style, see above
731 * @param {object} parentDocument
732 * @return {Promise} for referred object
733 */
734 public getByRef(
735 ref: ICommunibaseDocumentReference,
736 parentDocument: ICommunibaseDocument
737 ): Promise<ICommunibaseDocument> {
738 if (
739 !(
740 ref &&
741 ref.rootDocumentEntityType &&
742 (ref.rootDocumentId || parentDocument)
743 )
744 ) {
745 return Promise.reject(
746 new Error(
747 "Please provide a documentReference object with a type and id"
748 )
749 );
750 }
751
752 const rootDocumentEntityTypeParts = ref.rootDocumentEntityType.split(".");
753 let parentDocumentPromise;
754 if (rootDocumentEntityTypeParts[0] !== "parent") {
755 parentDocumentPromise = this.getById(
756 ref.rootDocumentEntityType,
757 ref.rootDocumentId
758 );
759 } else {
760 parentDocumentPromise = Promise.resolve(parentDocument);
761 }
762
763 if (!(ref.path && ref.path.length && ref.path.length > 0)) {
764 return parentDocumentPromise;
765 }
766
767 /* tslint:disable no-parameter-reassignment */
768 return parentDocumentPromise.then((result: ICommunibaseDocument | null) => {
769 ref.path.some(pathNibble => {
770 if (result && result[pathNibble.field]) {
771 if (
772 !result[pathNibble.field].some(
773 (subDocument: ICommunibaseDocument) => {
774 if (subDocument._id === pathNibble.objectId) {
775 result = subDocument;
776 return true;
777 }
778 return false;
779 }
780 )
781 ) {
782 result = null;
783 return true;
784 }
785 return false;
786 }
787 result = null;
788 return true;
789 });
790 if (result) {
791 return result;
792 }
793 throw new Error(
794 "The referred object within it's parent could not be found"
795 );
796 });
797 /* tslint:enable no-parameter-reassignment */
798 }
799
800 /**
801 *
802 * @param {string} objectType - E.g. Event
803 * @param {array} aggregationPipeline - E.g. A MongoDB-specific Aggregation Pipeline
804 * @see http://docs.mongodb.org/manual/core/aggregation-pipeline/
805 *
806 * E.g. [
807 * { "$match": { "_id": {"$ObjectId": "52f8fb85fae15e6d0806e7c7"} } },
808 * { "$unwind": "$participants" },
809 * { "$group": { "_id": "$_id", "participantCount": { "$sum": 1 } } }
810 * ]
811 */
812 public aggregate(
813 objectType: CommunibaseEntityType,
814 aggregationPipeline: Array<{}>
815 ): Promise<Array<{}>> {
816 if (!aggregationPipeline || !aggregationPipeline.length) {
817 return Promise.reject(
818 new Error("Please provide a valid Aggregation Pipeline.")
819 );
820 }
821
822 let hash: string;
823 if (this.cache) {
824 hash = JSON.stringify([objectType, aggregationPipeline]);
825 let objectTypeAggregateCache = this.cache.aggregateCaches[objectType];
826 if (!objectTypeAggregateCache) {
827 objectTypeAggregateCache = lruCache(1000); // // 1000 getIds are this.cached, per entityType
828 this.cache.aggregateCaches[objectType] = objectTypeAggregateCache;
829 }
830 const result = objectTypeAggregateCache.get(hash);
831 if (result) {
832 return Promise.resolve(result);
833 }
834 }
835
836 const deferred = defer();
837 this.queue.push({
838 deferred,
839 url: `${this.serviceUrl + objectType}.json/aggregate`,
840 options: {
841 method: "POST",
842 data: aggregationPipeline
843 }
844 });
845
846 const resultPromise = deferred.promise;
847
848 if (this.cache) {
849 return resultPromise.then(result => {
850 if (this.cache) {
851 const objectTypeAggregateCache = this.cache.aggregateCaches[
852 objectType
853 ];
854 if (objectTypeAggregateCache) {
855 objectTypeAggregateCache.set(hash, result);
856 }
857 }
858 return result;
859 });
860 }
861
862 return resultPromise;
863 }
864
865 /**
866 * Finalize an invoice by its ID
867 *
868 * @param invoiceId
869 * @returns {*}
870 */
871 public finalizeInvoice(invoiceId: string): Promise<ICommunibaseDocument> {
872 const deferred = defer();
873 this.queue.push({
874 deferred,
875 url: `${this.serviceUrl}Invoice.json/finalize/${invoiceId}`,
876 options: {
877 method: "POST"
878 }
879 });
880 return deferred.promise;
881 }
882
883 /**
884 * @param communibaseAdministrationId
885 * @param socketServiceUrl
886 */
887 public enableCache(
888 communibaseAdministrationId: string,
889 socketServiceUrl: string
890 ): void {
891 this.cache = {
892 getIdsCaches: {},
893 aggregateCaches: {},
894 dirtySock: socketIoClient.connect(socketServiceUrl, { port: "443" }),
895 objectCache: {},
896 isAvailable(objectType, objectId): boolean {
897 if (!cache) {
898 return false;
899 }
900 return !!(
901 cache.objectCache[objectType] &&
902 cache.objectCache[objectType][objectId]
903 );
904 }
905 };
906 const { cache } = this;
907 cache.dirtySock.on("connect", () => {
908 cache.dirtySock.emit("join", `${communibaseAdministrationId}_dirty`);
909 });
910 cache.dirtySock.on("message", (dirtyness: string) => {
911 const dirtyInfo = dirtyness.split("|");
912 if (dirtyInfo.length !== 2) {
913 winston.warn(`${new Date()}: Got weird dirty sock data? ${dirtyness}`);
914 return;
915 }
916 cache.getIdsCaches[dirtyInfo[0]] = null;
917 cache.aggregateCaches[dirtyInfo[0]] = null;
918 if (dirtyInfo.length === 2 && cache.objectCache[dirtyInfo[0]]) {
919 cache.objectCache[dirtyInfo[0]][dirtyInfo[1]] = null;
920 }
921 });
922 }
923
924 private queueSearch<T extends ICommunibaseDocument = ICommunibaseDocument>(
925 objectType: CommunibaseEntityType,
926 selector: {},
927 params?: ICommunibaseParams
928 ): Promise<T[]> {
929 const deferred = defer();
930 this.queue.push({
931 deferred,
932 url: `${this.serviceUrl + objectType}.json/search`,
933 options: {
934 method: "POST",
935 data: selector,
936 params
937 }
938 });
939 return deferred.promise;
940 }
941
942 /**
943 * Bare boned retrieval by objectIds
944 * @returns {Promise}
945 */
946 private privateGetByIds<
947 T extends ICommunibaseDocument = ICommunibaseDocument
948 >(
949 objectType: CommunibaseEntityType,
950 objectIds: string[],
951 params?: {}
952 ): Promise<T[]> {
953 return this.queueSearch(
954 objectType,
955 {
956 _id: { $in: objectIds }
957 },
958 params
959 );
960 }
961
962 /**
963 * Default object retrieval: should provide cachable objects
964 */
965 private spoolQueue(): void {
966 Object.keys(this.getByIdQueue).forEach(objectType => {
967 const deferredsById = this.getByIdQueue[objectType];
968 const objectIds = Object.keys(deferredsById);
969
970 this.getByIdQueue[objectType] = {};
971 this.privateGetByIds(objectType, objectIds).then(
972 objects => {
973 const objectHash: {
974 [key: string]: ICommunibaseDocument;
975 } = objects.reduce(
976 (
977 previousValue: { [key: string]: ICommunibaseDocument },
978 object
979 ) => {
980 if (object._id) {
981 previousValue[object._id] = object;
982 }
983 return previousValue;
984 },
985 {}
986 );
987 objectIds.forEach((objectId: string) => {
988 if (objectHash[objectId]) {
989 deferredsById[objectId].resolve(objectHash[objectId]);
990 return;
991 }
992 deferredsById[objectId].reject(
993 new Error(`${objectId} is not found`)
994 );
995 });
996 },
997 err => {
998 objectIds.forEach(objectId => {
999 deferredsById[objectId].reject(err);
1000 });
1001 }
1002 );
1003 });
1004 this.getByIdPrimed = false;
1005 }
1006}
1007
1008// Backwards compatibility-ish
1009export default new Connector(process.env.COMMUNIBASE_KEY);
1010
\No newline at end of file