UNPKG

acebase-server

Version:

AceBase realtime database server (webserver endpoint to allow remote connections)

161 lines 9.55 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.addRoutes = exports.DataTransactionError = exports.TRANSACTION_TIMEOUT_MS = void 0; const acebase_1 = require("acebase"); const acebase_core_1 = require("acebase-core"); const rules_1 = require("../rules"); const error_1 = require("../shared/error"); const admin_1 = require("../shared/admin"); exports.TRANSACTION_TIMEOUT_MS = 10000; // 10s to finish a started transaction class DataTransactionError extends Error { constructor(code, message) { super(message); this.code = code; } } exports.DataTransactionError = DataTransactionError; const addRoutes = (env) => { const _transactions = new Map(); // Start transaction endpoint: env.router.post(`/transaction/${env.db.name}/start`, (req, res) => __awaiter(void 0, void 0, void 0, function* () { var _a, _b, _c, _d; const data = req.body; const LOG_ACTION = 'data.transaction.start'; const LOG_DETAILS = { ip: req.ip, uid: (_b = (_a = req.user) === null || _a === void 0 ? void 0 : _a.uid) !== null && _b !== void 0 ? _b : null, path: data.path }; // Pre-check read/write access const access = yield env.rules.isOperationAllowed(req.user, data.path, 'transact'); if (!access.allow) { env.log.error(LOG_ACTION, 'unauthorized', Object.assign(Object.assign({}, LOG_DETAILS), { rule_code: access.code, rule_path: (_c = access.rulePath) !== null && _c !== void 0 ? _c : null }), access.details); return (0, error_1.sendUnauthorizedError)(res, access.code, access.message); } // Start transaction const tx = { id: acebase_core_1.ID.generate(), started: Date.now(), path: data.path, context: req.context, finish: undefined, timeout: setTimeout(() => { _transactions.delete(tx.id); tx.finish(); // Finish without value cancels the transaction }, exports.TRANSACTION_TIMEOUT_MS), }; _transactions.set(tx.id, tx); try { env.debug.verbose(`Transaction ${tx.id} starting...`); // const ref = db.ref(tx.path); const donePromise = env.db.api.transaction(tx.path, (val) => __awaiter(void 0, void 0, void 0, function* () { var _e; env.debug.verbose(`Transaction ${tx.id} started with value: `, val); const access = yield env.rules.isOperationAllowed(req.user, data.path, 'get', { value: val, context: req.context }); if (!access.allow) { env.log.error(LOG_ACTION, 'unauthorized', Object.assign(Object.assign({}, LOG_DETAILS), { rule_code: access.code, rule_path: (_e = access.rulePath) !== null && _e !== void 0 ? _e : null }), access.details); (0, error_1.sendUnauthorizedError)(res, access.code, access.message); return; // Return undefined to cancel transaction } const currentValue = acebase_core_1.Transport.serialize(val); const promise = new Promise((resolve) => { tx.finish = (val) => { env.debug.verbose(`Transaction ${tx.id} finishing with value: `, val); _transactions.delete(tx.id); resolve(val); return donePromise; }; }); res.send({ id: tx.id, value: currentValue }); return promise; }), { context: tx.context }); } catch (err) { yield (tx === null || tx === void 0 ? void 0 : tx.finish()); // Finish without value to cancel the transaction env.debug.error(`failed to start transaction on "${tx.path}":`, err); env.log.error(LOG_ACTION, (_d = err.code) !== null && _d !== void 0 ? _d : 'unexpected', LOG_DETAILS, typeof err.code === 'undefined' ? err : null); (0, error_1.sendUnexpectedError)(res, err); } })); // Finish transaction endpoint: env.router.post(`/transaction/${env.db.name}/finish`, (req, res) => __awaiter(void 0, void 0, void 0, function* () { var _f, _g, _h, _j, _k, _l; const data = req.body; const LOG_ACTION = 'data.transaction.finish'; const LOG_DETAILS = { ip: req.ip, uid: (_g = (_f = req.user) === null || _f === void 0 ? void 0 : _f.uid) !== null && _g !== void 0 ? _g : null, path: data.path }; const tx = _transactions.get(data.id); if (!tx || tx.path !== data.path) { env.log.error(LOG_ACTION, tx ? 'wrong_path' : 'not_found', Object.assign(Object.assign({}, LOG_DETAILS), { id: data.id, tx_path: (_h = tx === null || tx === void 0 ? void 0 : tx.path) !== null && _h !== void 0 ? _h : null })); res.statusCode = 410; // Gone res.send(`transaction not found`); return; } clearTimeout(tx.timeout); _transactions.delete(tx.id); // Finish transaction try { // Check again if a 'write' to this path is allowed by this user let access = yield env.rules.isOperationAllowed(req.user, tx.path, 'set'); if (!access.allow) { throw new rules_1.AccessRuleValidationError(access); } let cancel = false; if (typeof data.value === 'object' && (data.value === null || Object.keys(data.value).length === 0)) { // Returning undefined from a transaction callback should cancel the transaction // acebase-client (Transport.serialize) serializes value undefined as { val: undefined, map: undefined }, which // then is sent to the server as an empty object: {} cancel = true; } else if (typeof ((_j = data.value) === null || _j === void 0 ? void 0 : _j.val) === 'undefined' || !['string', 'object', 'undefined'].includes(typeof ((_k = data.value) === null || _k === void 0 ? void 0 : _k.map))) { throw new DataTransactionError('invalid_serialized_value', 'The sent value is not properly serialized'); } const newValue = cancel ? undefined : acebase_core_1.Transport.deserialize(data.value); if (tx.path === '' && !(0, admin_1.isAdmin)(req.user) && newValue !== null && typeof newValue === 'object') { // Non-admin user: remove any private properties from the update object Object.keys(newValue).filter(key => key.startsWith('__')).forEach(key => delete newValue[key]); } // Check if the value to be written is allowed access = yield env.rules.isOperationAllowed(req.user, tx.path, 'set', { value: newValue, context: tx.context }); if (!access.allow) { throw new rules_1.AccessRuleValidationError(access); } const result = yield tx.finish(newValue); // NEW: capture cursor and return it in the response context header if (!tx.context) { tx.context = {}; } tx.context.acebase_cursor = result.cursor; res.setHeader('AceBase-Context', JSON.stringify(tx.context)); res.send('done'); } catch (err) { tx.finish(); // Finish without value cancels the transaction if (err instanceof rules_1.AccessRuleValidationError) { const access = err.result; env.log.error(LOG_ACTION, 'unauthorized', Object.assign(Object.assign({}, LOG_DETAILS), { rule_code: access.code, rule_path: (_l = access.rulePath) !== null && _l !== void 0 ? _l : null }), access.details); return (0, error_1.sendUnauthorizedError)(res, access.code, access.message); } else if (err instanceof acebase_1.SchemaValidationError) { env.log.error(LOG_ACTION, 'schema_validation_failed', Object.assign(Object.assign({}, LOG_DETAILS), { reason: err.reason })); res.status(422).send({ code: 'schema_validation_failed', message: err.message }); } else if (err instanceof DataTransactionError) { env.log.error(LOG_ACTION, err.code, Object.assign(Object.assign({}, LOG_DETAILS), { message: err.message })); (0, error_1.sendBadRequestError)(res, err); } else { env.debug.error(`failed to finish transaction on "${tx.path}":`, err); env.log.error(LOG_ACTION, 'unexpected', LOG_DETAILS, err); (0, error_1.sendError)(res, err); } } })); }; exports.addRoutes = addRoutes; exports.default = exports.addRoutes; //# sourceMappingURL=data-transaction.js.map