UNPKG

@mkeen/rxcouch

Version:

Real Time RxJs Based CouchDB Client

335 lines 19.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CouchDB = void 0; var rxjs_1 = require("rxjs"); var operators_1 = require("rxjs/operators"); var rxhttp_1 = require("@mkeen/rxhttp"); var couchurls_1 = require("./couchurls"); var types_1 = require("./types"); var couchdbsession_1 = require("./couchdbsession"); var sugar_1 = require("./sugar"); var couchdbdocumentcollection_1 = require("./couchdbdocumentcollection"); var CouchDB = /** @class */ (function () { function CouchDB(rxCouchConfig, couchSession, rxhttpDebug) { var _this = this; if (couchSession === void 0) { couchSession = new couchdbsession_1.CouchDBSession(types_1.AuthorizationBehavior.open); } if (rxhttpDebug === void 0) { rxhttpDebug = false; } this.couchSession = couchSession; this.rxhttpDebug = rxhttpDebug; this.documents = new couchdbdocumentcollection_1.CouchDBDocumentCollection(); this.changeFeedAbort = new rxjs_1.Subject(); this.appDocChanges = {}; this.changeFeedSubscription = null; rxCouchConfig = Object.assign({}, rxCouchConfig); this.databaseName = new rxjs_1.BehaviorSubject(sugar_1.entityOrDefault(rxCouchConfig.dbName, '_users')); this.host = new rxjs_1.BehaviorSubject(sugar_1.entityOrDefault(rxCouchConfig.host, '127.0.0.1')); this.port = new rxjs_1.BehaviorSubject(sugar_1.entityOrDefault(rxCouchConfig.port, 5984)); this.ssl = new rxjs_1.BehaviorSubject(sugar_1.entityOrDefault(rxCouchConfig.ssl, false)); this.trackChanges = new rxjs_1.BehaviorSubject(sugar_1.entityOrDefault(rxCouchConfig.trackChanges, true)); this.config() .pipe(operators_1.distinctUntilChanged(), operators_1.debounceTime(0)).subscribe(function (config) { var idsEmpty = config[types_1.IDS].length === 0; if (idsEmpty || !config[types_1.TRACK_CHANGES]) { _this.closeChangeFeed(); } else { _this.configureChangeFeed(config); } }); } CouchDB.prototype.configureChangeFeed = function (config) { var _this = this; if (this.changeFeedSubscription) { this.changeFeedAbort.next(true); this.changeFeedSubscription.unsubscribe(); this.changeFeedSubscription = null; } this.changeFeedSubscription = this.changes(this.changeFeedAbort, config).subscribe(function (update) { if (_this.documents.changed(update.doc)) { _this.stopListeningForLocalChanges(update.doc._id); _this.documents.doc(update.doc); _this.listenForLocalChanges(update.doc._id); } }); }; CouchDB.prototype.reconfigure = function (rxCouchConfig) { sugar_1.nextIfChanged(this.databaseName, rxCouchConfig.dbName); sugar_1.nextIfChanged(this.host, rxCouchConfig.host); sugar_1.nextIfChanged(this.port, rxCouchConfig.port); sugar_1.nextIfChanged(this.ssl, rxCouchConfig.ssl); sugar_1.nextIfChanged(this.trackChanges, rxCouchConfig.trackChanges); }; CouchDB.prototype.closeChangeFeed = function () { this.changeFeedAbort.next(true); }; CouchDB.prototype.config = function () { return rxjs_1.combineLatest(this.documents.ids, this.databaseName, this.host, this.port, this.ssl, this.couchSession.cookie, this.trackChanges, this.couchSession.authenticated); }; CouchDB.prototype.doc = function (document) { var _this = this; return rxjs_1.Observable.create(function (observer) { if (typeof (document) === 'string') { if (_this.documents.isKnownDocument(document)) { observer.next(_this.documents.doc(document)); _this.listenForLocalChanges(document); } else { _this.getDocument(document, observer); } } else { if (_this.documents.changed(document)) { _this.saveDocument(document).pipe(operators_1.take(1)).subscribe(function (doc) { document._rev = doc.rev; document._id = doc.id; observer.next(_this.documents.doc(document)); }, function (err) { observer.error(err); }, function () { observer.complete(); }); } else { observer.next(_this.documents.doc(document._id)); } } }).pipe(operators_1.mergeAll(), operators_1.finalize(function () { })); }; CouchDB.prototype.find = function (query) { var _this = this; return rxjs_1.Observable.create(function (observer) { _this.config().pipe(operators_1.filter(function (config) { return config[types_1.AUTHENTICATED]; }), operators_1.take(1), operators_1.map(function (config) { return _this.httpRequestWithAuthRetry(config, couchurls_1.CouchUrls.find(config), rxhttp_1.FetchBehavior.simpleWithHeaders, 'POST', JSON.stringify(query)); }), operators_1.mergeAll(), operators_1.take(1), operators_1.map(function (findResponse) { return findResponse.docs.map(function (document) { return document; }); })).subscribe(function (documents) { observer.next(documents); }); }); }; CouchDB.prototype.changes = function (stopChanges, config) { var _this = this; if (stopChanges === void 0) { stopChanges = new rxjs_1.Subject(); } return rxjs_1.Observable.create(function (observer) { if (!config) { _this.config().pipe(operators_1.filter(function (config) { return config[types_1.AUTHENTICATED]; }), operators_1.take(1)).subscribe(function (config) { return _this.durableHttpRequest(config, couchurls_1.CouchUrls.changes(config), observer, stopChanges, rxhttp_1.FetchBehavior.stream); }); } else { return _this.durableHttpRequest(config, couchurls_1.CouchUrls.changesWithIds(config), observer, stopChanges, rxhttp_1.FetchBehavior.stream, 'POST', { doc_ids: config[types_1.IDS] }); } }).pipe(operators_1.finalize(function () { stopChanges.next(true); }), operators_1.filter(function (update) { return !update.last_seq; })); }; CouchDB.prototype.delete = function (docs) { var _this = this; return rxjs_1.Observable.create(function (observer) { _this.bulkModify(docs.map(function (doc) { return Object.assign(doc, { _deleted: true }); }), observer); }); }; CouchDB.prototype.edit = function (docs) { var _this = this; return rxjs_1.Observable.create(function (observer) { _this.bulkModify(docs, observer); }); }; CouchDB.prototype.all = function () { var _this = this; return this.config().pipe(operators_1.filter(function (config) { return config[types_1.AUTHENTICATED]; }), operators_1.take(1), operators_1.map(function (config) { return _this.httpRequestWithAuthRetry(config, couchurls_1.CouchUrls._all_docs(config), rxhttp_1.FetchBehavior.simple, 'GET'); }), operators_1.mergeAll()); }; CouchDB.prototype.createDb = function (name) { var _this = this; return this.config().pipe(operators_1.filter(function (config) { return config[types_1.AUTHENTICATED]; }), operators_1.take(1), operators_1.map(function (config) { return _this.httpRequestWithAuthRetry(config, couchurls_1.CouchUrls.database(config, name), rxhttp_1.FetchBehavior.simple, 'PUT'); }), operators_1.mergeAll()); }; CouchDB.prototype.deleteDb = function (name) { var _this = this; return this.config().pipe(operators_1.filter(function (config) { return config[types_1.AUTHENTICATED]; }), operators_1.take(1), operators_1.map(function (config) { return _this.httpRequestWithAuthRetry(config, couchurls_1.CouchUrls.database(config, name), rxhttp_1.FetchBehavior.simple, 'DELETE'); }), operators_1.mergeAll()); }; CouchDB.prototype.secureDb = function (name, securityObject) { var _this = this; return this.config().pipe(operators_1.filter(function (config) { return config[types_1.AUTHENTICATED]; }), operators_1.take(1), operators_1.map(function (config) { return _this.httpRequestWithAuthRetry(config, couchurls_1.CouchUrls.databaseSecurity(config, name), rxhttp_1.FetchBehavior.simple, 'PUT', securityObject); }), operators_1.mergeAll()); }; CouchDB.prototype.uuids = function (count) { var _this = this; if (count === void 0) { count = 1; } return this.config().pipe(operators_1.filter(function (config) { return config[types_1.AUTHENTICATED]; }), operators_1.take(1), operators_1.map(function (config) { return _this.httpRequestWithAuthRetry(config, couchurls_1.CouchUrls.uuids(config, count), rxhttp_1.FetchBehavior.simple, 'GET'); }), operators_1.mergeAll()); }; CouchDB.prototype.bulkModify = function (docs, observer // make this api better. having to pass in an observable is weird. would ) { var _this = this; this.config().pipe(operators_1.filter(function (config) { return config[types_1.AUTHENTICATED]; }), operators_1.take(1), operators_1.map(function (config) { return _this.httpRequestWithAuthRetry(config, couchurls_1.CouchUrls.documentDelete(config), rxhttp_1.FetchBehavior.simple, 'POST', JSON.stringify({ docs: docs })); }), operators_1.mergeAll()).subscribe(function (response) { _this.stopListeningForLocalChanges(response.id); _this.documents.remove(response.id); observer.next(response); observer.complete(); }); }; CouchDB.prototype.getDocument = function (documentId, observer // make this api better. having to pass in an observable is weird. would ) { var _this = this; this.config().pipe(operators_1.filter(function (config) { return config[types_1.AUTHENTICATED]; }), operators_1.take(1), operators_1.map(function (config) { return _this.httpRequestWithAuthRetry(config, couchurls_1.CouchUrls.document(config, documentId), rxhttp_1.FetchBehavior.simple, 'GET'); }), operators_1.mergeAll()).subscribe(function (doc) { if (_this.documents.isStoredCouchDBDocument(doc)) { observer.next(_this.documents.doc(doc)); _this.listenForLocalChanges(doc._id); } else { // todo. use partial document as a find query and return result IF there is exactly one result. otherwise, error observer.error(doc); } observer.complete(); }, function (err) { return observer.error(err); }, function () { return observer.complete(); }); }; CouchDB.prototype.saveDocument = function (document) { var _this = this; return this.config().pipe(operators_1.filter(function (config) { return config[types_1.AUTHENTICATED]; }), operators_1.take(1), operators_1.map(function (config) { return _this.httpRequestWithAuthRetry(config, couchurls_1.CouchUrls.document(config, !_this.documents.isPreDocument(document) ? document._id : undefined), rxhttp_1.FetchBehavior.simple, _this.documents.isPreDocument(document) ? 'POST' : 'PUT', JSON.stringify(document)); }), operators_1.mergeAll()); }; CouchDB.prototype.durableHttpRequest = function (config, url, observer, stopChanges, behavior, method, body, httpRequest, cycle, backoff) { var _this = this; if (behavior === void 0) { behavior = rxhttp_1.FetchBehavior.stream; } if (method === void 0) { method = 'POST'; } if (body === void 0) { body = {}; } if (httpRequest === void 0) { httpRequest = this.httpRequest(config, url, behavior, method, body && typeof (body) === 'object' ? JSON.stringify(body) : body); } if (cycle === void 0) { cycle = 1; } if (backoff === void 0) { backoff = 100; } body = body && typeof (body) === 'object' ? JSON.stringify(body) : body; httpRequest.fetch().pipe(operators_1.takeUntil(stopChanges)) .subscribe(function (response) { observer.next(response); }, function (errorInfo) { var _b, _c, _d; if (errorInfo.errorCode === 401) { (!((_b = _this.couchSession) === null || _b === void 0 ? void 0 : _b.loginAttemptMade.value) ? (_c = _this.couchSession) === null || _c === void 0 ? void 0 : _c.authenticate : (_d = _this.couchSession) === null || _d === void 0 ? void 0 : _d.reauthenticate)().pipe(operators_1.take(1)).subscribe(function (success) { if (success) { _this.durableHttpRequest(config, url, observer, stopChanges, behavior, method, body, undefined, cycle + 1 < 10 ? cycle + 1 : 10, backoff); } else { observer.error(errorInfo); } }); } else if (!errorInfo.errorCode) { rxjs_1.timer(backoff * cycle).pipe(operators_1.take(1)).subscribe(function (_complete) { _this.durableHttpRequest(config, url, observer, stopChanges, behavior, method, body, undefined, cycle + 1 < 10 ? cycle + 1 : 10, backoff); }); } }, function () { rxjs_1.timer(backoff * cycle).pipe(operators_1.take(1)).subscribe(function (_complete) { _this.durableHttpRequest(config, url, observer, stopChanges, behavior, method, body, undefined, cycle + 1 < 10 ? cycle + 1 : 10, backoff); }); }); }; CouchDB.prototype.httpRequestWithAuthRetry = function (config, url, behavior, method, body, httpRequest) { var _this = this; if (behavior === void 0) { behavior = rxhttp_1.FetchBehavior.simpleWithHeaders; } if (method === void 0) { method = 'GET'; } if (body === void 0) { body = undefined; } if (httpRequest === void 0) { httpRequest = this.httpRequest(config, url, behavior, method, body); } return rxjs_1.Observable.create(function (observer) { var _b; (behavior === rxhttp_1.FetchBehavior.simpleWithHeaders ? httpRequest.fetch().pipe(operators_1.tap((_b = _this.couchSession) === null || _b === void 0 ? void 0 : _b.saveCookie), operators_1.map(_this.couchSession ? _this.couchSession.extractResponse : function (_a) { return null; })) : httpRequest.fetch()).subscribe(function (response) { observer.next(rxjs_1.of(response)); }, function (errorMessage) { var _b, _c, _d; if (errorMessage.errorCode === 401 || errorMessage.errorCode === 403) { console.log("[rxcouch] auth failed " + JSON.stringify(errorMessage)); (!((_b = _this.couchSession) === null || _b === void 0 ? void 0 : _b.loginAttemptMade.value) ? (_c = _this.couchSession) === null || _c === void 0 ? void 0 : _c.authenticate : (_d = _this.couchSession) === null || _d === void 0 ? void 0 : _d.reauthenticate)().pipe(operators_1.take(1)).subscribe(function (authResponse) { if (authResponse) { observer.next(_this.httpRequestWithAuthRetry(config, url, behavior, method, body)); } else { console.log("[rxcouch] REauth failed " + JSON.stringify(errorMessage)); observer.error(errorMessage); } }, function (error) { observer.error(error); }, function () { // Some day, possibly use this as a hook for retrying connections }); } else { observer.error(errorMessage); } }, function () { }); }).pipe(operators_1.mergeAll(), operators_1.finalize(function () { httpRequest.cancel(); })); }; CouchDB.prototype.httpRequest = function (config, url, behavior, method, body) { if (behavior === void 0) { behavior = rxhttp_1.FetchBehavior.simpleWithHeaders; } if (method === void 0) { method = 'GET'; } if (body === void 0) { body = undefined; } return new rxhttp_1.HttpRequest(url, this.httpRequestOptions(config, method, body), behavior, !this.rxhttpDebug); }; CouchDB.prototype.httpRequestOptions = function (config, method, body) { var httpOptions = { method: method }; if (body) { httpOptions.body = body; } if (config[types_1.COOKIE].length) { // If cookie auth is being used in browser, it will be implicitly sent with all outgoing requests. The below // has process === 'object' present because we don't manually inject this header unless running on node. if (this.couchSession.authorizationBehavior === types_1.AuthorizationBehavior.cookie && typeof process === 'object') { httpOptions.headers = { Cookie: this.cookieForRequestHeader(config[types_1.COOKIE]), }; } else if (this.couchSession.authorizationBehavior === types_1.AuthorizationBehavior.jwt) { httpOptions.headers = { Authorization: "Bearer ${config[COOKIE]}", }; } } return httpOptions; }; CouchDB.prototype.cookieForRequestHeader = function (cookie) { return cookie.split(';')[0].trim(); }; CouchDB.prototype.stopListeningForLocalChanges = function (doc_id) { if (this.appDocChanges[doc_id] !== undefined) { this.appDocChanges[doc_id].unsubscribe(); delete this.appDocChanges[doc_id]; } }; CouchDB.prototype.listenForLocalChanges = function (doc_id) { var _this = this; if (this.appDocChanges[doc_id] === undefined) { // kind of a gross way to check if we're already listening. shouldn't be necessary. this.appDocChanges[doc_id] = this.documents.doc(doc_id).pipe(operators_1.skip(1)).subscribe(function (changedDoc) { if (doc_id !== changedDoc._id) { console.warn('document mismatch. change ignored.'); // this is only here because its possible to change a doc id. return; // and i havent even attempted to handle that case yet. } if (_this.documents.changed(changedDoc)) { _this.stopListeningForLocalChanges(changedDoc._id); _this.doc(changedDoc).pipe(operators_1.take(1)).subscribe(function (_e) { }); } }); } }; return CouchDB; }()); exports.CouchDB = CouchDB; //# sourceMappingURL=couchdb.js.map