rxdb-server
Version:
RxDB Server Plugin
253 lines (248 loc) • 9.69 kB
JavaScript
import { normalizeMangoQuery } from 'rxdb/plugins/core';
import { filter, mergeMap } from 'rxjs';
import { ensureNotFalsy } from 'rxdb/plugins/utils';
import { docContainsServerOnlyFields, doesContainRegexQuerySelector, getAuthDataByRequest, getDocAllowedMatcher, removeServerOnlyFieldsMonad, setCors } from "./helper.js";
export var REST_PATHS = ['query', 'query/observe', 'get', 'set', 'delete'
// TODO
/*
'attachments/add',
'attachments/delete',
'events'
*/];
export var RxServerRestEndpoint = function RxServerRestEndpoint(server, name, collection, queryModifier, changeValidator, serverOnlyFields, cors) {
var _this = this;
this.type = 'rest';
this.server = server;
this.name = name;
this.collection = collection;
this.serverOnlyFields = serverOnlyFields;
this.cors = cors;
var adapter = server.adapter;
setCors(this.server, [this.name].join('/'), cors);
blockPreviousReplicationVersionPathsRest(this.server, [this.name].join('/'), collection.schema.version);
this.urlPath = [this.name, collection.schema.version].join('/');
var primaryPath = this.collection.schema.primaryPath;
this.queryModifier = (authData, query) => {
if (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 && docContainsServerOnlyFields(serverOnlyFields, change.assumedMasterState) || docContainsServerOnlyFields(serverOnlyFields, change.newDocumentState)) {
return false;
}
return changeValidator(authData, change);
};
var removeServerOnlyFields = removeServerOnlyFieldsMonad(this.serverOnlyFields);
this.server.adapter.post(this.server.serverApp, '/' + this.urlPath + '/query', async (req, res) => {
ensureNotFalsy(adapter.getRequestBody(req), 'req body is empty');
var authData = await getAuthDataByRequest(this.server, req, res);
if (!authData) {
return;
}
var useQuery;
try {
useQuery = this.queryModifier(ensureNotFalsy(authData), normalizeMangoQuery(this.collection.schema.jsonSchema, adapter.getRequestBody(req)));
} catch (err) {
adapter.closeConnection(res, 400, 'Bad Request');
return;
}
var rxQuery = this.collection.find(useQuery);
var result = await rxQuery.exec();
adapter.setResponseHeader(res, 'Content-Type', 'application/json');
adapter.endResponseJson(res, {
documents: result.map(d => removeServerOnlyFields(d.toJSON()))
});
});
/**
* It is not possible to send data with server send events,
* so we send the query as query parameter in base64
* like ?query=e3NlbGVjdG9yOiB7fX0=
*/
this.server.adapter.get(this.server.serverApp, '/' + this.urlPath + '/query/observe', async (req, res) => {
var authData = await getAuthDataByRequest(this.server, req, res);
if (!authData) {
return;
}
adapter.setSSEHeaders(res);
var useQuery = this.queryModifier(ensureNotFalsy(authData), normalizeMangoQuery(this.collection.schema.jsonSchema, JSON.parse(atob(adapter.getRequestQuery(req).query))));
var rxQuery = this.collection.find(useQuery);
var subscription = rxQuery.$.pipe(mergeMap(async result => {
var resultData = result.map(doc => removeServerOnlyFields(doc.toJSON()));
/**
* The auth-data might be expired
* so we re-run the auth parsing each time
* before emitting the new results.
*/
try {
authData = await server.authHandler(adapter.getRequestHeaders(req));
} catch (err) {
adapter.closeConnection(res, 401, 'Unauthorized');
return null;
}
return resultData;
}), filter(f => f !== null)).subscribe(resultData => {
adapter.responseWrite(res, 'data: ' + JSON.stringify(resultData) + '\n\n');
});
/**
* @link https://youtu.be/0PcMuYGJPzM?si=AxkczxcMaUwhh8k9&t=363
*/
adapter.onRequestClose(req, () => {
subscription.unsubscribe();
adapter.endResponse(res);
});
});
this.server.adapter.post(this.server.serverApp, '/' + this.urlPath + '/get', async (req, res) => {
var authData = await getAuthDataByRequest(this.server, req, res);
if (!authData) {
return;
}
var ids = adapter.getRequestBody(req);
var rxQuery = this.collection.findByIds(ids);
var resultMap = await rxQuery.exec();
var resultValues = Array.from(resultMap.values());
var docMatcher = getDocAllowedMatcher(this, ensureNotFalsy(authData));
var useDocs = resultValues.map(d => d.toJSON());
useDocs = useDocs.filter(d => docMatcher(d));
useDocs = useDocs.map(d => removeServerOnlyFields(d));
adapter.setResponseHeader(res, 'Content-Type', 'application/json');
adapter.endResponseJson(res, {
documents: useDocs
});
});
this.server.adapter.post(this.server.serverApp, '/' + this.urlPath + '/set', async (req, res) => {
var authData = await getAuthDataByRequest(this.server, req, res);
if (!authData) {
return;
}
var docDataMatcherWrite = getDocAllowedMatcher(this, ensureNotFalsy(authData));
var docsData = adapter.getRequestBody(req);
for (var docData of docsData) {
var allowed = docDataMatcherWrite(docData);
if (!allowed) {
adapter.closeConnection(res, 403, 'Forbidden');
return;
}
}
function onWriteError(err, docData) {
if (err.rxdb && err.code === 'CONFLICT') {
// just retry on conflicts
docsData.push(docData);
} else {
adapter.closeConnection(res, 500, 'Internal Server Error');
throw err;
}
}
while (docsData.length > 0) {
var promises = [];
var docs = await collection.findByIds(docsData.map(d => d[primaryPath])).exec();
var useDocsData = docsData.slice();
docsData = [];
var _loop = async function (_docData) {
var id = _docData[primaryPath];
var doc = docs.get(id);
if (!doc) {
promises.push(_this.collection.insert(_docData).catch(err => onWriteError(err, _docData)));
} else {
var isAllowed = _this.changeValidator(authData, {
newDocumentState: removeServerOnlyFields(_docData),
assumedMasterState: removeServerOnlyFields(doc.toJSON(true))
});
if (!isAllowed) {
adapter.closeConnection(res, 403, 'Forbidden');
return {
v: void 0
};
}
promises.push(doc.patch(_docData).catch(err => onWriteError(err, _docData)));
}
},
_ret;
for (var _docData of useDocsData) {
_ret = await _loop(_docData);
if (_ret) return _ret.v;
}
await Promise.all(promises);
}
adapter.setResponseHeader(res, 'Content-Type', 'application/json');
adapter.endResponseJson(res, {});
});
this.server.adapter.post(this.server.serverApp, '/' + this.urlPath + '/delete', async (req, res) => {
var authData = await getAuthDataByRequest(this.server, req, res);
if (!authData) {
return;
}
var docDataMatcherWrite = getDocAllowedMatcher(this, ensureNotFalsy(authData));
var ids = adapter.getRequestBody(req);
while (ids.length > 0) {
var useIds = ids.slice(0);
ids = [];
var promises = [];
var docsMap = await this.collection.findByIds(useIds).exec();
var _loop2 = async function (id) {
var doc = docsMap.get(id);
if (doc) {
var isAllowedDoc = docDataMatcherWrite(doc.toJSON(true));
if (!isAllowedDoc) {
adapter.closeConnection(res, 403, 'Forbidden');
return {
v: void 0
};
}
var isAllowedChange = _this.changeValidator(authData, {
newDocumentState: doc.toJSON(true),
assumedMasterState: doc.toJSON(true)
});
if (!isAllowedChange) {
adapter.closeConnection(res, 403, 'Forbidden');
return {
v: void 0
};
}
promises.push(doc.remove().catch(err => {
if (err.rxdb && err.code === 'CONFLICT') {
// just retry on conflicts
ids.push(id);
} else {
adapter.closeConnection(res, 500, 'Internal Server Error');
throw err;
}
}));
}
},
_ret2;
for (var id of useIds) {
_ret2 = await _loop2(id);
if (_ret2) return _ret2.v;
}
await Promise.all(promises);
}
adapter.setResponseHeader(res, 'Content-Type', 'application/json');
adapter.endResponseJson(res, {});
});
};
/**
* "block" the previous version urls and send a 426 on them so that
* the clients know they must update.
*/
export function blockPreviousReplicationVersionPathsRest(server, path, currentVersion) {
var v = 0;
var _loop3 = 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.
*/
['', 'query', 'query/observe', 'get', 'set', 'delete'].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) {
_loop3();
}
}
//# sourceMappingURL=endpoint-rest.js.map