rxdb-server
Version:
RxDB Server Plugin
196 lines (193 loc) • 8.41 kB
JavaScript
"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