UNPKG

rxdb-server

Version:
196 lines (193 loc) 8.41 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RxServerReplicationEndpoint = void 0; exports.blockPreviousReplicationVersionPaths = blockPreviousReplicationVersionPaths; var _core = require("rxdb/plugins/core"); var _replicationWebsocket = require("rxdb/plugins/replication-websocket"); var _rxjs = require("rxjs"); var _utils = require("rxdb/plugins/utils"); var _helper = require("./helper.js"); var RxServerReplicationEndpoint = exports.RxServerReplicationEndpoint = function RxServerReplicationEndpoint(server, name, collection, queryModifier, changeValidator, serverOnlyFields, cors) { this.type = 'replication'; this.server = server; this.name = name; this.collection = collection; this.serverOnlyFields = serverOnlyFields; this.cors = cors; var adapter = this.server.adapter; (0, _helper.setCors)(this.server, [this.name].join('/'), cors); blockPreviousReplicationVersionPaths(this.server, [this.name].join('/'), collection.schema.version); this.urlPath = [this.name, collection.schema.version].join('/'); var primaryPath = this.collection.schema.primaryPath; var replicationHandler = (0, _replicationWebsocket.getReplicationHandlerByCollection)(this.server.database, collection.name); this.queryModifier = (authData, query) => { if ((0, _helper.doesContainRegexQuerySelector)(query.selector)) { throw new Error('$regex queries not allowed because of DOS-attacks'); } return queryModifier(authData, query); }; this.changeValidator = (authData, change) => { if (change.assumedMasterState && (0, _helper.docContainsServerOnlyFields)(serverOnlyFields, change.assumedMasterState) || (0, _helper.docContainsServerOnlyFields)(serverOnlyFields, change.newDocumentState)) { return false; } return changeValidator(authData, change); }; var removeServerOnlyFields = (0, _helper.removeServerOnlyFieldsMonad)(this.serverOnlyFields); var mergeServerDocumentFields = (0, _helper.mergeServerDocumentFieldsMonad)(this.serverOnlyFields); this.server.adapter.get(this.server.serverApp, '/' + this.urlPath + '/pull', async (req, res) => { var authData = await (0, _helper.getAuthDataByRequest)(this.server, req, res); if (!authData) { return; } var urlQuery = adapter.getRequestQuery(req); var id = urlQuery.id ? urlQuery.id : ''; var lwt = urlQuery.lwt ? parseFloat(urlQuery.lwt) : 0; var limit = urlQuery.limit ? parseInt(urlQuery.limit, 10) : 1; var plainQuery = (0, _core.getChangedDocumentsSinceQuery)(this.collection.storageInstance, limit, { id, lwt }); var useQueryChanges = this.queryModifier((0, _utils.ensureNotFalsy)(authData), plainQuery); var prepared = (0, _core.prepareQuery)(this.collection.schema.jsonSchema, useQueryChanges); var result = await this.collection.storageInstance.query(prepared); var newCheckpoint = result.documents.length === 0 ? { id, lwt } : { id: (0, _utils.ensureNotFalsy)((0, _utils.lastOfArray)(result.documents))[primaryPath], lwt: (0, _utils.ensureNotFalsy)((0, _utils.lastOfArray)(result.documents))._meta.lwt }; var responseDocuments = result.documents.map(d => removeServerOnlyFields(d)); adapter.setResponseHeader(res, 'Content-Type', 'application/json'); adapter.endResponseJson(res, { documents: responseDocuments, checkpoint: newCheckpoint }); }); this.server.adapter.post(this.server.serverApp, '/' + this.urlPath + '/push', async (req, res) => { var authData = await (0, _helper.getAuthDataByRequest)(this.server, req, res); if (!authData) { return; } var docDataMatcherWrite = (0, _helper.getDocAllowedMatcher)(this, (0, _utils.ensureNotFalsy)(authData)); var rows = adapter.getRequestBody(req); var ids = []; rows.forEach(row => ids.push(row.newDocumentState[primaryPath])); for (var row of rows) { // TODO remove this check if (row.assumedMasterState && row.assumedMasterState._meta) { throw new Error('body document contains meta!'); } } // ensure all writes are allowed var nonAllowedRow = rows.find(row => { if (!docDataMatcherWrite(row.newDocumentState) || row.assumedMasterState && !docDataMatcherWrite(row.assumedMasterState)) { return true; } }); if (nonAllowedRow) { adapter.closeConnection(res, 403, 'Forbidden'); return; } var hasInvalidChange = false; var currentStateDocsArray = await this.collection.storageInstance.findDocumentsById(ids, true); var currentStateDocs = new Map(); currentStateDocsArray.forEach(d => currentStateDocs.set(d[primaryPath], d)); var useRows = rows.map(row => { var id = row.newDocumentState[primaryPath]; var isChangeValid = this.changeValidator((0, _utils.ensureNotFalsy)(authData), { newDocumentState: removeServerOnlyFields(row.newDocumentState), assumedMasterState: removeServerOnlyFields(row.assumedMasterState) }); if (!isChangeValid) { hasInvalidChange = true; } var serverDoc = currentStateDocs.get(id); return { newDocumentState: mergeServerDocumentFields(row.newDocumentState, serverDoc), assumedMasterState: mergeServerDocumentFields(row.assumedMasterState, serverDoc) }; }); if (hasInvalidChange) { adapter.closeConnection(res, 403, 'Forbidden'); return; } var conflicts = await replicationHandler.masterWrite(useRows); adapter.setResponseHeader(res, 'Content-Type', 'application/json'); adapter.endResponseJson(res, conflicts); }); this.server.adapter.get(this.server.serverApp, '/' + this.urlPath + '/pullStream', async (req, res) => { var authData = await (0, _helper.getAuthDataByRequest)(this.server, req, res); if (!authData) { return; } adapter.setSSEHeaders(res); var docDataMatcherStream = (0, _helper.getDocAllowedMatcher)(this, (0, _utils.ensureNotFalsy)(authData)); var subscription = replicationHandler.masterChangeStream$.pipe((0, _rxjs.mergeMap)(async changes => { /** * The auth-data might be expired * so we re-run the auth parsing each time * before emitting an event. */ var authData; try { authData = await server.authHandler(adapter.getRequestHeaders(req)); } catch (err) { adapter.closeConnection(res, 401, 'Unauthorized'); return null; } if (changes === 'RESYNC') { return changes; } else { var useDocs = changes.documents.filter(d => docDataMatcherStream(d)); return { documents: useDocs, checkpoint: changes.checkpoint }; } }), (0, _rxjs.filter)(f => f !== null && (f === 'RESYNC' || f.documents.length > 0))).subscribe(filteredAndModified => { if (filteredAndModified === 'RESYNC') { adapter.responseWrite(res, 'data: ' + JSON.stringify(filteredAndModified) + '\n\n'); } else { var responseDocuments = (0, _utils.ensureNotFalsy)(filteredAndModified).documents.map(d => removeServerOnlyFields(d)); adapter.responseWrite(res, 'data: ' + JSON.stringify({ documents: responseDocuments, checkpoint: (0, _utils.ensureNotFalsy)(filteredAndModified).checkpoint }) + '\n\n'); } }); /** * @link https://youtu.be/0PcMuYGJPzM?si=AxkczxcMaUwhh8k9&t=363 */ adapter.onRequestClose(req, () => { subscription.unsubscribe(); adapter.endResponse(res); }); }); }; /** * "block" the previous version urls and send a 426 on them so that * the clients know they must update. */ function blockPreviousReplicationVersionPaths(server, path, currentVersion) { var v = 0; var _loop = function () { var version = v; /** * Some adapters do not allow regex or handle them property (like Koa), * so to make it easier, use the hard-coded array of path parts. */ ['', 'pull', 'push', 'pullStream'].forEach(subPath => { server.adapter.all(server.serverApp, '/' + path + '/' + version + '/' + subPath, (req, res) => { server.adapter.closeConnection(res, 426, 'Outdated version ' + version + ' (newest is ' + currentVersion + ')'); }); }); v++; }; while (v < currentVersion) { _loop(); } } //# sourceMappingURL=endpoint-replication.js.map