@mkeen/rxcouch
Version:
Real Time RxJs Based CouchDB Client
335 lines • 19.4 kB
JavaScript
;
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