UNPKG

rxdb-server

Version:
259 lines (254 loc) 10.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RxServerRestEndpoint = exports.REST_PATHS = void 0; exports.blockPreviousReplicationVersionPathsRest = blockPreviousReplicationVersionPathsRest; var _core = require("rxdb/plugins/core"); var _rxjs = require("rxjs"); var _utils = require("rxdb/plugins/utils"); var _helper = require("./helper.js"); var REST_PATHS = exports.REST_PATHS = ['query', 'query/observe', 'get', 'set', 'delete' // TODO /* 'attachments/add', 'attachments/delete', 'events' */]; var RxServerRestEndpoint = exports.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; (0, _helper.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 ((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); this.server.adapter.post(this.server.serverApp, '/' + this.urlPath + '/query', async (req, res) => { (0, _utils.ensureNotFalsy)(adapter.getRequestBody(req), 'req body is empty'); var authData = await (0, _helper.getAuthDataByRequest)(this.server, req, res); if (!authData) { return; } var useQuery; try { useQuery = this.queryModifier((0, _utils.ensureNotFalsy)(authData), (0, _core.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 (0, _helper.getAuthDataByRequest)(this.server, req, res); if (!authData) { return; } adapter.setSSEHeaders(res); var useQuery = this.queryModifier((0, _utils.ensureNotFalsy)(authData), (0, _core.normalizeMangoQuery)(this.collection.schema.jsonSchema, JSON.parse(atob(adapter.getRequestQuery(req).query)))); var rxQuery = this.collection.find(useQuery); var subscription = rxQuery.$.pipe((0, _rxjs.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; }), (0, _rxjs.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 (0, _helper.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 = (0, _helper.getDocAllowedMatcher)(this, (0, _utils.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 (0, _helper.getAuthDataByRequest)(this.server, req, res); if (!authData) { return; } var docDataMatcherWrite = (0, _helper.getDocAllowedMatcher)(this, (0, _utils.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 (0, _helper.getAuthDataByRequest)(this.server, req, res); if (!authData) { return; } var docDataMatcherWrite = (0, _helper.getDocAllowedMatcher)(this, (0, _utils.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. */ 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