UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

274 lines 10.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.controllerBuilder = controllerBuilder; exports.mapQueryValues = mapQueryValues; exports.mapFindQuery = mapFindQuery; exports.getQueryFromCursorLastId = getQueryFromCursorLastId; exports.buildCursorLastId = buildCursorLastId; exports.checkProcessingAuthorization = checkProcessingAuthorization; exports.decrypt = decrypt; exports.getVersionFromDate = getVersionFromDate; const has_1 = __importDefault(require("lodash/has")); const isObject_1 = __importDefault(require("lodash/isObject")); const mapValues_1 = __importDefault(require("lodash/mapValues")); const pick_1 = __importDefault(require("lodash/pick")); const pickBy_1 = __importDefault(require("lodash/pickBy")); const utils_1 = require("../../utils"); const constants_1 = require("../../constants"); const DEFAULT_ERRORS = { message: { 'Invalid Model': 400, 'Event schema validation error': 400, 'Not Found': 404, 'Entity is readonly': 405, 'Imperative condition failed': 412, 'Entity must be created first': 422, 'State schema validation error': 422, }, code: { 11000: 409, }, }; function controllerBuilder(services, meter, handler, errors = {}) { const _errors = { message: { ...DEFAULT_ERRORS.message, ...errors.message, }, code: { ...DEFAULT_ERRORS.code, ...errors.code, }, }; return async (req, res, next) => { var _a; // @ts-ignore if (res.body) { return next(); } try { res.locals.meter = meter; res.locals.attributes = { model: req.params.model, }; meter({ state: 'request', model: req.params.model, }); await handler(req, res, next); } catch (err) { for (const key in _errors) { if (err[key] in _errors[key]) { let status = _errors[key][err[key]]; if (typeof status === 'function') { status = await status(err, req, res, next); } err.status = status; err.details = err.details || []; if ((_a = req === null || req === void 0 ? void 0 : req.params) === null || _a === void 0 ? void 0 : _a.model) { err.details.push({ model: req.params.model, }); } return next(err); } } next(err); } }; } const FORMATTER_REGEXP = /^([a-z]+)\((.*)\)$/; function mapQueryValues(Model, properties, obj, mustHash = true) { var _a, _b, _c; const propNames = Object.getOwnPropertyNames(obj); for (const name of propNames) { let prop = obj[name]; if (!Array.isArray(prop) && (0, isObject_1.default)(prop)) { obj[name] = mapQueryValues(Model, (_b = (_a = properties === null || properties === void 0 ? void 0 : properties[name]) === null || _a === void 0 ? void 0 : _a.properties) !== null && _b !== void 0 ? _b : {}, prop); continue; } let key = name; /** * @deprecated Backward compatibility for the previous * implementation of the `_must_hash` logic applied * globally to the query result. */ if (mustHash === true && Model.isEncryptedField(key)) { key = `hash(${name})`; } const [, formatter, _name] = (_c = FORMATTER_REGEXP.exec(key)) !== null && _c !== void 0 ? _c : []; if (formatter) { key = _name; switch (formatter) { case 'date': prop = Array.isArray(prop) ? prop.map((p) => (0, utils_1.getDate)(p)) : (0, utils_1.getDate)(prop); break; case 'hash': key = `${_name}.hash`; prop = Array.isArray(prop) ? prop.map((p) => Model.hashValue(p)) : Model.hashValue(prop); break; default: // ... } delete obj[name]; obj[key] = prop; } const { type } = properties[key] || {}; if (type !== 'array' && Array.isArray(prop)) { if (prop.length === 0) { return null; } obj[key] = { $in: prop, }; } } return obj; } function mapFindQuery(Model, query) { var _a, _b; const schema = Model.getSchema().model; const properties = schema.properties; const q = 'q' in query ? JSON.parse(query.q) : query; let _query = (0, pickBy_1.default)(q, (_v, k) => !k.startsWith('_')); const _options = { sort: { ...(0, mapValues_1.default)(q._sort || { created_at: 1, }, (v) => Number.parseInt(v, 10)), _id: (_b = (_a = q === null || q === void 0 ? void 0 : q._sort) === null || _a === void 0 ? void 0 : _a._id) !== null && _b !== void 0 ? _b : 1, }, projection: (0, mapValues_1.default)(q._fields || {}, (v) => Number.parseInt(v, 10)), }; if ('_fields' in q && Object.keys(_options.projection).length > 0) { _options.projection = { ..._options.projection, // Add mandatory fields for find and walk pagination logic [Model.getCorrelationField()]: 1, created_at: 1, updated_at: 1, version: 1, _id: 1, }; } /** * @deprecated in favor of v2 request with `hash()` * formatter */ const mustHash = q._must_hash === true; _query = mapQueryValues(Model, properties, _query, mustHash); if (_query === null) { return { query: null, options: _options, }; } _query = (0, utils_1.deepCoerce)(_query, schema); /** * @deprecated in favor of v2 request without special * field `_q` */ if (q._q) { try { q._q = JSON.parse(q._q); q._q = JSON.parse(q._q); } catch (err) { // .. } _query = { ..._query, ...(0, utils_1.deepCoerce)(q._q, schema), }; } return { query: _query, options: _options, }; } function getQueryFromCursorLastId(Model, options, cursorLastId, cursorLastCorrelationId) { const _revertId = JSON.parse(Buffer.from(cursorLastId, 'hex').toString()); const { query: _lastIdQuery } = mapFindQuery(Model, _revertId); const cursorQuery = (0, mapValues_1.default)(_lastIdQuery, (val, key) => { if (options.sort[key] === 1) { // Following condition for backward compatibility: // `2024-06-26`: Nominal case should be `$gte` with `cursorLastCorrelationId` return { [cursorLastCorrelationId ? '$gte' : '$gt']: val }; } // Following condition for backward compatibility: // `2024-06-26`: Nominal case should be `$gte` with `cursorLastCorrelationId` return { [cursorLastCorrelationId ? '$lte' : '$lt']: val }; }); return cursorQuery; } function buildCursorLastId(entity, options) { return entity ? Buffer.from(JSON.stringify((0, pick_1.default)(entity, Object.keys(options.sort)))).toString('hex') : ''; } function checkProcessingAuthorization(services, tokenId, modelConfig, data) { var _a, _b, _c, _d; if (((_c = (_b = (_a = services === null || services === void 0 ? void 0 : services.config) === null || _a === void 0 ? void 0 : _a.features) === null || _b === void 0 ? void 0 : _b.api) === null || _c === void 0 ? void 0 : _c.checkProcessingAuthorization) !== true) { return; } if (Array.isArray(modelConfig.processings)) { const authorizedProcessings = modelConfig.processings.filter((processing) => { var _a; return ((_a = processing.tokens) !== null && _a !== void 0 ? _a : [tokenId]).includes(tokenId); }); for (const encryptedField of (_d = modelConfig.encrypted_fields) !== null && _d !== void 0 ? _d : []) { if ((0, has_1.default)(data, encryptedField) === false) { continue; } const isProcessingAuthorizedOnField = authorizedProcessings.find((processing) => processing.field === encryptedField); if (!isProcessingAuthorizedOnField) { throw new Error('Unauthorized field processing'); } } } } async function decrypt(services, access, modelName, data, fields) { var _a; const Model = services.models.getModel(modelName); const modelConfig = Model.getModelConfig(); checkProcessingAuthorization(services, access.id, modelConfig, data); (_a = services.models) === null || _a === void 0 ? void 0 : _a.log(50, modelConfig.name, data[Model.getCorrelationField()], '[decrypt] Entity decrypted', { model: modelConfig.name, correlation_id: data[Model.getCorrelationField()], id: access.id, level: access.level, persist: true, }); return Model.decrypt(data, fields); } async function getVersionFromDate(services, Model, correlationId, version) { if (version === undefined) { return version; } if (typeof version === 'number') { return version; } if (!constants_1.REGEXP_DATE_ISO_STRING_8601.test(version)) { return Number.parseInt(version, 10); } const lastEvent = await Model.getEventsCollection(Model.db(services.mongodb)).findOne({ [Model.getCorrelationField()]: correlationId, created_at: { $lte: (0, utils_1.getDate)(version), }, }, { sort: { version: -1, }, }); if (!lastEvent) { throw new Error('Not Found'); } return lastEvent.version; } //# sourceMappingURL=index.js.map