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